carrick 0.3.108 → 0.3.109

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.108",
3
+ "version": "0.3.109",
4
4
  "description": "Maps your entire TypeScript codebase across services and repositories, giving AI agents full context on existing types, routes, and function behaviours over MCP before they write duplicate or breaking code.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -53,20 +53,19 @@
53
53
  "@clack/prompts": "1.8.1",
54
54
  "picocolors": "1.1.1",
55
55
  "pnpm": "10.34.5",
56
- "ts-morph": "^25.0.1",
57
- "typescript": "^5.8.0",
56
+ "ts-morph": "28.0.0",
57
+ "typescript": "6.0.2",
58
58
  "zod": "^3.23.0"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@carrick-tools/cli-darwin-arm64": "0.3.108",
62
- "@carrick-tools/cli-darwin-x64": "0.3.108",
63
- "@carrick-tools/cli-linux-arm64": "0.3.108",
64
- "@carrick-tools/cli-linux-x64": "0.3.108",
65
- "@carrick-tools/cli-win32-x64": "0.3.108"
61
+ "@carrick-tools/cli-darwin-arm64": "0.3.109",
62
+ "@carrick-tools/cli-darwin-x64": "0.3.109",
63
+ "@carrick-tools/cli-linux-arm64": "0.3.109",
64
+ "@carrick-tools/cli-linux-x64": "0.3.109",
65
+ "@carrick-tools/cli-win32-x64": "0.3.109"
66
66
  },
67
67
  "devDependencies": {
68
- "@types/node": "^24.13.3",
69
- "typescript": "^5.9.2"
68
+ "@types/node": "^24.13.3"
70
69
  },
71
70
  "publishConfig": {
72
71
  "access": "public",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "carrick",
3
3
  "description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
4
- "version": "0.3.108",
4
+ "version": "0.3.109",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -18,6 +18,7 @@
18
18
  import ts from 'typescript';
19
19
  import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
+ import { sidecarCompilerOptions } from './compiler-options.js';
21
22
  import { findDisqualifyingTopTypes, provenanceOf } from './deep-walk.js';
22
23
  export function openProbeProgram(probesDir, plans) {
23
24
  if (plans.length === 0)
@@ -35,7 +36,7 @@ export function openProbeProgram(probesDir, plans) {
35
36
  .filter((f) => fs.existsSync(f));
36
37
  if (fileNames.length === 0)
37
38
  return undefined;
38
- const program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true });
39
+ const program = ts.createProgram(fileNames, sidecarCompilerOptions({ ...parsed.options, noEmit: true }));
39
40
  return { program, checker: program.getTypeChecker(), probesDir };
40
41
  }
41
42
  catch {
@@ -53,3 +53,4 @@ export interface AssembleOptions {
53
53
  export declare function assembleWorkspace(opts: AssembleOptions): AssembledWorkspace;
54
54
  /** Write the generated probe files into the assembled probes package. */
55
55
  export declare function writeProbes(ws: AssembledWorkspace, plans: ProbePlan[]): void;
56
+ export declare const CHECKER_TSCONFIG: string;
@@ -195,8 +195,11 @@ const NPMRC = [
195
195
  // TS2717 collisions so the poison rule can convert them to honest
196
196
  // unverifiables). noUnusedLocals/Parameters:false keep TS6133/6196 off the
197
197
  // gate/assignment lines. moduleResolution bundler accepts both node16
198
- // `.js`-suffixed and extensionless specifiers in one program.
199
- const CHECKER_TSCONFIG = JSON.stringify({
198
+ // `.js`-suffixed and extensionless specifiers in one program. The rest is
199
+ // what `sidecarCompilerOptions` sets on every other program (carrick#2019):
200
+ // `tsc` reads this file itself, so each value is written out, and a test
201
+ // holds the file to what that function would make of it.
202
+ export const CHECKER_TSCONFIG = JSON.stringify({
200
203
  compilerOptions: {
201
204
  strict: true,
202
205
  exactOptionalPropertyTypes: false,
@@ -210,6 +213,14 @@ const CHECKER_TSCONFIG = JSON.stringify({
210
213
  types: [],
211
214
  noEmit: true,
212
215
  forceConsistentCasingInFileNames: true,
216
+ esModuleInterop: false,
217
+ allowSyntheticDefaultImports: true,
218
+ resolveJsonModule: true,
219
+ alwaysStrict: true,
220
+ noUncheckedSideEffectImports: false,
221
+ libReplacement: true,
222
+ stableTypeOrdering: true,
223
+ ignoreDeprecations: '6.0',
213
224
  },
214
225
  include: ['probes/**/*.ts'],
215
226
  }, null, 2) + '\n';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The options every program the sidecar builds carries (carrick#2019).
3
+ *
4
+ * Every `ts.createProgram`, every ts-morph `Project` and the `tsconfig.json`
5
+ * the check hands to `tsc` goes through `sidecarCompilerOptions`, whatever
6
+ * its options came from: a project's tsconfig, a snapshot of one, a Deno
7
+ * config, or the sidecar's own literal set.
8
+ *
9
+ * Two things are added:
10
+ *
11
+ * - `stableTypeOrdering`. Without it the compiler prints a union's members in
12
+ * the order the checker first met them, and an object made by a mapped type
13
+ * lists its properties in the order its keys were created, so the same type
14
+ * printed text that depended on what the process had read before. With it
15
+ * the checker orders types by their content, and one process prints the
16
+ * same text whatever it did first.
17
+ * - TypeScript 5's effective value for every option whose default TypeScript 6
18
+ * changed, where the options leave it unset (`typescript5Defaults`). A
19
+ * project written against TypeScript 5 is read as its own compiler reads
20
+ * it, and as the sidecar read it before it moved to 6. A project that sets
21
+ * an option keeps its own value. Several of those values are deprecated in
22
+ * TypeScript 6 (target ES5, moduleResolution node10, esModuleInterop false),
23
+ * and so are options many projects still set themselves (baseUrl), so
24
+ * `ignoreDeprecations` is set too: a deprecated option still works in 6, and
25
+ * the error it would raise is not the project's.
26
+ */
27
+ import ts from 'typescript';
28
+ /**
29
+ * For each option whose default TypeScript 6 changed and `options` leaves
30
+ * unset, the value TypeScript 5 (5.9, its last minor) computes for it from
31
+ * the options that are set. Only unset options appear in the result.
32
+ */
33
+ export declare function typescript5Defaults(options: ts.CompilerOptions): ts.CompilerOptions;
34
+ /**
35
+ * `options` with TypeScript 5's defaults filled in and the sidecar's own set.
36
+ * Takes ts-morph's `CompilerOptions` as well as the compiler's: the two
37
+ * declare the same option enums separately, and the values are the same.
38
+ */
39
+ export declare function sidecarCompilerOptions<O extends object>(options: O): O;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The options every program the sidecar builds carries (carrick#2019).
3
+ *
4
+ * Every `ts.createProgram`, every ts-morph `Project` and the `tsconfig.json`
5
+ * the check hands to `tsc` goes through `sidecarCompilerOptions`, whatever
6
+ * its options came from: a project's tsconfig, a snapshot of one, a Deno
7
+ * config, or the sidecar's own literal set.
8
+ *
9
+ * Two things are added:
10
+ *
11
+ * - `stableTypeOrdering`. Without it the compiler prints a union's members in
12
+ * the order the checker first met them, and an object made by a mapped type
13
+ * lists its properties in the order its keys were created, so the same type
14
+ * printed text that depended on what the process had read before. With it
15
+ * the checker orders types by their content, and one process prints the
16
+ * same text whatever it did first.
17
+ * - TypeScript 5's effective value for every option whose default TypeScript 6
18
+ * changed, where the options leave it unset (`typescript5Defaults`). A
19
+ * project written against TypeScript 5 is read as its own compiler reads
20
+ * it, and as the sidecar read it before it moved to 6. A project that sets
21
+ * an option keeps its own value. Several of those values are deprecated in
22
+ * TypeScript 6 (target ES5, moduleResolution node10, esModuleInterop false),
23
+ * and so are options many projects still set themselves (baseUrl), so
24
+ * `ignoreDeprecations` is set too: a deprecated option still works in 6, and
25
+ * the error it would raise is not the project's.
26
+ */
27
+ import ts from 'typescript';
28
+ /** Options the sidecar sets on every program, over anything else. */
29
+ const ALWAYS = {
30
+ stableTypeOrdering: true,
31
+ ignoreDeprecations: '6.0',
32
+ };
33
+ /**
34
+ * For each option whose default TypeScript 6 changed and `options` leaves
35
+ * unset, the value TypeScript 5 (5.9, its last minor) computes for it from
36
+ * the options that are set. Only unset options appear in the result.
37
+ */
38
+ export function typescript5Defaults(options) {
39
+ const fill = {};
40
+ const { ModuleKind: M, ModuleResolutionKind: R, ScriptTarget: T } = ts;
41
+ // 5.x ignores ES3 as it ignores no target: both take the default.
42
+ const statedTarget = options.target === T.ES3 ? undefined : options.target;
43
+ const target = statedTarget ??
44
+ (options.module === M.Node16 || options.module === M.Node18
45
+ ? T.ES2022
46
+ : options.module === M.Node20
47
+ ? T.ES2023
48
+ : options.module === M.NodeNext
49
+ ? T.ESNext
50
+ : T.ES5);
51
+ if (statedTarget === undefined)
52
+ fill.target = target;
53
+ const module = options.module ?? (target >= T.ES2015 ? M.ES2015 : M.CommonJS);
54
+ if (options.module === undefined)
55
+ fill.module = module;
56
+ const moduleResolution = options.moduleResolution ??
57
+ (module === M.CommonJS
58
+ ? R.Node10
59
+ : module === M.Node16 || module === M.Node18 || module === M.Node20
60
+ ? R.Node16
61
+ : module === M.NodeNext
62
+ ? R.NodeNext
63
+ : module === M.Preserve
64
+ ? R.Bundler
65
+ : R.Classic);
66
+ if (options.moduleResolution === undefined)
67
+ fill.moduleResolution = moduleResolution;
68
+ const nodeFamily = module === M.Node16 || module === M.Node18 || module === M.Node20 || module === M.NodeNext;
69
+ const esModuleInterop = options.esModuleInterop ?? (nodeFamily || module === M.Preserve);
70
+ if (options.esModuleInterop === undefined)
71
+ fill.esModuleInterop = esModuleInterop;
72
+ if (options.allowSyntheticDefaultImports === undefined) {
73
+ fill.allowSyntheticDefaultImports = esModuleInterop || module === M.System || moduleResolution === R.Bundler;
74
+ }
75
+ if (options.resolveJsonModule === undefined) {
76
+ fill.resolveJsonModule = module === M.Node20 || module === M.NodeNext || moduleResolution === R.Bundler;
77
+ }
78
+ // Each strict flag a project leaves unset follows `strict`, which was off
79
+ // unless set; `alwaysStrict` followed it too.
80
+ if (options.strict === undefined)
81
+ fill.strict = false;
82
+ if (options.alwaysStrict === undefined)
83
+ fill.alwaysStrict = options.strict ?? false;
84
+ // Every package under each `typeRoots` directory (`@types/*`), not none.
85
+ if (options.types === undefined)
86
+ fill.types = ['*'];
87
+ if (options.noUncheckedSideEffectImports === undefined)
88
+ fill.noUncheckedSideEffectImports = false;
89
+ if (options.libReplacement === undefined)
90
+ fill.libReplacement = true;
91
+ return fill;
92
+ }
93
+ /**
94
+ * `options` with TypeScript 5's defaults filled in and the sidecar's own set.
95
+ * Takes ts-morph's `CompilerOptions` as well as the compiler's: the two
96
+ * declare the same option enums separately, and the values are the same.
97
+ */
98
+ export function sidecarCompilerOptions(options) {
99
+ return { ...options, ...typescript5Defaults(options), ...ALWAYS };
100
+ }
@@ -5,6 +5,7 @@ import * as path from 'node:path';
5
5
  import { pathToFileURL, fileURLToPath } from 'node:url';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { execFileSync } from 'node:child_process';
8
+ import { sidecarCompilerOptions } from './compiler-options.js';
8
9
  import { rewriteSpecifiers } from './specifiers.js';
9
10
  import { WriteGuard } from './guarded-fs.js';
10
11
  function readConfig(file) {
@@ -452,11 +453,11 @@ export class DenoProject {
452
453
  /** Runtime declarations belong to their producer, not the checker's globals. */
453
454
  function isolateRuntime(guard, typesDir, files, runtimeRel) {
454
455
  const runtimePath = path.join(typesDir, runtimeRel);
455
- const program = ts.createProgram(files.map(file => path.join(typesDir, file)), {
456
+ const program = ts.createProgram(files.map(file => path.join(typesDir, file)), sidecarCompilerOptions({
456
457
  strict: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext,
457
458
  module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
458
459
  types: [],
459
- });
460
+ }));
460
461
  const checker = program.getTypeChecker();
461
462
  const runtime = program.getSourceFile(runtimePath);
462
463
  const retained = new Set();
@@ -30,6 +30,7 @@
30
30
  import type { CaptureStubOptions, CaptureStubResult } from './api.js';
31
31
  export type { CaptureStubOptions, CaptureStubResult } from './api.js';
32
32
  export { DenoProject, findDenoConfig } from './deno-project.js';
33
+ export { sidecarCompilerOptions } from './compiler-options.js';
33
34
  export { serviceConfigPath } from './project-references.js';
34
35
  export { findServiceTsconfig } from './service-config.js';
35
36
  export { runCheck } from './check.js';
@@ -42,7 +42,9 @@ import { emitsAlike, ProjectGraph } from './project-references.js';
42
42
  import { findServiceTsconfig } from './service-config.js';
43
43
  import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
44
44
  import { WriteGuard } from './guarded-fs.js';
45
+ import { sidecarCompilerOptions } from './compiler-options.js';
45
46
  export { DenoProject, findDenoConfig } from './deno-project.js';
47
+ export { sidecarCompilerOptions } from './compiler-options.js';
46
48
  export { serviceConfigPath } from './project-references.js';
47
49
  export { findServiceTsconfig } from './service-config.js';
48
50
  // v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
@@ -282,7 +284,7 @@ export function captureStub(opts) {
282
284
  try {
283
285
  progress('emit', 'emitting declarations');
284
286
  guard.writeFile(entryPath, entryLines.join('\n') + '\n');
285
- const emitOptions = {
287
+ const emitOptions = sidecarCompilerOptions({
286
288
  ...parsed.options,
287
289
  // The load-bearing trio: emit declarations without checking, so
288
290
  // type-error-laden and bare (no node_modules) checkouts still emit.
@@ -295,7 +297,7 @@ export function captureStub(opts) {
295
297
  incremental: false,
296
298
  outDir: staging,
297
299
  rootDir: entryDir,
298
- };
300
+ });
299
301
  written = emitDeclarations({
300
302
  rootNames: [entryPath, ...augmentationSources],
301
303
  options: emitOptions,
@@ -523,7 +525,7 @@ function emitDeclarations(args) {
523
525
  const emitted = new Map();
524
526
  const declarationSources = new Map();
525
527
  const sourceByEmitted = new Map();
526
- const program = ts.createProgram(args.rootNames, args.options, args.host);
528
+ const program = ts.createProgram(args.rootNames, sidecarCompilerOptions(args.options), args.host);
527
529
  const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
528
530
  emitted.set(fileName, text);
529
531
  if (sources?.[0])
@@ -690,10 +692,10 @@ function resolveAnchors(opts, parsed, ctx, deno) {
690
692
  ...new Set(opts.anchors
691
693
  .flatMap((a) => (a.source_file ? [path.join(ctx.repoRoot, a.source_file)] : []))),
692
694
  ].filter((f) => fs.existsSync(f));
693
- const options = {
695
+ const options = sidecarCompilerOptions({
694
696
  ...parsed.options,
695
697
  noEmit: true,
696
- };
698
+ });
697
699
  const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, deno?.host(options));
698
700
  // The checker binds every file of the program when it is first asked for,
699
701
  // which the first anchor would otherwise do: asked for here, so that the
@@ -24,16 +24,17 @@
24
24
  import ts from 'typescript';
25
25
  import * as fs from 'node:fs';
26
26
  import * as path from 'node:path';
27
+ import { sidecarCompilerOptions } from './compiler-options.js';
27
28
  import { isPublishedSemver } from './lockfile.js';
28
29
  const DECLARATION_SUFFIXES = ['', '.d.ts', '.d.mts', '.d.cts', '.ts', '.mts', '.cts', '/index.d.ts'];
29
- const RESOLUTION_OPTIONS = {
30
+ const RESOLUTION_OPTIONS = sidecarCompilerOptions({
30
31
  module: ts.ModuleKind.ESNext,
31
32
  moduleResolution: ts.ModuleResolutionKind.Bundler,
32
33
  target: ts.ScriptTarget.ESNext,
33
34
  noEmit: true,
34
35
  skipLibCheck: true,
35
36
  types: [],
36
- };
37
+ });
37
38
  /** DefinitelyTyped packages are imported by the runtime name (TS6137). */
38
39
  function publicNameOf(name) {
39
40
  if (!name.startsWith('@types/'))
@@ -32,6 +32,7 @@
32
32
  import ts from 'typescript';
33
33
  import * as fs from 'node:fs';
34
34
  import * as path from 'node:path';
35
+ import { sidecarCompilerOptions } from './compiler-options.js';
35
36
  import { collectSpecifiers, declarationCandidates, isDeclarationFileName, isRelative, packageNameOf, } from './specifiers.js';
36
37
  import { repairDanglingImports } from './repair-dangling.js';
37
38
  import { findDisqualifyingTopTypes, findUnresolvedPlaceholders, isErrorPlaceholder, provenanceOf, unemittedModuleProvenance, unresolvedInTreeProvenance, } from './deep-walk.js';
@@ -78,7 +79,7 @@ export function selfCheckStub(args) {
78
79
  }
79
80
  }
80
81
  function runSelfCheck(args, treeFiles, repaired) {
81
- const options = {
82
+ const options = sidecarCompilerOptions({
82
83
  noEmit: true,
83
84
  strict: true,
84
85
  // MUST be false: the whole stub tree is .d.ts (see module header).
@@ -86,7 +87,7 @@ function runSelfCheck(args, treeFiles, repaired) {
86
87
  module: ts.ModuleKind.ESNext,
87
88
  moduleResolution: ts.ModuleResolutionKind.Bundler,
88
89
  types: [],
89
- };
90
+ });
90
91
  const program = ts.createProgram(treeFiles, options, args.compilerHost?.(options));
91
92
  const checker = program.getTypeChecker();
92
93
  const diagnostics = ts.getPreEmitDiagnostics(program);
@@ -35,6 +35,7 @@
35
35
  import * as path from 'node:path';
36
36
  import * as fs from 'node:fs';
37
37
  import { Project, Node } from 'ts-morph';
38
+ import { sidecarCompilerOptions } from './capture/index.js';
38
39
  import { expandTypeStructural, } from './type-structural-expander.js';
39
40
  export class DefinitionResolver {
40
41
  /**
@@ -57,13 +58,13 @@ export class DefinitionResolver {
57
58
  }
58
59
  try {
59
60
  const stubProject = new Project({
60
- compilerOptions: {
61
+ compilerOptions: sidecarCompilerOptions({
61
62
  target: 99, // ESNext
62
63
  module: 99, // ESNext
63
64
  moduleResolution: 100, // Bundler
64
65
  strict: true,
65
66
  skipLibCheck: true,
66
- },
67
+ }),
67
68
  skipAddingFilesFromTsConfig: true,
68
69
  });
69
70
  for (const filePath of walkDtsFiles(typesDir)) {
@@ -28,6 +28,7 @@ import { Retyper } from './retype.js';
28
28
  import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
29
29
  import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
30
30
  import { mergeInferTimings } from './infer-timing.js';
31
+ import { addProgramFiles, listProgramFiles } from './program-files.js';
31
32
  // ===========================================================================
32
33
  // Module-level state
33
34
  // ===========================================================================
@@ -46,6 +47,13 @@ let components = new Map();
46
47
  * (carrick#1927).
47
48
  */
48
49
  const definitionResolver = new DefinitionResolver();
50
+ /** The init'd project loader, or the error every project-backed action gives without one. */
51
+ function initializedLoader() {
52
+ if (!projectLoader?.isInitialized()) {
53
+ throw new Error('Sidecar not initialized. Call init first.');
54
+ }
55
+ return projectLoader;
56
+ }
49
57
  /**
50
58
  * Get the project-backed components, building the project if this is the
51
59
  * first request that needs it.
@@ -53,15 +61,12 @@ const definitionResolver = new DefinitionResolver();
53
61
  * @throws if init has not run, or if the project cannot be built
54
62
  */
55
63
  function projectComponents(key = '') {
56
- if (!projectLoader?.isInitialized()) {
57
- throw new Error('Sidecar not initialized. Call init first.');
58
- }
64
+ // Bound here rather than read from the module slot inside the components:
65
+ // a re-init drops `components` and points the slot at another service, and
66
+ // nothing built over this project may follow it there.
67
+ const loader = initializedLoader();
59
68
  let built = components.get(key);
60
69
  if (!built) {
61
- // Bound here rather than read from the module slot inside the components:
62
- // a re-init drops `components` and points the slot at another service, and
63
- // nothing built over this project may follow it there.
64
- const loader = projectLoader;
65
70
  const project = loader.getProjectFor(key);
66
71
  const repoRoot = loader.getRepoRoot();
67
72
  // The module graph, where the project resolved through one, is the only
@@ -501,6 +506,55 @@ function handleResolveDefinitions(request) {
501
506
  };
502
507
  }
503
508
  }
509
+ /**
510
+ * Handle the 'list_program_files' action - the root files the default
511
+ * project's program was built from, in order (carrick#2027). It is the only
512
+ * program a request grows: a file a referenced project owns is in that
513
+ * project's program from its tsconfig, so a program built for an owning
514
+ * project (carrick#1604) holds the same files in every process. A process
515
+ * that has not built the default project lists nothing, and does not build
516
+ * it to answer.
517
+ */
518
+ function handleListProgramFiles(request) {
519
+ try {
520
+ const project = initializedLoader().builtProject();
521
+ const files = project ? listProgramFiles(project) : [];
522
+ log(`Listing ${files.length} program file(s)`);
523
+ return { request_id: request.request_id, status: 'success', files };
524
+ }
525
+ catch (err) {
526
+ const error = err instanceof Error ? err.message : String(err);
527
+ logError(`Listing program files failed: ${error}`);
528
+ return { request_id: request.request_id, status: 'error', errors: [error] };
529
+ }
530
+ }
531
+ /**
532
+ * Handle the 'add_program_files' action - add the given files to the default
533
+ * project's program in the given order, rebuilding it at most once
534
+ * (carrick#2027). A file a referenced project owns is skipped, as an `infer`
535
+ * on it would never add it to this program: its owner's program already holds
536
+ * it.
537
+ */
538
+ function handleAddProgramFiles(request) {
539
+ try {
540
+ const loader = initializedLoader();
541
+ const startTime = performance.now();
542
+ const files = request.files
543
+ .map((file) => path.resolve(loader.getRepoRoot(), file))
544
+ .filter((file) => loader.projectKeyFor(file) === '');
545
+ const added = files.length === 0
546
+ ? 0
547
+ : addProgramFiles(loader.getProject(), files, (file, reason) => logError(`Not added to the program: ${file}: ${reason}`));
548
+ log(`Added ${added} of ${request.files.length} file(s) to the program in ` +
549
+ `${Math.round(performance.now() - startTime)}ms`);
550
+ return { request_id: request.request_id, status: 'success', added };
551
+ }
552
+ catch (err) {
553
+ const error = err instanceof Error ? err.message : String(err);
554
+ logError(`Adding program files failed: ${error}`);
555
+ return { request_id: request.request_id, status: 'error', errors: [error] };
556
+ }
557
+ }
504
558
  /**
505
559
  * Handle the 'health' action - report initialization status
506
560
  */
@@ -553,6 +607,10 @@ function handleRequest(request) {
553
607
  return handleVerifyLibraryClaims(request);
554
608
  case 'list_library_surface':
555
609
  return handleListLibrarySurface(request);
610
+ case 'list_program_files':
611
+ return handleListProgramFiles(request);
612
+ case 'add_program_files':
613
+ return handleAddProgramFiles(request);
556
614
  case 'health':
557
615
  return handleHealth(request);
558
616
  case 'shutdown':
@@ -648,6 +706,31 @@ function processLine(line) {
648
706
  const response = handleRequest(parseResult.request);
649
707
  writeResponse(response);
650
708
  }
709
+ /** How often the sidecar looks for the process that started it. */
710
+ const PARENT_CHECK_MS = 1000;
711
+ /**
712
+ * Exit once the process that started this one is gone (carrick#2029).
713
+ *
714
+ * Its death closes stdin, which ends this process, unless another process
715
+ * still holds the write end. Then the only sign is that this process has a
716
+ * new parent: whatever adopts it (init, or a subreaper on Linux) has a pid of
717
+ * its own. So the parent at startup is kept and compared, never 1. A process
718
+ * already adopted at startup has no parent to watch, and stdin alone decides.
719
+ *
720
+ * The timer is unref'd: it never keeps an otherwise finished process alive.
721
+ * A handler that blocks the event loop holds it off until it returns.
722
+ */
723
+ function exitWhenParentIsGone() {
724
+ const startedBy = process.ppid;
725
+ if (startedBy <= 1)
726
+ return;
727
+ setInterval(() => {
728
+ if (process.ppid !== startedBy) {
729
+ log(`parent ${startedBy} is gone, exiting`);
730
+ process.exit(0);
731
+ }
732
+ }, PARENT_CHECK_MS).unref();
733
+ }
651
734
  /**
652
735
  * Start the message loop
653
736
  */
@@ -663,6 +746,7 @@ function main() {
663
746
  log('stdin closed, exiting');
664
747
  process.exit(0);
665
748
  });
749
+ exitWhenParentIsGone();
666
750
  // Handle process signals
667
751
  process.on('SIGINT', () => {
668
752
  log('SIGINT received, exiting');
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The compiler's own print of a type, with a module the print names written
3
+ * as the file that declares it (carrick#2019).
4
+ *
5
+ * When a type comes from a module the enclosing file does not import, the
6
+ * compiler writes it as `import("<module>").Name`. TypeScript 5 wrote the
7
+ * declaring file's path there: the node builder had no module-specifier host
8
+ * unless a declaration emit gave it one. TypeScript 6 gives every print a
9
+ * host, so the same type printed at an enclosing node reads
10
+ * `import("../model").Name` or `import("<package>").Name`: a specifier that
11
+ * depends on which file it was printed from, so one type printed from two
12
+ * files gives two texts. Everything downstream reads the declaring file's
13
+ * path: the scanner's path scrub writes it repo-relative or as
14
+ * `<package>@<version>`, and a capture pastes an inferred text into its
15
+ * surface entry, where a specifier relative to another file names nothing.
16
+ *
17
+ * These functions run the compiler's own code with no host: a tracker whose
18
+ * `moduleResolverHost` is `false` (falsy, so the builder writes the declaring
19
+ * file; not nullish, so the builder does not supply its own). Nothing in the
20
+ * text is rewritten afterwards. `typeToStringAsDeclared` is the checker's
21
+ * `typeToString`, step for step, with that one tracker.
22
+ *
23
+ * Only prints with an enclosing node differ: with none, the compiler has no
24
+ * file to write a specifier relative to and writes the declaring file
25
+ * anyway. A print that must name modules the way a declaration file would
26
+ * (the capture's surface emit) passes a host of its own and does not use
27
+ * this.
28
+ */
29
+ import { ts } from 'ts-morph';
30
+ /**
31
+ * The compiler's internal `SymbolTracker`: callbacks the node builder reports
32
+ * to, and the module-specifier host it writes specifiers with.
33
+ */
34
+ export interface SymbolTracker {
35
+ moduleResolverHost?: unknown;
36
+ }
37
+ /**
38
+ * The checker's `typeToTypeNode`, writing each module a node names as the
39
+ * file that declares it. A caller's own tracker keeps its callbacks.
40
+ */
41
+ export declare function typeToTypeNodeAsDeclared(checker: ts.TypeChecker, type: ts.Type, enclosing: ts.Node | undefined, flags: ts.NodeBuilderFlags, tracker?: SymbolTracker): ts.TypeNode | undefined;
42
+ /**
43
+ * The checker's `typeToString(type, enclosing, flags)`, writing each module
44
+ * the print names as the file that declares it. `noErrorTruncation` is the
45
+ * program's option of that name, which `typeToString` reads when `flags`
46
+ * leave truncation on.
47
+ */
48
+ export declare function typeToStringAsDeclared(checker: ts.TypeChecker, type: ts.Type, enclosing: ts.Node | undefined, flags?: ts.TypeFormatFlags, noErrorTruncation?: boolean): string;