carrick 0.3.53

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 (114) hide show
  1. package/LICENSE.md +99 -0
  2. package/README.md +79 -0
  3. package/bin/carrick.mjs +168 -0
  4. package/dist/channel.d.ts +16 -0
  5. package/dist/channel.js +37 -0
  6. package/dist/channel.js.map +1 -0
  7. package/dist/cli.d.ts +29 -0
  8. package/dist/cli.js +60 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/contract.d.ts +154 -0
  11. package/dist/contract.js +141 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/diagnostics.d.ts +54 -0
  14. package/dist/diagnostics.js +199 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/hook/post-edit.d.ts +13 -0
  17. package/dist/hook/post-edit.js +76 -0
  18. package/dist/hook/post-edit.js.map +1 -0
  19. package/dist/hook/session-start.d.ts +2 -0
  20. package/dist/hook/session-start.js +44 -0
  21. package/dist/hook/session-start.js.map +1 -0
  22. package/dist/init/identity.d.ts +20 -0
  23. package/dist/init/identity.js +60 -0
  24. package/dist/init/identity.js.map +1 -0
  25. package/dist/init/repos.d.ts +19 -0
  26. package/dist/init/repos.js +46 -0
  27. package/dist/init/repos.js.map +1 -0
  28. package/dist/init/run.d.ts +11 -0
  29. package/dist/init/run.js +228 -0
  30. package/dist/init/run.js.map +1 -0
  31. package/dist/init/settings.d.ts +50 -0
  32. package/dist/init/settings.js +138 -0
  33. package/dist/init/settings.js.map +1 -0
  34. package/dist/log.d.ts +2 -0
  35. package/dist/log.js +35 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/native.d.ts +48 -0
  38. package/dist/native.js +127 -0
  39. package/dist/native.js.map +1 -0
  40. package/dist/render.d.ts +65 -0
  41. package/dist/render.js +334 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/root.d.ts +30 -0
  44. package/dist/root.js +82 -0
  45. package/dist/root.js.map +1 -0
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +255 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/templates.d.ts +18 -0
  50. package/dist/templates.js +66 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +72 -0
  53. package/plugin/.claude-plugin/plugin.json +6 -0
  54. package/plugin/.lsp.json +14 -0
  55. package/plugin/hooks/hooks.json +27 -0
  56. package/sidecar/dist/src/bundler.d.ts +141 -0
  57. package/sidecar/dist/src/bundler.js +680 -0
  58. package/sidecar/dist/src/capture/anchors.d.ts +61 -0
  59. package/sidecar/dist/src/capture/anchors.js +1132 -0
  60. package/sidecar/dist/src/capture/api.d.ts +378 -0
  61. package/sidecar/dist/src/capture/api.js +10 -0
  62. package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
  63. package/sidecar/dist/src/capture/augmentations.js +60 -0
  64. package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
  65. package/sidecar/dist/src/capture/check-classify.js +189 -0
  66. package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
  67. package/sidecar/dist/src/capture/check-deep.js +91 -0
  68. package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
  69. package/sidecar/dist/src/capture/check-poison.js +155 -0
  70. package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
  71. package/sidecar/dist/src/capture/check-probe.js +147 -0
  72. package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
  73. package/sidecar/dist/src/capture/check-scrub.js +71 -0
  74. package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
  75. package/sidecar/dist/src/capture/check-workspace.js +218 -0
  76. package/sidecar/dist/src/capture/check.d.ts +39 -0
  77. package/sidecar/dist/src/capture/check.js +450 -0
  78. package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
  79. package/sidecar/dist/src/capture/deep-walk.js +243 -0
  80. package/sidecar/dist/src/capture/index.d.ts +36 -0
  81. package/sidecar/dist/src/capture/index.js +477 -0
  82. package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
  83. package/sidecar/dist/src/capture/lockfile.js +490 -0
  84. package/sidecar/dist/src/capture/machinery.d.ts +59 -0
  85. package/sidecar/dist/src/capture/machinery.js +160 -0
  86. package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
  87. package/sidecar/dist/src/capture/node-builder.js +123 -0
  88. package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
  89. package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
  90. package/sidecar/dist/src/capture/self-check.d.ts +38 -0
  91. package/sidecar/dist/src/capture/self-check.js +317 -0
  92. package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
  93. package/sidecar/dist/src/capture/specifiers.js +56 -0
  94. package/sidecar/dist/src/definition-resolver.d.ts +57 -0
  95. package/sidecar/dist/src/definition-resolver.js +153 -0
  96. package/sidecar/dist/src/index.d.ts +14 -0
  97. package/sidecar/dist/src/index.js +564 -0
  98. package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
  99. package/sidecar/dist/src/monorepo-builder.js +584 -0
  100. package/sidecar/dist/src/project-loader.d.ts +130 -0
  101. package/sidecar/dist/src/project-loader.js +399 -0
  102. package/sidecar/dist/src/type-inferrer.d.ts +940 -0
  103. package/sidecar/dist/src/type-inferrer.js +3540 -0
  104. package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
  105. package/sidecar/dist/src/type-structural-expander.js +283 -0
  106. package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
  107. package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
  108. package/sidecar/dist/src/types.d.ts +669 -0
  109. package/sidecar/dist/src/types.js +5 -0
  110. package/sidecar/dist/src/validators.d.ts +2214 -0
  111. package/sidecar/dist/src/validators.js +336 -0
  112. package/sidecar/package.json +6 -0
  113. package/templates/carrick.json +7 -0
  114. package/templates/carrick.yml +45 -0
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Main entry point for the type-sidecar
3
+ *
4
+ * This module implements a message loop that:
5
+ * 1. Listens on stdin for JSON requests
6
+ * 2. Processes each request (init, bundle, emit_surface, infer, build_workspace, check_compatibility, health, shutdown)
7
+ * 3. Writes JSON responses to stdout
8
+ *
9
+ * IMPORTANT:
10
+ * - stdout is ONLY for JSON responses
11
+ * - stderr is for logging
12
+ * - Process stays alive between requests (warm standby)
13
+ */
14
+ import * as readline from 'node:readline';
15
+ import { parseRequest } from './validators.js';
16
+ import { ProjectLoader } from './project-loader.js';
17
+ import { TypeBundler, SurfaceEmitter } from './bundler.js';
18
+ import { TypeInferrer } from './type-inferrer.js';
19
+ import { MonorepoBuilder } from './monorepo-builder.js';
20
+ import { DefinitionResolver } from './definition-resolver.js';
21
+ import { captureStub, runCheck } from './capture/index.js';
22
+ // ===========================================================================
23
+ // Module-level state
24
+ // ===========================================================================
25
+ let projectLoader = null;
26
+ let monorepoBuilder = null;
27
+ let initTimeMs = null;
28
+ let components = null;
29
+ /**
30
+ * Get the project-backed components, building the project if this is the
31
+ * first request that needs it.
32
+ *
33
+ * @throws if init has not run, or if the project cannot be built
34
+ */
35
+ function projectComponents() {
36
+ if (!projectLoader?.isInitialized()) {
37
+ throw new Error('Sidecar not initialized. Call init first.');
38
+ }
39
+ if (!components) {
40
+ const project = projectLoader.getProject();
41
+ const repoRoot = projectLoader.getRepoRoot();
42
+ components = {
43
+ typeBundler: new TypeBundler({ project, repoRoot }),
44
+ surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
45
+ typeInferrer: new TypeInferrer({ project }),
46
+ definitionResolver: new DefinitionResolver({ project }),
47
+ };
48
+ }
49
+ return components;
50
+ }
51
+ // ===========================================================================
52
+ // Request Handlers
53
+ // ===========================================================================
54
+ /**
55
+ * Handle the 'init' action - initialize the TypeScript project
56
+ */
57
+ function handleInit(request) {
58
+ const startTime = performance.now();
59
+ try {
60
+ log(`Initializing with repo_root: ${request.repo_root}`);
61
+ // Re-init re-scopes the sidecar to another root: drop everything built
62
+ // over the previous project before resolving the new one.
63
+ components = null;
64
+ projectLoader = new ProjectLoader({
65
+ repoRoot: request.repo_root,
66
+ tsconfigPath: request.tsconfig_path,
67
+ tsconfigSnapshot: request.tsconfig_snapshot,
68
+ pinnedDependencies: request.pinned_dependencies,
69
+ });
70
+ const result = projectLoader.load();
71
+ if (!result.success) {
72
+ return {
73
+ request_id: request.request_id,
74
+ status: 'error',
75
+ errors: [result.error || 'Unknown initialization error'],
76
+ init_time_ms: result.initTimeMs,
77
+ };
78
+ }
79
+ // The project and everything that reads it are built by the first request
80
+ // that needs them, so readiness costs the same on a bare checkout as on
81
+ // one with its dependencies installed (carrick#749).
82
+ // Initialize monorepo builder (doesn't need project)
83
+ monorepoBuilder = new MonorepoBuilder();
84
+ initTimeMs = result.initTimeMs || Math.round(performance.now() - startTime);
85
+ log(`Initialization complete in ${initTimeMs}ms`);
86
+ return {
87
+ request_id: request.request_id,
88
+ status: 'ready',
89
+ init_time_ms: initTimeMs,
90
+ };
91
+ }
92
+ catch (err) {
93
+ const error = err instanceof Error ? err.message : String(err);
94
+ logError(`Initialization failed: ${error}`);
95
+ return {
96
+ request_id: request.request_id,
97
+ status: 'error',
98
+ errors: [error],
99
+ init_time_ms: Math.round(performance.now() - startTime),
100
+ };
101
+ }
102
+ }
103
+ /**
104
+ * Handle the 'bundle' action - bundle explicit types (legacy)
105
+ */
106
+ function handleBundle(request) {
107
+ try {
108
+ log(`Bundling ${request.symbols.length} symbol(s)`);
109
+ const result = projectComponents().typeBundler.bundle(request.symbols);
110
+ if (!result.success) {
111
+ return {
112
+ request_id: request.request_id,
113
+ status: 'error',
114
+ dts_content: result.dts_content,
115
+ manifest: result.manifest,
116
+ symbol_failures: result.symbol_failures,
117
+ errors: result.errors,
118
+ };
119
+ }
120
+ return {
121
+ request_id: request.request_id,
122
+ status: 'success',
123
+ dts_content: result.dts_content,
124
+ manifest: result.manifest,
125
+ symbol_failures: result.symbol_failures,
126
+ };
127
+ }
128
+ catch (err) {
129
+ const error = err instanceof Error ? err.message : String(err);
130
+ logError(`Bundle failed: ${error}`);
131
+ return {
132
+ request_id: request.request_id,
133
+ status: 'error',
134
+ errors: [error],
135
+ };
136
+ }
137
+ }
138
+ /**
139
+ * Handle the 'emit_surface' action - emit a surface .d.ts with rewritten specifiers
140
+ */
141
+ function handleEmitSurface(request) {
142
+ try {
143
+ log(`Emitting surface for repo '${request.repo_name}' with ${request.payloads.length} payload(s)`);
144
+ const result = projectComponents().surfaceEmitter.emit(request.repo_name, request.payloads, request.output_path);
145
+ if (!result.success) {
146
+ return {
147
+ request_id: request.request_id,
148
+ status: 'error',
149
+ errors: result.errors,
150
+ };
151
+ }
152
+ return {
153
+ request_id: request.request_id,
154
+ status: 'success',
155
+ output_path: result.output_path,
156
+ surface_content: result.surface_content,
157
+ manifest: result.manifest,
158
+ };
159
+ }
160
+ catch (err) {
161
+ const error = err instanceof Error ? err.message : String(err);
162
+ logError(`Surface emission failed: ${error}`);
163
+ return {
164
+ request_id: request.request_id,
165
+ status: 'error',
166
+ errors: [error],
167
+ };
168
+ }
169
+ }
170
+ /**
171
+ * Handle the 'capture_v2' action - v2 "tsc as serializer" capture.
172
+ * Stateless by design: unlike bundle/infer it needs no init'd ts-morph
173
+ * project, only the repo's own tsconfig — this is the point of v2. The
174
+ * capture bundle behind this action is the seam ("seam, not split"): this
175
+ * dispatcher call is the only non-type reach-in.
176
+ */
177
+ function handleCaptureV2(request) {
178
+ try {
179
+ log(`capture_v2 for service '${request.service_name}' (${request.anchors.length} anchor(s))`);
180
+ const result = captureStub({
181
+ repoRoot: request.repo_root,
182
+ serviceName: request.service_name,
183
+ anchors: request.anchors,
184
+ outDir: request.out_dir,
185
+ tsconfigPath: request.tsconfig_path,
186
+ });
187
+ return {
188
+ request_id: request.request_id,
189
+ status: result.success ? 'success' : 'error',
190
+ result,
191
+ errors: result.errors.length > 0 ? result.errors : undefined,
192
+ };
193
+ }
194
+ catch (err) {
195
+ const error = err instanceof Error ? err.message : String(err);
196
+ logError(`capture_v2 failed: ${error}`);
197
+ return {
198
+ request_id: request.request_id,
199
+ status: 'error',
200
+ errors: [error],
201
+ };
202
+ }
203
+ }
204
+ /**
205
+ * Handle the 'check_v2' action - v2 "tsc as judge" compatibility check.
206
+ *
207
+ * Async by necessity: the vendored pnpm install can exceed the Rust client's
208
+ * read deadline, and running it off the event loop (spawn, not execSync) keeps
209
+ * the sidecar responsive. Emits `status: 'progress'` keepalive frames during
210
+ * install/check and one terminal `success`/`error` frame. Writes its own
211
+ * frames, so processLine hands off and does not write a response for it.
212
+ */
213
+ async function handleCheckV2Async(request) {
214
+ let phase = 'assembling';
215
+ const keepalive = setInterval(() => {
216
+ writeProgress(request.request_id, phase, `check ${phase}`);
217
+ }, 1500);
218
+ keepalive.unref();
219
+ try {
220
+ log(`check_v2: ${request.stubs.length} stub(s), ${request.pairs.length} pair(s)`);
221
+ const result = await runCheck({
222
+ stubs: request.stubs,
223
+ pairs: request.pairs,
224
+ workspaceRoot: request.workspace_root,
225
+ cleanup: request.keep_workspace !== true,
226
+ }, (p) => {
227
+ phase = p;
228
+ });
229
+ clearInterval(keepalive);
230
+ const response = {
231
+ request_id: request.request_id,
232
+ status: result.success ? 'success' : 'error',
233
+ result,
234
+ errors: result.errors.length > 0 ? result.errors : undefined,
235
+ };
236
+ writeResponse(response);
237
+ }
238
+ catch (err) {
239
+ clearInterval(keepalive);
240
+ const error = err instanceof Error ? err.message : String(err);
241
+ logError(`check_v2 failed: ${error}`);
242
+ writeResponse({
243
+ request_id: request.request_id,
244
+ status: 'error',
245
+ errors: [error],
246
+ });
247
+ }
248
+ }
249
+ /**
250
+ * Handle the 'infer' action - infer implicit types
251
+ */
252
+ function handleInfer(request) {
253
+ try {
254
+ log(`Inferring ${request.requests.length} type(s)`);
255
+ const result = projectComponents().typeInferrer.infer(request.requests, request.extraction_config);
256
+ return {
257
+ request_id: request.request_id,
258
+ status: result.success ? 'success' : 'error',
259
+ inferred_types: result.inferred_types,
260
+ errors: result.errors,
261
+ };
262
+ }
263
+ catch (err) {
264
+ const error = err instanceof Error ? err.message : String(err);
265
+ logError(`Inference failed: ${error}`);
266
+ return {
267
+ request_id: request.request_id,
268
+ status: 'error',
269
+ errors: [error],
270
+ };
271
+ }
272
+ }
273
+ /**
274
+ * Handle the 'build_workspace' action - build synthetic monorepo workspace
275
+ */
276
+ function handleBuildWorkspace(request) {
277
+ // MonorepoBuilder doesn't require project initialization
278
+ if (!monorepoBuilder) {
279
+ monorepoBuilder = new MonorepoBuilder();
280
+ }
281
+ try {
282
+ log(`Building synthetic workspace with ${request.repos.length} repo(s)`);
283
+ const result = monorepoBuilder.build(request.repos, request.workspace_root);
284
+ if (!result.success) {
285
+ return {
286
+ request_id: request.request_id,
287
+ status: 'error',
288
+ errors: result.errors,
289
+ };
290
+ }
291
+ return {
292
+ request_id: request.request_id,
293
+ status: 'success',
294
+ workspace_path: result.workspace_path,
295
+ stub_packages: result.stub_packages,
296
+ checker_path: result.checker_path,
297
+ };
298
+ }
299
+ catch (err) {
300
+ const error = err instanceof Error ? err.message : String(err);
301
+ logError(`Workspace build failed: ${error}`);
302
+ return {
303
+ request_id: request.request_id,
304
+ status: 'error',
305
+ errors: [error],
306
+ };
307
+ }
308
+ }
309
+ /**
310
+ * Handle the 'check_compatibility' action - run type compatibility checks
311
+ */
312
+ function handleCheckCompatibility(request) {
313
+ if (!monorepoBuilder) {
314
+ monorepoBuilder = new MonorepoBuilder();
315
+ }
316
+ try {
317
+ log(`Running compatibility checks: ${request.checks.length} check(s)`);
318
+ const result = monorepoBuilder.checkCompatibility(request.workspace_root, request.checks);
319
+ if (!result.success) {
320
+ return {
321
+ request_id: request.request_id,
322
+ status: 'error',
323
+ errors: result.errors,
324
+ };
325
+ }
326
+ return {
327
+ request_id: request.request_id,
328
+ status: 'success',
329
+ results: result.results,
330
+ diagnostics: result.diagnostics,
331
+ };
332
+ }
333
+ catch (err) {
334
+ const error = err instanceof Error ? err.message : String(err);
335
+ logError(`Compatibility check failed: ${error}`);
336
+ return {
337
+ request_id: request.request_id,
338
+ status: 'error',
339
+ errors: [error],
340
+ };
341
+ }
342
+ }
343
+ /**
344
+ * Handle the 'resolve_definitions' action - resolve surface aliases from a
345
+ * v2 capture stub package's declaration tree.
346
+ */
347
+ function handleResolveDefinitions(request) {
348
+ try {
349
+ log(`Resolving ${request.aliases.length} type alias(es) from ${request.stub_dir}`);
350
+ const results = projectComponents().definitionResolver.resolveFromStub(request.stub_dir, request.aliases);
351
+ return {
352
+ request_id: request.request_id,
353
+ status: 'success',
354
+ definitions: results.map((r) => ({
355
+ type_alias: r.type_alias,
356
+ definition: r.definition,
357
+ expanded: r.expanded,
358
+ })),
359
+ };
360
+ }
361
+ catch (err) {
362
+ const error = err instanceof Error ? err.message : String(err);
363
+ logError(`Definition resolution failed: ${error}`);
364
+ return {
365
+ request_id: request.request_id,
366
+ status: 'error',
367
+ errors: [error],
368
+ };
369
+ }
370
+ }
371
+ /**
372
+ * Handle the 'health' action - report initialization status
373
+ */
374
+ function handleHealth(request) {
375
+ const isReady = projectLoader?.isInitialized() ?? false;
376
+ return {
377
+ request_id: request.request_id,
378
+ status: isReady ? 'ready' : 'not_ready',
379
+ init_time_ms: initTimeMs ?? undefined,
380
+ };
381
+ }
382
+ /**
383
+ * Handle the 'shutdown' action - exit gracefully
384
+ */
385
+ function handleShutdown(request) {
386
+ log('Shutdown requested');
387
+ // Schedule exit after response is sent
388
+ setImmediate(() => {
389
+ log('Exiting');
390
+ process.exit(0);
391
+ });
392
+ return {
393
+ request_id: request.request_id,
394
+ status: 'success',
395
+ };
396
+ }
397
+ // ===========================================================================
398
+ // Request Router
399
+ // ===========================================================================
400
+ /**
401
+ * Route a request to the appropriate handler
402
+ */
403
+ function handleRequest(request) {
404
+ switch (request.action) {
405
+ case 'init':
406
+ return handleInit(request);
407
+ case 'bundle':
408
+ return handleBundle(request);
409
+ case 'emit_surface':
410
+ return handleEmitSurface(request);
411
+ case 'capture_v2':
412
+ return handleCaptureV2(request);
413
+ case 'infer':
414
+ return handleInfer(request);
415
+ case 'build_workspace':
416
+ return handleBuildWorkspace(request);
417
+ case 'check_compatibility':
418
+ return handleCheckCompatibility(request);
419
+ case 'resolve_definitions':
420
+ return handleResolveDefinitions(request);
421
+ case 'health':
422
+ return handleHealth(request);
423
+ case 'shutdown':
424
+ return handleShutdown(request);
425
+ default:
426
+ // TypeScript should catch this, but just in case
427
+ return {
428
+ request_id: request.request_id || 'unknown',
429
+ status: 'error',
430
+ errors: [`Unknown action: ${request.action}`],
431
+ };
432
+ }
433
+ }
434
+ // ===========================================================================
435
+ // Response Writer
436
+ // ===========================================================================
437
+ /**
438
+ * Write a JSON response to stdout
439
+ */
440
+ function writeResponse(response) {
441
+ const json = JSON.stringify(response);
442
+ process.stdout.write(json + '\n');
443
+ }
444
+ /**
445
+ * Write a non-terminal progress/keepalive frame (async install protocol).
446
+ * Distinct `status: 'progress'` so clients skip it and wait for the terminal
447
+ * success/error frame.
448
+ */
449
+ function writeProgress(requestId, phase, message) {
450
+ process.stdout.write(JSON.stringify({ request_id: requestId, status: 'progress', phase, message }) + '\n');
451
+ }
452
+ /**
453
+ * Write an error response for an invalid request
454
+ */
455
+ function writeErrorResponse(requestId, error) {
456
+ const response = {
457
+ request_id: requestId,
458
+ status: 'error',
459
+ errors: [error],
460
+ };
461
+ writeResponse(response);
462
+ }
463
+ // ===========================================================================
464
+ // Logging (to stderr only)
465
+ // ===========================================================================
466
+ function log(message) {
467
+ console.error(`[sidecar] ${message}`);
468
+ }
469
+ function logError(message) {
470
+ console.error(`[sidecar:error] ${message}`);
471
+ }
472
+ // ===========================================================================
473
+ // Main Entry Point
474
+ // ===========================================================================
475
+ /**
476
+ * Process a single line of input
477
+ */
478
+ function processLine(line) {
479
+ // Skip empty lines
480
+ const trimmed = line.trim();
481
+ if (!trimmed)
482
+ return;
483
+ // Parse JSON
484
+ let json;
485
+ try {
486
+ json = JSON.parse(trimmed);
487
+ }
488
+ catch (err) {
489
+ logError(`Invalid JSON: ${trimmed}`);
490
+ writeErrorResponse('unknown', `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
491
+ return;
492
+ }
493
+ // Validate request
494
+ const parseResult = parseRequest(json);
495
+ if (!parseResult.success) {
496
+ logError(`Invalid request: ${parseResult.error}`);
497
+ writeErrorResponse(json?.request_id || 'unknown', parseResult.error);
498
+ return;
499
+ }
500
+ // check_v2 runs async (spawned pnpm install + tsc) and writes its own
501
+ // progress + terminal frames; hand off and return.
502
+ if (parseResult.request.action === 'check_v2') {
503
+ void handleCheckV2Async(parseResult.request);
504
+ return;
505
+ }
506
+ // Handle request
507
+ const response = handleRequest(parseResult.request);
508
+ writeResponse(response);
509
+ }
510
+ /**
511
+ * Start the message loop
512
+ */
513
+ function main() {
514
+ log('Process started');
515
+ const rl = readline.createInterface({
516
+ input: process.stdin,
517
+ output: process.stdout,
518
+ terminal: false,
519
+ });
520
+ rl.on('line', processLine);
521
+ rl.on('close', () => {
522
+ log('stdin closed, exiting');
523
+ process.exit(0);
524
+ });
525
+ // Handle process signals
526
+ process.on('SIGINT', () => {
527
+ log('SIGINT received, exiting');
528
+ process.exit(0);
529
+ });
530
+ process.on('SIGTERM', () => {
531
+ log('SIGTERM received, exiting');
532
+ process.exit(0);
533
+ });
534
+ // Fail fast on uncaught errors. Continuing after one means serving later
535
+ // requests from a possibly corrupted state and returning garbage types
536
+ // with a success status; exiting surfaces as ProcessDied on the Rust side,
537
+ // which degrades cleanly (scan continues, type_extraction_status records
538
+ // the loss).
539
+ process.on('uncaughtException', (err) => {
540
+ logError(`Uncaught exception (exiting): ${err.message}`);
541
+ logError(err.stack || '');
542
+ process.exit(1);
543
+ });
544
+ process.on('unhandledRejection', (reason) => {
545
+ // This log is the last diagnostic before exit, so it must be actionable:
546
+ // a bare interpolation renders non-Error reasons as [object Object].
547
+ let detail;
548
+ if (reason instanceof Error) {
549
+ detail = reason.stack || reason.message;
550
+ }
551
+ else {
552
+ try {
553
+ detail = JSON.stringify(reason) ?? String(reason);
554
+ }
555
+ catch {
556
+ detail = String(reason);
557
+ }
558
+ }
559
+ logError(`Unhandled rejection (exiting): ${detail}`);
560
+ process.exit(1);
561
+ });
562
+ }
563
+ // Start the sidecar
564
+ main();
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Monorepo Builder - Builds synthetic monorepo workspaces for type compatibility checking
3
+ *
4
+ * This module implements the "Synthetic Monorepo (Stub Snapshot)" architecture:
5
+ * - Creates a workspace with stub packages for each repo
6
+ * - Each stub package has its own isolated node_modules with pinned dependencies
7
+ * - Generates path mappings so @carrick/{repoName}/{spec} resolves correctly
8
+ * - Creates a checker package that imports surfaces and asserts type compatibility
9
+ *
10
+ * Key design decisions:
11
+ * - Uses pnpm for better isolation between packages
12
+ * - Does NOT copy full repos - only uses captured artifacts
13
+ * - Deterministic: uses pinned dependency snapshots
14
+ */
15
+ import type { RepoMetadata, WorkspaceBuildResult, CompatibilityCheck, CompatibilityCheckResult } from './types.js';
16
+ /**
17
+ * MonorepoBuilder - Creates synthetic workspaces for cross-repo type checking
18
+ *
19
+ * Usage:
20
+ * const builder = new MonorepoBuilder();
21
+ * const result = builder.build(repos);
22
+ * const checkResult = builder.checkCompatibility(workspacePath, checks);
23
+ */
24
+ export declare class MonorepoBuilder {
25
+ /**
26
+ * Build the synthetic monorepo workspace
27
+ *
28
+ * @param repos - Metadata for each repository to include
29
+ * @param workspaceRoot - Root directory for the workspace (defaults to .carrick/workspace)
30
+ * @returns WorkspaceBuildResult
31
+ */
32
+ build(repos: RepoMetadata[], workspaceRoot?: string): WorkspaceBuildResult;
33
+ /**
34
+ * Run type compatibility checks in the workspace
35
+ *
36
+ * @param workspaceRoot - Path to the workspace root
37
+ * @param checks - Compatibility checks to run
38
+ * @returns CompatibilityCheckResult
39
+ */
40
+ checkCompatibility(workspaceRoot: string, checks: CompatibilityCheck[]): CompatibilityCheckResult;
41
+ /**
42
+ * Ensure a clean directory exists at the given path
43
+ */
44
+ private ensureCleanDirectory;
45
+ /**
46
+ * Create the root package.json for the workspace
47
+ */
48
+ private createRootPackageJson;
49
+ /**
50
+ * Create pnpm-workspace.yaml
51
+ */
52
+ private createPnpmWorkspaceYaml;
53
+ /**
54
+ * Create .npmrc to prevent hoisting that would blur package boundaries
55
+ */
56
+ private createNpmrc;
57
+ /**
58
+ * Create a stub package for a repository
59
+ *
60
+ * @returns Path to the created package
61
+ */
62
+ private createStubPackage;
63
+ /**
64
+ * Create package.json for a stub package
65
+ */
66
+ private createStubPackageJson;
67
+ /**
68
+ * Create tsconfig.json for a stub package from the snapshot
69
+ */
70
+ private createStubTsconfig;
71
+ /**
72
+ * Create the surface.d.ts file
73
+ */
74
+ private createSurfaceFile;
75
+ /**
76
+ * Create a placeholder surface file
77
+ */
78
+ private createPlaceholderSurface;
79
+ /**
80
+ * Get default tsconfig snapshot
81
+ */
82
+ private getDefaultTsconfigSnapshot;
83
+ /**
84
+ * Create the checker package that runs type compatibility checks
85
+ *
86
+ * @returns Path to the checker package
87
+ */
88
+ private createCheckerPackage;
89
+ /**
90
+ * Create package.json for the checker package
91
+ */
92
+ private createCheckerPackageJson;
93
+ /**
94
+ * Create tsconfig.json for the checker package with path mappings
95
+ */
96
+ private createCheckerTsconfig;
97
+ /**
98
+ * Create initial checks.ts file
99
+ */
100
+ private createInitialChecksFile;
101
+ /**
102
+ * Install dependencies using pnpm
103
+ */
104
+ private installDependencies;
105
+ /**
106
+ * Fallback: Install dependencies using npm
107
+ */
108
+ private installWithNpm;
109
+ /**
110
+ * Generate the checks.ts file with type assertions
111
+ */
112
+ private generateCheckFile;
113
+ /**
114
+ * Create a unique import name for a repo/alias combination
115
+ */
116
+ private makeImportName;
117
+ /**
118
+ * Run TypeScript type checking on the checker package
119
+ *
120
+ * @returns Object with success status and any error output
121
+ */
122
+ private runTypeCheck;
123
+ /**
124
+ * Parse the result of a type check for a specific compatibility check
125
+ */
126
+ private parseCheckResult;
127
+ private log;
128
+ private logError;
129
+ }