create-feltdb 0.4.0 → 0.4.2
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/README.md +21 -15
- package/dist/application-identity.js +132 -0
- package/dist/cli-scripts-generator.js +211 -0
- package/dist/cli.js +33 -9
- package/dist/create.js +103 -35
- package/dist/docker-compose-generator.js +258 -0
- package/dist/package-versions.js +1 -1
- package/dist/runtime-templates.js +272 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,29 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Interactive CLI tool to scaffold new FeltDB applications.
|
|
4
4
|
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g create-feltdb
|
|
9
|
-
```
|
|
10
|
-
|
|
11
5
|
## Usage
|
|
12
6
|
|
|
13
|
-
|
|
7
|
+
One command scaffolds, installs, and starts the application, Studio, and local
|
|
8
|
+
WebLLM agent (no global installation required):
|
|
14
9
|
|
|
15
10
|
```bash
|
|
16
|
-
npx create-feltdb my-app
|
|
11
|
+
npx --yes create-feltdb@latest my-app
|
|
17
12
|
```
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
Use the interactive menu, or accept its defaults non-interactively:
|
|
20
15
|
|
|
21
16
|
```bash
|
|
22
|
-
create-feltdb my-app
|
|
17
|
+
npx --yes create-feltdb@latest my-app --yes
|
|
23
18
|
```
|
|
24
19
|
|
|
25
20
|
### Options
|
|
26
21
|
|
|
27
|
-
- `--
|
|
22
|
+
- `--runtime <browser|node|self-hosted>`
|
|
23
|
+
- `--framework <react|vanilla>`
|
|
24
|
+
- `--no-distributed`
|
|
25
|
+
- `--no-agents`
|
|
26
|
+
- `--capabilities <search|vector|search,vector>`
|
|
27
|
+
- `--no-install` — generate without installing dependencies
|
|
28
|
+
- `--no-start` — do not start services after generation
|
|
29
|
+
- `--yes` or `-y` — skip the interactive menu and use defaults
|
|
30
|
+
- `--help` or `-h`
|
|
31
|
+
- `--version`
|
|
28
32
|
|
|
29
33
|
## What Gets Generated
|
|
30
34
|
|
|
@@ -47,16 +51,16 @@ my-app/
|
|
|
47
51
|
└── README.md
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
After running `create-feltdb`:
|
|
54
|
+
## Run It Again
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
57
|
cd my-app
|
|
56
|
-
npm install
|
|
57
58
|
npm run dev
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
This starts the app and Studio together. Self-hosted projects also start the
|
|
62
|
+
version-matched FeltDB server container and therefore require Docker.
|
|
63
|
+
|
|
60
64
|
## Default Configuration
|
|
61
65
|
|
|
62
66
|
The generated project uses:
|
|
@@ -64,6 +68,8 @@ The generated project uses:
|
|
|
64
68
|
- **Storage**: OPFS (with IndexedDB fallback)
|
|
65
69
|
- **Distributed**: Enabled by default
|
|
66
70
|
- **Capabilities**: Search enabled, Vector disabled
|
|
71
|
+
- **AI**: Private WebLLM agent enabled; model loads lazily in the browser
|
|
72
|
+
- **Studio**: Installed and served alongside the application
|
|
67
73
|
|
|
68
74
|
## License
|
|
69
75
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application identity management
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - FELTDB_APP_NAME: Human-readable application name
|
|
6
|
+
* - FELTDB_APP_ID: UUID-based stable identifier
|
|
7
|
+
* - FELTDB_VERSION: Semantic versioning
|
|
8
|
+
* - Version matching between components
|
|
9
|
+
*/
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
/**
|
|
12
|
+
* Generate a stable UUID from application name
|
|
13
|
+
* Ensures same name always produces same ID
|
|
14
|
+
*/
|
|
15
|
+
export function generateAppId(appName) {
|
|
16
|
+
const hash = crypto
|
|
17
|
+
.createHash('sha256')
|
|
18
|
+
.update(appName)
|
|
19
|
+
.digest('hex');
|
|
20
|
+
// Convert to UUID v5-like format
|
|
21
|
+
const uuid = [
|
|
22
|
+
hash.substring(0, 8),
|
|
23
|
+
hash.substring(8, 12),
|
|
24
|
+
'5' + hash.substring(13, 16),
|
|
25
|
+
((parseInt(hash.substring(16, 18), 16) & 0x3f) | 0x80).toString(16).padStart(2, '0') +
|
|
26
|
+
hash.substring(18, 20),
|
|
27
|
+
hash.substring(20, 32),
|
|
28
|
+
].join('-');
|
|
29
|
+
return uuid;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize application name for use in identifiers
|
|
33
|
+
*/
|
|
34
|
+
export function sanitizeAppName(name) {
|
|
35
|
+
return (name
|
|
36
|
+
// Replace spaces with hyphens
|
|
37
|
+
.replace(/\s+/g, '-')
|
|
38
|
+
// Remove non-alphanumeric except hyphens
|
|
39
|
+
.replace(/[^a-z0-9-]/gi, '')
|
|
40
|
+
// Remove leading/trailing hyphens
|
|
41
|
+
.replace(/^-+|-+$/g, '')
|
|
42
|
+
// Limit to 50 chars
|
|
43
|
+
.substring(0, 50)
|
|
44
|
+
.toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get current FeltDB version from package.json
|
|
48
|
+
*/
|
|
49
|
+
export function getCurrentFeltDBVersion() {
|
|
50
|
+
// This would be replaced at build time with the actual version
|
|
51
|
+
return '0.4.2';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create application identity manifest
|
|
55
|
+
*/
|
|
56
|
+
export function createApplicationIdentity(appName, feltdbVersion) {
|
|
57
|
+
const version = feltdbVersion || getCurrentFeltDBVersion();
|
|
58
|
+
const sanitized = sanitizeAppName(appName);
|
|
59
|
+
const appId = generateAppId(sanitized);
|
|
60
|
+
return {
|
|
61
|
+
name: sanitized,
|
|
62
|
+
id: appId,
|
|
63
|
+
version,
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
components: {
|
|
66
|
+
app: version,
|
|
67
|
+
feltdb: version,
|
|
68
|
+
studio: version,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate version manifest for Docker image metadata
|
|
74
|
+
*/
|
|
75
|
+
export function generateVersionLabels(identity) {
|
|
76
|
+
return {
|
|
77
|
+
'app.feltdb/app-name': identity.name,
|
|
78
|
+
'app.feltdb/app-id': identity.id,
|
|
79
|
+
'app.feltdb/version': identity.version,
|
|
80
|
+
'app.feltdb/created': identity.createdAt,
|
|
81
|
+
'app.feltdb/component.app': identity.components.app,
|
|
82
|
+
'app.feltdb/component.feltdb': identity.components.feltdb,
|
|
83
|
+
'app.feltdb/component.studio': identity.components.studio,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validate version compatibility
|
|
88
|
+
*/
|
|
89
|
+
export function validateVersionCompatibility(appVersion, feltdbVersion, studioVersion) {
|
|
90
|
+
const warnings = [];
|
|
91
|
+
// All components should be same major.minor version for safety
|
|
92
|
+
const extractMajorMinor = (v) => v.split('.').slice(0, 2).join('.');
|
|
93
|
+
const appMajorMinor = extractMajorMinor(appVersion);
|
|
94
|
+
const feltdbMajorMinor = extractMajorMinor(feltdbVersion);
|
|
95
|
+
const studioMajorMinor = extractMajorMinor(studioVersion);
|
|
96
|
+
if (appMajorMinor !== feltdbMajorMinor) {
|
|
97
|
+
warnings.push(`App version ${appVersion} differs from FeltDB ${feltdbVersion}`);
|
|
98
|
+
}
|
|
99
|
+
if (feltdbMajorMinor !== studioMajorMinor) {
|
|
100
|
+
warnings.push(`FeltDB version ${feltdbVersion} differs from Studio ${studioVersion}`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
compatible: warnings.length === 0,
|
|
104
|
+
warnings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Generate .feltdb/identity.json manifest
|
|
109
|
+
*/
|
|
110
|
+
export function generateIdentityManifest(identity) {
|
|
111
|
+
return JSON.stringify(identity, null, 2);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Generate environment file with version information
|
|
115
|
+
*/
|
|
116
|
+
export function generateVersionEnv(identity) {
|
|
117
|
+
return `# Application Identity
|
|
118
|
+
# Generated at: ${identity.createdAt}
|
|
119
|
+
|
|
120
|
+
FELTDB_APP_NAME=${identity.name}
|
|
121
|
+
FELTDB_APP_ID=${identity.id}
|
|
122
|
+
FELTDB_VERSION=${identity.version}
|
|
123
|
+
|
|
124
|
+
# Component Versions
|
|
125
|
+
FELTDB_APP_VERSION=${identity.components.app}
|
|
126
|
+
FELTDB_CORE_VERSION=${identity.components.feltdb}
|
|
127
|
+
FELTDB_STUDIO_VERSION=${identity.components.studio}
|
|
128
|
+
|
|
129
|
+
# Build Info
|
|
130
|
+
FELTDB_BUILT_AT=${new Date().toISOString()}
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI lifecycle scripts for FeltDB applications
|
|
3
|
+
*
|
|
4
|
+
* Generates scripts for common operations:
|
|
5
|
+
* - feltdb:up - Start services
|
|
6
|
+
* - feltdb:down - Stop services
|
|
7
|
+
* - feltdb:logs - View logs
|
|
8
|
+
* - feltdb:status - Check health
|
|
9
|
+
* - feltdb:reset - Reset data
|
|
10
|
+
* - feltdb:ps - List running services
|
|
11
|
+
*/
|
|
12
|
+
export function generateUpScript() {
|
|
13
|
+
return `#!/bin/bash
|
|
14
|
+
|
|
15
|
+
# Start FeltDB application stack
|
|
16
|
+
# Usage: npm run feltdb:up
|
|
17
|
+
|
|
18
|
+
set -e
|
|
19
|
+
|
|
20
|
+
echo "🚀 Starting FeltDB application..."
|
|
21
|
+
|
|
22
|
+
# Load environment
|
|
23
|
+
if [ -f .env.local ]; then
|
|
24
|
+
export \$(cat .env.local | grep -v '^#' | xargs)
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Build and start containers
|
|
28
|
+
docker-compose up -d
|
|
29
|
+
|
|
30
|
+
echo "⏳ Waiting for services to be healthy..."
|
|
31
|
+
|
|
32
|
+
# Wait for FeltDB
|
|
33
|
+
TIMEOUT=60
|
|
34
|
+
ELAPSED=0
|
|
35
|
+
while [ \$ELAPSED -lt \$TIMEOUT ]; do
|
|
36
|
+
if curl -s -f http://localhost:7700/health > /dev/null 2>&1; then
|
|
37
|
+
STATE=\$(curl -s http://localhost:7700/health | grep -o '"state":"[^"]*' | cut -d'"' -f4)
|
|
38
|
+
if [ "\$STATE" = "ready" ] || [ "\$STATE" = "recovering" ]; then
|
|
39
|
+
echo "✅ FeltDB is \$STATE"
|
|
40
|
+
break
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
sleep 2
|
|
44
|
+
ELAPSED=\$((ELAPSED + 2))
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
# Wait for application
|
|
48
|
+
ELAPSED=0
|
|
49
|
+
while [ \$ELAPSED -lt \$TIMEOUT ]; do
|
|
50
|
+
if curl -s -f http://localhost:3000/health > /dev/null 2>&1; then
|
|
51
|
+
echo "✅ Application is ready"
|
|
52
|
+
break
|
|
53
|
+
fi
|
|
54
|
+
sleep 2
|
|
55
|
+
ELAPSED=\$((ELAPSED + 2))
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
echo ""
|
|
59
|
+
echo "✨ FeltDB stack is running!"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Services:"
|
|
62
|
+
echo " FeltDB: http://localhost:7700"
|
|
63
|
+
echo " App: http://localhost:3000"
|
|
64
|
+
echo " Studio: http://localhost:8000"
|
|
65
|
+
echo ""
|
|
66
|
+
echo "Next: npm run feltdb:logs"
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
export function generateDownScript() {
|
|
70
|
+
return `#!/bin/bash
|
|
71
|
+
|
|
72
|
+
# Stop FeltDB application stack
|
|
73
|
+
# Usage: npm run feltdb:down
|
|
74
|
+
|
|
75
|
+
echo "🛑 Stopping FeltDB application..."
|
|
76
|
+
|
|
77
|
+
docker-compose down
|
|
78
|
+
|
|
79
|
+
echo "✅ Services stopped"
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
export function generateLogsScript() {
|
|
83
|
+
return `#!/bin/bash
|
|
84
|
+
|
|
85
|
+
# View logs from FeltDB stack
|
|
86
|
+
# Usage: npm run feltdb:logs [service]
|
|
87
|
+
# npm run feltdb:logs feltdb
|
|
88
|
+
# npm run feltdb:logs app
|
|
89
|
+
# npm run feltdb:logs studio
|
|
90
|
+
|
|
91
|
+
SERVICE=\$1
|
|
92
|
+
|
|
93
|
+
if [ -z "\$SERVICE" ]; then
|
|
94
|
+
echo "📋 Logs from all services (Ctrl+C to exit):"
|
|
95
|
+
docker-compose logs -f
|
|
96
|
+
else
|
|
97
|
+
echo "📋 Logs from \$SERVICE (Ctrl+C to exit):"
|
|
98
|
+
docker-compose logs -f \$SERVICE
|
|
99
|
+
fi
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
export function generateStatusScript() {
|
|
103
|
+
return `#!/bin/bash
|
|
104
|
+
|
|
105
|
+
# Check health and status of FeltDB stack
|
|
106
|
+
# Usage: npm run feltdb:status
|
|
107
|
+
|
|
108
|
+
echo "🔍 FeltDB Application Status"
|
|
109
|
+
echo ""
|
|
110
|
+
|
|
111
|
+
# Docker Compose status
|
|
112
|
+
echo "Containers:"
|
|
113
|
+
docker-compose ps
|
|
114
|
+
|
|
115
|
+
echo ""
|
|
116
|
+
|
|
117
|
+
# FeltDB health
|
|
118
|
+
echo "FeltDB Health:"
|
|
119
|
+
if RESPONSE=\$(curl -s -m 5 http://localhost:7700/health 2>/dev/null); then
|
|
120
|
+
STATE=\$(echo \$RESPONSE | grep -o '"state":"[^"]*' | cut -d'"' -f4)
|
|
121
|
+
VERSION=\$(echo \$RESPONSE | grep -o '"version":"[^"]*' | cut -d'"' -f4)
|
|
122
|
+
echo " State: \$STATE"
|
|
123
|
+
echo " Version: \$VERSION"
|
|
124
|
+
else
|
|
125
|
+
echo " ❌ No response"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Application health
|
|
129
|
+
echo ""
|
|
130
|
+
echo "Application Health:"
|
|
131
|
+
if RESPONSE=\$(curl -s -m 5 http://localhost:3000/health 2>/dev/null); then
|
|
132
|
+
echo " ✅ Responding"
|
|
133
|
+
else
|
|
134
|
+
echo " ❌ No response"
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
echo ""
|
|
138
|
+
echo "Log tail: npm run feltdb:logs"
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
export function generateResetScript() {
|
|
142
|
+
return `#!/bin/bash
|
|
143
|
+
|
|
144
|
+
# Reset FeltDB data (WARNING: destructive)
|
|
145
|
+
# Usage: npm run feltdb:reset
|
|
146
|
+
|
|
147
|
+
echo "⚠️ This will delete all FeltDB data!"
|
|
148
|
+
read -p "Are you sure? (type 'yes' to confirm): " CONFIRM
|
|
149
|
+
|
|
150
|
+
if [ "\$CONFIRM" != "yes" ]; then
|
|
151
|
+
echo "Cancelled."
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo "🧹 Resetting FeltDB data..."
|
|
156
|
+
|
|
157
|
+
# Stop containers
|
|
158
|
+
docker-compose down
|
|
159
|
+
|
|
160
|
+
# Remove volumes
|
|
161
|
+
docker volume rm \$(docker volume ls -q | grep feltdb_data) 2>/dev/null || true
|
|
162
|
+
|
|
163
|
+
# Restart
|
|
164
|
+
echo "🚀 Restarting services..."
|
|
165
|
+
docker-compose up -d
|
|
166
|
+
|
|
167
|
+
echo "✅ Data reset complete"
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
export function generatePsScript() {
|
|
171
|
+
return `#!/bin/bash
|
|
172
|
+
|
|
173
|
+
# List running FeltDB services
|
|
174
|
+
# Usage: npm run feltdb:ps
|
|
175
|
+
|
|
176
|
+
docker-compose ps
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
export function generatePackageJsonScripts(runtime) {
|
|
180
|
+
const scripts = {
|
|
181
|
+
'feltdb:up': 'bash scripts/feltdb-up.sh',
|
|
182
|
+
'feltdb:down': 'bash scripts/feltdb-down.sh',
|
|
183
|
+
'feltdb:logs': 'bash scripts/feltdb-logs.sh',
|
|
184
|
+
'feltdb:status': 'bash scripts/feltdb-status.sh',
|
|
185
|
+
'feltdb:reset': 'bash scripts/feltdb-reset.sh',
|
|
186
|
+
'feltdb:ps': 'bash scripts/feltdb-ps.sh',
|
|
187
|
+
};
|
|
188
|
+
if (runtime === 'browser') {
|
|
189
|
+
// Browser doesn't need container scripts
|
|
190
|
+
return {
|
|
191
|
+
dev: 'vite',
|
|
192
|
+
build: 'tsc && vite build',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (runtime === 'node') {
|
|
196
|
+
return {
|
|
197
|
+
dev: 'tsx src/index.ts',
|
|
198
|
+
build: 'tsc',
|
|
199
|
+
start: 'node dist/index.js',
|
|
200
|
+
...scripts,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (runtime === 'self-hosted') {
|
|
204
|
+
return {
|
|
205
|
+
dev: 'docker-compose up -d --build',
|
|
206
|
+
build: 'docker-compose build',
|
|
207
|
+
...scripts,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return scripts;
|
|
211
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import readline from 'readline';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
10
11
|
import { createProject } from './create.js';
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
function parseArgs(args) {
|
|
@@ -38,6 +39,15 @@ function parseArgs(args) {
|
|
|
38
39
|
}
|
|
39
40
|
return options;
|
|
40
41
|
}
|
|
42
|
+
function run(command, args, cwd) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn(command, args, { cwd, stdio: 'inherit' });
|
|
45
|
+
child.once('error', reject);
|
|
46
|
+
child.once('exit', code => code === 0
|
|
47
|
+
? resolve()
|
|
48
|
+
: reject(new Error(`${command} exited with status ${code ?? 'unknown'}`)));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
41
51
|
async function select(message, choices, initialValue) {
|
|
42
52
|
let selected = Math.max(0, choices.findIndex(choice => choice.value === initialValue));
|
|
43
53
|
const input = process.stdin;
|
|
@@ -114,11 +124,11 @@ async function promptForOptions(defaults) {
|
|
|
114
124
|
async function main() {
|
|
115
125
|
const args = process.argv.slice(2);
|
|
116
126
|
if (args.includes('--help') || args.includes('-h')) {
|
|
117
|
-
console.log(`create-feltdb 0.4.
|
|
127
|
+
console.log(`create-feltdb 0.4.2\n\nUsage: create-feltdb [project-name] [options]\n\nOptions:\n --runtime <browser|node|self-hosted>\n --framework <react|vanilla>\n --no-distributed\n --no-agents\n --capabilities <list>\n --no-install\n --no-start\n -y, --yes\n -h, --help\n --version`);
|
|
118
128
|
return;
|
|
119
129
|
}
|
|
120
130
|
if (args.includes('--version')) {
|
|
121
|
-
console.log('0.4.
|
|
131
|
+
console.log('0.4.2');
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
124
134
|
// Find project name (first non-flag argument)
|
|
@@ -135,6 +145,8 @@ async function main() {
|
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
const shouldAutoYes = args.includes('--yes') || args.includes('-y');
|
|
148
|
+
const shouldInstall = !args.includes('--no-install');
|
|
149
|
+
const shouldStart = !args.includes('--no-start');
|
|
138
150
|
let options = parseArgs(args);
|
|
139
151
|
console.log('\n✨ Creating FeltDB Application\n');
|
|
140
152
|
if (!shouldAutoYes) {
|
|
@@ -158,16 +170,28 @@ async function main() {
|
|
|
158
170
|
capabilities: options.capabilities,
|
|
159
171
|
});
|
|
160
172
|
console.log('\n✅ FeltDB application created successfully!\n');
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
174
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
175
|
+
if (shouldInstall) {
|
|
176
|
+
console.log('📦 Installing application, Studio, and local AI dependencies...\n');
|
|
177
|
+
await run(npm, ['install'], projectDir);
|
|
178
|
+
}
|
|
179
|
+
if (shouldStart) {
|
|
180
|
+
if (!shouldInstall) {
|
|
181
|
+
console.log(`Start after installing dependencies:\n cd ${projectName}\n npm install\n npm run dev\n`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log('\n🚀 Starting the application and FeltDB Studio...\n');
|
|
185
|
+
await run(npm, ['run', 'dev'], projectDir);
|
|
186
|
+
}
|
|
166
187
|
}
|
|
167
188
|
else {
|
|
168
|
-
console.log(
|
|
189
|
+
console.log('Project is ready. Start everything with:');
|
|
190
|
+
console.log(` cd ${projectName}`);
|
|
191
|
+
if (!shouldInstall)
|
|
192
|
+
console.log(' npm install');
|
|
193
|
+
console.log(' npm run dev');
|
|
169
194
|
}
|
|
170
|
-
console.log('');
|
|
171
195
|
}
|
|
172
196
|
catch (error) {
|
|
173
197
|
console.error('❌ Failed to create project:', error);
|
package/dist/create.js
CHANGED
|
@@ -54,6 +54,7 @@ export async function createProject(options) {
|
|
|
54
54
|
},
|
|
55
55
|
devDependencies: {
|
|
56
56
|
'@feltdb/cli': feltdbPackageRange,
|
|
57
|
+
'@feltdb/studio': feltdbPackageRange,
|
|
57
58
|
typescript: '^5.0.0',
|
|
58
59
|
'@types/node': '^20.0.0',
|
|
59
60
|
vite: '^8.2.1',
|
|
@@ -66,6 +67,9 @@ export async function createProject(options) {
|
|
|
66
67
|
packageJson.devDependencies['@types/react'] = '^18.0.0';
|
|
67
68
|
packageJson.devDependencies['@types/react-dom'] = '^18.0.0';
|
|
68
69
|
}
|
|
70
|
+
if (hasAgents) {
|
|
71
|
+
packageJson.dependencies['@feltdb/webllm'] = feltdbPackageRange;
|
|
72
|
+
}
|
|
69
73
|
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
70
74
|
// Create feltdb.config.json
|
|
71
75
|
const feltdbConfig = {
|
|
@@ -153,52 +157,62 @@ export async function createProject(options) {
|
|
|
153
157
|
}
|
|
154
158
|
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
|
155
159
|
// Create main application files
|
|
160
|
+
const runtimeOptions = runtime === 'browser'
|
|
161
|
+
? "{ namespace: '" + applicationName + "', browser: true }"
|
|
162
|
+
: runtime === 'self-hosted'
|
|
163
|
+
? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
|
|
164
|
+
: "{ namespace: '" + applicationName + "', memory: true }";
|
|
156
165
|
const feltdbTs = `import { createFeltDB } from '@feltdb/core';
|
|
157
166
|
|
|
158
|
-
export const db = createFeltDB({
|
|
159
|
-
namespace: '${applicationName}',
|
|
160
|
-
memory: true, // Development-only; configure \`server\` for durable customer data.
|
|
161
|
-
});
|
|
167
|
+
export const db = createFeltDB(${runtimeOptions});
|
|
162
168
|
|
|
163
169
|
// Collections
|
|
164
170
|
export const documents = db.collection('documents');
|
|
165
171
|
export const reports = db.collection('reports');
|
|
166
172
|
`;
|
|
167
173
|
fs.writeFileSync(path.join(srcDir, 'feltdb.ts'), feltdbTs);
|
|
168
|
-
// Create a
|
|
174
|
+
// Create a real local-inference agent for browser projects.
|
|
169
175
|
if (hasAgents) {
|
|
170
|
-
const agentTs = `import {
|
|
176
|
+
const agentTs = `import { WebLLMProvider } from '@feltdb/webllm';
|
|
177
|
+
import { db, reports } from '../../src/feltdb';
|
|
171
178
|
|
|
172
179
|
/**
|
|
173
|
-
* Researcher
|
|
174
|
-
*
|
|
175
|
-
* This agent demonstrates distributed agent execution
|
|
176
|
-
* across peers in the FeltDB fabric.
|
|
180
|
+
* Researcher metadata is registered in FeltDB; inference runs privately in
|
|
181
|
+
* the browser through WebLLM and its worker.
|
|
177
182
|
*/
|
|
178
183
|
export const researcher = db.defineAgent({
|
|
179
184
|
name: 'researcher',
|
|
180
|
-
|
|
185
|
+
version: 1,
|
|
186
|
+
description: 'Analyze documents with private, on-device inference',
|
|
181
187
|
capabilities: [
|
|
182
188
|
'document-read',
|
|
183
|
-
'vector-search',
|
|
184
189
|
'report-write'
|
|
185
190
|
],
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log('Goal:', options.goal);
|
|
189
|
-
console.log('Inputs:', options.inputs);
|
|
190
|
-
|
|
191
|
-
// The agent will execute capabilities across peers
|
|
192
|
-
// Results are automatically acquired from remote locations
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
status: 'complete',
|
|
196
|
-
results: {
|
|
197
|
-
message: 'Research completed successfully'
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
}
|
|
191
|
+
goals: ['Summarize documents without sending prompts off-device'],
|
|
192
|
+
constraints: { maxIterations: 1 },
|
|
201
193
|
});
|
|
194
|
+
|
|
195
|
+
let provider: WebLLMProvider | undefined;
|
|
196
|
+
|
|
197
|
+
export async function runResearcher(
|
|
198
|
+
prompt: string,
|
|
199
|
+
onProgress?: (message: string) => void,
|
|
200
|
+
) {
|
|
201
|
+
provider ??= new WebLLMProvider({
|
|
202
|
+
onProgress: ({ progress, text }) =>
|
|
203
|
+
onProgress?.(\`${'${Math.round(progress * 100)}'}% ${'${text}'}\`),
|
|
204
|
+
});
|
|
205
|
+
const content = await provider.generate([
|
|
206
|
+
{ role: 'system', content: 'You are a concise research assistant. Keep all reasoning local.' },
|
|
207
|
+
{ role: 'user', content: prompt },
|
|
208
|
+
]);
|
|
209
|
+
await reports.insert({
|
|
210
|
+
title: prompt.slice(0, 80) || 'Local research',
|
|
211
|
+
content,
|
|
212
|
+
createdAt: new Date(),
|
|
213
|
+
});
|
|
214
|
+
return content;
|
|
215
|
+
}
|
|
202
216
|
`;
|
|
203
217
|
fs.writeFileSync(path.join(feltdbDir, 'agents', 'researcher.ts'), agentTs);
|
|
204
218
|
}
|
|
@@ -228,12 +242,51 @@ export const capabilities = {
|
|
|
228
242
|
fs.writeFileSync(path.join(feltdbDir, 'capabilities', 'index.ts'), capabilitiesTs);
|
|
229
243
|
// Create main application file based on framework
|
|
230
244
|
if (framework === 'react') {
|
|
245
|
+
const agentImport = hasAgents
|
|
246
|
+
? "import { runResearcher } from '../feltdb/agents/researcher';"
|
|
247
|
+
: '';
|
|
248
|
+
const agentState = hasAgents
|
|
249
|
+
? ` const [prompt, setPrompt] = useState('Summarize why local-first applications are useful.');
|
|
250
|
+
const [answer, setAnswer] = useState('');
|
|
251
|
+
const [modelStatus, setModelStatus] = useState('Model not loaded');
|
|
252
|
+
const [generating, setGenerating] = useState(false);`
|
|
253
|
+
: '';
|
|
254
|
+
const agentHandler = hasAgents
|
|
255
|
+
? `
|
|
256
|
+
const handleResearch = async () => {
|
|
257
|
+
setGenerating(true);
|
|
258
|
+
setAnswer('');
|
|
259
|
+
try {
|
|
260
|
+
setAnswer(await runResearcher(prompt, setModelStatus));
|
|
261
|
+
setModelStatus('Ready — inference stayed in this browser');
|
|
262
|
+
} catch (error) {
|
|
263
|
+
setModelStatus(error instanceof Error ? error.message : String(error));
|
|
264
|
+
} finally {
|
|
265
|
+
setGenerating(false);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
`
|
|
269
|
+
: '';
|
|
270
|
+
const agentMarkup = hasAgents
|
|
271
|
+
? `
|
|
272
|
+
<section className="researcher">
|
|
273
|
+
<h2>Private WebLLM Researcher</h2>
|
|
274
|
+
<p>{modelStatus}</p>
|
|
275
|
+
<textarea value={prompt} onChange={event => setPrompt(event.target.value)} rows={4} />
|
|
276
|
+
<button onClick={handleResearch} disabled={generating || !prompt.trim()}>
|
|
277
|
+
{generating ? 'Running locally…' : 'Run researcher'}
|
|
278
|
+
</button>
|
|
279
|
+
{answer && <article><h3>Result</h3><p>{answer}</p></article>}
|
|
280
|
+
</section>`
|
|
281
|
+
: '';
|
|
231
282
|
const appTsx = `import React, { useState, useEffect } from 'react';
|
|
232
283
|
import { db, documents } from './feltdb';
|
|
284
|
+
${agentImport}
|
|
233
285
|
|
|
234
286
|
export function App() {
|
|
235
287
|
const [docs, setDocs] = useState<any[]>([]);
|
|
236
288
|
const [loading, setLoading] = useState(true);
|
|
289
|
+
${agentState}
|
|
237
290
|
|
|
238
291
|
useEffect(() => {
|
|
239
292
|
const loadDocs = async () => {
|
|
@@ -264,6 +317,7 @@ export function App() {
|
|
|
264
317
|
console.error('Error adding document:', err);
|
|
265
318
|
}
|
|
266
319
|
};
|
|
320
|
+
${agentHandler}
|
|
267
321
|
|
|
268
322
|
return (
|
|
269
323
|
<div className="app">
|
|
@@ -306,6 +360,7 @@ export function App() {
|
|
|
306
360
|
)}
|
|
307
361
|
<button onClick={handleAddDocument}>Add Document</button>
|
|
308
362
|
</section>
|
|
363
|
+
${agentMarkup}
|
|
309
364
|
</main>
|
|
310
365
|
|
|
311
366
|
<style>{\`
|
|
@@ -418,6 +473,20 @@ export function App() {
|
|
|
418
473
|
button:hover {
|
|
419
474
|
background: #5568d3;
|
|
420
475
|
}
|
|
476
|
+
|
|
477
|
+
textarea {
|
|
478
|
+
box-sizing: border-box;
|
|
479
|
+
display: block;
|
|
480
|
+
margin: 12px 0;
|
|
481
|
+
padding: 10px;
|
|
482
|
+
width: 100%;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.researcher {
|
|
486
|
+
border-top: 1px solid #eee;
|
|
487
|
+
margin-top: 30px;
|
|
488
|
+
padding-top: 20px;
|
|
489
|
+
}
|
|
421
490
|
\`}</style>
|
|
422
491
|
</div>
|
|
423
492
|
);
|
|
@@ -468,8 +537,10 @@ main().catch(console.error);
|
|
|
468
537
|
fs.writeFileSync(path.join(projectDir, 'index.html'), indexHtml);
|
|
469
538
|
// Create .env.example
|
|
470
539
|
const envExample = `# FeltDB Configuration
|
|
471
|
-
|
|
472
|
-
|
|
540
|
+
VITE_FELTDB_API_KEY=
|
|
541
|
+
VITE_FELTDB_URL=http://localhost:7700
|
|
542
|
+
# Override the versioned self-hosted container when needed:
|
|
543
|
+
FELTDB_IMAGE=
|
|
473
544
|
NODE_ENV=development
|
|
474
545
|
`;
|
|
475
546
|
fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
|
|
@@ -554,12 +625,9 @@ feltdb keys create --name development --scope '*'
|
|
|
554
625
|
|
|
555
626
|
## Agents
|
|
556
627
|
|
|
557
|
-
${hasAgents ? `The \`researcher\` agent
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
- Executes on compatible peers
|
|
561
|
-
- Creates workflows
|
|
562
|
-
- Publishes results` : 'No agents configured'}
|
|
628
|
+
${hasAgents ? `The \`researcher\` agent runs real private inference with
|
|
629
|
+
\`@feltdb/webllm\`. The model downloads on first use, runs in a Web Worker, and
|
|
630
|
+
caches its artifacts in the browser. Generated reports are stored in FeltDB.` : 'No agents configured'}
|
|
563
631
|
|
|
564
632
|
## Capabilities
|
|
565
633
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose generator for self-hosted FeltDB deployments
|
|
3
|
+
*
|
|
4
|
+
* Generates:
|
|
5
|
+
* - docker-compose.yml with three services (FeltDB, App, Studio)
|
|
6
|
+
* - Dockerfile for application container
|
|
7
|
+
* - .dockerignore for optimized builds
|
|
8
|
+
* - Environment configuration
|
|
9
|
+
*/
|
|
10
|
+
export function generateDockerCompose(config) {
|
|
11
|
+
return `version: '3.9'
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
feltdb:
|
|
15
|
+
image: rkendel1/feltdb:latest
|
|
16
|
+
container_name: \${COMPOSE_PROJECT_NAME}-feltdb
|
|
17
|
+
environment:
|
|
18
|
+
FELTDB_APP_NAME: \${FELTDB_APP_NAME:-${config.appName}}
|
|
19
|
+
FELTDB_APP_ID: \${FELTDB_APP_ID:-${config.appId}}
|
|
20
|
+
FELTDB_VERSION: \${FELTDB_VERSION:-${config.version}}
|
|
21
|
+
FELTDB_PEERS: \${FELTDB_PEERS:-}
|
|
22
|
+
FELTDB_REPLICATION_PORT: ${config.replicationPort}
|
|
23
|
+
NODE_ENV: \${NODE_ENV:-production}
|
|
24
|
+
ports:
|
|
25
|
+
- "\${FELTDB_API_PORT:-${config.port}}:7700"
|
|
26
|
+
- "\${FELTDB_REPLICATION_PORT:-${config.replicationPort}}:${config.replicationPort}"
|
|
27
|
+
volumes:
|
|
28
|
+
- feltdb_data:/data
|
|
29
|
+
- ./feltdb.flow:/app/feltdb.flow:ro
|
|
30
|
+
healthcheck:
|
|
31
|
+
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
|
|
32
|
+
interval: 10s
|
|
33
|
+
timeout: 5s
|
|
34
|
+
retries: 3
|
|
35
|
+
start_period: 30s
|
|
36
|
+
restart: unless-stopped
|
|
37
|
+
networks:
|
|
38
|
+
- feltdb_network
|
|
39
|
+
labels:
|
|
40
|
+
app.feltdb/component: "database"
|
|
41
|
+
app.feltdb/version: "${config.version}"
|
|
42
|
+
|
|
43
|
+
app:
|
|
44
|
+
build:
|
|
45
|
+
context: .
|
|
46
|
+
dockerfile: Dockerfile
|
|
47
|
+
args:
|
|
48
|
+
NODE_ENV: production
|
|
49
|
+
container_name: \${COMPOSE_PROJECT_NAME}-app
|
|
50
|
+
environment:
|
|
51
|
+
FELTDB_API_URL: http://feltdb:7700
|
|
52
|
+
FELTDB_APP_NAME: \${FELTDB_APP_NAME:-${config.appName}}
|
|
53
|
+
NODE_ENV: \${NODE_ENV:-production}
|
|
54
|
+
ports:
|
|
55
|
+
- "\${APP_PORT:-3000}:3000"
|
|
56
|
+
depends_on:
|
|
57
|
+
feltdb:
|
|
58
|
+
condition: service_healthy
|
|
59
|
+
volumes:
|
|
60
|
+
- ./src:/app/src
|
|
61
|
+
- app_node_modules:/app/node_modules
|
|
62
|
+
healthcheck:
|
|
63
|
+
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
|
64
|
+
interval: 10s
|
|
65
|
+
timeout: 5s
|
|
66
|
+
retries: 3
|
|
67
|
+
start_period: 30s
|
|
68
|
+
restart: unless-stopped
|
|
69
|
+
networks:
|
|
70
|
+
- feltdb_network
|
|
71
|
+
labels:
|
|
72
|
+
app.feltdb/component: "application"
|
|
73
|
+
app.feltdb/version: "${config.version}"
|
|
74
|
+
|
|
75
|
+
studio:
|
|
76
|
+
image: rkendel1/feltdb-studio:latest
|
|
77
|
+
container_name: \${COMPOSE_PROJECT_NAME}-studio
|
|
78
|
+
environment:
|
|
79
|
+
FELTDB_API_URL: http://feltdb:7700
|
|
80
|
+
APP_URL: http://app:3000
|
|
81
|
+
ports:
|
|
82
|
+
- "\${STUDIO_PORT:-${config.studioPort}}:8000"
|
|
83
|
+
depends_on:
|
|
84
|
+
- feltdb
|
|
85
|
+
- app
|
|
86
|
+
networks:
|
|
87
|
+
- feltdb_network
|
|
88
|
+
labels:
|
|
89
|
+
app.feltdb/component: "studio"
|
|
90
|
+
|
|
91
|
+
networks:
|
|
92
|
+
feltdb_network:
|
|
93
|
+
driver: bridge
|
|
94
|
+
labels:
|
|
95
|
+
app.feltdb/network: "internal"
|
|
96
|
+
|
|
97
|
+
volumes:
|
|
98
|
+
feltdb_data:
|
|
99
|
+
driver: local
|
|
100
|
+
labels:
|
|
101
|
+
app.feltdb/data: "durable-operations-log"
|
|
102
|
+
app_node_modules:
|
|
103
|
+
driver: local
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
export function generateDockerfile(config, framework) {
|
|
107
|
+
const buildCommand = framework === 'react' ? 'npm run build' : 'npm run build';
|
|
108
|
+
return `# FeltDB Application Container
|
|
109
|
+
# Multi-stage build for optimized production image
|
|
110
|
+
|
|
111
|
+
FROM node:18-alpine AS builder
|
|
112
|
+
|
|
113
|
+
WORKDIR /app
|
|
114
|
+
|
|
115
|
+
# Install dependencies
|
|
116
|
+
COPY package.json package-lock.json ./
|
|
117
|
+
RUN npm ci --only=production && npm cache clean --force
|
|
118
|
+
|
|
119
|
+
# Copy source
|
|
120
|
+
COPY . .
|
|
121
|
+
|
|
122
|
+
# Build application
|
|
123
|
+
RUN ${buildCommand}
|
|
124
|
+
|
|
125
|
+
# Final stage
|
|
126
|
+
FROM node:18-alpine
|
|
127
|
+
|
|
128
|
+
WORKDIR /app
|
|
129
|
+
|
|
130
|
+
# Install health check utility
|
|
131
|
+
RUN apk add --no-cache curl
|
|
132
|
+
|
|
133
|
+
# Copy built application
|
|
134
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
135
|
+
COPY --from=builder /app/dist ./dist
|
|
136
|
+
COPY --from=builder /app/package.json ./package.json
|
|
137
|
+
COPY --from=builder /app/feltdb.flow ./feltdb.flow
|
|
138
|
+
|
|
139
|
+
# Create data directory for Node runtime
|
|
140
|
+
RUN mkdir -p /data && chmod 755 /data
|
|
141
|
+
|
|
142
|
+
# Non-root user for security
|
|
143
|
+
RUN addgroup -g 1001 -S nodejs
|
|
144
|
+
RUN adduser -S nodejs -u 1001
|
|
145
|
+
USER nodejs
|
|
146
|
+
|
|
147
|
+
# Health check
|
|
148
|
+
HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=30s \\
|
|
149
|
+
CMD curl -f http://localhost:3000/health || exit 1
|
|
150
|
+
|
|
151
|
+
# Startup
|
|
152
|
+
ENV NODE_ENV=production
|
|
153
|
+
EXPOSE 3000
|
|
154
|
+
|
|
155
|
+
CMD ["node", "dist/index.js"]
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
export function generateDockerIgnore() {
|
|
159
|
+
return `node_modules
|
|
160
|
+
npm-debug.log
|
|
161
|
+
dist
|
|
162
|
+
build
|
|
163
|
+
.git
|
|
164
|
+
.gitignore
|
|
165
|
+
.env
|
|
166
|
+
.env.local
|
|
167
|
+
.env.*.local
|
|
168
|
+
.DS_Store
|
|
169
|
+
*.log
|
|
170
|
+
.feltdb/keys.json
|
|
171
|
+
.feltdb/connection.json
|
|
172
|
+
.nextc
|
|
173
|
+
.next
|
|
174
|
+
coverage
|
|
175
|
+
.nyc_output
|
|
176
|
+
.cache
|
|
177
|
+
.turbo
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
export function generateDotEnvLocal(config) {
|
|
181
|
+
return `# Docker Compose Environment
|
|
182
|
+
# Generated by create-feltdb for \${config.appName}
|
|
183
|
+
|
|
184
|
+
COMPOSE_PROJECT_NAME=${config.appName.toLowerCase().replace(/[^a-z0-9]/g, '-')}
|
|
185
|
+
|
|
186
|
+
# FeltDB Configuration
|
|
187
|
+
FELTDB_APP_NAME=${config.appName}
|
|
188
|
+
FELTDB_APP_ID=${config.appId}
|
|
189
|
+
FELTDB_VERSION=${config.version}
|
|
190
|
+
FELTDB_API_PORT=${config.port}
|
|
191
|
+
FELTDB_REPLICATION_PORT=${config.replicationPort}
|
|
192
|
+
FELTDB_PEERS=
|
|
193
|
+
|
|
194
|
+
# Application Configuration
|
|
195
|
+
APP_PORT=3000
|
|
196
|
+
FELTDB_API_URL=http://feltdb:7700
|
|
197
|
+
|
|
198
|
+
# Studio Configuration
|
|
199
|
+
STUDIO_PORT=${config.studioPort}
|
|
200
|
+
|
|
201
|
+
# Runtime
|
|
202
|
+
NODE_ENV=production
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
export function generateHealthCheckScript() {
|
|
206
|
+
return `#!/bin/bash
|
|
207
|
+
|
|
208
|
+
# Multi-service health check orchestration
|
|
209
|
+
# Ensures all services are ready before marking deployment as healthy
|
|
210
|
+
|
|
211
|
+
set -e
|
|
212
|
+
|
|
213
|
+
TIMEOUT=60
|
|
214
|
+
ELAPSED=0
|
|
215
|
+
INTERVAL=2
|
|
216
|
+
|
|
217
|
+
echo "[Health Check] Starting orchestration check (timeout: \${TIMEOUT}s)"
|
|
218
|
+
|
|
219
|
+
# Wait for FeltDB
|
|
220
|
+
echo "[Health Check] Checking FeltDB service..."
|
|
221
|
+
while [ \$ELAPSED -lt \$TIMEOUT ]; do
|
|
222
|
+
if curl -s -f http://feltdb:7700/health > /dev/null 2>&1; then
|
|
223
|
+
STATE=\$(curl -s http://feltdb:7700/health | grep -o '"state":"[^"]*' | cut -d'"' -f4)
|
|
224
|
+
if [ "\$STATE" = "ready" ] || [ "\$STATE" = "recovering" ]; then
|
|
225
|
+
echo "[Health Check] FeltDB is \$STATE"
|
|
226
|
+
break
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
sleep \$INTERVAL
|
|
230
|
+
ELAPSED=\$((ELAPSED + INTERVAL))
|
|
231
|
+
done
|
|
232
|
+
|
|
233
|
+
if [ \$ELAPSED -ge \$TIMEOUT ]; then
|
|
234
|
+
echo "[Health Check] FAILED: FeltDB did not respond within timeout"
|
|
235
|
+
exit 1
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# Wait for application
|
|
239
|
+
echo "[Health Check] Checking application service..."
|
|
240
|
+
ELAPSED=0
|
|
241
|
+
while [ \$ELAPSED -lt \$TIMEOUT ]; do
|
|
242
|
+
if curl -s -f http://app:3000/health > /dev/null 2>&1; then
|
|
243
|
+
echo "[Health Check] Application is ready"
|
|
244
|
+
break
|
|
245
|
+
fi
|
|
246
|
+
sleep \$INTERVAL
|
|
247
|
+
ELAPSED=\$((ELAPSED + INTERVAL))
|
|
248
|
+
done
|
|
249
|
+
|
|
250
|
+
if [ \$ELAPSED -ge \$TIMEOUT ]; then
|
|
251
|
+
echo "[Health Check] FAILED: Application did not respond within timeout"
|
|
252
|
+
exit 1
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
echo "[Health Check] All services healthy - deployment ready"
|
|
256
|
+
exit 0
|
|
257
|
+
`;
|
|
258
|
+
}
|
package/dist/package-versions.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.4.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.4.2';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-specific templates and configuration
|
|
3
|
+
*
|
|
4
|
+
* Generates runtime-appropriate file structure and configuration
|
|
5
|
+
* for Browser, Node, and Self-hosted deployment modes.
|
|
6
|
+
*/
|
|
7
|
+
export function generateBrowserRuntime(config) {
|
|
8
|
+
return {
|
|
9
|
+
feltdbInit: `import { createFeltDB } from '@feltdb/core';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Browser Runtime
|
|
13
|
+
*
|
|
14
|
+
* Local-first FeltDB with durable IndexedDB storage.
|
|
15
|
+
* No server required. Full offline capability.
|
|
16
|
+
*
|
|
17
|
+
* AppID: ${config.appId}
|
|
18
|
+
*/
|
|
19
|
+
export const db = createFeltDB({
|
|
20
|
+
namespace: '${config.appName}',
|
|
21
|
+
runtime: 'browser',
|
|
22
|
+
storage: {
|
|
23
|
+
type: 'indexeddb',
|
|
24
|
+
durable: true,
|
|
25
|
+
},
|
|
26
|
+
${config.distributed ? `
|
|
27
|
+
replication: {
|
|
28
|
+
enabled: false, // Browser cannot initiate replication
|
|
29
|
+
},` : ''}
|
|
30
|
+
health: {
|
|
31
|
+
onStateChange: (state: 'starting' | 'recovering' | 'ready' | 'failed') => {
|
|
32
|
+
console.log(\`[FeltDB Health] Browser state changed: \${state}\`);
|
|
33
|
+
window.dispatchEvent(new CustomEvent('feltdb:health', { detail: { state } }));
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Collections
|
|
39
|
+
export const documents = db.collection('documents');
|
|
40
|
+
export const reports = db.collection('reports');
|
|
41
|
+
`,
|
|
42
|
+
envExample: `# Browser Runtime
|
|
43
|
+
# No environment configuration needed.
|
|
44
|
+
# Data persists locally in IndexedDB.
|
|
45
|
+
NODE_ENV=development
|
|
46
|
+
`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function generateNodeRuntime(config) {
|
|
50
|
+
return {
|
|
51
|
+
feltdbInit: `import { createFeltDB } from '@feltdb/core';
|
|
52
|
+
import path from 'path';
|
|
53
|
+
import { fileURLToPath } from 'url';
|
|
54
|
+
import fs from 'fs';
|
|
55
|
+
|
|
56
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Node Runtime
|
|
60
|
+
*
|
|
61
|
+
* Server-side FeltDB with durable file-based storage.
|
|
62
|
+
* Can participate in distributed replication.
|
|
63
|
+
* Persistent operation log to /data directory.
|
|
64
|
+
*
|
|
65
|
+
* AppID: ${config.appId}
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
const dataDir = process.env.FELTDB_DATA_DIR || path.join(__dirname, '..', 'data');
|
|
69
|
+
if (!fs.existsSync(dataDir)) {
|
|
70
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const db = createFeltDB({
|
|
74
|
+
namespace: '${config.appName}',
|
|
75
|
+
runtime: 'node',
|
|
76
|
+
storage: {
|
|
77
|
+
type: 'file',
|
|
78
|
+
path: dataDir,
|
|
79
|
+
durable: true,
|
|
80
|
+
fsync: true,
|
|
81
|
+
},
|
|
82
|
+
${config.distributed ? `
|
|
83
|
+
replication: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
port: parseInt(process.env.FELTDB_REPLICATION_PORT || '9000'),
|
|
86
|
+
},` : ''}
|
|
87
|
+
health: {
|
|
88
|
+
onStateChange: (state: 'starting' | 'recovering' | 'ready' | 'failed') => {
|
|
89
|
+
console.log(\`[FeltDB Health] Node state changed: \${state}\`);
|
|
90
|
+
// Emit to monitoring system
|
|
91
|
+
process.emit('feltdb:health', { state, timestamp: Date.now() });
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Collections
|
|
97
|
+
export const documents = db.collection('documents');
|
|
98
|
+
export const reports = db.collection('reports');
|
|
99
|
+
|
|
100
|
+
// Startup hook
|
|
101
|
+
export async function startup() {
|
|
102
|
+
console.log(\`[FeltDB] Starting Node runtime\`);
|
|
103
|
+
console.log(\`[FeltDB] App: ${config.appName} (${config.appId})\`);
|
|
104
|
+
console.log(\`[FeltDB] Version: ${config.version}\`);
|
|
105
|
+
console.log(\`[FeltDB] Data directory: \${dataDir}\`);
|
|
106
|
+
${config.distributed ? `console.log(\`[FeltDB] Replication enabled on port \${process.env.FELTDB_REPLICATION_PORT || 9000}\`);` : ''}
|
|
107
|
+
await db.ready();
|
|
108
|
+
console.log(\`[FeltDB] Ready\`);
|
|
109
|
+
}
|
|
110
|
+
`,
|
|
111
|
+
envExample: `# Node Runtime
|
|
112
|
+
FELTDB_DATA_DIR=./data
|
|
113
|
+
FELTDB_REPLICATION_PORT=9000
|
|
114
|
+
NODE_ENV=development
|
|
115
|
+
`,
|
|
116
|
+
startupScript: `#!/usr/bin/env node
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Node runtime startup orchestration
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
import { db, startup } from './feltdb.js';
|
|
123
|
+
|
|
124
|
+
async function main() {
|
|
125
|
+
try {
|
|
126
|
+
await startup();
|
|
127
|
+
|
|
128
|
+
// Start HTTP server for API
|
|
129
|
+
const { createServer } = await import('./server.js');
|
|
130
|
+
const server = createServer(db);
|
|
131
|
+
const port = process.env.FELTDB_API_PORT || 7700;
|
|
132
|
+
server.listen(port, () => {
|
|
133
|
+
console.log(\`[HTTP] API server listening on port \${port}\`);
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('[FeltDB] Startup failed:', err);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main();
|
|
142
|
+
`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export function generateSelfHostedRuntime(config) {
|
|
146
|
+
return {
|
|
147
|
+
feltdbInit: `import { createFeltDB } from '@feltdb/core';
|
|
148
|
+
import path from 'path';
|
|
149
|
+
import { fileURLToPath } from 'url';
|
|
150
|
+
import fs from 'fs';
|
|
151
|
+
|
|
152
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Self-hosted Runtime
|
|
156
|
+
*
|
|
157
|
+
* Containerized FeltDB with Docker Compose orchestration.
|
|
158
|
+
* Durable file-based storage with persistent volumes.
|
|
159
|
+
* Full replication topology with multi-node support.
|
|
160
|
+
*
|
|
161
|
+
* AppID: ${config.appId}
|
|
162
|
+
* Version: ${config.version}
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
const dataDir = process.env.FELTDB_DATA_DIR || '/data';
|
|
166
|
+
if (!fs.existsSync(dataDir)) {
|
|
167
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const db = createFeltDB({
|
|
171
|
+
namespace: '${config.appName}',
|
|
172
|
+
runtime: 'self-hosted',
|
|
173
|
+
storage: {
|
|
174
|
+
type: 'file',
|
|
175
|
+
path: dataDir,
|
|
176
|
+
durable: true,
|
|
177
|
+
fsync: true,
|
|
178
|
+
},
|
|
179
|
+
replication: {
|
|
180
|
+
enabled: true,
|
|
181
|
+
port: 9000,
|
|
182
|
+
peers: (process.env.FELTDB_PEERS || '').split(',').filter(Boolean),
|
|
183
|
+
},
|
|
184
|
+
health: {
|
|
185
|
+
onStateChange: (state: 'starting' | 'recovering' | 'ready' | 'failed') => {
|
|
186
|
+
console.log(\`[\${new Date().toISOString()}] [FeltDB] \${state.toUpperCase()}\`);
|
|
187
|
+
// Structured logging for container orchestration
|
|
188
|
+
process.emit('feltdb:health', { state, timestamp: Date.now() });
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Collections
|
|
194
|
+
export const documents = db.collection('documents');
|
|
195
|
+
export const reports = db.collection('reports');
|
|
196
|
+
|
|
197
|
+
// Startup with health check loop
|
|
198
|
+
export async function startup() {
|
|
199
|
+
console.log(\`[\${new Date().toISOString()}] [FeltDB] Container starting\`);
|
|
200
|
+
console.log(\`[FeltDB] App: ${config.appName} (${config.appId})\`);
|
|
201
|
+
console.log(\`[FeltDB] Version: ${config.version}\`);
|
|
202
|
+
console.log(\`[FeltDB] Runtime: self-hosted\`);
|
|
203
|
+
console.log(\`[FeltDB] Data directory: \${dataDir}\`);
|
|
204
|
+
|
|
205
|
+
await db.ready();
|
|
206
|
+
console.log(\`[\${new Date().toISOString()}] [FeltDB] READY\`);
|
|
207
|
+
}
|
|
208
|
+
`,
|
|
209
|
+
envExample: `# Self-hosted Docker Runtime
|
|
210
|
+
FELTDB_DATA_DIR=/data
|
|
211
|
+
FELTDB_APP_NAME=${config.appName}
|
|
212
|
+
FELTDB_APP_ID=${config.appId}
|
|
213
|
+
FELTDB_VERSION=${config.version}
|
|
214
|
+
FELTDB_REPLICATION_PORT=9000
|
|
215
|
+
FELTDB_PEERS=
|
|
216
|
+
NODE_ENV=production
|
|
217
|
+
`,
|
|
218
|
+
healthCheck: `#!/bin/sh
|
|
219
|
+
|
|
220
|
+
# Health check for Docker container
|
|
221
|
+
# Verifies FeltDB is responding and not in RECOVERING state
|
|
222
|
+
|
|
223
|
+
HEALTH_URL="http://localhost:7700/health"
|
|
224
|
+
TIMEOUT=5
|
|
225
|
+
|
|
226
|
+
# Query health endpoint
|
|
227
|
+
RESPONSE=\$(curl -s -m \${TIMEOUT} \${HEALTH_URL} 2>/dev/null)
|
|
228
|
+
|
|
229
|
+
if [ -z "\$RESPONSE" ]; then
|
|
230
|
+
echo "UNHEALTHY: No response from health endpoint"
|
|
231
|
+
exit 1
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
STATE=\$(echo \$RESPONSE | grep -o '"state":"[^"]*' | cut -d'"' -f4)
|
|
235
|
+
|
|
236
|
+
case "\$STATE" in
|
|
237
|
+
ready)
|
|
238
|
+
echo "HEALTHY: State is ready"
|
|
239
|
+
exit 0
|
|
240
|
+
;;
|
|
241
|
+
recovering)
|
|
242
|
+
echo "STARTING: Recovering from disk"
|
|
243
|
+
exit 0
|
|
244
|
+
;;
|
|
245
|
+
starting)
|
|
246
|
+
echo "STARTING: Initializing"
|
|
247
|
+
exit 0
|
|
248
|
+
;;
|
|
249
|
+
failed)
|
|
250
|
+
echo "UNHEALTHY: State is failed"
|
|
251
|
+
exit 1
|
|
252
|
+
;;
|
|
253
|
+
*)
|
|
254
|
+
echo "UNKNOWN: State is \$STATE"
|
|
255
|
+
exit 2
|
|
256
|
+
;;
|
|
257
|
+
esac
|
|
258
|
+
`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export function getTemplate(config) {
|
|
262
|
+
switch (config.runtime) {
|
|
263
|
+
case 'browser':
|
|
264
|
+
return generateBrowserRuntime(config);
|
|
265
|
+
case 'node':
|
|
266
|
+
return generateNodeRuntime(config);
|
|
267
|
+
case 'self-hosted':
|
|
268
|
+
return generateSelfHostedRuntime(config);
|
|
269
|
+
default:
|
|
270
|
+
throw new Error(`Unknown runtime: ${config.runtime}`);
|
|
271
|
+
}
|
|
272
|
+
}
|