@feltdb/core 0.6.5 → 0.6.7
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.js +127 -0
- package/dist/cli/commands.js +57 -18
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +34 -1
- package/dist/create/create.js +1 -2
- package/dist/create/docker-compose-generator.js +1 -2
- package/dist/create/package-versions.js +1 -1
- package/dist/create/workspace-initialization.js +10 -16
- package/dist/workspace/workspace-connection.js +27 -2
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
const FRAMEWORKS = [
|
|
5
|
+
{ name: 'Next.js', packages: ['next'], pattern: /(?:^|\s)next(?:\s+dev)?(?:\s|$)/ },
|
|
6
|
+
{ name: 'Astro', packages: ['astro'], pattern: /(?:^|\s)astro(?:\s+dev)?(?:\s|$)/ },
|
|
7
|
+
{ name: 'Nuxt', packages: ['nuxt'], pattern: /(?:^|\s)(?:nuxi|nuxt)(?:\s+dev)?(?:\s|$)/ },
|
|
8
|
+
{ name: 'SvelteKit', packages: ['@sveltejs/kit'], pattern: /(?:^|\s)svelte-kit(?:\s+dev)?(?:\s|$)/ },
|
|
9
|
+
{ name: 'Remix', packages: ['@remix-run/dev'], pattern: /(?:^|\s)remix(?:\s+vite:dev)?(?:\s|$)/ },
|
|
10
|
+
{ name: 'Vite', packages: ['vite'], pattern: /(?:^|\s)vite(?:\s|$)/ },
|
|
11
|
+
];
|
|
12
|
+
function packageManagerAt(root, declared) {
|
|
13
|
+
const declaredName = declared?.split('@')[0];
|
|
14
|
+
if (declaredName === 'npm' || declaredName === 'pnpm' || declaredName === 'yarn' || declaredName === 'bun')
|
|
15
|
+
return declaredName;
|
|
16
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml')))
|
|
17
|
+
return 'pnpm';
|
|
18
|
+
if (fs.existsSync(path.join(root, 'yarn.lock')))
|
|
19
|
+
return 'yarn';
|
|
20
|
+
if (fs.existsSync(path.join(root, 'bun.lock')) || fs.existsSync(path.join(root, 'bun.lockb')))
|
|
21
|
+
return 'bun';
|
|
22
|
+
return 'npm';
|
|
23
|
+
}
|
|
24
|
+
function configuredPort(script = '') {
|
|
25
|
+
const match = script.match(/(?:^|\s)(?:PORT=|--port(?:=|\s+)|-p\s+)(\d{1,5})(?:\s|$)/i);
|
|
26
|
+
if (!match)
|
|
27
|
+
return undefined;
|
|
28
|
+
const port = Number(match[1]);
|
|
29
|
+
return port > 0 && port <= 65535 ? port : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function detectApplication(root) {
|
|
32
|
+
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);
|
|
43
|
+
const script = typeof manifest.scripts?.dev === 'string' ? manifest.scripts.dev.trim() : undefined;
|
|
44
|
+
const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
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.');
|
|
65
|
+
}
|
|
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(/[),.;]+$/, ''));
|
|
70
|
+
}
|
|
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));
|
|
81
|
+
}
|
|
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
|
+
}
|
|
89
|
+
}
|
|
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}`);
|
|
125
|
+
});
|
|
126
|
+
return { child, url };
|
|
127
|
+
}
|
package/dist/cli/commands.js
CHANGED
|
@@ -8,7 +8,8 @@ import net from 'net';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import { spawn, spawnSync } from 'child_process';
|
|
10
10
|
import { createFeltDB, diffFlowSpec, formatFlowSpec, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
|
|
11
|
-
import { discoverWorkspace, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
|
|
11
|
+
import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
|
|
12
|
+
import { detectApplication, startApplication } from './application.js';
|
|
12
13
|
function loadProjectEnvironment(file = path.resolve('.env.local')) {
|
|
13
14
|
if (!fs.existsSync(file))
|
|
14
15
|
return;
|
|
@@ -430,13 +431,16 @@ function runLocalVite(args, waitForExit) {
|
|
|
430
431
|
async function handleDev(args) {
|
|
431
432
|
var _a, _b, _c;
|
|
432
433
|
if (args.includes('--help')) {
|
|
433
|
-
console.log('Usage: feltdb dev [--port
|
|
434
|
+
console.log('Usage: feltdb dev [--app-url URL | --app-port PORT] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
|
|
434
435
|
return;
|
|
435
436
|
}
|
|
436
437
|
loadProjectEnvironment();
|
|
437
438
|
console.log('🚀 Starting FeltDB development server...\n');
|
|
438
439
|
const projectDir = process.cwd();
|
|
439
440
|
const configPath = path.join(projectDir, 'feltdb.config.json');
|
|
441
|
+
// .feltdb/ is reserved local runtime state. Protect it before discovery,
|
|
442
|
+
// initialization, the authority, or pairing can persist anything there.
|
|
443
|
+
ensureWorkspaceGitIgnored(projectDir);
|
|
440
444
|
// Initialize workspace if needed
|
|
441
445
|
let workspace = discoverWorkspace(projectDir);
|
|
442
446
|
if (!workspace) {
|
|
@@ -452,7 +456,7 @@ async function handleDev(args) {
|
|
|
452
456
|
// Use default projectId if package.json parsing fails
|
|
453
457
|
}
|
|
454
458
|
}
|
|
455
|
-
workspace = initializeWorkspace(projectDir, projectId);
|
|
459
|
+
workspace = initializeWorkspace(projectDir, projectId, { gitProtection: false });
|
|
456
460
|
console.log(`✨ Initialized Development Workspace`);
|
|
457
461
|
console.log(` Workspace ID: ${workspace.workspaceId}\n`);
|
|
458
462
|
}
|
|
@@ -491,15 +495,10 @@ async function handleDev(args) {
|
|
|
491
495
|
const runtimeNamespace = config.runtime === 'managed'
|
|
492
496
|
? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
|
|
493
497
|
: config.namespace;
|
|
494
|
-
const requestedAppPort = Number(args.includes('--port') ? args[args.indexOf('--port') + 1] || '5173' : '5173');
|
|
495
498
|
const requestedStudioPort = Number(args.includes('--studio-port') ? args[args.indexOf('--studio-port') + 1] || '7701' : '7701');
|
|
496
|
-
const appPort = String(await availablePort(requestedAppPort));
|
|
497
499
|
const studioPort = String(await availablePort(requestedStudioPort));
|
|
498
|
-
if (appPort !== String(requestedAppPort))
|
|
499
|
-
console.log(`Port ${requestedAppPort} is busy; using ${appPort} for the application.`);
|
|
500
500
|
if (studioPort !== String(requestedStudioPort))
|
|
501
501
|
console.log(`Port ${requestedStudioPort} is busy; using ${studioPort} for Studio.`);
|
|
502
|
-
process.env.APP_PORT = appPort;
|
|
503
502
|
process.env.STUDIO_PORT = studioPort;
|
|
504
503
|
let selfHostedStarted = false;
|
|
505
504
|
let localAuthority = null;
|
|
@@ -550,11 +549,53 @@ async function handleDev(args) {
|
|
|
550
549
|
pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort);
|
|
551
550
|
pairingToken = token.token;
|
|
552
551
|
}
|
|
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;
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
runningApplication?.child.kill('SIGTERM');
|
|
583
|
+
pairingDiscoveryServer?.close();
|
|
584
|
+
await localAuthority?.close();
|
|
585
|
+
stopSelfHosted();
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
553
588
|
console.log('FeltDB Dev Server');
|
|
554
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}`);
|
|
555
596
|
console.log(` Namespace: ${config.namespace}`);
|
|
556
|
-
console.log(` Runtime:
|
|
557
|
-
console.log(` Storage:
|
|
597
|
+
console.log(` Runtime: ${config.runtime}`);
|
|
598
|
+
console.log(` Storage: ${config.storage}`);
|
|
558
599
|
console.log(` Distributed: ${config.distributed}`);
|
|
559
600
|
if (workspace) {
|
|
560
601
|
console.log(` Workspace: ${workspace.workspaceId}\n`);
|
|
@@ -567,17 +608,15 @@ async function handleDev(args) {
|
|
|
567
608
|
}
|
|
568
609
|
console.log();
|
|
569
610
|
const open = !args.includes('--no-open');
|
|
570
|
-
console.log(`Application:
|
|
611
|
+
console.log(`Application: ${appUrl}`);
|
|
571
612
|
console.log(`Studio: http://127.0.0.1:${studioPort}\n`);
|
|
572
|
-
const viteArgs = ['--host', '127.0.0.1', '--port', appPort, '--strictPort', ...(open ? ['--open'] : [])];
|
|
573
|
-
const vite = runLocalVite(viteArgs, false);
|
|
574
613
|
let shuttingDown = false;
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
const stopAll = () => { shuttingDown = true;
|
|
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(); };
|
|
578
617
|
// In an inherited terminal Ctrl-C can reach Vite before this parent process.
|
|
579
618
|
// Never leave Studio (or its port) running after the application exits.
|
|
580
|
-
|
|
619
|
+
runningApplication?.child.once('exit', code => {
|
|
581
620
|
if (shuttingDown)
|
|
582
621
|
return;
|
|
583
622
|
stopSelfHosted();
|
|
@@ -590,7 +629,7 @@ async function handleDev(args) {
|
|
|
590
629
|
'--port', studioPort,
|
|
591
630
|
'--namespace', runtimeNamespace || 'default',
|
|
592
631
|
'--runtime', config.runtime || 'browser',
|
|
593
|
-
'--app-url',
|
|
632
|
+
'--app-url', appUrl,
|
|
594
633
|
...((config.runtime === 'self-hosted' || config.runtime === 'managed') && process.env.VITE_FELTDB_URL ? ['--connect', process.env.VITE_FELTDB_URL] : []),
|
|
595
634
|
...(open ? [] : ['--no-open']),
|
|
596
635
|
]);
|
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.7';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|
|
@@ -11,6 +11,37 @@ import fs from 'fs';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { randomBytes } from 'crypto';
|
|
13
13
|
import http from 'http';
|
|
14
|
+
import { spawnSync } from 'child_process';
|
|
15
|
+
const FELTDB_IGNORE_PATTERN = /^\/?\.feltdb\/?$/;
|
|
16
|
+
/**
|
|
17
|
+
* Reserve .feltdb/ for local FeltDB runtime state before any state is written.
|
|
18
|
+
* This only updates ignore configuration; it never changes Git's index/history.
|
|
19
|
+
*/
|
|
20
|
+
export function ensureWorkspaceGitIgnored(projectDir) {
|
|
21
|
+
const ignorePath = path.join(projectDir, '.gitignore');
|
|
22
|
+
const existing = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
|
|
23
|
+
const ignored = existing.split(/\r?\n/).some(line => FELTDB_IGNORE_PATTERN.test(line.trim()));
|
|
24
|
+
let added = false;
|
|
25
|
+
if (!ignored) {
|
|
26
|
+
const newline = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
27
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') && !existing.endsWith('\r') ? newline : '';
|
|
28
|
+
fs.writeFileSync(ignorePath, `${existing}${separator}.feltdb/${newline}`);
|
|
29
|
+
added = true;
|
|
30
|
+
}
|
|
31
|
+
const trackedCheck = spawnSync('git', ['-C', projectDir, 'ls-files', '--', '.feltdb'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
+
});
|
|
35
|
+
const tracked = trackedCheck.status === 0 && trackedCheck.stdout.trim().length > 0;
|
|
36
|
+
if (tracked) {
|
|
37
|
+
console.warn('⚠ FeltDB runtime state is already tracked by Git.');
|
|
38
|
+
console.warn(' .feltdb/ contains local runtime state and should not be committed.');
|
|
39
|
+
console.warn(' Add .feltdb/ to .gitignore and remove the existing files from Git');
|
|
40
|
+
console.warn(' tracking before pushing.');
|
|
41
|
+
console.warn(' FeltDB will not modify Git history automatically.');
|
|
42
|
+
}
|
|
43
|
+
return { added, tracked };
|
|
44
|
+
}
|
|
14
45
|
export function startPairingDiscoveryServer(token, port = 7799, host = '127.0.0.1') {
|
|
15
46
|
const authorityEndpoint = token.authorityEndpoint || token.endpoint;
|
|
16
47
|
if (!token.workspaceId || !authorityEndpoint || !token.namespace) {
|
|
@@ -123,7 +154,9 @@ export function generateWorkspaceId(projectId) {
|
|
|
123
154
|
* Initialize development workspace for a project
|
|
124
155
|
* Creates .feltdb/workspace.json with workspace discovery information.
|
|
125
156
|
*/
|
|
126
|
-
export function initializeWorkspace(projectDir, projectId) {
|
|
157
|
+
export function initializeWorkspace(projectDir, projectId, options = {}) {
|
|
158
|
+
if (options.gitProtection !== false)
|
|
159
|
+
ensureWorkspaceGitIgnored(projectDir);
|
|
127
160
|
const feltdbDir = path.join(projectDir, '.feltdb');
|
|
128
161
|
// Ensure .feltdb directory exists
|
|
129
162
|
if (!fs.existsSync(feltdbDir)) {
|
package/dist/create/create.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.6.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.6.7';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -20,7 +20,7 @@ export function generateWorkspaceId(projectId) {
|
|
|
20
20
|
* Initialize development workspace for a new project
|
|
21
21
|
*
|
|
22
22
|
* Creates .feltdb/workspace.json with the workspace discovery information.
|
|
23
|
-
* This file
|
|
23
|
+
* This file remains local and is used by all development tools
|
|
24
24
|
* (CLI, IDE, agents, browser extensions) to discover and connect to the
|
|
25
25
|
* same workspace.
|
|
26
26
|
*/
|
|
@@ -43,25 +43,18 @@ export function initializeWorkspace(projectDir, projectId) {
|
|
|
43
43
|
return discovery;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* The .feltdb/workspace.json is committed (pairing identity).
|
|
49
|
-
* But runtime files like pairing tokens should not be committed.
|
|
46
|
+
* Reserve the entire FeltDB runtime directory in .gitignore.
|
|
50
47
|
*/
|
|
51
48
|
export function appendWorkspaceGitignore(projectDir) {
|
|
52
49
|
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
'# FeltDB Development Workspace',
|
|
56
|
-
'.feltdb/pairing.json',
|
|
57
|
-
'.feltdb/state.json',
|
|
58
|
-
'.feltdb/*.log',
|
|
59
|
-
'',
|
|
60
|
-
].join('\n');
|
|
50
|
+
const entryExists = (contents) => contents.split(/\r?\n/)
|
|
51
|
+
.some(line => /^\/?\.feltdb\/?$/.test(line.trim()));
|
|
61
52
|
if (fs.existsSync(gitignorePath)) {
|
|
62
53
|
const existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
63
|
-
if (!existing
|
|
64
|
-
|
|
54
|
+
if (!entryExists(existing)) {
|
|
55
|
+
const newline = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
56
|
+
const separator = existing.length && !existing.endsWith('\n') && !existing.endsWith('\r') ? newline : '';
|
|
57
|
+
fs.appendFileSync(gitignorePath, `${separator}.feltdb/${newline}`);
|
|
65
58
|
}
|
|
66
59
|
}
|
|
67
60
|
else {
|
|
@@ -70,7 +63,8 @@ export function appendWorkspaceGitignore(projectDir) {
|
|
|
70
63
|
'dist/',
|
|
71
64
|
'build/',
|
|
72
65
|
'.env.local',
|
|
73
|
-
|
|
66
|
+
'.feltdb/',
|
|
67
|
+
'',
|
|
74
68
|
].join('\n');
|
|
75
69
|
fs.writeFileSync(gitignorePath, content);
|
|
76
70
|
}
|
|
@@ -407,13 +407,38 @@ class HttpWorkspaceTransport {
|
|
|
407
407
|
}
|
|
408
408
|
subscribe(workspaceId, handler) {
|
|
409
409
|
let stopped = false;
|
|
410
|
+
let delivering = false;
|
|
411
|
+
let deliveryPending = false;
|
|
410
412
|
const subscribedAt = Date.now();
|
|
413
|
+
const deliver = async () => {
|
|
414
|
+
if (stopped)
|
|
415
|
+
return;
|
|
416
|
+
if (delivering) {
|
|
417
|
+
deliveryPending = true;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
delivering = true;
|
|
421
|
+
try {
|
|
422
|
+
await this.deliverEvents(workspaceId, subscribedAt, handler);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
delivering = false;
|
|
426
|
+
if (deliveryPending) {
|
|
427
|
+
deliveryPending = false;
|
|
428
|
+
void deliver();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
411
432
|
const unsubscribe = this.db.subscribe_changes(collection => {
|
|
412
433
|
if (collection !== EVENT_COLLECTION || stopped)
|
|
413
434
|
return;
|
|
414
|
-
void
|
|
435
|
+
void deliver();
|
|
415
436
|
});
|
|
416
|
-
|
|
437
|
+
// The event log is authoritative. Reconcile immediately and periodically so
|
|
438
|
+
// an event cannot be stranded in the small window before SSE is established.
|
|
439
|
+
void deliver();
|
|
440
|
+
const reconciliation = setInterval(() => void deliver(), 250);
|
|
441
|
+
return () => { stopped = true; clearInterval(reconciliation); unsubscribe(); };
|
|
417
442
|
}
|
|
418
443
|
async appendEvent(input, type, value) {
|
|
419
444
|
const event = { id: generateEventId(), ...input, type, value, timestamp: Date.now() };
|
package/package.json
CHANGED