@ismaelsoilet/synapsejs 1.8.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -6
- package/bin/synapse.ts +291 -241
- package/package.json +2 -2
- package/src/client/components.tsx +459 -1
- package/src/client/context.tsx +23 -3
- package/src/client/error-boundary.tsx +59 -0
- package/src/client/hooks.ts +100 -1
- package/src/client/toast.tsx +219 -0
- package/src/client/upload.ts +153 -0
- package/src/client.ts +37 -2
- package/src/compiler/client-bundler.ts +94 -2
- package/src/compiler/context-bench.ts +9 -1
- package/src/compiler/contract.ts +31 -4
- package/src/compiler/db-schema-generator.ts +39 -8
- package/src/compiler/fields-parser.ts +28 -1
- package/src/compiler/impact-analyzer.ts +31 -1
- package/src/compiler/migration-runner.ts +10 -2
- package/src/compiler/optional-coverage.ts +137 -0
- package/src/compiler/oracle-runner.ts +18 -4
- package/src/compiler/scaffolder.ts +227 -9
- package/src/compiler/slice-discovery.ts +8 -1
- package/src/compiler/slice-splitter.ts +68 -6
- package/src/compiler/slice-templates.ts +234 -10
- package/src/compiler/typescript-api.ts +33 -0
- package/src/core/action-context.ts +101 -15
- package/src/core/config.ts +88 -10
- package/src/core/cron-parser.ts +167 -0
- package/src/core/database-client.ts +103 -6
- package/src/core/index.ts +5 -0
- package/src/core/jobs.ts +37 -0
- package/src/core/oauth-client.ts +322 -0
- package/src/core/path-guard.ts +105 -0
- package/src/core/postgres-client.ts +37 -0
- package/src/core/postgres-queue.ts +76 -9
- package/src/core/query-builder.ts +166 -34
- package/src/core/session-context.ts +45 -4
- package/src/core/session-token.ts +124 -8
- package/src/core/sqlite-client.ts +20 -0
- package/src/core/storage.ts +60 -19
- package/src/core/topics.ts +84 -0
- package/src/core/webhook-signature.ts +151 -0
- package/src/index.ts +2 -1
- package/src/mcp/server.ts +317 -218
- package/src/mcp/tools.ts +329 -0
- package/src/project-status.ts +433 -0
- package/src/runtime/client-entry.ts +11 -5
- package/src/runtime/network-guard.ts +3 -1
- package/src/runtime/queue-engine.ts +167 -5
- package/src/runtime/rate-limiter.ts +14 -4
- package/src/runtime/security-headers.ts +35 -0
- package/src/runtime/server.ts +1174 -236
- package/src/runtime/uploads.ts +199 -19
- package/src/version.ts +9 -2
- package/templates/starter/package.json +2 -2
- package/templates/starter/tsconfig.json +1 -0
package/src/mcp/server.ts
CHANGED
|
@@ -28,11 +28,38 @@ import { machineContract } from '../compiler/contract';
|
|
|
28
28
|
import { analyzeImpact } from '../compiler/impact-analyzer';
|
|
29
29
|
import { rollbackSliceMigrations, runSliceMigrations } from '../compiler/migration-runner';
|
|
30
30
|
import { runSliceOracles } from '../compiler/oracle-runner';
|
|
31
|
-
import { type SliceTemplate, scaffoldCrud, scaffoldSlice } from '../compiler/scaffolder';
|
|
31
|
+
import { type SliceTemplate, scaffoldCrud, scaffoldDomain, scaffoldSlice } from '../compiler/scaffolder';
|
|
32
32
|
import { checkSchemaDrift } from '../compiler/schema-drift';
|
|
33
33
|
import { findSliceFiles, resolveSlicesDir } from '../compiler/slice-discovery';
|
|
34
34
|
import { artifactDirectory, splitSlice, verifySplit, writeSplitArtifacts } from '../compiler/slice-splitter';
|
|
35
|
+
import { validatePathSegment } from '../core/path-guard';
|
|
35
36
|
import { SYNAPSE_VERSION } from '../version';
|
|
37
|
+
import { MCP_TOOLS } from './tools';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Caps a tool payload so a broken workspace cannot flood the caller's context, and
|
|
41
|
+
* makes every path relative to the project root so a tool result carries no absolute
|
|
42
|
+
* filesystem location.
|
|
43
|
+
*/
|
|
44
|
+
const MCP_MAX_ITEMS = 100;
|
|
45
|
+
|
|
46
|
+
export function capItems<T>(items: T[]): { items: T[]; truncated: boolean; total: number } {
|
|
47
|
+
return {
|
|
48
|
+
items: items.slice(0, MCP_MAX_ITEMS),
|
|
49
|
+
truncated: items.length > MCP_MAX_ITEMS,
|
|
50
|
+
total: items.length
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function toRelativePath(root: string, value: unknown): unknown {
|
|
55
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const relative = path.relative(root, value);
|
|
60
|
+
|
|
61
|
+
return relative.startsWith('..') ? path.basename(value) : relative;
|
|
62
|
+
}
|
|
36
63
|
|
|
37
64
|
interface JsonRpcRequest {
|
|
38
65
|
jsonrpc: string;
|
|
@@ -110,220 +137,7 @@ export class SynapseMcpServer {
|
|
|
110
137
|
jsonrpc: '2.0',
|
|
111
138
|
id,
|
|
112
139
|
result: {
|
|
113
|
-
tools:
|
|
114
|
-
{
|
|
115
|
-
name: 'synapse_get_repo_map',
|
|
116
|
-
description:
|
|
117
|
-
'Get compressed codebase skeleton map (.codebase/repo-map.d.ts) for AI context (<3000 tokens)',
|
|
118
|
-
inputSchema: { type: 'object', properties: {} }
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'synapse_get_db_schema',
|
|
122
|
-
description:
|
|
123
|
-
'Get the centralized database schema catalog (.codebase/db-schema.d.ts) for AI context and type-safe relational queries',
|
|
124
|
-
inputSchema: { type: 'object', properties: {} }
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: 'synapse_check',
|
|
128
|
-
description:
|
|
129
|
-
'Run machine-centric compiler diagnostics, returning exact JSON coordinates (file, line, col, message)',
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: 'object',
|
|
132
|
-
properties: {
|
|
133
|
-
targetFile: { type: 'string', description: 'Optional specific file to check' }
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: 'synapse_split',
|
|
139
|
-
description:
|
|
140
|
-
'Run slice splitter and leak verification gates to enforce server/client isolation and zero leakage',
|
|
141
|
-
inputSchema: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: {
|
|
144
|
-
targetSlice: { type: 'string', description: 'Optional specific slice name to split' }
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: 'synapse_run_pbt',
|
|
150
|
-
description: 'Execute Fast-Check Property-Based Testing (PBT) invariant test suite across all slices',
|
|
151
|
-
inputSchema: { type: 'object', properties: {} }
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: 'synapse_scaffold_slice',
|
|
155
|
-
description:
|
|
156
|
-
'Scaffold a new fullstack atomic vertical slice with TypeBox, Result, Action, React UI, and PBT oracles',
|
|
157
|
-
inputSchema: {
|
|
158
|
-
type: 'object',
|
|
159
|
-
required: ['domain', 'name'],
|
|
160
|
-
properties: {
|
|
161
|
-
domain: { type: 'string', description: 'Domain name (e.g. billing, customers, orders)' },
|
|
162
|
-
name: { type: 'string', description: 'Slice name in kebab-case (e.g. cancel-subscription)' },
|
|
163
|
-
template: {
|
|
164
|
-
type: 'string',
|
|
165
|
-
enum: ['create', 'list', 'update', 'delete', 'login', 'oauth-github', 'crud'],
|
|
166
|
-
description: 'Optional slice template shape (default: create)'
|
|
167
|
-
},
|
|
168
|
-
fields: {
|
|
169
|
-
type: 'string',
|
|
170
|
-
description:
|
|
171
|
-
'Optional fields grammar, e.g. "name:string,email:string,status:enum(ACTIVE|INACTIVE),price:number"'
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
name: 'synapse_contract',
|
|
178
|
-
description:
|
|
179
|
-
'Return the machine contract: slice exports and suffixes, Result and HTTP semantics, session rules, addressing, SSR behavior and the gates to run',
|
|
180
|
-
inputSchema: { type: 'object', properties: {} }
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: 'synapse_migrate',
|
|
184
|
-
description:
|
|
185
|
-
'Auto-discover and apply sliceSchema DDL declarations across slices into the active database',
|
|
186
|
-
inputSchema: { type: 'object', properties: {} }
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: 'synapse_check_db_drift',
|
|
190
|
-
description:
|
|
191
|
-
'Inspects live database catalog against sliceSchema DDL contracts, identifying missing tables, missing columns, and orphan tables',
|
|
192
|
-
inputSchema: { type: 'object', properties: {} }
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
name: 'synapse_diff_impact',
|
|
196
|
-
description:
|
|
197
|
-
'Analyzes cross-slice dependencies (Foreign Keys, shared module imports, and table references) to report all slices impacted by a file or table change',
|
|
198
|
-
inputSchema: {
|
|
199
|
-
type: 'object',
|
|
200
|
-
required: ['target'],
|
|
201
|
-
properties: {
|
|
202
|
-
target: {
|
|
203
|
-
type: 'string',
|
|
204
|
-
description: 'Slice file path, slice name, shared module path, or table name'
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
name: 'synapse_rollback',
|
|
211
|
-
description: 'Roll back applied sliceSchema migrations by executing their -- down: statements',
|
|
212
|
-
inputSchema: {
|
|
213
|
-
type: 'object',
|
|
214
|
-
properties: {
|
|
215
|
-
targetSlice: { type: 'string', description: 'Optional target slice to rollback' },
|
|
216
|
-
steps: { type: 'number', description: 'Number of recent migrations to rollback (default: 1)' }
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
name: 'synapse_test_gate',
|
|
222
|
-
description:
|
|
223
|
-
'Execute Fast-Check PBT oracles and immediately triage failures with Jev System One semantic test-gate to prevent LLM token waste',
|
|
224
|
-
inputSchema: {
|
|
225
|
-
type: 'object',
|
|
226
|
-
properties: {}
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
name: 'synapse_abort_check',
|
|
231
|
-
description:
|
|
232
|
-
'Evaluate plan and error history with Jev System One to detect doomed trajectories, circular loops, or high-risk refactor dead ends',
|
|
233
|
-
inputSchema: {
|
|
234
|
-
type: 'object',
|
|
235
|
-
required: ['plan', 'history'],
|
|
236
|
-
properties: {
|
|
237
|
-
plan: { type: 'string', description: 'Proposed implementation plan or architectural change' },
|
|
238
|
-
history: { type: 'string', description: 'Recent attempt history, error logs, or failure reasons' }
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
name: 'synapse_verify_completion',
|
|
244
|
-
description:
|
|
245
|
-
'Verify slice implementation and test outputs against acceptance criteria using Jev System One before committing work',
|
|
246
|
-
inputSchema: {
|
|
247
|
-
type: 'object',
|
|
248
|
-
required: ['criteria', 'output'],
|
|
249
|
-
properties: {
|
|
250
|
-
criteria: { type: 'string', description: 'Acceptance criteria or requirement specifications' },
|
|
251
|
-
output: {
|
|
252
|
-
type: 'string',
|
|
253
|
-
description: 'Actual implementation summary, test output, or slice behavior'
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
name: 'synapse_reasoning_effort',
|
|
260
|
-
description:
|
|
261
|
-
'Dynamically modulate agent reasoning effort (Astra-Jev) to low/medium/high based on step context (lowering effort for mechanical commands like split/migrate/skeleton)',
|
|
262
|
-
inputSchema: {
|
|
263
|
-
type: 'object',
|
|
264
|
-
required: ['context'],
|
|
265
|
-
properties: {
|
|
266
|
-
context: {
|
|
267
|
-
type: 'string',
|
|
268
|
-
description: 'Immediate command or task context, e.g. "synapse split", "git commit"'
|
|
269
|
-
},
|
|
270
|
-
provider: {
|
|
271
|
-
type: 'string',
|
|
272
|
-
description: 'Target LLM provider, e.g. "deepseek", "anthropic", "openai"'
|
|
273
|
-
},
|
|
274
|
-
sessionContextTokens: {
|
|
275
|
-
type: 'number',
|
|
276
|
-
description: 'Estimated session context token count to protect prompt cache'
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'synapse_triage_error',
|
|
283
|
-
description:
|
|
284
|
-
'Triage an arbitrary test failure or execution error log using Jev System One to classify root causes (env_missing vs flaky_transient vs deep_logic) and avoid token waste',
|
|
285
|
-
inputSchema: {
|
|
286
|
-
type: 'object',
|
|
287
|
-
required: ['failureLog'],
|
|
288
|
-
properties: {
|
|
289
|
-
failureLog: {
|
|
290
|
-
type: 'string',
|
|
291
|
-
description: 'Error log, stack trace or terminal output to triage'
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
name: 'synapse_route_task',
|
|
298
|
-
description:
|
|
299
|
-
'Semantically route a development task to the optimal model/reasoning tier via Jev System One',
|
|
300
|
-
inputSchema: {
|
|
301
|
-
type: 'object',
|
|
302
|
-
required: ['task'],
|
|
303
|
-
properties: {
|
|
304
|
-
task: {
|
|
305
|
-
type: 'string',
|
|
306
|
-
description: 'Description of the coding, refactoring, or diagnostic task'
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
name: 'synapse_evaluate_nudge',
|
|
313
|
-
description:
|
|
314
|
-
'Evaluate whether an agent is prematurely stopping or needs a proactive nudge to continue verification',
|
|
315
|
-
inputSchema: {
|
|
316
|
-
type: 'object',
|
|
317
|
-
required: ['transcriptTail'],
|
|
318
|
-
properties: {
|
|
319
|
-
transcriptTail: {
|
|
320
|
-
type: 'string',
|
|
321
|
-
description: 'Recent transcript steps or conversation history tail'
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
]
|
|
140
|
+
tools: MCP_TOOLS
|
|
327
141
|
}
|
|
328
142
|
};
|
|
329
143
|
}
|
|
@@ -405,7 +219,22 @@ export class SynapseMcpServer {
|
|
|
405
219
|
);
|
|
406
220
|
} else if (toolName === 'synapse_check') {
|
|
407
221
|
const report = runMachineVerifications(this.root, args.targetFile);
|
|
408
|
-
|
|
222
|
+
const issues = capItems(report.issues).items.map((issue) => ({
|
|
223
|
+
...issue,
|
|
224
|
+
file: toRelativePath(this.root, issue.file)
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
contentText = JSON.stringify(
|
|
228
|
+
{
|
|
229
|
+
...report,
|
|
230
|
+
issues,
|
|
231
|
+
issuesTruncated: report.issues.length > MCP_MAX_ITEMS,
|
|
232
|
+
reportedIssues: issues.length,
|
|
233
|
+
totalIssues: report.issues.length
|
|
234
|
+
},
|
|
235
|
+
null,
|
|
236
|
+
2
|
|
237
|
+
);
|
|
409
238
|
} else if (toolName === 'synapse_split') {
|
|
410
239
|
const resolution = resolveSlicesDir(this.root);
|
|
411
240
|
if (!resolution.ok) {
|
|
@@ -475,13 +304,15 @@ export class SynapseMcpServer {
|
|
|
475
304
|
}
|
|
476
305
|
|
|
477
306
|
const splitStatus = failed || slices.length === 0 ? 'FAIL' : 'PASS';
|
|
307
|
+
const boundedSlices = capItems(slices);
|
|
478
308
|
contentText = JSON.stringify(
|
|
479
309
|
{
|
|
480
310
|
status: splitStatus,
|
|
481
311
|
operation: 'SLICE_SPLIT',
|
|
482
312
|
slicesDir: path.relative(this.root, resolution.value.slicesDir),
|
|
483
313
|
totalProcessed: slices.length,
|
|
484
|
-
|
|
314
|
+
slicesTruncated: boundedSlices.truncated,
|
|
315
|
+
slices: boundedSlices.items
|
|
485
316
|
},
|
|
486
317
|
null,
|
|
487
318
|
2
|
|
@@ -584,6 +415,42 @@ export class SynapseMcpServer {
|
|
|
584
415
|
2
|
|
585
416
|
);
|
|
586
417
|
}
|
|
418
|
+
} else if (toolName === 'synapse_scaffold_domain') {
|
|
419
|
+
const domainRes = scaffoldDomain(args.domain, this.root, args.fields);
|
|
420
|
+
if (!domainRes.ok) {
|
|
421
|
+
return {
|
|
422
|
+
jsonrpc: '2.0',
|
|
423
|
+
id,
|
|
424
|
+
result: {
|
|
425
|
+
isError: true,
|
|
426
|
+
content: [
|
|
427
|
+
{
|
|
428
|
+
type: 'text',
|
|
429
|
+
text: JSON.stringify(
|
|
430
|
+
{
|
|
431
|
+
status: 'FAIL',
|
|
432
|
+
code: domainRes.error.code,
|
|
433
|
+
message: domainRes.error.message,
|
|
434
|
+
candidates: domainRes.error.candidates
|
|
435
|
+
},
|
|
436
|
+
null,
|
|
437
|
+
2
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
contentText = JSON.stringify(
|
|
446
|
+
{
|
|
447
|
+
status: 'PASS',
|
|
448
|
+
domain: args.domain,
|
|
449
|
+
createdFiles: domainRes.value.map((f) => path.relative(this.root, f))
|
|
450
|
+
},
|
|
451
|
+
null,
|
|
452
|
+
2
|
|
453
|
+
);
|
|
587
454
|
} else if (toolName === 'synapse_contract') {
|
|
588
455
|
contentText = JSON.stringify(machineContract(), null, 2);
|
|
589
456
|
} else if (toolName === 'synapse_migrate') {
|
|
@@ -617,7 +484,20 @@ export class SynapseMcpServer {
|
|
|
617
484
|
};
|
|
618
485
|
}
|
|
619
486
|
const report = analyzeImpact(args.target, this.root);
|
|
620
|
-
|
|
487
|
+
const bounded = capItems(report.impactedSlices);
|
|
488
|
+
contentText = JSON.stringify(
|
|
489
|
+
{
|
|
490
|
+
...report,
|
|
491
|
+
impactedSlices: bounded.items.map((slice) => ({
|
|
492
|
+
...slice,
|
|
493
|
+
filePath: toRelativePath(this.root, slice.filePath)
|
|
494
|
+
})),
|
|
495
|
+
impactedTruncated: bounded.truncated,
|
|
496
|
+
totalImpacted: report.totalImpacted
|
|
497
|
+
},
|
|
498
|
+
null,
|
|
499
|
+
2
|
|
500
|
+
);
|
|
621
501
|
} else if (toolName === 'synapse_rollback') {
|
|
622
502
|
const report = await rollbackSliceMigrations(this.root, undefined, {
|
|
623
503
|
targetSlice: args.targetSlice,
|
|
@@ -659,6 +539,225 @@ export class SynapseMcpServer {
|
|
|
659
539
|
const transcriptTail = String(args.transcriptTail || '');
|
|
660
540
|
const result: NudgeGateResult = await shouldNudgeContinuation(transcriptTail);
|
|
661
541
|
contentText = JSON.stringify(result, null, 2);
|
|
542
|
+
} else if (toolName === 'synapse_inspect_routes') {
|
|
543
|
+
const resolution = resolveSlicesDir(this.root);
|
|
544
|
+
if (!resolution.ok) {
|
|
545
|
+
return {
|
|
546
|
+
jsonrpc: '2.0',
|
|
547
|
+
id,
|
|
548
|
+
result: {
|
|
549
|
+
isError: true,
|
|
550
|
+
content: [
|
|
551
|
+
{
|
|
552
|
+
type: 'text',
|
|
553
|
+
text: JSON.stringify({ status: 'FAIL', code: resolution.error.code }, null, 2)
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const sliceFiles = findSliceFiles(resolution.value.slicesDir);
|
|
560
|
+
const routes = sliceFiles.map((file) => {
|
|
561
|
+
const rel = path.relative(resolution.value.slicesDir, file);
|
|
562
|
+
const parts = rel.split(path.sep);
|
|
563
|
+
const domain = parts[0] || 'general';
|
|
564
|
+
const name = path.basename(file, '.slice.tsx');
|
|
565
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
566
|
+
return {
|
|
567
|
+
sliceKey: `${domain}/${name}`,
|
|
568
|
+
routePath: `/${domain}/${name}`,
|
|
569
|
+
rpcPath: `/_synapse/rpc/${domain}/${name}`,
|
|
570
|
+
dynamicPath: `/${domain}/${name}/:id`,
|
|
571
|
+
filePath: toRelativePath(this.root, file),
|
|
572
|
+
hasAction: /export\s+async\s+function\s+\w+Action\b/.test(code),
|
|
573
|
+
hasLoader: /export\s+async\s+function\s+\w+Loader\b/.test(code),
|
|
574
|
+
hasComponent: /export\s+function\s+\w+(Trigger|View|Form|Component)\b/.test(code),
|
|
575
|
+
hasCache: /export\s+const\s+sliceCache\s*=/.test(code),
|
|
576
|
+
hasJob: /\bdefineJob\s*[<(]/.test(code)
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
contentText = JSON.stringify(
|
|
580
|
+
{
|
|
581
|
+
status: 'PASS',
|
|
582
|
+
operation: 'INSPECT_ROUTES',
|
|
583
|
+
totalRoutes: routes.length,
|
|
584
|
+
routes
|
|
585
|
+
},
|
|
586
|
+
null,
|
|
587
|
+
2
|
|
588
|
+
);
|
|
589
|
+
} else if (toolName === 'synapse_test_slice') {
|
|
590
|
+
const targetSlice = String(args.targetSlice || '');
|
|
591
|
+
if (!targetSlice) {
|
|
592
|
+
return {
|
|
593
|
+
jsonrpc: '2.0',
|
|
594
|
+
id,
|
|
595
|
+
result: {
|
|
596
|
+
isError: true,
|
|
597
|
+
content: [
|
|
598
|
+
{
|
|
599
|
+
type: 'text',
|
|
600
|
+
text: JSON.stringify({ status: 'FAIL', code: 'MISSING_TARGET_SLICE' }, null, 2)
|
|
601
|
+
}
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const report = await runSliceOracles(this.root, { targetSlice });
|
|
607
|
+
contentText = JSON.stringify(report, null, 2);
|
|
608
|
+
} else if (toolName === 'synapse_generate_seed') {
|
|
609
|
+
const targetSlice = String(args.targetSlice || '');
|
|
610
|
+
const count = Math.min(Math.max(Number(args.count) || 5, 1), 50);
|
|
611
|
+
const resolution = resolveSlicesDir(this.root);
|
|
612
|
+
if (!resolution.ok) {
|
|
613
|
+
return {
|
|
614
|
+
jsonrpc: '2.0',
|
|
615
|
+
id,
|
|
616
|
+
result: {
|
|
617
|
+
isError: true,
|
|
618
|
+
content: [
|
|
619
|
+
{
|
|
620
|
+
type: 'text',
|
|
621
|
+
text: JSON.stringify({ status: 'FAIL', code: resolution.error.code }, null, 2)
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const sliceFiles = findSliceFiles(resolution.value.slicesDir);
|
|
628
|
+
const matchedFile = sliceFiles.find((f) => {
|
|
629
|
+
const name = path.basename(f, '.slice.tsx');
|
|
630
|
+
const rel = path.relative(resolution.value.slicesDir, f);
|
|
631
|
+
return name === targetSlice || rel.startsWith(targetSlice) || rel.includes(targetSlice);
|
|
632
|
+
});
|
|
633
|
+
if (!matchedFile) {
|
|
634
|
+
contentText = JSON.stringify(
|
|
635
|
+
{
|
|
636
|
+
status: 'FAIL',
|
|
637
|
+
code: 'SLICE_NOT_FOUND',
|
|
638
|
+
message: `Fatia "${targetSlice}" não encontrada para gerar seed.`
|
|
639
|
+
},
|
|
640
|
+
null,
|
|
641
|
+
2
|
|
642
|
+
);
|
|
643
|
+
} else {
|
|
644
|
+
const code = fs.readFileSync(matchedFile, 'utf-8');
|
|
645
|
+
const tableMatch = code.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z0-9_]+)/i);
|
|
646
|
+
const tableName = tableMatch
|
|
647
|
+
? tableMatch[1]
|
|
648
|
+
: path.basename(matchedFile, '.slice.tsx').replace(/-/g, '_');
|
|
649
|
+
const { getDatabase } = await import('../core/database-factory');
|
|
650
|
+
const dbClient = getDatabase();
|
|
651
|
+
const generatedRows: Record<string, unknown>[] = [];
|
|
652
|
+
let insertedCount = 0;
|
|
653
|
+
for (let i = 1; i <= count; i++) {
|
|
654
|
+
const row = {
|
|
655
|
+
id: crypto.randomUUID(),
|
|
656
|
+
name: `Seed Item ${i}`,
|
|
657
|
+
sku: `SEED-${Math.random().toString(36).substring(2, 7).toUpperCase()}`,
|
|
658
|
+
price: parseFloat((10 + Math.random() * 90).toFixed(2)),
|
|
659
|
+
stock: Math.floor(1 + Math.random() * 100),
|
|
660
|
+
status: i % 3 === 0 ? 'low_stock' : 'active',
|
|
661
|
+
created_at: new Date().toISOString()
|
|
662
|
+
};
|
|
663
|
+
generatedRows.push(row);
|
|
664
|
+
try {
|
|
665
|
+
await dbClient.insert(tableName, row);
|
|
666
|
+
insertedCount++;
|
|
667
|
+
} catch {
|
|
668
|
+
try {
|
|
669
|
+
await dbClient.query(`INSERT INTO ${tableName} (id, name) VALUES ($1, $2)`, [row.id, row.name]);
|
|
670
|
+
insertedCount++;
|
|
671
|
+
} catch {}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
await dbClient.close?.();
|
|
675
|
+
contentText = JSON.stringify(
|
|
676
|
+
{
|
|
677
|
+
status: insertedCount > 0 ? 'PASS' : 'FAIL',
|
|
678
|
+
operation: 'GENERATE_SEED',
|
|
679
|
+
slice: targetSlice,
|
|
680
|
+
table: tableName,
|
|
681
|
+
...(insertedCount > 0 ? {} : { code: 'SEED_INSERT_FAILED' }),
|
|
682
|
+
recordsSynthesized: count,
|
|
683
|
+
recordsInserted: insertedCount,
|
|
684
|
+
sample: generatedRows[0]
|
|
685
|
+
},
|
|
686
|
+
null,
|
|
687
|
+
2
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
} else if (toolName === 'synapse_scaffold_component') {
|
|
691
|
+
const domain = String(args.domain || 'general');
|
|
692
|
+
const name = String(args.name || 'component');
|
|
693
|
+
const componentType = String(args.componentType || 'modal');
|
|
694
|
+
|
|
695
|
+
const domainValid = validatePathSegment(domain);
|
|
696
|
+
const nameValid = validatePathSegment(name);
|
|
697
|
+
if (!domainValid.ok || !nameValid.ok) {
|
|
698
|
+
return {
|
|
699
|
+
jsonrpc: '2.0',
|
|
700
|
+
id,
|
|
701
|
+
result: {
|
|
702
|
+
isError: true,
|
|
703
|
+
content: [
|
|
704
|
+
{
|
|
705
|
+
type: 'text',
|
|
706
|
+
text: JSON.stringify({ status: 'FAIL', code: 'INVALID_PATH_SEGMENT' }, null, 2)
|
|
707
|
+
}
|
|
708
|
+
]
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const resolution = resolveSlicesDir(this.root);
|
|
714
|
+
let slicesDir: string;
|
|
715
|
+
if (resolution.ok) {
|
|
716
|
+
slicesDir = resolution.value.slicesDir;
|
|
717
|
+
} else if (resolution.error.code === 'AMBIGUOUS_SLICES_DIR') {
|
|
718
|
+
return {
|
|
719
|
+
jsonrpc: '2.0',
|
|
720
|
+
id,
|
|
721
|
+
result: {
|
|
722
|
+
isError: true,
|
|
723
|
+
content: [
|
|
724
|
+
{
|
|
725
|
+
type: 'text',
|
|
726
|
+
text: JSON.stringify({ status: 'FAIL', code: resolution.error.code }, null, 2)
|
|
727
|
+
}
|
|
728
|
+
]
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
} else {
|
|
732
|
+
slicesDir = path.join(path.resolve(this.root), 'src', 'slices');
|
|
733
|
+
}
|
|
734
|
+
const outPath = path.join(slicesDir, domain, `${name}.tsx`);
|
|
735
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
736
|
+
const pascal = name
|
|
737
|
+
.split(/[-_]/)
|
|
738
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
739
|
+
.join('');
|
|
740
|
+
let componentCode = '';
|
|
741
|
+
if (componentType === 'modal') {
|
|
742
|
+
componentCode = `import React, { useState } from 'react';\nimport { Modal, Button } from 'synapsejs/client';\n\nexport function ${pascal}() {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <div>\n <Button onClick={() => setIsOpen(true)}>Abrir ${pascal}</Button>\n <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="${pascal}">\n <p className="text-slate-300">Conteúdo interativo do modal gerenciado no cliente.</p>\n </Modal>\n </div>\n );\n}\n`;
|
|
743
|
+
} else if (componentType === 'drawer') {
|
|
744
|
+
componentCode = `import React, { useState } from 'react';\nimport { Drawer, Button } from 'synapsejs/client';\n\nexport function ${pascal}() {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <div>\n <Button onClick={() => setIsOpen(true)}>Exibir ${pascal}</Button>\n <Drawer isOpen={isOpen} onClose={() => setIsOpen(false)} title="${pascal}">\n <p className="text-slate-300">Painel lateral deslizante.</p>\n </Drawer>\n </div>\n );\n}\n`;
|
|
745
|
+
} else if (componentType === 'tabs') {
|
|
746
|
+
componentCode = `import React from 'react';\nimport { Tabs, TabList, Tab, TabPanel } from 'synapsejs/client';\n\nexport function ${pascal}() {\n return (\n <Tabs defaultValue="tab1">\n <TabList>\n <Tab value="tab1">Aba 1</Tab>\n <Tab value="tab2">Aba 2</Tab>\n </TabList>\n <TabPanel value="tab1"><div className="p-4 text-slate-300">Painel 1</div></TabPanel>\n <TabPanel value="tab2"><div className="p-4 text-slate-300">Painel 2</div></TabPanel>\n </Tabs>\n );\n}\n`;
|
|
747
|
+
} else {
|
|
748
|
+
componentCode = `import React from 'react';\nimport { DropdownMenu, DropdownItem, Button } from 'synapsejs/client';\n\nexport function ${pascal}() {\n return (\n <DropdownMenu trigger={<Button variant="secondary">Ações ▾</Button>}>\n <DropdownItem onClick={() => alert('Opção 1')}>Opção 1</DropdownItem>\n <DropdownItem variant="danger" onClick={() => alert('Excluir')}>Excluir</DropdownItem>\n </DropdownMenu>\n );\n}\n`;
|
|
749
|
+
}
|
|
750
|
+
fs.writeFileSync(outPath, componentCode, 'utf-8');
|
|
751
|
+
contentText = JSON.stringify(
|
|
752
|
+
{
|
|
753
|
+
status: 'PASS',
|
|
754
|
+
operation: 'SCAFFOLD_COMPONENT',
|
|
755
|
+
componentType,
|
|
756
|
+
filePath: toRelativePath(this.root, outPath)
|
|
757
|
+
},
|
|
758
|
+
null,
|
|
759
|
+
2
|
|
760
|
+
);
|
|
662
761
|
} else {
|
|
663
762
|
return {
|
|
664
763
|
jsonrpc: '2.0',
|