@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.
Files changed (147) hide show
  1. package/README.md +25 -0
  2. package/dist/application-development.d.ts +78 -0
  3. package/dist/application-development.d.ts.map +1 -0
  4. package/dist/application-development.js +153 -0
  5. package/dist/application-manifest.d.ts +6 -1
  6. package/dist/application-manifest.d.ts.map +1 -1
  7. package/dist/application-manifest.js +10 -3
  8. package/dist/canonical-application.d.ts +27 -0
  9. package/dist/canonical-application.d.ts.map +1 -0
  10. package/dist/canonical-application.js +46 -0
  11. package/dist/cli/ai-model-config.js +97 -0
  12. package/dist/cli/cli.js +1 -1
  13. package/dist/cli/commands.js +664 -120
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/managed-environment.js +6 -0
  16. package/dist/cli/managed-project.js +6 -0
  17. package/dist/cli/proposal-client.js +63 -0
  18. package/dist/cli/publish-engine.js +165 -0
  19. package/dist/cli/source-sync.js +183 -0
  20. package/dist/create/cli.js +11 -2
  21. package/dist/create/create.js +38 -16
  22. package/dist/create/managed-account.js +82 -11
  23. package/dist/create/package-versions.js +1 -1
  24. package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
  25. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  26. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  27. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  28. package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
  29. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
  30. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
  32. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
  33. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
  34. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
  35. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
  36. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
  37. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
  38. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
  39. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
  40. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
  41. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
  42. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
  43. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
  44. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
  45. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
  46. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
  47. package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
  48. package/dist/create/template/default-project/README.md +57 -0
  49. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  50. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  51. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  52. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  53. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  54. package/dist/create/template/default-project/feltdb.flow +174 -0
  55. package/dist/create/template/default-project/src/App.tsx +20 -0
  56. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  57. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  58. package/dist/create/template/default-project/src/index.tsx +14 -0
  59. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  60. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  61. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  62. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  63. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  64. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  65. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  66. package/dist/create/template/default-project/src/styles-application.css +21 -0
  67. package/dist/create/template/default-project/src/styles.css +1 -0
  68. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  69. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  70. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  71. package/dist/db.d.ts +49 -1
  72. package/dist/db.d.ts.map +1 -1
  73. package/dist/db.js +77 -0
  74. package/dist/error-codes.d.ts +20 -1
  75. package/dist/error-codes.d.ts.map +1 -1
  76. package/dist/error-codes.js +25 -0
  77. package/dist/flowspec.d.ts +1 -0
  78. package/dist/flowspec.d.ts.map +1 -1
  79. package/dist/flowspec.js +66 -11
  80. package/dist/http-client.d.ts +135 -14
  81. package/dist/http-client.d.ts.map +1 -1
  82. package/dist/http-client.js +79 -31
  83. package/dist/http-db.d.ts +15 -2
  84. package/dist/http-db.d.ts.map +1 -1
  85. package/dist/http-db.js +113 -5
  86. package/dist/index-core.d.ts +5 -0
  87. package/dist/index-core.d.ts.map +1 -1
  88. package/dist/index-core.js +5 -0
  89. package/dist/module-intent.d.ts +37 -0
  90. package/dist/module-intent.d.ts.map +1 -0
  91. package/dist/module-intent.js +81 -0
  92. package/dist/module.d.ts +76 -0
  93. package/dist/module.d.ts.map +1 -0
  94. package/dist/module.js +61 -0
  95. package/dist/proposal.d.ts +161 -0
  96. package/dist/proposal.d.ts.map +1 -0
  97. package/dist/proposal.js +232 -0
  98. package/dist/studio/app.d.ts +5 -1
  99. package/dist/studio/app.d.ts.map +1 -1
  100. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  101. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  102. package/dist/studio/components/AskFeltDB.d.ts +21 -0
  103. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  104. package/dist/studio/components/ContractInspector.d.ts +7 -0
  105. package/dist/studio/components/ContractInspector.d.ts.map +1 -0
  106. package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
  107. package/dist/studio/components/ManagedInstancePanel.js +1 -5
  108. package/dist/studio/components/ModuleExplorer.d.ts +6 -0
  109. package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
  110. package/dist/studio/components/ProposalBrowser.d.ts +12 -0
  111. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  112. package/dist/studio/components/StateExplorer.d.ts +6 -7
  113. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  114. package/dist/studio/components/index.d.ts +4 -0
  115. package/dist/studio/components/index.d.ts.map +1 -1
  116. package/dist/studio/components/index.js +4 -4
  117. package/dist/studio/components-CNSNrmA9.js +2787 -0
  118. package/dist/studio/index.js +214 -157
  119. package/dist/studio/studio.css +1 -1
  120. package/dist/studio/utils/index.d.ts +2 -0
  121. package/dist/studio/utils/index.d.ts.map +1 -1
  122. package/dist/studio/utils/index.js +14 -12
  123. package/dist/studio/utils/managed-instance.d.ts +0 -4
  124. package/dist/studio/utils/managed-instance.d.ts.map +1 -1
  125. package/dist/studio/utils/managed-instance.js +0 -4
  126. package/dist/studio/utils/proposal-api.d.ts +53 -0
  127. package/dist/studio/utils/proposal-api.d.ts.map +1 -0
  128. package/dist/studio/utils/proposal-api.js +91 -0
  129. package/dist/studio/utils/service-api.d.ts +81 -0
  130. package/dist/studio/utils/service-api.d.ts.map +1 -0
  131. package/dist/studio/utils/service-api.js +51 -0
  132. package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
  133. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
  134. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  135. package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
  136. package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
  137. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  138. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  139. package/dist/studio-app/index.html +2 -2
  140. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  141. package/dist/workspace/local-development-authority.d.ts +3 -0
  142. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  143. package/dist/workspace/local-development-authority.js +124 -2
  144. package/package.json +1 -1
  145. package/dist/studio/components-Dxuhrv8_.js +0 -1832
  146. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  147. 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.7.3';
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,6 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function readManagedProjectState(cwd = process.cwd()) {
4
+ const file = path.join(cwd, '.feltdb', 'managed.json');
5
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
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
+ }
@@ -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, '../template'),
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) {
@@ -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 http://localhost:7700',
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: './src',
182
+ rootDir: '.',
180
183
  strict: true,
181
184
  esModuleInterop: true,
182
185
  skipLibCheck: true,
183
186
  forceConsistentCasingInFileNames: true,
184
- moduleResolution: 'node',
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: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || import.meta.env.VITE_FELTDB_URL || '', token: import.meta.env.VITE_FELTDB_MANAGED_API_KEY || import.meta.env.VITE_FELTDB_API_KEY || '' } }"
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 Key for authenticating with FeltDB servers
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
- VITE_FELTDB_MANAGED_NAMESPACE=
1235
- VITE_FELTDB_MANAGED_ENVIRONMENT=production
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 creates \`.env.local\` with \`VITE_FELTDB_MANAGED_URL\` and \`VITE_FELTDB_MANAGED_API_KEY\` after account setup. Studio uses that same connection for state, health, operations, and API-key administration.
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 \`VITE_FELTDB_MANAGED_URL\` and \`VITE_FELTDB_MANAGED_API_KEY\` in \`.env.local\`.
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
- These are required when connecting to an authenticated self-hosted or managed FeltDB instance.
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