@feltdb/core 0.6.8 → 0.6.10

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 (99) hide show
  1. package/dist/cli/application.js +55 -88
  2. package/dist/cli/commands.js +206 -95
  3. package/dist/cli/index.js +1 -1
  4. package/dist/collection.d.ts +110 -2
  5. package/dist/collection.d.ts.map +1 -1
  6. package/dist/collection.js +228 -3
  7. package/dist/create/package-versions.js +1 -1
  8. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
  9. package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
  10. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
  11. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
  12. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
  13. package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
  14. package/dist/db.d.ts +59 -0
  15. package/dist/db.d.ts.map +1 -1
  16. package/dist/db.js +94 -2
  17. package/dist/embedded-transaction.d.ts +77 -0
  18. package/dist/embedded-transaction.d.ts.map +1 -0
  19. package/dist/embedded-transaction.js +102 -0
  20. package/dist/feltdb.d.ts +49 -1
  21. package/dist/feltdb.d.ts.map +1 -1
  22. package/dist/file-db.d.ts +33 -0
  23. package/dist/file-db.d.ts.map +1 -1
  24. package/dist/file-db.js +128 -4
  25. package/dist/freshness.d.ts +119 -0
  26. package/dist/freshness.d.ts.map +1 -0
  27. package/dist/freshness.js +115 -0
  28. package/dist/http-db.d.ts +48 -0
  29. package/dist/http-db.d.ts.map +1 -1
  30. package/dist/http-db.js +77 -1
  31. package/dist/index-backend.d.ts +8 -0
  32. package/dist/index-backend.d.ts.map +1 -1
  33. package/dist/index-backend.js +13 -0
  34. package/dist/index-core.d.ts +78 -0
  35. package/dist/index-core.d.ts.map +1 -0
  36. package/dist/index-core.js +72 -0
  37. package/dist/index-manager.d.ts +11 -0
  38. package/dist/index-manager.d.ts.map +1 -1
  39. package/dist/index-manager.js +30 -13
  40. package/dist/index-types.d.ts +17 -0
  41. package/dist/index-types.d.ts.map +1 -1
  42. package/dist/index.browser.d.ts +14 -0
  43. package/dist/index.browser.d.ts.map +1 -0
  44. package/dist/index.browser.js +13 -0
  45. package/dist/index.d.ts +8 -59
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +11 -54
  48. package/dist/indexeddb-db.d.ts +32 -0
  49. package/dist/indexeddb-db.d.ts.map +1 -1
  50. package/dist/indexeddb-db.js +151 -1
  51. package/dist/memory-db.d.ts +26 -0
  52. package/dist/memory-db.d.ts.map +1 -1
  53. package/dist/memory-db.js +86 -0
  54. package/dist/runtime-registry.d.ts +32 -0
  55. package/dist/runtime-registry.d.ts.map +1 -0
  56. package/dist/runtime-registry.js +38 -0
  57. package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
  58. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  59. package/dist/studio-app/assets/index-D3rT0SJi.js +28 -0
  60. package/dist/studio-app/index.html +1 -1
  61. package/dist/transaction.d.ts +98 -0
  62. package/dist/transaction.d.ts.map +1 -0
  63. package/dist/transaction.js +86 -0
  64. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  65. package/dist/workspace/browser.d.ts +22 -0
  66. package/dist/workspace/browser.d.ts.map +1 -0
  67. package/dist/workspace/browser.js +17 -0
  68. package/dist/workspace/git-identity.d.ts +35 -0
  69. package/dist/workspace/git-identity.d.ts.map +1 -0
  70. package/dist/workspace/git-identity.js +81 -0
  71. package/dist/workspace/index.d.ts +11 -1
  72. package/dist/workspace/index.d.ts.map +1 -1
  73. package/dist/workspace/index.js +6 -0
  74. package/dist/workspace/investigation-analysis.d.ts +47 -0
  75. package/dist/workspace/investigation-analysis.d.ts.map +1 -0
  76. package/dist/workspace/investigation-analysis.js +181 -0
  77. package/dist/workspace/investigation-lifecycle-manager.d.ts +86 -0
  78. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
  79. package/dist/workspace/investigation-lifecycle-manager.js +229 -0
  80. package/dist/workspace/investigation-supervisor.d.ts +100 -0
  81. package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
  82. package/dist/workspace/investigation-supervisor.js +236 -0
  83. package/dist/workspace/runtime-investigation.d.ts +1 -1
  84. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  85. package/dist/workspace/runtime-investigation.js +46 -21
  86. package/dist/workspace/runtime-observation.d.ts +79 -0
  87. package/dist/workspace/runtime-observation.d.ts.map +1 -0
  88. package/dist/workspace/runtime-observation.js +152 -0
  89. package/dist/workspace/runtime-observer.d.ts +51 -0
  90. package/dist/workspace/runtime-observer.d.ts.map +1 -0
  91. package/dist/workspace/runtime-observer.js +268 -0
  92. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  93. package/dist/workspace/workspace-connection.js +14 -5
  94. package/dist/workspace/workspace-types.d.ts +113 -0
  95. package/dist/workspace/workspace-types.d.ts.map +1 -1
  96. package/package.json +12 -4
  97. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
  98. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  99. package/dist/studio-app/assets/index-BF45M757.js +0 -28
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
+ import net from 'net';
2
3
  import path from 'path';
3
- import { spawn } from 'child_process';
4
4
  const FRAMEWORKS = [
5
5
  { name: 'Next.js', packages: ['next'], pattern: /(?:^|\s)next(?:\s+dev)?(?:\s|$)/ },
6
6
  { name: 'Astro', packages: ['astro'], pattern: /(?:^|\s)astro(?:\s+dev)?(?:\s|$)/ },
@@ -21,107 +21,74 @@ function packageManagerAt(root, declared) {
21
21
  return 'bun';
22
22
  return 'npm';
23
23
  }
24
- function configuredPort(script = '') {
24
+ function scriptPort(script = '') {
25
25
  const match = script.match(/(?:^|\s)(?:PORT=|--port(?:=|\s+)|-p\s+)(\d{1,5})(?:\s|$)/i);
26
26
  if (!match)
27
27
  return undefined;
28
28
  const port = Number(match[1]);
29
29
  return port > 0 && port <= 65535 ? port : undefined;
30
30
  }
31
+ function readJson(file, malformedMessage) {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
34
+ }
35
+ catch (error) {
36
+ throw new Error(`${malformedMessage} (${error.message})`);
37
+ }
38
+ }
39
+ /** Read application metadata only. This function never starts an application. */
31
40
  export function detectApplication(root) {
32
41
  const packageFile = path.join(root, 'package.json');
33
- let manifest = {};
34
- if (fs.existsSync(packageFile)) {
35
- try {
36
- manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
37
- }
38
- catch (error) {
39
- throw new Error(`Cannot detect application: malformed package.json (${error.message})`);
40
- }
41
- }
42
- const manager = packageManagerAt(root, typeof manifest.packageManager === 'string' ? manifest.packageManager : undefined);
42
+ const manifest = fs.existsSync(packageFile) ? readJson(packageFile, 'Cannot inspect application: malformed package.json') : {};
43
43
  const script = typeof manifest.scripts?.dev === 'string' ? manifest.scripts.dev.trim() : undefined;
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
- const invokesFeltDb = !!script && /(?:^|\s)feltdb\s+dev(?:\s|$)/.test(script);
47
- if (script && !invokesFeltDb) {
48
- const executable = process.platform === 'win32' ? `${manager}.cmd` : manager;
49
- return {
50
- packageManager: manager,
51
- framework: framework?.name || 'Custom',
52
- devScript: script,
53
- command: executable,
54
- args: manager === 'npm' || manager === 'bun' ? ['run', 'dev'] : ['dev'],
55
- configuredPort: configuredPort(script),
56
- };
57
- }
58
- // Projects generated by FeltDB historically use `feltdb dev` as their script.
59
- // Keep their Vite application working without recursively spawning this CLI.
60
- if (framework?.name === 'Vite' || fs.existsSync(path.join(root, 'index.html'))) {
61
- const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
62
- return { packageManager: manager, framework: 'Vite', devScript: script, command: executable, args: ['exec', '--', 'vite'] };
63
- }
64
- throw new Error('Cannot determine how to start the application. Add a package.json dev script or pass --app-url.');
46
+ const feltConfigFile = path.join(root, 'feltdb.config.json');
47
+ const feltConfig = fs.existsSync(feltConfigFile) ? readJson(feltConfigFile, 'Cannot inspect application: malformed feltdb.config.json') : {};
48
+ const configuredUrl = typeof feltConfig.appUrl === 'string' ? feltConfig.appUrl
49
+ : typeof feltConfig.applicationUrl === 'string' ? feltConfig.applicationUrl : undefined;
50
+ return {
51
+ packageManager: packageManagerAt(root, typeof manifest.packageManager === 'string' ? manifest.packageManager : undefined),
52
+ framework: framework?.name || (script ? 'Custom' : 'Unknown'),
53
+ devScript: script,
54
+ configuredPort: scriptPort(script),
55
+ configuredUrl,
56
+ };
65
57
  }
66
- const URL_PATTERN = /https?:\/\/(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])(?::\d{1,5})?(?:\/[^\s\x1b]*)?/gi;
67
- export function applicationUrls(output) {
68
- const plain = output.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, '');
69
- return [...plain.matchAll(URL_PATTERN)].map(match => match[0].replace(/[),.;]+$/, ''));
58
+ export function applicationUrlCandidate(project, environment = process.env) {
59
+ if (environment.FELTDB_APP_URL)
60
+ return environment.FELTDB_APP_URL;
61
+ if (project.configuredUrl)
62
+ return project.configuredUrl;
63
+ if (project.configuredPort)
64
+ return `http://127.0.0.1:${project.configuredPort}`;
65
+ return undefined;
70
66
  }
71
- export function startApplication(project, root, options = {}) {
72
- const env = { ...process.env };
73
- if (options.appPort)
74
- env.PORT = String(options.appPort);
75
- const args = [...project.args];
76
- // This is only the legacy Vite fallback, never an override of an existing script.
77
- if (project.framework === 'Vite' && !project.devScript?.replace(/\s+/g, ' ').match(/^(?:npm run )?vite/)) {
78
- args.push('--host', '127.0.0.1');
79
- if (options.appPort)
80
- args.push('--port', String(options.appPort));
67
+ export function applicationIsRunning(url, timeoutMs = 750) {
68
+ let parsed;
69
+ try {
70
+ parsed = new URL(url);
81
71
  }
82
- if (options.appPort && project.devScript) {
83
- const flag = project.framework === 'Next.js' ? '-p' : '--port';
84
- if (['Vite', 'Next.js', 'Astro', 'Nuxt', 'SvelteKit', 'Remix'].includes(project.framework)) {
85
- if (project.packageManager === 'npm')
86
- args.push('--');
87
- args.push(flag, String(options.appPort));
88
- }
72
+ catch {
73
+ return Promise.resolve(false);
89
74
  }
90
- const child = spawn(project.command, args, { cwd: root, env, stdio: ['inherit', 'pipe', 'pipe'] });
91
- const url = new Promise((resolve, reject) => {
92
- let settled = false;
93
- const finish = (value) => { if (!settled) {
94
- settled = true;
95
- clearTimeout(timer);
96
- resolve(value);
97
- } };
98
- const inspect = (chunk) => {
99
- const text = chunk.toString();
100
- process.stdout.write(text);
101
- const found = applicationUrls(text);
102
- if (found.length)
103
- finish(found[0]);
104
- };
105
- child.stdout?.on('data', inspect);
106
- child.stderr?.on('data', inspect);
107
- child.once('error', error => { if (!settled) {
108
- settled = true;
109
- clearTimeout(timer);
110
- reject(error);
111
- } });
112
- child.once('exit', code => { if (!settled) {
113
- settled = true;
114
- clearTimeout(timer);
115
- reject(new Error(`Application exited before reporting its URL (status ${code ?? 'unknown'})`));
116
- } });
117
- const timer = setTimeout(() => {
118
- if (!settled) {
119
- settled = true;
120
- reject(new Error('Unable to determine application URL from dev-server output. Pass --app-url or --app-port.'));
121
- }
122
- }, options.startupTimeoutMs ?? 30000);
123
- if (options.appPort)
124
- finish(`http://127.0.0.1:${options.appPort}`);
75
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
76
+ return Promise.resolve(false);
77
+ const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
78
+ return new Promise(resolve => {
79
+ const socket = net.createConnection({ host: parsed.hostname, port });
80
+ const done = (running) => { socket.destroy(); resolve(running); };
81
+ socket.setTimeout(timeoutMs);
82
+ socket.once('connect', () => done(true));
83
+ socket.once('timeout', () => done(false));
84
+ socket.once('error', () => done(false));
125
85
  });
126
- return { child, url };
86
+ }
87
+ /** Discover only a deterministically configured, already-running application. */
88
+ export async function discoverApplicationUrl(root) {
89
+ const project = detectApplication(root);
90
+ const candidate = applicationUrlCandidate(project);
91
+ if (!candidate)
92
+ return { project };
93
+ return { project, url: await applicationIsRunning(candidate) ? candidate : undefined };
127
94
  }
@@ -7,9 +7,9 @@ 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 { createFeltDB, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
11
11
  import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
12
- import { detectApplication, startApplication } from './application.js';
12
+ import { detectApplication, discoverApplicationUrl } from './application.js';
13
13
  function loadProjectEnvironment(file = path.resolve('.env.local')) {
14
14
  if (!fs.existsSync(file))
15
15
  return;
@@ -55,6 +55,8 @@ export async function handleCommand(command, args) {
55
55
  return handleWorkspace(args);
56
56
  case 'dev':
57
57
  return handleDev(args);
58
+ case 'investigations':
59
+ return handleInvestigations(args);
58
60
  case 'build':
59
61
  return handleBuild(args);
60
62
  case 'check':
@@ -76,6 +78,92 @@ export async function handleCommand(command, args) {
76
78
  return handleHelp();
77
79
  }
78
80
  }
81
+ /**
82
+ * Inspect the durable investigation store.
83
+ *
84
+ * The evidence chain is only useful if a developer can get back to it after
85
+ * the browser is closed, so it is queryable from the command line.
86
+ */
87
+ function handleInvestigations(args) {
88
+ if (args.includes('--help')) {
89
+ console.log('Usage: feltdb investigations [<investigation-id>] [--json]\n');
90
+ console.log('List durable runtime investigations, or show one in full.\n');
91
+ console.log('Options:');
92
+ console.log(' --json Emit machine-readable JSON');
93
+ return;
94
+ }
95
+ const store = new InvestigationLifecycleManager(process.cwd());
96
+ const json = args.includes('--json');
97
+ const id = args.find(value => !value.startsWith('--'));
98
+ if (id) {
99
+ const investigation = store.loadInvestigation(id);
100
+ if (!investigation)
101
+ throw new Error(`Investigation not found: ${id}`);
102
+ if (json) {
103
+ console.log(JSON.stringify(investigation, null, 2));
104
+ return;
105
+ }
106
+ const observation = investigation.originalObservation;
107
+ console.log(`\nInvestigation ${investigation.id}`);
108
+ console.log(` Workspace: ${investigation.workspaceId}`);
109
+ console.log(` Investigation ${investigation.investigationState}`);
110
+ console.log(` Remediation: ${investigation.remediationState}`);
111
+ console.log(` Verification: ${investigation.verificationState}\n`);
112
+ if (observation) {
113
+ console.log('Observation');
114
+ console.log(` ${observation.method} ${observation.url} -> HTTP ${observation.status}`);
115
+ console.log(` ${observation.browser || 'unknown'} / ${observation.runtime || 'unknown'}`
116
+ + `${observation.durationMs !== undefined ? ` / ${observation.durationMs}ms` : ''}\n`);
117
+ }
118
+ if (investigation.evidence?.length) {
119
+ console.log('Evidence (observed by the runtime)');
120
+ for (const item of investigation.evidence)
121
+ console.log(` [${item.kind}] ${item.statement}`);
122
+ console.log();
123
+ }
124
+ if (investigation.finding) {
125
+ console.log('Finding (FeltDB analysis, not an observation)');
126
+ console.log(` ${investigation.finding.statement}`);
127
+ console.log(` confidence: ${investigation.finding.confidence}\n`);
128
+ }
129
+ if (investigation.remediation?.changeIdentity) {
130
+ console.log('Remediation');
131
+ console.log(` ${investigation.remediation.changeKind}: ${investigation.remediation.changeIdentity}`);
132
+ if (investigation.remediation.changedPaths.length) {
133
+ console.log(` changed: ${investigation.remediation.changedPaths.join(', ')}`);
134
+ }
135
+ console.log();
136
+ }
137
+ if (investigation.verificationAttempt) {
138
+ const attempt = investigation.verificationAttempt;
139
+ console.log('Verification');
140
+ console.log(` ${attempt.result}: ${attempt.summary}`);
141
+ console.log(` ${attempt.originalStatus} -> ${attempt.observedStatus ?? 'no matching observation'}\n`);
142
+ }
143
+ return;
144
+ }
145
+ const investigations = store.listInvestigations();
146
+ if (json) {
147
+ console.log(JSON.stringify(investigations, null, 2));
148
+ return;
149
+ }
150
+ if (investigations.length === 0) {
151
+ console.log('\nNo investigations recorded.');
152
+ console.log('Run "feltdb dev", exercise the application, and a runtime defect will open one.\n');
153
+ return;
154
+ }
155
+ console.log(`\n${investigations.length} investigation${investigations.length === 1 ? '' : 's'}\n`);
156
+ for (const investigation of investigations) {
157
+ const observation = investigation.originalObservation;
158
+ const summary = observation
159
+ ? `${observation.method} ${observation.url} -> ${observation.status}`
160
+ : investigation.observationId;
161
+ console.log(` ${investigation.id} ${investigation.verificationState.padEnd(12)} ${summary}`);
162
+ }
163
+ const metrics = store.metrics();
164
+ console.log(`\n ${metrics.observations} observations, ${metrics.evidenceRecords} evidence records, `
165
+ + `${metrics.serializedBytes} bytes, loaded in ${metrics.loadMilliseconds}ms\n`);
166
+ }
79
167
  function flowFile(args) {
80
168
  const candidate = args.find(value => !value.startsWith('--')) || 'feltdb.flow';
81
169
  return path.resolve(candidate);
@@ -414,37 +502,29 @@ async function handleWorkspaceConnect(args) {
414
502
  console.log(' This feature will be implemented in the next phase');
415
503
  console.log(' (when @feltdb/client workspace APIs are available)\n');
416
504
  }
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
505
  async function handleDev(args) {
432
506
  var _a, _b, _c;
507
+ const projectDir = process.cwd();
433
508
  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]');
509
+ console.log('Usage: feltdb dev [--app-url URL] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
510
+ console.log('\nOptions:');
511
+ console.log(' --app-url <url> Application URL (e.g., http://127.0.0.1:3000)');
512
+ console.log(' --studio-port <port> FeltDB Studio port (default: 7701)');
513
+ console.log(' --authority-port <port> Authority port (default: 7700)');
514
+ console.log(' --discovery-port <port> Pairing discovery port (default: 7799)');
515
+ console.log(' --no-open Do not open Studio in browser');
435
516
  return;
436
517
  }
437
518
  loadProjectEnvironment();
438
519
  console.log('🚀 Starting FeltDB development server...\n');
439
- const projectDir = process.cwd();
440
- const configPath = path.join(projectDir, 'feltdb.config.json');
441
520
  // .feltdb/ is reserved local runtime state. Protect it before discovery,
442
521
  // initialization, the authority, or pairing can persist anything there.
522
+ // This runs after `--help` returns, so asking for help never writes anything.
443
523
  ensureWorkspaceGitIgnored(projectDir);
524
+ const configPath = path.join(projectDir, 'feltdb.config.json');
444
525
  // Initialize workspace if needed
445
526
  let workspace = discoverWorkspace(projectDir);
446
527
  if (!workspace) {
447
- // Get project name from package.json
448
528
  const packageJsonPath = path.join(projectDir, 'package.json');
449
529
  let projectId = path.basename(projectDir);
450
530
  if (fs.existsSync(packageJsonPath)) {
@@ -460,6 +540,8 @@ async function handleDev(args) {
460
540
  console.log(`✨ Initialized Development Workspace`);
461
541
  console.log(` Workspace ID: ${workspace.workspaceId}\n`);
462
542
  }
543
+ // Investigation lifecycle manager will be initialized when needed
544
+ // const investigationManager = new InvestigationLifecycleManager(projectDir);
463
545
  // Create feltdb.config.json if it doesn't exist
464
546
  if (!fs.existsSync(configPath)) {
465
547
  const packageJsonPath = path.join(projectDir, 'package.json');
@@ -478,11 +560,10 @@ async function handleDev(args) {
478
560
  runtime: 'browser',
479
561
  storage: 'indexeddb',
480
562
  distributed: true,
481
- agents: { enabled: false },
482
- capabilities: { search: true },
563
+ applicationUrl: null,
483
564
  };
484
565
  fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
485
- console.log(`✨ Created feltdb.config.json with default configuration\n`);
566
+ console.log(`✨ Created feltdb.config.json\n`);
486
567
  }
487
568
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
488
569
  if (config.runtime === 'managed') {
@@ -495,10 +576,59 @@ async function handleDev(args) {
495
576
  const runtimeNamespace = config.runtime === 'managed'
496
577
  ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
497
578
  : config.namespace;
579
+ // Application URL precedence:
580
+ // 1. --app-url (highest priority)
581
+ // 2. feltdb.config.json applicationUrl
582
+ // 3. Deterministically discovered running application
583
+ // 4. No application (lowest priority - allowed)
584
+ const hasAppUrlArg = args.includes('--app-url');
585
+ const appUrlArgument = hasAppUrlArg ? args[args.indexOf('--app-url') + 1] : undefined;
586
+ if (hasAppUrlArg && !appUrlArgument)
587
+ throw new Error('--app-url requires a URL');
588
+ if (appUrlArgument) {
589
+ try {
590
+ const parsed = new URL(appUrlArgument);
591
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
592
+ throw new Error('unsupported protocol');
593
+ }
594
+ catch {
595
+ throw new Error(`Invalid --app-url: ${appUrlArgument}`);
596
+ }
597
+ }
598
+ let appUrl;
599
+ let detectedApplication;
600
+ // Priority 1: --app-url takes highest priority
601
+ if (appUrlArgument) {
602
+ appUrl = appUrlArgument;
603
+ try {
604
+ detectedApplication = detectApplication(projectDir);
605
+ }
606
+ catch {
607
+ detectedApplication = { packageManager: 'npm', framework: 'External' };
608
+ }
609
+ }
610
+ else {
611
+ // Priority 2: Check feltdb.config.json applicationUrl
612
+ if (config.applicationUrl) {
613
+ appUrl = config.applicationUrl;
614
+ try {
615
+ detectedApplication = detectApplication(projectDir);
616
+ }
617
+ catch {
618
+ detectedApplication = { packageManager: 'npm', framework: 'External' };
619
+ }
620
+ }
621
+ else {
622
+ // Priority 3: Try to discover an existing running application
623
+ const discovered = await discoverApplicationUrl(projectDir);
624
+ detectedApplication = discovered.project;
625
+ appUrl = discovered.url;
626
+ }
627
+ }
498
628
  const requestedStudioPort = Number(args.includes('--studio-port') ? args[args.indexOf('--studio-port') + 1] || '7701' : '7701');
499
629
  const studioPort = String(await availablePort(requestedStudioPort));
500
630
  if (studioPort !== String(requestedStudioPort))
501
- console.log(`Port ${requestedStudioPort} is busy; using ${studioPort} for Studio.`);
631
+ console.log(`⚠ Port ${requestedStudioPort} is busy; using ${studioPort} for Studio.\n`);
502
632
  process.env.STUDIO_PORT = studioPort;
503
633
  let selfHostedStarted = false;
504
634
  let localAuthority = null;
@@ -549,91 +679,69 @@ async function handleDev(args) {
549
679
  pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort);
550
680
  pairingToken = token.token;
551
681
  }
552
- const hasAppUrl = args.includes('--app-url');
553
- const appUrlArgument = hasAppUrl ? args[args.indexOf('--app-url') + 1] : undefined;
554
- if (hasAppUrl && !appUrlArgument)
555
- throw new Error('--app-url requires a URL');
556
- const hasAppPort = args.includes('--app-port') || args.includes('--port');
557
- const appPortValue = args.includes('--app-port')
558
- ? args[args.indexOf('--app-port') + 1]
559
- : args.includes('--port') ? args[args.indexOf('--port') + 1] : undefined;
560
- if (!hasAppUrl && hasAppPort && !appPortValue)
561
- throw new Error('--app-port requires a port');
562
- const appPort = appPortValue ? Number(appPortValue) : undefined;
563
- if (appUrlArgument) {
564
- try {
565
- const parsed = new URL(appUrlArgument);
566
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
567
- throw new Error('unsupported protocol');
568
- }
569
- catch {
570
- throw new Error(`Invalid --app-url: ${appUrlArgument}`);
571
- }
572
- }
573
- if (!hasAppUrl && appPortValue && (!Number.isInteger(appPort) || appPort < 1 || appPort > 65535))
574
- throw new Error(`Invalid --app-port: ${appPortValue}`);
575
- const application = appUrlArgument ? undefined : detectApplication(projectDir);
576
- const runningApplication = application ? startApplication(application, projectDir, { appPort }) : undefined;
577
- let appUrl;
578
- try {
579
- appUrl = appUrlArgument || await runningApplication.url;
682
+ // Output startup information
683
+ console.log('✨ Development Workspace');
684
+ if (workspace) {
685
+ console.log(` Workspace ID: ${workspace.workspaceId}`);
580
686
  }
581
- catch (error) {
582
- runningApplication?.child.kill('SIGTERM');
583
- pairingDiscoveryServer?.close();
584
- await localAuthority?.close();
585
- stopSelfHosted();
586
- throw error;
687
+ console.log();
688
+ console.log('🔗 Development Workspace');
689
+ if (pairingToken) {
690
+ console.log(` Pairing Code: ${pairingToken}`);
691
+ console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
587
692
  }
693
+ console.log();
588
694
  console.log('FeltDB Dev Server');
589
- console.log(` Authority: ${process.env.VITE_FELTDB_URL}`);
590
- console.log(` Studio: http://127.0.0.1:${studioPort}`);
591
- console.log(` Pairing: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
592
- console.log('Application');
593
- console.log(` Framework: ${application?.framework || 'External'}`);
594
- console.log(` Dev command: ${application ? [application.command.replace(/\.cmd$/, ''), ...application.args].join(' ') : '(already running)'}`);
595
- console.log(` URL: ${appUrl}`);
596
- console.log(` Namespace: ${config.namespace}`);
597
- console.log(` Runtime: ${config.runtime}`);
598
- console.log(` Storage: ${config.storage}`);
695
+ console.log(` Namespace: ${config.namespace}`);
696
+ console.log(` Runtime: ${config.runtime}`);
697
+ console.log(` Storage: ${config.storage}`);
599
698
  console.log(` Distributed: ${config.distributed}`);
600
- if (workspace) {
601
- console.log(` Workspace: ${workspace.workspaceId}\n`);
602
- console.log(`🔗 Development Workspace`);
603
- if (pairingToken) {
604
- console.log(` Pairing Code: ${pairingToken}`);
605
- console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
606
- console.log(` Clients can use this code to auto-discover the workspace`);
607
- }
608
- }
609
699
  console.log();
610
- const open = !args.includes('--no-open');
611
- console.log(`Application: ${appUrl}`);
612
- console.log(`Studio: http://127.0.0.1:${studioPort}\n`);
613
- let shuttingDown = false;
614
- const stopApplication = () => { if (runningApplication && !runningApplication.child.killed)
615
- runningApplication.child.kill('SIGTERM'); };
616
- const stopAll = () => { shuttingDown = true; stopApplication(); pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
617
- // In an inherited terminal Ctrl-C can reach Vite before this parent process.
618
- // Never leave Studio (or its port) running after the application exits.
619
- runningApplication?.child.once('exit', code => {
620
- if (shuttingDown)
621
- return;
622
- stopSelfHosted();
623
- process.exit(code ?? 0);
624
- });
700
+ if (appUrl) {
701
+ console.log(`Application: ${appUrl}`);
702
+ console.log(`Authority: ${process.env.VITE_FELTDB_URL}`);
703
+ console.log();
704
+ console.log(`Using existing application at ${appUrl}`);
705
+ console.log(`app=${encodeURIComponent(appUrl)}\n`);
706
+ }
707
+ else {
708
+ console.log('❌ No application URL is configured.\n');
709
+ console.log('Start your application, then run:\n');
710
+ console.log(' feltdb dev --app-url http://127.0.0.1:<port>\n');
711
+ console.log('Or set applicationUrl in feltdb.config.json\n');
712
+ 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);
715
+ console.log();
716
+ }
717
+ const stopAll = () => { pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
625
718
  process.once('exit', stopAll);
626
719
  process.once('SIGINT', () => { stopAll(); process.exit(130); });
627
720
  process.once('SIGTERM', () => { stopAll(); process.exit(143); });
721
+ const open = !args.includes('--no-open');
628
722
  await handleStudio([
629
723
  '--port', studioPort,
630
724
  '--namespace', runtimeNamespace || 'default',
631
725
  '--runtime', config.runtime || 'browser',
632
- '--app-url', appUrl,
726
+ ...(appUrl ? ['--app-url', appUrl] : []),
633
727
  ...((config.runtime === 'self-hosted' || config.runtime === 'managed') && process.env.VITE_FELTDB_URL ? ['--connect', process.env.VITE_FELTDB_URL] : []),
634
728
  ...(open ? [] : ['--no-open']),
635
729
  ]);
636
730
  }
731
+ function runLocalVite(args, waitForExit) {
732
+ const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
733
+ const child = spawn(executable, ['exec', '--', 'vite', ...args], {
734
+ cwd: process.cwd(),
735
+ stdio: 'inherit',
736
+ env: process.env,
737
+ });
738
+ if (!waitForExit)
739
+ return child;
740
+ return new Promise((resolve, reject) => {
741
+ child.once('error', reject);
742
+ child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Vite exited with status ${code ?? 'unknown'}`)));
743
+ });
744
+ }
637
745
  async function handleBuild(args) {
638
746
  console.log('🔨 Building FeltDB application...\n');
639
747
  const configPath = path.join(process.cwd(), 'feltdb.config.json');
@@ -904,11 +1012,13 @@ async function handleStudio(args) {
904
1012
  const require = createRequire(import.meta.url);
905
1013
  const packageFile = require.resolve('@feltdb/core/package.json');
906
1014
  const root = path.join(path.dirname(packageFile), 'dist', 'studio-app');
1015
+ if (!fs.existsSync(path.join(root, 'index.html'))) {
1016
+ console.log('⚠️ Studio application artifacts not found.');
1017
+ return;
1018
+ }
907
1019
  const projectFlow = ['feltdb.flow', ...fs.readdirSync(process.cwd()).filter(value => value.endsWith('.flow'))]
908
1020
  .map(value => path.resolve(process.cwd(), value))
909
1021
  .find((value, index, values) => values.indexOf(value) === index && fs.existsSync(value));
910
- if (!fs.existsSync(path.join(root, 'index.html')))
911
- throw new Error('Studio application artifact is missing; reinstall @feltdb/core');
912
1022
  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' };
913
1023
  const server = http.createServer((request, response) => {
914
1024
  const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
@@ -971,6 +1081,7 @@ Commands:
971
1081
  connect Connect to remote FeltDB
972
1082
  status Show connection status
973
1083
  dev Start development server
1084
+ investigations List or show durable runtime investigations
974
1085
  build Build FeltDB application
975
1086
  check Validate configuration
976
1087
  inspect Inspect runtime state
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.8';
26
+ const VERSION = '0.6.10';
27
27
  function prompt(question) {
28
28
  const rl = readline.createInterface({
29
29
  input: process.stdin,