@feltdb/core 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application-development.d.ts +76 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +137 -0
- package/dist/application-manifest.d.ts +5 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +26 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +45 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/commands.js +466 -105
- package/dist/cli/index.js +1 -1
- package/dist/cli/proposal-client.js +54 -0
- package/dist/cli/publish-engine.js +162 -0
- package/dist/cli/source-sync.js +139 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +30 -6
- package/dist/create/managed-account.js +14 -7
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +31 -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-server/src/identity.rs +46 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
- 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 +169 -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.map +1 -1
- package/dist/flowspec.js +27 -6
- 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 +3 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +3 -0
- package/dist/proposal.d.ts +85 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +105 -0
- package/dist/studio/app.d.ts +3 -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 +20 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +3 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/index.d.ts +2 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/{components-Dxuhrv8_.js → components-Bm4sjhcX.js} +335 -5
- package/dist/studio/index.js +168 -157
- package/dist/studio/studio.css +1 -1
- package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
- package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
- 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 +62 -1
- package/package.json +1 -1
- 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/commands.js
CHANGED
|
@@ -8,12 +8,16 @@ import net from 'net';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import { spawn, spawnSync } from 'child_process';
|
|
10
10
|
import { randomBytes } from 'crypto';
|
|
11
|
-
import { createFeltDB, diffFlowSpec,
|
|
11
|
+
import { createFeltDB, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
|
|
12
12
|
import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
|
|
13
13
|
import { applicationUrlCandidate, detectApplication, discoverApplicationUrl } from './application.js';
|
|
14
14
|
import { resolveApplicationLifecycle, startManagedApplication, waitForManagedApplication } from './application-lifecycle.js';
|
|
15
15
|
import { createDevelopmentSession, developmentSessionEnvironment, developmentSessionSummary, transitionDevelopmentSession } from './development-session.js';
|
|
16
16
|
import { openApplication, openStudio } from './browser-opening.js';
|
|
17
|
+
import { publishApplication } from './publish-engine.js';
|
|
18
|
+
import { applyFlowProposal, describeProposal, loadLocalDevelopmentContract, reviewFlowProposal, syncGeneratedSource } from './source-sync.js';
|
|
19
|
+
import { configureAiModel, generateConfiguredFlowProposal, publicAiModelConfiguration } from './ai-model-config.js';
|
|
20
|
+
import { proposalService } from './proposal-client.js';
|
|
17
21
|
function loadProjectEnvironment(file = path.resolve('.env.local')) {
|
|
18
22
|
if (!fs.existsSync(file))
|
|
19
23
|
return;
|
|
@@ -79,11 +83,177 @@ export async function handleCommand(command, args) {
|
|
|
79
83
|
return handleFlowDeploy(args);
|
|
80
84
|
case 'publish':
|
|
81
85
|
return handleManagedPublish(args);
|
|
86
|
+
case 'ai':
|
|
87
|
+
return handleAi(args);
|
|
88
|
+
case 'proposals':
|
|
89
|
+
return handleProposals(args);
|
|
90
|
+
case 'sync':
|
|
91
|
+
return handleSourceSync(args);
|
|
82
92
|
case 'help':
|
|
83
93
|
default:
|
|
84
94
|
return handleHelp();
|
|
85
95
|
}
|
|
86
96
|
}
|
|
97
|
+
function proposalFile(args) {
|
|
98
|
+
const value = args.find(argument => !argument.startsWith('--') && !['review', 'apply'].includes(argument));
|
|
99
|
+
if (!value)
|
|
100
|
+
throw new Error('A proposal JSON file is required');
|
|
101
|
+
return path.resolve(value);
|
|
102
|
+
}
|
|
103
|
+
function validateRepositoryBuild(args) {
|
|
104
|
+
const packagePath = path.resolve('package.json');
|
|
105
|
+
const projectPackage = fs.existsSync(packagePath) ? JSON.parse(fs.readFileSync(packagePath, 'utf8')) : {};
|
|
106
|
+
if (!projectPackage.scripts?.build || args.includes('--no-build'))
|
|
107
|
+
return;
|
|
108
|
+
console.log('Validating the updated application before recording applied status…');
|
|
109
|
+
const built = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { cwd: process.cwd(), stdio: 'inherit' });
|
|
110
|
+
if (built.status !== 0)
|
|
111
|
+
throw new Error(`APPLICATION_VALIDATION_FAILED: build exited ${built.status ?? 'without a status'}; proposal remains approved for recovery`);
|
|
112
|
+
}
|
|
113
|
+
async function handleAi(args) {
|
|
114
|
+
loadProjectEnvironment();
|
|
115
|
+
if (args[0] === 'model') {
|
|
116
|
+
const action = args[1] || 'show';
|
|
117
|
+
if (action === 'show') {
|
|
118
|
+
const model = publicAiModelConfiguration();
|
|
119
|
+
console.log(`Provider: ${model.provider}\nModel: ${model.model}\nLocation: ${model.local ? 'local browser (private)' : model.baseUrl}\nCredential: ${model.credentialConfigured ? 'configured securely' : 'not configured'}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (action === 'set') {
|
|
123
|
+
const option = (name) => args.includes(name) ? args[args.indexOf(name) + 1] : undefined;
|
|
124
|
+
const provider = (option('--provider') || 'local-webllm');
|
|
125
|
+
if (!['local-webllm', 'openai-compatible'].includes(provider))
|
|
126
|
+
throw new Error('Provider must be local-webllm or openai-compatible');
|
|
127
|
+
const modelName = option('--model');
|
|
128
|
+
if (!modelName)
|
|
129
|
+
throw new Error('--model is required');
|
|
130
|
+
const apiKey = args.includes('--api-key-stdin') ? fs.readFileSync(0, 'utf8').trim() : process.env.FELTDB_AI_API_KEY;
|
|
131
|
+
const configured = configureAiModel({ provider, model: modelName, baseUrl: option('--base-url'), apiKey: apiKey || undefined,
|
|
132
|
+
credentialRequired: !args.includes('--no-credential') });
|
|
133
|
+
console.log(`✓ AI model configured: ${configured.provider} / ${configured.model}`);
|
|
134
|
+
console.log(provider === 'openai-compatible' ? 'Credential is stored in the operating-system vault; Studio receives only provider metadata.' : 'Inference and prompts stay in the browser.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
throw new Error('Usage: feltdb ai model [show|set --provider <local-webllm|openai-compatible> --model <id> [--base-url <url>] [--api-key-stdin|--no-credential]]');
|
|
138
|
+
}
|
|
139
|
+
if (!args.length || args.every(argument => ['--dry-run', '--preview', '--ephemeral', '--no-open'].includes(argument))) {
|
|
140
|
+
console.log('Opening the local Ask FeltDB experience. Generation stays in your browser via WebGPU.');
|
|
141
|
+
return handleStudio(args, studioUrl => console.log(`Ask FeltDB: ${studioUrl}`));
|
|
142
|
+
}
|
|
143
|
+
const action = args[0];
|
|
144
|
+
if (action === 'context') {
|
|
145
|
+
const context = await loadLocalDevelopmentContract();
|
|
146
|
+
console.log(JSON.stringify({ snapshot: context.snapshot, flowHash: context.flowHash }, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (action === 'review') {
|
|
150
|
+
const input = JSON.parse(fs.readFileSync(proposalFile(args.slice(1)), 'utf8'));
|
|
151
|
+
const proposal = input;
|
|
152
|
+
const review = await reviewFlowProposal(proposal);
|
|
153
|
+
console.log(`\n${proposal.summary || 'FeltDB change proposal'}`);
|
|
154
|
+
for (const line of describeProposal(review))
|
|
155
|
+
console.log(` ${line}`);
|
|
156
|
+
if (!describeProposal(review).length)
|
|
157
|
+
console.log(' No contract changes.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (action === 'apply') {
|
|
161
|
+
const target = args.slice(1).find(value => !value.startsWith('--'));
|
|
162
|
+
if (!target)
|
|
163
|
+
throw new Error('A proposal id or JSON file is required');
|
|
164
|
+
const stored = target.startsWith('p_') ? await proposalService().get(target) : undefined;
|
|
165
|
+
const proposal = stored ? { baseContractHash: stored.base_contract_hash, baseFlowHash: stored.base_flow_hash,
|
|
166
|
+
proposedFlow: stored.proposed_flow, summary: stored.summary, changes: stored.contract_diff, warnings: stored.warnings }
|
|
167
|
+
: JSON.parse(fs.readFileSync(path.resolve(target), 'utf8'));
|
|
168
|
+
if (stored && stored.status !== 'approved')
|
|
169
|
+
throw new Error(`Proposal must be approved before apply; current status is ${stored.status}`);
|
|
170
|
+
if (!stored && !args.includes('--approve'))
|
|
171
|
+
throw new Error('Explicit approval is required: rerun with --approve');
|
|
172
|
+
const current = await loadLocalDevelopmentContract();
|
|
173
|
+
if (stored && (stored.application_id !== current.snapshot.application.application_id || stored.environment !== current.snapshot.application.environment)) {
|
|
174
|
+
throw new Error(`PROPOSAL_SCOPE_MISMATCH: ${stored.application_id}/${stored.environment} cannot apply to ${current.snapshot.application.application_id}/${current.snapshot.application.environment}`);
|
|
175
|
+
}
|
|
176
|
+
const alreadyWritten = stored && await import('@feltdb/core').then(module => module.flowSourceHash(stored.proposed_flow)) === current.flowHash;
|
|
177
|
+
if (alreadyWritten) {
|
|
178
|
+
console.log('Detected an approved proposal whose flow is already present; reconciling the interrupted application.');
|
|
179
|
+
const recoveredReview = await reviewFlowProposal({ ...proposal, baseContractHash: current.snapshot.contract_hash, baseFlowHash: current.flowHash });
|
|
180
|
+
syncGeneratedSource(recoveredReview, process.cwd(), { replace: args.includes('--replace-generated') });
|
|
181
|
+
validateRepositoryBuild(args);
|
|
182
|
+
await proposalService().transition(stored.id, 'applied', 'cli-repository-applier', true);
|
|
183
|
+
console.log(`✓ Reconciled and marked ${stored.id} applied`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const reviewOnly = await reviewFlowProposal(proposal);
|
|
187
|
+
console.log(`\n${proposal.summary || 'FeltDB change proposal'}`);
|
|
188
|
+
for (const line of describeProposal(reviewOnly))
|
|
189
|
+
console.log(` ${line}`);
|
|
190
|
+
console.log(` Source impact: feltdb.flow, src/feltdb/generated-contract.ts`);
|
|
191
|
+
if (args.includes('--dry-run') || args.includes('--preview')) {
|
|
192
|
+
console.log('✓ Preview complete. No files, flow, contract, or published state were changed.');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const review = await applyFlowProposal(proposal, process.cwd(), {
|
|
196
|
+
allowDirty: args.includes('--allow-dirty'),
|
|
197
|
+
approveAuthorization: args.includes('--approve-authorization'),
|
|
198
|
+
replaceGenerated: args.includes('--replace-generated'),
|
|
199
|
+
});
|
|
200
|
+
console.log(`✓ Applied validated flow proposal (${review.proposedContract.hashes.application})`);
|
|
201
|
+
for (const file of review.written)
|
|
202
|
+
console.log(` ${file}`);
|
|
203
|
+
validateRepositoryBuild(args);
|
|
204
|
+
if (stored)
|
|
205
|
+
await proposalService().transition(stored.id, 'applied', 'cli-repository-applier', true);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Usage: feltdb ai [context|review <proposal.json>|apply <proposal.json>]');
|
|
209
|
+
}
|
|
210
|
+
async function handleProposals(args) {
|
|
211
|
+
loadProjectEnvironment();
|
|
212
|
+
const client = proposalService();
|
|
213
|
+
const action = args[0] || 'list';
|
|
214
|
+
const id = args[1];
|
|
215
|
+
if (action === 'list') {
|
|
216
|
+
const { proposals } = await client.list();
|
|
217
|
+
for (const value of proposals.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))))
|
|
218
|
+
console.log(`${value.id}\t${value.status}\t${value.summary}`);
|
|
219
|
+
if (!proposals.length)
|
|
220
|
+
console.log('No proposals.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!id)
|
|
224
|
+
throw new Error(`feltdb proposals ${action} requires a proposal id`);
|
|
225
|
+
if (action === 'get') {
|
|
226
|
+
console.log(JSON.stringify({ proposal: await client.get(id), history: (await client.history(id)).events }, null, 2));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (action === 'diff') {
|
|
230
|
+
const value = await client.get(id);
|
|
231
|
+
console.log(value.contract_diff.join('\n'));
|
|
232
|
+
console.log(JSON.stringify(value.source_plan, null, 2));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (action === 'approve' || action === 'reject') {
|
|
236
|
+
const value = await client.transition(id, action === 'approve' ? 'approved' : 'rejected', 'local-developer', false, args.includes('--approve-authorization'));
|
|
237
|
+
console.log(`✓ ${value.id} ${value.status}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
throw new Error('Usage: feltdb proposals [list|get|diff|approve|reject] [id]');
|
|
241
|
+
}
|
|
242
|
+
async function handleSourceSync(args) {
|
|
243
|
+
loadProjectEnvironment();
|
|
244
|
+
const context = await loadLocalDevelopmentContract();
|
|
245
|
+
const proposal = {
|
|
246
|
+
baseContractHash: context.snapshot.contract_hash,
|
|
247
|
+
baseFlowHash: context.flowHash,
|
|
248
|
+
proposedFlow: context.source,
|
|
249
|
+
summary: 'Synchronize generated source with feltdb.flow',
|
|
250
|
+
changes: [], warnings: [],
|
|
251
|
+
};
|
|
252
|
+
const review = await reviewFlowProposal(proposal);
|
|
253
|
+
const result = syncGeneratedSource(review, process.cwd(), { replace: args.includes('--replace-generated') });
|
|
254
|
+
for (const file of result.written)
|
|
255
|
+
console.log(`✓ Synchronized ${file}`);
|
|
256
|
+
}
|
|
87
257
|
/**
|
|
88
258
|
* Inspect the durable investigation store.
|
|
89
259
|
*
|
|
@@ -234,83 +404,29 @@ async function handleFlowDeploy(args) {
|
|
|
234
404
|
}
|
|
235
405
|
}
|
|
236
406
|
async function handleManagedPublish(args) {
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const token = process.env.VITE_FELTDB_MANAGED_API_KEY;
|
|
246
|
-
const tenantId = process.env.VITE_FELTDB_MANAGED_TENANT_ID;
|
|
247
|
-
const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID;
|
|
248
|
-
const namespace = process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace;
|
|
249
|
-
const environment = process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production';
|
|
250
|
-
const missing = [
|
|
251
|
-
['VITE_FELTDB_MANAGED_URL', url],
|
|
252
|
-
['VITE_FELTDB_MANAGED_API_KEY', token],
|
|
253
|
-
['VITE_FELTDB_MANAGED_TENANT_ID', tenantId],
|
|
254
|
-
['VITE_FELTDB_MANAGED_APPLICATION_ID', applicationId],
|
|
255
|
-
['VITE_FELTDB_MANAGED_NAMESPACE', namespace],
|
|
256
|
-
].filter(([, value]) => !value).map(([name]) => name);
|
|
257
|
-
if (missing.length)
|
|
258
|
-
throw new Error(`Managed publish configuration is incomplete in .env.local: ${missing.join(', ')}`);
|
|
259
|
-
const file = flowFile(args);
|
|
260
|
-
const spec = readFlowSpec(file);
|
|
261
|
-
const diagnostics = validateFlowSpec(spec).filter(value => value.severity === 'error');
|
|
262
|
-
if (diagnostics.length)
|
|
263
|
-
throw new Error(diagnostics.map(value => value.message).join('; '));
|
|
264
|
-
console.log('FeltDB Publish');
|
|
265
|
-
console.log(`Application: ${spec.app}`);
|
|
266
|
-
console.log(`Environment: ${environment}`);
|
|
267
|
-
console.log(`Namespace: ${namespace}`);
|
|
268
|
-
console.log(`API: ${url}`);
|
|
269
|
-
console.log('✓ Configuration loaded');
|
|
270
|
-
if (!args.includes('--no-build')) {
|
|
271
|
-
const manifestPath = path.resolve('package.json');
|
|
272
|
-
if (!fs.existsSync(manifestPath))
|
|
273
|
-
throw new Error('package.json is required to build the application');
|
|
274
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
275
|
-
if (!manifest.scripts?.build)
|
|
276
|
-
throw new Error('package.json must define a build script for managed publish');
|
|
277
|
-
const build = spawnSync('npm', ['run', 'build'], { cwd: process.cwd(), stdio: 'inherit' });
|
|
278
|
-
if (build.error)
|
|
279
|
-
throw build.error;
|
|
280
|
-
if (build.status !== 0)
|
|
281
|
-
throw new Error(`Application build failed with exit code ${build.status}`);
|
|
282
|
-
}
|
|
283
|
-
console.log('✓ Build completed');
|
|
284
|
-
console.log('✓ Application validated');
|
|
285
|
-
const remote = createFeltDB({ namespace, server: { url: url, token: token } });
|
|
286
|
-
try {
|
|
287
|
-
const current = await remote.collection('_flow_apps').get(spec.app);
|
|
288
|
-
const migration = planFlowSpecMigration(current?.spec ?? emptyFlowSpec(spec.app), spec);
|
|
289
|
-
const destructive = migration.filter(value => value.safety === 'destructive');
|
|
290
|
-
if (destructive.length && !args.includes('--allow-destructive')) {
|
|
291
|
-
throw new Error(`Publish contains destructive changes (${destructive.map(value => value.target).join(', ')}); review them and rerun with --allow-destructive`);
|
|
407
|
+
const valueAfter = (flag) => { const index = args.indexOf(flag); return index >= 0 ? args[index + 1] : undefined; };
|
|
408
|
+
const confirm = async (question) => {
|
|
409
|
+
if (!process.stdin.isTTY)
|
|
410
|
+
return false;
|
|
411
|
+
const readline = await import('node:readline/promises');
|
|
412
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
413
|
+
try {
|
|
414
|
+
return !/^(n|no)$/i.test((await prompt.question(`${question} [Y/n] `)).trim());
|
|
292
415
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
console.log(`Deployment: ${publishState.deploymentId}`);
|
|
308
|
-
console.log(`Version: ${result.version}`);
|
|
309
|
-
console.log('Your app is live.');
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
await remote.close();
|
|
313
|
-
}
|
|
416
|
+
finally {
|
|
417
|
+
prompt.close();
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
await publishApplication({
|
|
421
|
+
environment: valueAfter('--environment'),
|
|
422
|
+
message: valueAfter('--message'),
|
|
423
|
+
dryRun: args.includes('--dry-run'),
|
|
424
|
+
status: args.includes('--status'),
|
|
425
|
+
yes: args.includes('--yes'),
|
|
426
|
+
approveDestructive: args.includes('--approve-destructive-changes') || args.includes('--allow-destructive'),
|
|
427
|
+
noBuild: args.includes('--no-build'),
|
|
428
|
+
confirm,
|
|
429
|
+
});
|
|
314
430
|
}
|
|
315
431
|
async function handleServer(args) {
|
|
316
432
|
const require = createRequire(import.meta.url);
|
|
@@ -500,17 +616,39 @@ For more info, visit: https://github.com/rkendel1/feltdb
|
|
|
500
616
|
`);
|
|
501
617
|
}
|
|
502
618
|
async function handleConnect(args) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
process.
|
|
508
|
-
|
|
619
|
+
loadProjectEnvironment();
|
|
620
|
+
const projectFile = path.resolve('feltdb.config.json');
|
|
621
|
+
const project = fs.existsSync(projectFile) ? JSON.parse(fs.readFileSync(projectFile, 'utf8')) : {};
|
|
622
|
+
const url = (args[0]
|
|
623
|
+
|| process.env.VITE_FELTDB_MANAGED_URL
|
|
624
|
+
|| process.env.VITE_FELTDB_URL
|
|
625
|
+
|| project.server?.url
|
|
626
|
+
|| (project.runtime === 'browser' ? '' : 'http://localhost:7700')).replace(/\/$/, '');
|
|
627
|
+
if (!url)
|
|
628
|
+
throw new Error('This project uses the embedded browser runtime; there is no server endpoint to connect to. Run npm run dev.');
|
|
509
629
|
console.log('🔗 Connecting to FeltDB Server\n');
|
|
510
630
|
console.log(`URL: ${url}`);
|
|
511
|
-
console.log('Discovering node...');
|
|
512
|
-
console.log('Checking capabilities...');
|
|
513
631
|
console.log('Verifying connection...\n');
|
|
632
|
+
const controller = new AbortController();
|
|
633
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
634
|
+
let health;
|
|
635
|
+
try {
|
|
636
|
+
const response = await fetch(`${url}/health`, {
|
|
637
|
+
headers: process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY
|
|
638
|
+
? { Authorization: `Bearer ${process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY}` }
|
|
639
|
+
: {},
|
|
640
|
+
signal: controller.signal,
|
|
641
|
+
});
|
|
642
|
+
if (!response.ok)
|
|
643
|
+
throw new Error(`HTTP ${response.status}`);
|
|
644
|
+
health = await response.json();
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
throw new Error(`FeltDB is not running at ${url}. Start it with npm run feltdb:studio (or the project's configured runtime command). ${error instanceof Error ? error.message : error}`);
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
clearTimeout(timeout);
|
|
651
|
+
}
|
|
514
652
|
// Store connection info
|
|
515
653
|
const configDir = path.join(process.cwd(), '.feltdb');
|
|
516
654
|
if (!fs.existsSync(configDir)) {
|
|
@@ -520,15 +658,16 @@ async function handleConnect(args) {
|
|
|
520
658
|
fs.writeFileSync(connFile, JSON.stringify({
|
|
521
659
|
url,
|
|
522
660
|
connected: new Date().toISOString(),
|
|
523
|
-
authenticated:
|
|
661
|
+
authenticated: Boolean(process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY),
|
|
662
|
+
namespace: project.namespace || process.env.VITE_FELTDB_MANAGED_NAMESPACE || 'default',
|
|
663
|
+
runtime: project.runtime || 'self-hosted',
|
|
524
664
|
}, null, 2));
|
|
525
665
|
console.log('✅ Connected to FeltDB\n');
|
|
526
666
|
console.log('Endpoint: ' + url);
|
|
527
|
-
console.log(
|
|
528
|
-
console.log(
|
|
529
|
-
console.log(
|
|
530
|
-
console.log(
|
|
531
|
-
console.log('Health: ✓\n');
|
|
667
|
+
console.log(`Namespace: ${project.namespace || process.env.VITE_FELTDB_MANAGED_NAMESPACE || 'default'}`);
|
|
668
|
+
console.log(`Authenticated: ${process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY ? 'yes' : 'no'}`);
|
|
669
|
+
console.log(`Runtime: ${project.runtime || 'self-hosted'}`);
|
|
670
|
+
console.log(`Health: ${health.status || 'healthy'}\n`);
|
|
532
671
|
console.log('Use: feltdb status (to check connection)\n');
|
|
533
672
|
}
|
|
534
673
|
async function handleExplain(args) {
|
|
@@ -561,20 +700,39 @@ async function handleStatus() {
|
|
|
561
700
|
console.log('Fabric: Enabled');
|
|
562
701
|
console.log('Authentication: Development');
|
|
563
702
|
console.log('Health: ✓\n');
|
|
703
|
+
await displayContractStatus();
|
|
564
704
|
return;
|
|
565
705
|
}
|
|
566
706
|
const connection = JSON.parse(fs.readFileSync(connFile, 'utf-8'));
|
|
567
707
|
console.log('Connected to Server\n');
|
|
568
708
|
console.log(`Endpoint: ${connection.url}`);
|
|
569
|
-
console.log(
|
|
709
|
+
console.log(`Namespace: ${connection.namespace || 'default'}`);
|
|
570
710
|
console.log(`Authenticated: ${connection.authenticated ? 'yes' : 'no'}`);
|
|
571
|
-
console.log(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
711
|
+
console.log(`Runtime: ${connection.runtime || 'self-hosted'}`);
|
|
712
|
+
try {
|
|
713
|
+
const response = await fetch(`${String(connection.url).replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(5000) });
|
|
714
|
+
if (!response.ok)
|
|
715
|
+
throw new Error(`HTTP ${response.status}`);
|
|
716
|
+
const health = await response.json();
|
|
717
|
+
console.log(`Health: ${health.status || 'healthy'}\n`);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
console.log('Health: unavailable');
|
|
721
|
+
console.log('Start the configured runtime and run feltdb connect again.\n');
|
|
722
|
+
}
|
|
723
|
+
await displayContractStatus();
|
|
724
|
+
}
|
|
725
|
+
async function displayContractStatus() {
|
|
726
|
+
if (!fs.existsSync(path.resolve('feltdb.flow')))
|
|
727
|
+
return;
|
|
728
|
+
loadProjectEnvironment();
|
|
729
|
+
const current = await loadLocalDevelopmentContract();
|
|
730
|
+
const publishedFile = path.resolve('.feltdb/last-published.json');
|
|
731
|
+
const published = fs.existsSync(publishedFile) ? JSON.parse(fs.readFileSync(publishedFile, 'utf8')) : undefined;
|
|
732
|
+
console.log('Application contract');
|
|
733
|
+
console.log(`Source: ${current.snapshot.contract_hash}`);
|
|
734
|
+
console.log(`Published: ${published?.contractHash || 'unknown'}`);
|
|
735
|
+
console.log(`Drift: ${!published?.contractHash ? 'unknown' : published.contractHash === current.snapshot.contract_hash ? 'synchronized' : 'source differs from published'}\n`);
|
|
578
736
|
}
|
|
579
737
|
async function handleWorkspace(args) {
|
|
580
738
|
const subcommand = args[0] || 'status';
|
|
@@ -776,10 +934,16 @@ async function handleDev(args) {
|
|
|
776
934
|
if (!process.env.VITE_FELTDB_URL) {
|
|
777
935
|
const authorityPort = Number(args.includes('--authority-port') ? args[args.indexOf('--authority-port') + 1] || '7700' : '7700');
|
|
778
936
|
localAuthority = await startLocalDevelopmentAuthority({
|
|
779
|
-
dataDir: path.join(projectDir, '.feltdb', 'authority'),
|
|
937
|
+
dataDir: path.join(projectDir, '.feltdb-data', 'authority'),
|
|
780
938
|
port: authorityPort,
|
|
939
|
+
proposalContext: () => loadLocalDevelopmentContract(projectDir),
|
|
781
940
|
});
|
|
782
941
|
process.env.VITE_FELTDB_URL = localAuthority.endpoint;
|
|
942
|
+
const connectionsPath = path.join(projectDir, '.feltdb', 'connections.json');
|
|
943
|
+
const connections = fs.existsSync(connectionsPath) ? JSON.parse(fs.readFileSync(connectionsPath, 'utf8')) : {};
|
|
944
|
+
fs.writeFileSync(connectionsPath, `${JSON.stringify({ ...connections, developmentAuthority: {
|
|
945
|
+
endpoint: localAuthority.endpoint, proposalAuthorityToken: localAuthority.proposalAuthorityToken,
|
|
946
|
+
} }, null, 2)}\n`, { mode: 0o600 });
|
|
783
947
|
}
|
|
784
948
|
const authorityUrl = process.env.VITE_FELTDB_URL;
|
|
785
949
|
if (!authorityUrl)
|
|
@@ -1286,9 +1450,13 @@ async function handleStudio(args, onReady) {
|
|
|
1286
1450
|
console.log('⚠️ Studio application artifacts not found.');
|
|
1287
1451
|
return;
|
|
1288
1452
|
}
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1453
|
+
const canonicalFlow = path.resolve(process.cwd(), 'feltdb.flow');
|
|
1454
|
+
const projectFlow = fs.existsSync(canonicalFlow) ? canonicalFlow : undefined;
|
|
1455
|
+
const studioMutationToken = randomBytes(24).toString('hex');
|
|
1456
|
+
const projectPackage = fs.existsSync(path.resolve('package.json')) ? JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8')) : {};
|
|
1457
|
+
const webllmEnabled = Boolean(projectPackage.dependencies?.['@feltdb/webllm'] || projectPackage.devDependencies?.['@feltdb/webllm']);
|
|
1458
|
+
const previewOnly = args.includes('--ephemeral');
|
|
1459
|
+
const proposalClient = proposalService(process.cwd(), session.authorityUrl);
|
|
1292
1460
|
const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.json': 'application/json' };
|
|
1293
1461
|
const server = http.createServer(async (request, response) => {
|
|
1294
1462
|
const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
|
|
@@ -1299,12 +1467,184 @@ async function handleStudio(args, onReady) {
|
|
|
1299
1467
|
const managedPublish = runtime === 'managed' ? {
|
|
1300
1468
|
url: process.env.VITE_FELTDB_MANAGED_URL || connectUrl || '',
|
|
1301
1469
|
namespace: process.env.VITE_FELTDB_MANAGED_NAMESPACE || namespace,
|
|
1302
|
-
token: process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY || '',
|
|
1303
1470
|
tenantId: process.env.VITE_FELTDB_MANAGED_TENANT_ID || '',
|
|
1304
1471
|
applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || '',
|
|
1305
1472
|
environment: process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
|
|
1473
|
+
publishEndpoint: '/_feltdb/publish',
|
|
1306
1474
|
} : undefined;
|
|
1307
|
-
response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || process.env.VITE_FELTDB_MANAGED_API_KEY || '' : '', managedPublish, session }));
|
|
1475
|
+
response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || process.env.VITE_FELTDB_MANAGED_API_KEY || '' : '', managedPublish, session, sourceSync: { enabled: true, webllmEnabled, previewOnly, token: studioMutationToken, aiModel: publicAiModelConfiguration() } }));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (pathname === '/_feltdb/contract') {
|
|
1479
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1480
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
1481
|
+
try {
|
|
1482
|
+
const current = await loadLocalDevelopmentContract();
|
|
1483
|
+
const lastPublished = fs.existsSync(path.resolve('.feltdb/last-published.json'))
|
|
1484
|
+
? JSON.parse(fs.readFileSync(path.resolve('.feltdb/last-published.json'), 'utf8')) : undefined;
|
|
1485
|
+
let runningContract;
|
|
1486
|
+
let runningSnapshot;
|
|
1487
|
+
const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || lastPublished?.applicationId;
|
|
1488
|
+
if (connectUrl && applicationId) {
|
|
1489
|
+
try {
|
|
1490
|
+
const endpoint = new URL('/v1/application', connectUrl);
|
|
1491
|
+
endpoint.searchParams.set('application_id', applicationId);
|
|
1492
|
+
endpoint.searchParams.set('environment', process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || lastPublished?.environment || 'production');
|
|
1493
|
+
const headers = {};
|
|
1494
|
+
const credential = process.env.FELTDB_MANAGED_CONTROL_API_KEY || process.env.VITE_FELTDB_API_KEY;
|
|
1495
|
+
if (credential)
|
|
1496
|
+
headers.authorization = `Bearer ${credential}`;
|
|
1497
|
+
const discovered = await fetch(endpoint, { headers });
|
|
1498
|
+
if (discovered.ok) {
|
|
1499
|
+
const application = await discovered.json();
|
|
1500
|
+
runningSnapshot = application.snapshot;
|
|
1501
|
+
runningContract = runningSnapshot?.contract_hash ?? application.contract_hash;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
catch { /* Drift remains explicitly unknown while the runtime is unavailable. */ }
|
|
1505
|
+
}
|
|
1506
|
+
const knownContracts = [current.snapshot.contract_hash, runningContract, lastPublished?.contractHash].filter(Boolean);
|
|
1507
|
+
response.end(JSON.stringify({ snapshot: runningSnapshot ?? current.snapshot, flowHash: current.flowHash, source: current.source,
|
|
1508
|
+
drift: { source: current.snapshot.contract_hash, running: runningContract, published: lastPublished?.contractHash,
|
|
1509
|
+
synchronized: new Set(knownContracts).size <= 1 } }));
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
response.statusCode = 400;
|
|
1513
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1514
|
+
}
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (pathname === '/_feltdb/proposals/list' && request.method === 'GET') {
|
|
1518
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1519
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
1520
|
+
try {
|
|
1521
|
+
response.end(JSON.stringify(await proposalClient.list()));
|
|
1522
|
+
}
|
|
1523
|
+
catch (error) {
|
|
1524
|
+
response.statusCode = 503;
|
|
1525
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1526
|
+
}
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const proposalDetail = pathname.match(/^\/_feltdb\/proposals\/(p_[A-Za-z0-9]+)$/);
|
|
1530
|
+
if (proposalDetail && request.method === 'GET') {
|
|
1531
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1532
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
1533
|
+
try {
|
|
1534
|
+
response.end(JSON.stringify({ proposal: await proposalClient.get(proposalDetail[1]), history: (await proposalClient.history(proposalDetail[1])).events }));
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
response.statusCode = 404;
|
|
1538
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1539
|
+
}
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
// Studio proposes and previews. CLI applies to the repository. FeltDB validates
|
|
1543
|
+
// and stores the proposal as application state. Human approves. Nothing else writes source.
|
|
1544
|
+
if (pathname === '/_feltdb/proposals' || pathname === '/_feltdb/proposals/approve' || pathname === '/_feltdb/proposals/reject') {
|
|
1545
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1546
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
1547
|
+
if (request.method !== 'POST') {
|
|
1548
|
+
response.statusCode = 405;
|
|
1549
|
+
response.end(JSON.stringify({ error: 'POST required' }));
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (request.headers['x-feltdb-studio-token'] !== studioMutationToken) {
|
|
1553
|
+
response.statusCode = 403;
|
|
1554
|
+
response.end(JSON.stringify({ error: 'Invalid Studio source-sync token' }));
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
try {
|
|
1558
|
+
const chunks = [];
|
|
1559
|
+
for await (const chunk of request)
|
|
1560
|
+
chunks.push(Buffer.from(chunk));
|
|
1561
|
+
const input = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
|
|
1562
|
+
if (pathname.endsWith('/approve') || pathname.endsWith('/reject')) {
|
|
1563
|
+
if (previewOnly)
|
|
1564
|
+
throw new Error('Preview-only sessions cannot approve or reject persisted proposals');
|
|
1565
|
+
if (input.confirm !== true)
|
|
1566
|
+
throw new Error('Explicit proposal confirmation is required');
|
|
1567
|
+
const proposal = await proposalClient.transition(String(input.proposalId || ''), pathname.endsWith('/approve') ? 'approved' : 'rejected', 'studio-developer', false, input.approveAuthorization === true);
|
|
1568
|
+
response.end(JSON.stringify({ status: proposal.status, proposalId: proposal.id,
|
|
1569
|
+
command: proposal.status === 'approved' ? `feltdb ai apply ${proposal.id}` : undefined }));
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
const proposal = input.proposal;
|
|
1573
|
+
const review = await reviewFlowProposal(proposal);
|
|
1574
|
+
const changes = describeProposal(review);
|
|
1575
|
+
let stored;
|
|
1576
|
+
if (!previewOnly) {
|
|
1577
|
+
const current = await loadLocalDevelopmentContract();
|
|
1578
|
+
stored = await proposalClient.create({ application_id: current.snapshot.application.application_id,
|
|
1579
|
+
environment: current.snapshot.application.environment, base_contract_hash: proposal.baseContractHash,
|
|
1580
|
+
base_flow_hash: proposal.baseFlowHash, proposed_flow: proposal.proposedFlow, summary: proposal.summary,
|
|
1581
|
+
warnings: proposal.warnings, actor: 'studio-developer', metadata: { model: publicAiModelConfiguration().model, created_from: 'studio' } });
|
|
1582
|
+
await proposalClient.validate(stored.id, 'feltdb-validator', review.proposedContract.manifest);
|
|
1583
|
+
stored = (await proposalClient.preview(stored.id, 'studio-developer')).proposal;
|
|
1584
|
+
}
|
|
1585
|
+
response.end(JSON.stringify({ status: 'previewed', proposalId: stored?.id ?? 'ephemeral', changes,
|
|
1586
|
+
sourcePlan: stored?.source_plan.files.map(value => value.path) ?? ['feltdb.flow', 'src/feltdb/generated-contract.ts'], previewOnly,
|
|
1587
|
+
authorizationChanges: review.authorizationChanges, migration: review.migration,
|
|
1588
|
+
contractHash: review.proposedContract.hashes.application }));
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
catch (error) {
|
|
1592
|
+
response.statusCode = 400;
|
|
1593
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1594
|
+
}
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (pathname === '/_feltdb/ai/generate') {
|
|
1598
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1599
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
1600
|
+
if (request.method !== 'POST') {
|
|
1601
|
+
response.statusCode = 405;
|
|
1602
|
+
response.end(JSON.stringify({ error: 'POST required' }));
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (request.headers['x-feltdb-studio-token'] !== studioMutationToken) {
|
|
1606
|
+
response.statusCode = 403;
|
|
1607
|
+
response.end(JSON.stringify({ error: 'Invalid Studio token' }));
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
const chunks = [];
|
|
1612
|
+
for await (const chunk of request)
|
|
1613
|
+
chunks.push(Buffer.from(chunk));
|
|
1614
|
+
const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1615
|
+
if (!input.intent?.trim())
|
|
1616
|
+
throw new Error('An AI change request is required');
|
|
1617
|
+
const current = await loadLocalDevelopmentContract();
|
|
1618
|
+
const proposal = await generateConfiguredFlowProposal({ intent: input.intent, snapshot: current.snapshot, currentFlow: current.source });
|
|
1619
|
+
response.end(JSON.stringify({ proposal }));
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
response.statusCode = 400;
|
|
1623
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1624
|
+
}
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (pathname === '/_feltdb/publish') {
|
|
1628
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1629
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
1630
|
+
if (request.method !== 'POST') {
|
|
1631
|
+
response.statusCode = 405;
|
|
1632
|
+
response.end(JSON.stringify({ error: 'POST required' }));
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
const chunks = [];
|
|
1637
|
+
for await (const chunk of request)
|
|
1638
|
+
chunks.push(Buffer.from(chunk));
|
|
1639
|
+
const input = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
|
|
1640
|
+
const messages = [];
|
|
1641
|
+
const result = await publishApplication({ dryRun: Boolean(input.dryRun), status: Boolean(input.status), yes: true, noBuild: true, environment: input.environment, approveDestructive: Boolean(input.approveDestructive), message: input.message, log: message => messages.push(message) });
|
|
1642
|
+
response.end(JSON.stringify({ status: result.status, messages, version: result.revision?.revision_number, validation: result.validation, changes: result.diff?.changes, securityChanges: result.diff?.security_changes }));
|
|
1643
|
+
}
|
|
1644
|
+
catch (error) {
|
|
1645
|
+
response.statusCode = 400;
|
|
1646
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1647
|
+
}
|
|
1308
1648
|
return;
|
|
1309
1649
|
}
|
|
1310
1650
|
if (pathname === '/_feltdb/project') {
|
|
@@ -1406,6 +1746,19 @@ Commands:
|
|
|
1406
1746
|
diff [file] Diff FlowSpec against the last deployed model
|
|
1407
1747
|
deploy [file] Validate, version, and deploy FlowSpec
|
|
1408
1748
|
publish [file] Build and publish/update a managed application
|
|
1749
|
+
ai Open the canonical interactive local-AI workflow
|
|
1750
|
+
ai --dry-run Open an ephemeral preview-only AI session
|
|
1751
|
+
ai model show Show the selected local or user-provided model
|
|
1752
|
+
ai model set Select WebLLM or an OpenAI-compatible model
|
|
1753
|
+
ai context Show the secret-free contract grounding snapshot
|
|
1754
|
+
ai review <file> Validate and review a WebLLM flow proposal
|
|
1755
|
+
ai apply <id> Apply an approved proposal and synchronize source
|
|
1756
|
+
proposals List durable FeltDB application proposals
|
|
1757
|
+
proposals get <id> Show a proposal and its lifecycle history
|
|
1758
|
+
proposals diff <id> Show contract and structured source impact
|
|
1759
|
+
proposals approve <id> Approve a previewed proposal
|
|
1760
|
+
proposals reject <id> Reject a proposed/validated/previewed proposal
|
|
1761
|
+
sync Regenerate owned source from feltdb.flow
|
|
1409
1762
|
studio Launch FeltDB Studio (developer interface)
|
|
1410
1763
|
server Start self-hosted FeltDB server
|
|
1411
1764
|
keys Manage API keys
|
|
@@ -1444,6 +1797,13 @@ Connection:
|
|
|
1444
1797
|
doctor Check system health
|
|
1445
1798
|
help Show this help message
|
|
1446
1799
|
|
|
1800
|
+
Publish:
|
|
1801
|
+
feltdb publish --dry-run Validate and preview without activation
|
|
1802
|
+
feltdb publish --yes Publish to the configured environment
|
|
1803
|
+
feltdb publish --status Compare local and active cloud versions
|
|
1804
|
+
feltdb publish --environment staging Override the configured environment
|
|
1805
|
+
feltdb publish --approve-destructive-changes --yes
|
|
1806
|
+
|
|
1447
1807
|
Inspect Sub-commands:
|
|
1448
1808
|
feltdb inspect Show runtime overview
|
|
1449
1809
|
feltdb inspect peers Show connected peers
|
|
@@ -1467,6 +1827,7 @@ Examples:
|
|
|
1467
1827
|
feltdb status
|
|
1468
1828
|
feltdb dev
|
|
1469
1829
|
feltdb build
|
|
1830
|
+
feltdb publish --dry-run
|
|
1470
1831
|
feltdb check
|
|
1471
1832
|
feltdb inspect
|
|
1472
1833
|
feltdb inspect peers
|