@feltdb/core 0.7.3 → 0.8.0
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 +25 -0
- package/dist/application-development.d.ts +78 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +153 -0
- package/dist/application-manifest.d.ts +6 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +27 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +46 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/cli.js +1 -1
- package/dist/cli/commands.js +664 -120
- package/dist/cli/index.js +1 -1
- package/dist/cli/managed-environment.js +6 -0
- package/dist/cli/managed-project.js +6 -0
- package/dist/cli/proposal-client.js +63 -0
- package/dist/cli/publish-engine.js +165 -0
- package/dist/cli/source-sync.js +183 -0
- package/dist/create/cli.js +11 -2
- package/dist/create/create.js +38 -16
- package/dist/create/managed-account.js +82 -11
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/create/template/default-project/README.md +57 -0
- package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
- package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
- package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
- package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
- package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
- package/dist/create/template/default-project/feltdb.flow +174 -0
- package/dist/create/template/default-project/src/App.tsx +20 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
- package/dist/create/template/default-project/src/feltdb.ts +87 -0
- package/dist/create/template/default-project/src/index.tsx +14 -0
- package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
- package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
- package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
- package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
- package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
- package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
- package/dist/create/template/default-project/src/styles-application.css +21 -0
- package/dist/create/template/default-project/src/styles.css +1 -0
- package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
- package/dist/create/template/default-project/workflows/invitation.ts +30 -0
- package/dist/create/template/default-project/workflows/project-created.ts +2 -0
- package/dist/db.d.ts +49 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +77 -0
- package/dist/error-codes.d.ts +20 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +25 -0
- package/dist/flowspec.d.ts +1 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +66 -11
- package/dist/http-client.d.ts +135 -14
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +79 -31
- package/dist/http-db.d.ts +15 -2
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +113 -5
- package/dist/index-core.d.ts +5 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +5 -0
- package/dist/module-intent.d.ts +37 -0
- package/dist/module-intent.d.ts.map +1 -0
- package/dist/module-intent.js +81 -0
- package/dist/module.d.ts +76 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +61 -0
- package/dist/proposal.d.ts +161 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +232 -0
- package/dist/studio/app.d.ts +5 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +21 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ContractInspector.d.ts +7 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
- package/dist/studio/components/ManagedInstancePanel.js +1 -5
- package/dist/studio/components/ModuleExplorer.d.ts +6 -0
- package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +12 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/StateExplorer.d.ts +6 -7
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.d.ts +4 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/components-CNSNrmA9.js +2787 -0
- package/dist/studio/index.js +214 -157
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/index.d.ts +2 -0
- package/dist/studio/utils/index.d.ts.map +1 -1
- package/dist/studio/utils/index.js +14 -12
- package/dist/studio/utils/managed-instance.d.ts +0 -4
- package/dist/studio/utils/managed-instance.d.ts.map +1 -1
- package/dist/studio/utils/managed-instance.js +0 -4
- package/dist/studio/utils/proposal-api.d.ts +53 -0
- package/dist/studio/utils/proposal-api.d.ts.map +1 -0
- package/dist/studio/utils/proposal-api.js +91 -0
- package/dist/studio/utils/service-api.d.ts +81 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -0
- package/dist/studio/utils/service-api.js +51 -0
- package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
- package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
- package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
- package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +3 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +124 -2
- package/package.json +1 -1
- package/dist/studio/components-Dxuhrv8_.js +0 -1832
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
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.
|
|
26
|
+
const VERSION = '0.8.0';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function resolveManagedAuthorityEnvironment(environment) {
|
|
2
|
+
environment.VITE_FELTDB_URL || (environment.VITE_FELTDB_URL = environment.VITE_FELTDB_MANAGED_URL);
|
|
3
|
+
if (!environment.VITE_FELTDB_URL) {
|
|
4
|
+
throw new Error('Managed runtime requires VITE_FELTDB_MANAGED_URL in .env.local. Re-run managed setup or add your managed connection.');
|
|
5
|
+
}
|
|
6
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseFlowSpec } from '@feltdb/core';
|
|
4
|
+
export function developmentProposalConnection(cwd = process.cwd()) {
|
|
5
|
+
const file = path.join(cwd, '.feltdb', 'connections.json');
|
|
6
|
+
const stored = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')).developmentAuthority : undefined;
|
|
7
|
+
const endpoint = process.env.VITE_FELTDB_URL || process.env.VITE_FELTDB_MANAGED_URL || stored?.endpoint;
|
|
8
|
+
if (!endpoint)
|
|
9
|
+
throw new Error('No running FeltDB proposal service. Start feltdb dev.');
|
|
10
|
+
const config = fs.existsSync(path.join(cwd, 'feltdb.config.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, 'feltdb.config.json'), 'utf8')) : {};
|
|
11
|
+
const published = fs.existsSync(path.join(cwd, '.feltdb', 'last-published.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, '.feltdb', 'last-published.json'), 'utf8')) : {};
|
|
12
|
+
const managed = fs.existsSync(path.join(cwd, '.feltdb', 'managed.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, '.feltdb', 'managed.json'), 'utf8')) : {};
|
|
13
|
+
const flowApplication = fs.existsSync(path.join(cwd, 'feltdb.flow')) ? parseFlowSpec(fs.readFileSync(path.join(cwd, 'feltdb.flow'), 'utf8')).app : undefined;
|
|
14
|
+
return { endpoint: endpoint.replace(/\/$/, ''), proposalAuthorityToken: stored?.proposalAuthorityToken,
|
|
15
|
+
applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || managed.applicationId || published.applicationId || config.applicationId || flowApplication,
|
|
16
|
+
environment: managed.environment || published.environment || 'local' };
|
|
17
|
+
}
|
|
18
|
+
export class ProposalServiceClient {
|
|
19
|
+
constructor(connection, token = process.env.FELTDB_MANAGED_CONTROL_API_KEY || '') {
|
|
20
|
+
this.connection = connection;
|
|
21
|
+
this.token = token;
|
|
22
|
+
}
|
|
23
|
+
async request(pathname, init = {}) {
|
|
24
|
+
const response = await fetch(`${this.connection.endpoint}${pathname}`, { ...init, headers: {
|
|
25
|
+
...(init.body ? { 'content-type': 'application/json' } : {}), ...(this.token ? { authorization: `Bearer ${this.token}` } : {}), ...init.headers,
|
|
26
|
+
} });
|
|
27
|
+
const body = await response.json().catch(() => ({}));
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new Error(body.error || body.message || `Proposal service failed (${response.status})`);
|
|
30
|
+
return body;
|
|
31
|
+
}
|
|
32
|
+
scoped(pathname) {
|
|
33
|
+
const query = new URLSearchParams();
|
|
34
|
+
if (this.connection.applicationId)
|
|
35
|
+
query.set('application_id', this.connection.applicationId);
|
|
36
|
+
if (this.connection.environment)
|
|
37
|
+
query.set('environment', this.connection.environment);
|
|
38
|
+
return `${pathname}?${query}`;
|
|
39
|
+
}
|
|
40
|
+
actor(input = {}) { return { application_id: this.connection.applicationId, environment: this.connection.environment, ...input }; }
|
|
41
|
+
create(input) { return this.request('/v1/proposals', { method: 'POST', body: JSON.stringify(input) }); }
|
|
42
|
+
list(filters = {}) { const query = new URLSearchParams(); if (this.connection.applicationId)
|
|
43
|
+
query.set('application_id', this.connection.applicationId); query.set('environment', filters.environment || this.connection.environment || 'local'); if (filters.status)
|
|
44
|
+
query.set('status', filters.status); if (filters.limit)
|
|
45
|
+
query.set('limit', String(filters.limit)); if (filters.cursor)
|
|
46
|
+
query.set('cursor', filters.cursor); return this.request(`/v1/proposals?${query}`); }
|
|
47
|
+
get(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}`)); }
|
|
48
|
+
history(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}/history`)); }
|
|
49
|
+
status(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}/status`)); }
|
|
50
|
+
validate(id, actor = 'local-developer', proposedManifest) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/validate`, { method: 'POST', body: JSON.stringify(this.actor({ actor, proposed_manifest: proposedManifest })) }); }
|
|
51
|
+
preview(id, actor = 'local-developer') { return this.request(`/v1/proposals/${encodeURIComponent(id)}/preview`, { method: 'POST', body: JSON.stringify(this.actor({ actor })) }); }
|
|
52
|
+
getPreview(id) { return this.request(this.scoped(`/v1/previews/${encodeURIComponent(id)}`)); }
|
|
53
|
+
approve(id, actor = 'local-developer', approveAuthorization = false) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/approve`, { method: 'POST', body: JSON.stringify(this.actor({ actor, approve_authorization: approveAuthorization })) }); }
|
|
54
|
+
reject(id, actor = 'local-developer', reason) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/reject`, { method: 'POST', body: JSON.stringify(this.actor({ actor, reason })) }); }
|
|
55
|
+
transition(id, status, actor = 'local-developer', repositoryAuthority = false, approveAuthorization = false) {
|
|
56
|
+
return this.request(`/v1/proposals/${encodeURIComponent(id)}/status`, { method: 'PATCH', headers: repositoryAuthority && this.connection.proposalAuthorityToken
|
|
57
|
+
? { 'FeltDB-Proposal-Authority': this.connection.proposalAuthorityToken, 'FeltDB-Repository-Apply': '1' } : {}, body: JSON.stringify(this.actor({ status, actor, approve_authorization: approveAuthorization })) });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function proposalService(cwd = process.cwd(), endpoint) {
|
|
61
|
+
const connection = developmentProposalConnection(cwd);
|
|
62
|
+
return new ProposalServiceClient(endpoint ? { ...connection, endpoint } : connection);
|
|
63
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { ApplicationManifestClient, canonicalApplicationBytes, flowSpecToManifest, parseFlowSpec, validateFlowSpec } from '@feltdb/core';
|
|
6
|
+
function sha256(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
7
|
+
function loadEnv(file) {
|
|
8
|
+
const values = {};
|
|
9
|
+
if (!fs.existsSync(file))
|
|
10
|
+
return values;
|
|
11
|
+
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
12
|
+
const line = raw.trim();
|
|
13
|
+
if (!line || line.startsWith('#'))
|
|
14
|
+
continue;
|
|
15
|
+
const separator = line.indexOf('=');
|
|
16
|
+
if (separator < 1)
|
|
17
|
+
continue;
|
|
18
|
+
values[line.slice(0, separator).trim()] = line.slice(separator + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
19
|
+
}
|
|
20
|
+
return values;
|
|
21
|
+
}
|
|
22
|
+
export function discoverPublishConfiguration(cwd = process.cwd(), environmentOverride) {
|
|
23
|
+
const configFile = path.join(cwd, 'feltdb.config.json');
|
|
24
|
+
if (!fs.existsSync(configFile))
|
|
25
|
+
throw new Error('feltdb.config.json is required for publish');
|
|
26
|
+
const project = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
27
|
+
const file = loadEnv(path.join(cwd, '.env.local'));
|
|
28
|
+
const managedFile = path.join(cwd, '.feltdb', 'managed.json');
|
|
29
|
+
const managed = fs.existsSync(managedFile) ? JSON.parse(fs.readFileSync(managedFile, 'utf8')) : {};
|
|
30
|
+
const value = (name) => process.env[name] || file[name];
|
|
31
|
+
const configuration = {
|
|
32
|
+
url: value('VITE_FELTDB_MANAGED_URL') || managed.url, token: value('FELTDB_MANAGED_CONTROL_API_KEY'),
|
|
33
|
+
tenantId: managed.tenantId || value('VITE_FELTDB_MANAGED_TENANT_ID'), applicationId: value('VITE_FELTDB_MANAGED_APPLICATION_ID') || managed.applicationId,
|
|
34
|
+
namespace: managed.namespace || value('VITE_FELTDB_MANAGED_NAMESPACE') || project.namespace,
|
|
35
|
+
environment: environmentOverride || managed.environment || value('VITE_FELTDB_MANAGED_ENVIRONMENT') || 'production',
|
|
36
|
+
};
|
|
37
|
+
const missing = Object.entries(configuration).filter(([, value]) => !value).map(([name]) => name);
|
|
38
|
+
if (missing.length)
|
|
39
|
+
throw new Error(`Managed publish configuration is incomplete in .env.local: ${missing.join(', ')}`);
|
|
40
|
+
return configuration;
|
|
41
|
+
}
|
|
42
|
+
function filesBelow(root, directory) {
|
|
43
|
+
const target = path.join(root, directory);
|
|
44
|
+
if (!fs.existsSync(target))
|
|
45
|
+
return [];
|
|
46
|
+
return fs.readdirSync(target, { withFileTypes: true }).flatMap(entry => {
|
|
47
|
+
const relative = path.join(directory, entry.name);
|
|
48
|
+
if (entry.isSymbolicLink())
|
|
49
|
+
throw new Error(`Publish artifacts cannot be symbolic links: ${relative}`);
|
|
50
|
+
return entry.isDirectory() ? filesBelow(root, relative) : entry.isFile() ? [relative] : [];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function buildPublishBundle(cwd, configuration) {
|
|
54
|
+
const flowPath = path.join(cwd, 'feltdb.flow');
|
|
55
|
+
if (!fs.existsSync(flowPath))
|
|
56
|
+
throw new Error('feltdb.flow is required for publish');
|
|
57
|
+
const flowSource = fs.readFileSync(flowPath, 'utf8');
|
|
58
|
+
const spec = parseFlowSpec(flowSource);
|
|
59
|
+
const diagnostics = validateFlowSpec(spec).filter(value => value.severity === 'error');
|
|
60
|
+
if (diagnostics.length)
|
|
61
|
+
throw new Error(`Publish validation failed: ${diagnostics.map(value => value.message).join('; ')}`);
|
|
62
|
+
const deployable = ['feltdb.flow', ...['workflows', 'agents', 'capabilities'].flatMap(directory => filesBelow(cwd, directory))].sort();
|
|
63
|
+
const artifacts = deployable.map(relative => {
|
|
64
|
+
const content = Buffer.from(fs.readFileSync(path.join(cwd, relative)));
|
|
65
|
+
const mediaType = relative.endsWith('.flow') ? 'text/x-feltdb-flow' : relative.endsWith('.json') ? 'application/json' : relative.endsWith('.ts') ? 'text/typescript' : 'text/plain';
|
|
66
|
+
return { path: relative.split(path.sep).join('/'), hash: `sha256:${sha256(content)}`, bytes: content.byteLength, mediaType, content: content.toString('base64') };
|
|
67
|
+
});
|
|
68
|
+
const artifactHash = `sha256:${sha256(artifacts.map(value => `${value.path}\0${value.hash}`).join('\n'))}`;
|
|
69
|
+
const manifest = flowSpecToManifest(spec, configuration.tenantId, configuration.applicationId);
|
|
70
|
+
const contractHash = (value) => `sha256:${sha256(Buffer.from(canonicalApplicationBytes(value)))}`;
|
|
71
|
+
manifest.metadata.labels = {
|
|
72
|
+
...(manifest.metadata.labels || {}),
|
|
73
|
+
'feltdb.dsl_version': String(spec.version),
|
|
74
|
+
'feltdb.model_hash': contractHash({ collections: manifest.collections, indexes: manifest.indexes, references: manifest.references }),
|
|
75
|
+
'feltdb.authorization_hash': contractHash(manifest.policies),
|
|
76
|
+
'feltdb.workflow_hash': contractHash({ workflows: manifest.workflows, triggers: manifest.triggers, schedules: manifest.schedules }),
|
|
77
|
+
'feltdb.agent_hash': contractHash(manifest.agents),
|
|
78
|
+
'feltdb.capability_hash': contractHash(manifest.capabilities),
|
|
79
|
+
'feltdb.contract_hash': contractHash({ dsl_version: spec.version, manifest }),
|
|
80
|
+
'feltdb.flow_hash': `sha256:${sha256(flowSource.replace(/\r\n/g, '\n'))}`,
|
|
81
|
+
'feltdb.artifact_hash': artifactHash,
|
|
82
|
+
'feltdb.namespace': configuration.namespace,
|
|
83
|
+
};
|
|
84
|
+
manifest.artifacts = artifacts.map(value => ({ name: value.path, uri: `data:${value.mediaType};base64,${value.content}`, media_type: value.mediaType }));
|
|
85
|
+
const git = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
86
|
+
const sourceRevision = git.status === 0 ? git.stdout.trim() : undefined;
|
|
87
|
+
if (sourceRevision)
|
|
88
|
+
manifest.metadata.labels['feltdb.source_revision'] = sourceRevision;
|
|
89
|
+
return { manifest, artifacts, artifactHash, sourceRevision };
|
|
90
|
+
}
|
|
91
|
+
function sameDefinition(left, right) {
|
|
92
|
+
return left.metadata.labels?.['feltdb.artifact_hash'] === right.metadata.labels?.['feltdb.artifact_hash'];
|
|
93
|
+
}
|
|
94
|
+
function activeRevision(revisions, pointers, environment) {
|
|
95
|
+
const id = pointers[environment];
|
|
96
|
+
return revisions.find(value => value.revision_id === id);
|
|
97
|
+
}
|
|
98
|
+
function describeChanges(current, bundle) {
|
|
99
|
+
if (!current)
|
|
100
|
+
return bundle.artifacts.map(value => `added ${value.path}`);
|
|
101
|
+
const old = new Map(current.manifest.artifacts.map(value => [value.name, value.uri]));
|
|
102
|
+
const next = new Map(bundle.manifest.artifacts.map(value => [value.name, value.uri]));
|
|
103
|
+
return [...new Set([...old.keys(), ...next.keys()])].sort().flatMap(name => !old.has(name) ? [`added ${name}`] : !next.has(name) ? [`removed ${name}`] : old.get(name) !== next.get(name) ? [`modified ${name}`] : []);
|
|
104
|
+
}
|
|
105
|
+
export async function publishApplication(options = {}) {
|
|
106
|
+
const cwd = options.cwd || process.cwd();
|
|
107
|
+
const log = options.log || console.log;
|
|
108
|
+
const configuration = discoverPublishConfiguration(cwd, options.environment);
|
|
109
|
+
const bundle = buildPublishBundle(cwd, configuration);
|
|
110
|
+
const client = new ApplicationManifestClient(configuration.url.replace(/\/$/, ''), configuration.token);
|
|
111
|
+
const cloud = await client.revisions(configuration.applicationId);
|
|
112
|
+
const current = activeRevision(cloud.revisions, cloud.environment_pointers, configuration.environment);
|
|
113
|
+
const changes = describeChanges(current, bundle);
|
|
114
|
+
log(options.dryRun ? 'FeltDB Publish Preview' : options.status ? 'FeltDB Publish Status' : 'FeltDB Publish');
|
|
115
|
+
log(`Application: ${bundle.manifest.metadata.name}`);
|
|
116
|
+
log(`Environment: ${configuration.environment}`);
|
|
117
|
+
log(`Namespace: ${configuration.namespace}`);
|
|
118
|
+
if (options.status) {
|
|
119
|
+
log(`Local: ${bundle.artifactHash}`);
|
|
120
|
+
log(`Cloud version: ${current?.revision_number ?? 'none'}`);
|
|
121
|
+
log(current && sameDefinition(current.manifest, bundle.manifest) ? 'Status: ✓ Synchronized' : 'Status: ↑ Changes ready to publish');
|
|
122
|
+
return { status: current && sameDefinition(current.manifest, bundle.manifest) ? 'synchronized' : 'changes', current, bundle };
|
|
123
|
+
}
|
|
124
|
+
if (current && sameDefinition(current.manifest, bundle.manifest)) {
|
|
125
|
+
log(`✓ Local application matches cloud version ${current.revision_number}.`);
|
|
126
|
+
log('Nothing to publish.');
|
|
127
|
+
return { status: 'unchanged', current, bundle };
|
|
128
|
+
}
|
|
129
|
+
log('Changes:');
|
|
130
|
+
changes.forEach(value => log(` ${value}`));
|
|
131
|
+
if (!options.noBuild) {
|
|
132
|
+
const packageFile = path.join(cwd, 'package.json');
|
|
133
|
+
const packageManifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
134
|
+
if (!packageManifest.scripts?.build)
|
|
135
|
+
throw new Error('package.json must define a build script for publish');
|
|
136
|
+
const result = spawnSync('npm', ['run', 'build'], { cwd, stdio: 'inherit' });
|
|
137
|
+
if (result.error)
|
|
138
|
+
throw result.error;
|
|
139
|
+
if (result.status !== 0)
|
|
140
|
+
throw new Error(`Application build failed with exit code ${result.status}`);
|
|
141
|
+
}
|
|
142
|
+
const draft = await client.createDraft(configuration.applicationId, current?.revision_id);
|
|
143
|
+
const updated = await client.updateDraft(configuration.applicationId, draft, bundle.manifest);
|
|
144
|
+
const validation = await client.validateDraft(configuration.applicationId, updated.draft_id);
|
|
145
|
+
if (!validation.valid)
|
|
146
|
+
throw new Error(`Publish validation failed. No cloud changes were activated. ${validation.issues.map(value => `${value.path}: ${value.message}`).join('; ')}`);
|
|
147
|
+
if (options.dryRun) {
|
|
148
|
+
log(`Validation: ✓ ${validation.compatibility}`);
|
|
149
|
+
log('No changes have been published.');
|
|
150
|
+
return { status: 'preview', current, bundle, validation };
|
|
151
|
+
}
|
|
152
|
+
if (!options.yes && !(await options.confirm?.('Publish these changes?')))
|
|
153
|
+
throw new Error('Publish cancelled');
|
|
154
|
+
const revision = await client.commitDraft(configuration.applicationId, updated.draft_id);
|
|
155
|
+
const diff = await client.diff(configuration.applicationId, revision.revision_id, current?.revision_id);
|
|
156
|
+
if ((diff.classification === 'BREAKING' || diff.classification === 'MIGRATION_REQUIRED' || diff.security_changes.length) && !options.approveDestructive) {
|
|
157
|
+
throw new Error(`Publish requires explicit approval before activation (${diff.classification}${diff.security_changes.length ? ', authorization boundary changed' : ''}). Immutable version ${revision.revision_number} was staged but the active cloud version was not changed.`);
|
|
158
|
+
}
|
|
159
|
+
const promoted = await client.promoteEnvironment(configuration.applicationId, configuration.environment, revision.revision_id, current?.revision_id, options.message || 'Published from feltdb CLI', Boolean(options.approveDestructive));
|
|
160
|
+
fs.mkdirSync(path.join(cwd, '.feltdb'), { recursive: true });
|
|
161
|
+
fs.writeFileSync(path.join(cwd, '.feltdb', 'last-published.json'), `${JSON.stringify({ applicationId: configuration.applicationId, tenantId: configuration.tenantId, namespace: configuration.namespace, environment: configuration.environment, version: revision.revision_number, revisionId: revision.revision_id, contractHash: bundle.manifest.metadata.labels?.['feltdb.contract_hash'], artifactHash: bundle.artifactHash, sourceRevision: bundle.sourceRevision, publishedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
162
|
+
log(`✓ Published immutable version ${revision.revision_number}`);
|
|
163
|
+
log(`✓ Activated in ${configuration.environment}`);
|
|
164
|
+
return { status: 'published', current, bundle, revision, diff, promoted };
|
|
165
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, } from '@feltdb/core';
|
|
6
|
+
import { parseFlowSpec } from '@feltdb/core';
|
|
7
|
+
const FLOW_FILE = 'feltdb.flow';
|
|
8
|
+
const GENERATED_FILE = 'src/feltdb/generated-contract.ts';
|
|
9
|
+
const SYNC_STATE = '.feltdb/source-sync.json';
|
|
10
|
+
function digest(value) {
|
|
11
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
12
|
+
}
|
|
13
|
+
function readJson(file) {
|
|
14
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : undefined;
|
|
15
|
+
}
|
|
16
|
+
function gitDirty(cwd, files) {
|
|
17
|
+
const result = spawnSync('git', ['status', '--porcelain', '--', ...files], { cwd, encoding: 'utf8' });
|
|
18
|
+
if (result.status !== 0)
|
|
19
|
+
return [];
|
|
20
|
+
return result.stdout.split(/\r?\n/).filter(Boolean).map(line => line.slice(3));
|
|
21
|
+
}
|
|
22
|
+
export async function repositoryContext(cwd = process.cwd(), plannedFiles = [FLOW_FILE, GENERATED_FILE]) {
|
|
23
|
+
const git = (args) => spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
24
|
+
const rootResult = git(['rev-parse', '--show-toplevel']);
|
|
25
|
+
const root = rootResult.status === 0 ? rootResult.stdout.trim() : path.resolve(cwd);
|
|
26
|
+
const branchResult = git(['branch', '--show-current']);
|
|
27
|
+
const commitResult = git(['rev-parse', 'HEAD']);
|
|
28
|
+
const context = await loadLocalDevelopmentContract(root);
|
|
29
|
+
return { root, branch: branchResult.status === 0 ? branchResult.stdout.trim() || undefined : undefined,
|
|
30
|
+
commit: commitResult.status === 0 ? commitResult.stdout.trim() : undefined, dirty: gitDirty(root, plannedFiles),
|
|
31
|
+
flowPath: path.join(root, FLOW_FILE), flowHash: context.flowHash };
|
|
32
|
+
}
|
|
33
|
+
export function assertProposalSourcePlan(sourcePlan) {
|
|
34
|
+
const allowed = new Set([FLOW_FILE, GENERATED_FILE]);
|
|
35
|
+
if (!sourcePlan?.files?.length)
|
|
36
|
+
throw new Error('SOURCE_PLAN_BLOCKED: Proposal has no source operations');
|
|
37
|
+
for (const operation of sourcePlan.files) {
|
|
38
|
+
if (!allowed.has(operation.path) || !['add', 'modify'].includes(operation.operation))
|
|
39
|
+
throw new Error(`SOURCE_PLAN_BLOCKED: unsupported ${operation.operation} operation for ${operation.path}`);
|
|
40
|
+
}
|
|
41
|
+
if (!sourcePlan.files.some(value => value.path === FLOW_FILE))
|
|
42
|
+
throw new Error(`SOURCE_PLAN_BLOCKED: ${FLOW_FILE} synchronization is required`);
|
|
43
|
+
}
|
|
44
|
+
export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
|
|
45
|
+
const flowPath = path.join(cwd, FLOW_FILE);
|
|
46
|
+
if (!fs.existsSync(flowPath))
|
|
47
|
+
throw new Error(`${FLOW_FILE} is required`);
|
|
48
|
+
const source = fs.readFileSync(flowPath, 'utf8');
|
|
49
|
+
const project = readJson(path.join(cwd, 'feltdb.config.json')) ?? {};
|
|
50
|
+
const managed = readJson(path.join(cwd, '.feltdb', 'managed.json')) ?? {};
|
|
51
|
+
const published = readJson(path.join(cwd, '.feltdb', 'last-published.json'));
|
|
52
|
+
const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID
|
|
53
|
+
|| managed.applicationId || published?.applicationId || project.applicationId || parseFlowSpec(source).app;
|
|
54
|
+
const tenantId = managed.tenantId || published?.tenantId || 'local';
|
|
55
|
+
const environment = managed.environment || published?.environment || 'local';
|
|
56
|
+
const contract = await compileApplicationContract(parseFlowSpec(source), tenantId, applicationId);
|
|
57
|
+
return {
|
|
58
|
+
source,
|
|
59
|
+
flowHash: await flowSourceHash(source),
|
|
60
|
+
snapshot: createContractSnapshot(contract, {
|
|
61
|
+
environment,
|
|
62
|
+
contractVersion: published?.version ?? 1,
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function reviewFlowProposal(proposal, cwd = process.cwd()) {
|
|
67
|
+
const current = await loadLocalDevelopmentContract(cwd);
|
|
68
|
+
return validateFlowProposal({ proposal, currentFlow: current.source, snapshot: current.snapshot });
|
|
69
|
+
}
|
|
70
|
+
function generatedContractSource(review) {
|
|
71
|
+
const snapshot = createContractSnapshot(review.proposedContract, {
|
|
72
|
+
environment: 'local',
|
|
73
|
+
contractVersion: review.proposal.baseContractHash === review.proposedContract.hashes.application ? 1 : 2,
|
|
74
|
+
});
|
|
75
|
+
const collections = review.proposedSpec.collections.map(collection => ({
|
|
76
|
+
name: collection.name,
|
|
77
|
+
fields: collection.fields.map(field => ({ name: field.name, type: field.type, optional: field.optional })),
|
|
78
|
+
}));
|
|
79
|
+
const typeName = (value) => {
|
|
80
|
+
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
81
|
+
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
82
|
+
};
|
|
83
|
+
const valueType = (value) => {
|
|
84
|
+
const normalized = value.toLowerCase().replace(/\?$/, '');
|
|
85
|
+
if (['int', 'integer', 'float', 'number'].includes(normalized))
|
|
86
|
+
return 'number';
|
|
87
|
+
if (['bool', 'boolean'].includes(normalized))
|
|
88
|
+
return 'boolean';
|
|
89
|
+
if (['json', 'object'].includes(normalized))
|
|
90
|
+
return 'unknown';
|
|
91
|
+
if (normalized.endsWith('[]'))
|
|
92
|
+
return `${valueType(normalized.slice(0, -2))}[]`;
|
|
93
|
+
return 'string';
|
|
94
|
+
};
|
|
95
|
+
const types = review.proposedSpec.collections.map(collection => {
|
|
96
|
+
const fields = collection.fields.map(field => ` ${JSON.stringify(field.name)}${field.optional ? '?' : ''}: ${valueType(field.type)};`);
|
|
97
|
+
return `export interface ${typeName(collection.name)}Record {\n id: string;\n${fields.join('\n')}\n}`;
|
|
98
|
+
}).join('\n\n');
|
|
99
|
+
const bindings = review.proposedSpec.collections.map(collection => ` ${JSON.stringify(collection.name)}: database.collection<${typeName(collection.name)}Record>(${JSON.stringify(collection.name)}),`).join('\n');
|
|
100
|
+
return `/* Generated by feltdb sync from feltdb.flow. Do not edit this file directly. */\n`
|
|
101
|
+
+ `export const FELTDB_CONTRACT = ${JSON.stringify(snapshot, null, 2)} as const;\n\n`
|
|
102
|
+
+ `export const FELTDB_COLLECTIONS = ${JSON.stringify(collections, null, 2)} as const;\n\n`
|
|
103
|
+
+ `${types}\n\n`
|
|
104
|
+
+ `export interface FeltDBCollectionBinder {\n collection<T>(name: string): unknown;\n}\n\n`
|
|
105
|
+
+ `/** Bind the generated, typed collection map to the project's FeltDB instance. */\n`
|
|
106
|
+
+ `export function bindGeneratedCollections(database: FeltDBCollectionBinder) {\n return {\n${bindings}\n };\n}\n`;
|
|
107
|
+
}
|
|
108
|
+
export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
|
|
109
|
+
const target = path.join(cwd, GENERATED_FILE);
|
|
110
|
+
const statePath = path.join(cwd, SYNC_STATE);
|
|
111
|
+
const state = readJson(statePath) ?? { files: {} };
|
|
112
|
+
if (fs.existsSync(target)) {
|
|
113
|
+
const actual = digest(fs.readFileSync(target));
|
|
114
|
+
const recorded = state.files?.[GENERATED_FILE];
|
|
115
|
+
if ((!recorded || recorded !== actual) && !options.replace) {
|
|
116
|
+
throw new Error(`SOURCE_SYNC_CONFLICT: ${GENERATED_FILE} has developer changes; review it or rerun with --replace-generated`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const source = generatedContractSource(review);
|
|
120
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
121
|
+
fs.writeFileSync(target, source);
|
|
122
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
123
|
+
fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, contractHash: review.proposedContract.hashes.application, files: { [GENERATED_FILE]: digest(source) } }, null, 2)}\n`, { mode: 0o600 });
|
|
124
|
+
return { written: [GENERATED_FILE] };
|
|
125
|
+
}
|
|
126
|
+
function assertGeneratedSourceSafe(cwd, replace = false) {
|
|
127
|
+
const target = path.join(cwd, GENERATED_FILE);
|
|
128
|
+
if (!fs.existsSync(target) || replace)
|
|
129
|
+
return;
|
|
130
|
+
const state = readJson(path.join(cwd, SYNC_STATE));
|
|
131
|
+
const recorded = state?.files?.[GENERATED_FILE];
|
|
132
|
+
if (!recorded || recorded !== digest(fs.readFileSync(target))) {
|
|
133
|
+
throw new Error(`SOURCE_SYNC_CONFLICT: ${GENERATED_FILE} has developer changes; review it or rerun with --replace-generated`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function applyFlowProposal(proposal, cwd = process.cwd(), options = {}) {
|
|
137
|
+
const review = await reviewFlowProposal(proposal, cwd);
|
|
138
|
+
const dirty = gitDirty(cwd, [FLOW_FILE, GENERATED_FILE]);
|
|
139
|
+
if (dirty.length && !options.allowDirty)
|
|
140
|
+
throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(', ')}`);
|
|
141
|
+
if (review.authorizationChanges.length && !options.approveAuthorization) {
|
|
142
|
+
throw new Error(`AUTHORIZATION_APPROVAL_REQUIRED: ${review.authorizationChanges.map(change => change.policy).join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
if (options.syncSource !== false)
|
|
145
|
+
assertGeneratedSourceSafe(cwd, options.replaceGenerated);
|
|
146
|
+
fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g, '\n'));
|
|
147
|
+
const synchronized = options.syncSource === false ? { written: [] } : syncGeneratedSource(review, cwd, { replace: options.replaceGenerated });
|
|
148
|
+
return { ...review, written: [FLOW_FILE, ...synchronized.written] };
|
|
149
|
+
}
|
|
150
|
+
/** Apply and verify as one developer-visible transaction, restoring exact prior bytes on failure. */
|
|
151
|
+
export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), options = {}) {
|
|
152
|
+
const files = [FLOW_FILE, GENERATED_FILE, SYNC_STATE].map(relative => ({ relative, absolute: path.join(cwd, relative) }));
|
|
153
|
+
const before = files.map(file => ({ ...file, existed: fs.existsSync(file.absolute), bytes: fs.existsSync(file.absolute) ? fs.readFileSync(file.absolute) : undefined }));
|
|
154
|
+
try {
|
|
155
|
+
const result = await applyFlowProposal(proposal, cwd, options);
|
|
156
|
+
options.verify?.();
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
for (const file of before) {
|
|
161
|
+
if (file.existed && file.bytes) {
|
|
162
|
+
fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
|
|
163
|
+
fs.writeFileSync(file.absolute, file.bytes);
|
|
164
|
+
}
|
|
165
|
+
else if (fs.existsSync(file.absolute))
|
|
166
|
+
fs.rmSync(file.absolute, { force: true });
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export function describeProposal(review) {
|
|
172
|
+
return [
|
|
173
|
+
...review.diff.added.map(value => `+ ${value}`),
|
|
174
|
+
...review.diff.changed.map(value => `~ ${value}`),
|
|
175
|
+
...review.diff.removed.map(value => `- ${value}`),
|
|
176
|
+
...review.authorizationChanges.flatMap(value => [
|
|
177
|
+
`AUTHORIZATION CHANGE${value.widening ? ' (possible widening)' : ''}: ${value.policy}`,
|
|
178
|
+
` before: ${(value.before ?? ['<absent>']).join(' | ')}`,
|
|
179
|
+
` after: ${(value.after ?? ['<removed>']).join(' | ')}`,
|
|
180
|
+
]),
|
|
181
|
+
...review.migration.filter(value => value.safety !== 'safe').map(value => `${value.safety.toUpperCase()}: ${value.target} — ${value.detail}`),
|
|
182
|
+
];
|
|
183
|
+
}
|
package/dist/create/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import readline from 'readline';
|
|
|
10
10
|
import { spawn, spawnSync } from 'child_process';
|
|
11
11
|
import { createProject } from './create.js';
|
|
12
12
|
import { FELTDB_PACKAGE_VERSION } from './package-versions.js';
|
|
13
|
-
import { configureManagedAccount } from './managed-account.js';
|
|
13
|
+
import { configureManagedAccount, finalizeManagedProvisioning } from './managed-account.js';
|
|
14
14
|
import { localFeltdbExecutable } from './development-handoff.js';
|
|
15
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
function parseArgs(args) {
|
|
@@ -308,7 +308,7 @@ Learn more: https://github.com/rkendel1/feltdb`);
|
|
|
308
308
|
await createProject({
|
|
309
309
|
projectName,
|
|
310
310
|
autoYes: shouldAutoYes,
|
|
311
|
-
templatesDir: path.join(__dirname, '
|
|
311
|
+
templatesDir: path.join(__dirname, 'template'),
|
|
312
312
|
runtime: options.runtime,
|
|
313
313
|
framework: options.framework,
|
|
314
314
|
distributed: options.distributed,
|
|
@@ -331,9 +331,18 @@ Learn more: https://github.com/rkendel1/feltdb`);
|
|
|
331
331
|
}
|
|
332
332
|
console.log('\n✅ FeltDB application created successfully!\n');
|
|
333
333
|
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
334
|
+
if (options.runtime === 'managed' && !shouldInstall) {
|
|
335
|
+
throw new Error('Managed provisioning requires dependency installation so the initial revision can be built, published, promoted, and verified. Remove --no-install.');
|
|
336
|
+
}
|
|
334
337
|
if (shouldInstall) {
|
|
335
338
|
console.log(`📦 Installing application, Studio${options.webllm ? ', and local WebLLM app builder' : ''} dependencies...\n`);
|
|
336
339
|
await run(npm, ['install'], projectDir);
|
|
340
|
+
if (options.runtime === 'managed') {
|
|
341
|
+
console.log('\n☁️ Publishing and verifying the initial managed revision...\n');
|
|
342
|
+
await run(localFeltdbExecutable(projectDir), ['publish', '--yes', '--approve-destructive-changes'], projectDir);
|
|
343
|
+
const finalized = await finalizeManagedProvisioning({ projectDir });
|
|
344
|
+
console.log(`✓ Managed runtime verified at revision ${finalized.revisionId}`);
|
|
345
|
+
}
|
|
337
346
|
}
|
|
338
347
|
if (shouldStart) {
|
|
339
348
|
if (!shouldInstall) {
|
package/dist/create/create.js
CHANGED
|
@@ -49,9 +49,12 @@ export async function createProject(options) {
|
|
|
49
49
|
test: 'node --test',
|
|
50
50
|
feltdb: 'feltdb',
|
|
51
51
|
'feltdb:server': 'feltdb server',
|
|
52
|
-
'feltdb:connect': 'feltdb connect
|
|
52
|
+
'feltdb:connect': 'feltdb connect',
|
|
53
53
|
'feltdb:status': 'feltdb status',
|
|
54
54
|
'feltdb:studio': 'feltdb studio',
|
|
55
|
+
'feltdb:publish': 'feltdb publish',
|
|
56
|
+
'feltdb:ai': 'feltdb ai',
|
|
57
|
+
'feltdb:sync': 'feltdb sync',
|
|
55
58
|
'feltdb:validate': 'feltdb validate',
|
|
56
59
|
'feltdb:diff': 'feltdb diff',
|
|
57
60
|
'feltdb:deploy': 'feltdb deploy',
|
|
@@ -176,25 +179,26 @@ ${hasAgents ? ` agent WorkspaceAssistant {
|
|
|
176
179
|
lib: ['ES2020', 'DOM'],
|
|
177
180
|
declaration: true,
|
|
178
181
|
outDir: './dist',
|
|
179
|
-
rootDir: '
|
|
182
|
+
rootDir: '.',
|
|
180
183
|
strict: true,
|
|
181
184
|
esModuleInterop: true,
|
|
182
185
|
skipLibCheck: true,
|
|
183
186
|
forceConsistentCasingInFileNames: true,
|
|
184
|
-
moduleResolution: '
|
|
187
|
+
moduleResolution: 'bundler',
|
|
185
188
|
},
|
|
186
|
-
include: ['src/**/*'],
|
|
189
|
+
include: framework === 'react' ? ['src/**/*', 'agents/**/*', 'capabilities/**/*', 'workflows/**/*'] : ['src/**/*'],
|
|
187
190
|
exclude: ['node_modules'],
|
|
188
191
|
};
|
|
189
192
|
if (framework === 'react') {
|
|
190
193
|
tsconfig.compilerOptions.jsx = 'react-jsx';
|
|
194
|
+
tsconfig.compilerOptions.types = ['vite/client'];
|
|
191
195
|
}
|
|
192
196
|
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
|
193
197
|
// Create main application files
|
|
194
198
|
const runtimeOptions = runtime === 'browser'
|
|
195
199
|
? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', browser: true }"
|
|
196
200
|
: runtime === 'managed'
|
|
197
|
-
? "{ namespace:
|
|
201
|
+
? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || '', applicationId: import.meta.env.VITE_FELTDB_MANAGED_APPLICATION_ID, environment: 'production' } }"
|
|
198
202
|
: runtime === 'self-hosted'
|
|
199
203
|
? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
|
|
200
204
|
: "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', memory: true }";
|
|
@@ -365,7 +369,7 @@ async function logActivity(event: Omit<ActivityEvent, 'id' | 'timestamp'>): Prom
|
|
|
365
369
|
`;
|
|
366
370
|
fs.writeFileSync(path.join(srcDir, 'feltdb.ts'), feltdbTs);
|
|
367
371
|
// Create a real local-inference agent for browser projects.
|
|
368
|
-
if (hasAgents && hasWebLLM) {
|
|
372
|
+
if (hasAgents && hasWebLLM && framework !== 'react') {
|
|
369
373
|
const agentTs = `import { WebLLMProvider } from '@feltdb/webllm';
|
|
370
374
|
import { db, reports } from '../../src/feltdb';
|
|
371
375
|
|
|
@@ -1185,6 +1189,17 @@ root.render(
|
|
|
1185
1189
|
);
|
|
1186
1190
|
`;
|
|
1187
1191
|
fs.writeFileSync(path.join(srcDir, 'index.tsx'), indexTsx);
|
|
1192
|
+
const canonicalTemplate = path.join(templatesDir, 'default-project');
|
|
1193
|
+
if (fs.existsSync(canonicalTemplate)) {
|
|
1194
|
+
fs.cpSync(path.join(canonicalTemplate, 'src'), srcDir, { recursive: true });
|
|
1195
|
+
for (const directory of ['agents', 'capabilities', 'workflows']) {
|
|
1196
|
+
fs.cpSync(path.join(canonicalTemplate, directory), path.join(projectDir, directory), { recursive: true });
|
|
1197
|
+
fs.rmSync(path.join(feltdbDir, directory), { recursive: true, force: true });
|
|
1198
|
+
}
|
|
1199
|
+
fs.writeFileSync(path.join(projectDir, 'feltdb.flow'), fs.readFileSync(path.join(canonicalTemplate, 'feltdb.flow'), 'utf8').replace('app FeltDBStarter', `app ${appName}`));
|
|
1200
|
+
const databasePath = path.join(srcDir, 'feltdb.ts');
|
|
1201
|
+
fs.writeFileSync(databasePath, fs.readFileSync(databasePath, 'utf8').replace(/export const managedRuntime[\s\S]*?\n\} : \{ namespace: import\.meta\.env\.VITE_FELTDB_NAMESPACE \|\| 'feltdb-starter', browser: true \}\);/, `export const managedRuntime = ${runtime === 'managed'};\nexport const db = createFeltDB(${runtimeOptions});`));
|
|
1202
|
+
}
|
|
1188
1203
|
}
|
|
1189
1204
|
else {
|
|
1190
1205
|
// Create vanilla JS app
|
|
@@ -1219,8 +1234,7 @@ main().catch(console.error);
|
|
|
1219
1234
|
const envExample = `# FeltDB Configuration
|
|
1220
1235
|
# Copy this file to .env.local and update the values
|
|
1221
1236
|
|
|
1222
|
-
# API
|
|
1223
|
-
# Leave empty for browser runtime; required for authenticated self-hosted and managed runtimes
|
|
1237
|
+
# API key for authenticated self-hosted browser access
|
|
1224
1238
|
VITE_FELTDB_API_KEY=
|
|
1225
1239
|
|
|
1226
1240
|
# FeltDB Server URL (self-hosted or managed runtime)
|
|
@@ -1228,11 +1242,10 @@ VITE_FELTDB_URL=http://localhost:7700
|
|
|
1228
1242
|
|
|
1229
1243
|
# Managed example: https://runtime.your-app.feltdb.com
|
|
1230
1244
|
VITE_FELTDB_MANAGED_URL=
|
|
1231
|
-
VITE_FELTDB_MANAGED_API_KEY=
|
|
1232
|
-
VITE_FELTDB_MANAGED_TENANT_ID=
|
|
1233
1245
|
VITE_FELTDB_MANAGED_APPLICATION_ID=
|
|
1234
|
-
|
|
1235
|
-
|
|
1246
|
+
# Server/control-plane only. Never rename with a VITE_ prefix.
|
|
1247
|
+
FELTDB_MANAGED_CONTROL_API_KEY=
|
|
1248
|
+
FELTDB_TOKEN=
|
|
1236
1249
|
|
|
1237
1250
|
# Override the default self-hosted container image
|
|
1238
1251
|
# By default the server image is built locally from the bundled source.
|
|
@@ -1371,7 +1384,7 @@ The self-hosted instance runs on \`http://localhost:7700\` by default.
|
|
|
1371
1384
|
}
|
|
1372
1385
|
\`\`\`
|
|
1373
1386
|
|
|
1374
|
-
Managed mode uses the same application API with a FeltDB-hosted endpoint. The CLI
|
|
1387
|
+
Managed mode uses the same application API with a FeltDB-hosted endpoint. The CLI writes only the public URL and application ID plus separate server-only publish and runtime credentials. Tenant, namespace, environment, and active revision are discovered deployment metadata under \`.feltdb/managed.json\`.
|
|
1375
1388
|
|
|
1376
1389
|
## Vector Search Status
|
|
1377
1390
|
|
|
@@ -1524,6 +1537,7 @@ build/
|
|
|
1524
1537
|
*.log
|
|
1525
1538
|
.DS_Store
|
|
1526
1539
|
.feltdb/
|
|
1540
|
+
.feltdb-data/
|
|
1527
1541
|
`;
|
|
1528
1542
|
fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignore);
|
|
1529
1543
|
// Create README
|
|
@@ -1639,7 +1653,7 @@ FeltDB runs through a dedicated server with Docker Compose and persistent data v
|
|
|
1639
1653
|
|
|
1640
1654
|
### Managed Runtime
|
|
1641
1655
|
|
|
1642
|
-
FeltDB provides the runtime endpoint, durable storage, synchronization, and background workload infrastructure. The CLI configures \`
|
|
1656
|
+
FeltDB provides the runtime endpoint, durable storage, synchronization, and background workload infrastructure. The CLI configures the public \`VITE_FELTDB_MANAGED_*\` identity, separate server-only control and data-plane credentials, and the promoted revision in \`.env.local\`. The browser API-key value stays empty because browser users receive short-lived actor sessions.
|
|
1643
1657
|
|
|
1644
1658
|
## Configuration
|
|
1645
1659
|
|
|
@@ -1669,12 +1683,14 @@ Create a \`.env.local\` file (copy from \`.env.example\`):
|
|
|
1669
1683
|
\`\`\`
|
|
1670
1684
|
VITE_FELTDB_API_KEY=your_api_key_here
|
|
1671
1685
|
VITE_FELTDB_URL=http://localhost:7700
|
|
1672
|
-
VITE_FELTDB_MANAGED_API_KEY=your_managed_api_key_here
|
|
1673
1686
|
VITE_FELTDB_MANAGED_URL=https://api.feltdb.com
|
|
1687
|
+
VITE_FELTDB_MANAGED_APPLICATION_ID=your_application_id
|
|
1688
|
+
FELTDB_MANAGED_CONTROL_API_KEY=your_server_only_control_key
|
|
1689
|
+
FELTDB_TOKEN=your_server_only_application_data_key
|
|
1674
1690
|
VITE_FELTDB_WEBSOCKET_URL=ws://localhost:7700
|
|
1675
1691
|
\`\`\`
|
|
1676
1692
|
|
|
1677
|
-
|
|
1693
|
+
Never expose \`FELTDB_MANAGED_CONTROL_API_KEY\` or \`FELTDB_TOKEN\` through a \`VITE_\` variable. Managed browser access uses \`db.auth.signUp()\` and \`db.auth.signIn()\`; the active revision is discovered rather than configured.
|
|
1678
1694
|
|
|
1679
1695
|
## Development
|
|
1680
1696
|
|
|
@@ -1795,6 +1811,12 @@ If port 5173 is in use:
|
|
|
1795
1811
|
MIT
|
|
1796
1812
|
`;
|
|
1797
1813
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1814
|
+
if (framework === 'react') {
|
|
1815
|
+
const canonicalReadme = path.join(templatesDir, 'default-project', 'README.md');
|
|
1816
|
+
if (fs.existsSync(canonicalReadme)) {
|
|
1817
|
+
fs.writeFileSync(path.join(projectDir, 'README.md'), fs.readFileSync(canonicalReadme, 'utf8').replace('# My FeltDB App', `# ${applicationName}`));
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1798
1820
|
// Initialize Development Workspace
|
|
1799
1821
|
// This creates .feltdb/workspace.json which enables all FeltDB-aware tools
|
|
1800
1822
|
// (CLI, IDE, agents, browser extensions) to discover and connect to the
|