@cortexkit/opencode-magic-context 0.49.28 → 0.49.29

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/dist/auto-search-worker.js +3 -3
  2. package/dist/chunk-embedding-write-worker.js +1 -1
  3. package/dist/config/commit-retention-recovery.d.ts +9 -0
  4. package/dist/config/commit-retention-recovery.d.ts.map +1 -0
  5. package/dist/config/index.d.ts +2 -1
  6. package/dist/config/index.d.ts.map +1 -1
  7. package/dist/config/live-child-output-cap.d.ts.map +1 -1
  8. package/dist/config/live-run-config.d.ts.map +1 -1
  9. package/dist/config/project-security.d.ts +1 -0
  10. package/dist/config/project-security.d.ts.map +1 -1
  11. package/dist/{conflict-warning-hook-tgnw019g.js → conflict-warning-hook-xgskea1v.js} +3 -3
  12. package/dist/{context-dxfztzsd.js → context-836j3vte.js} +63 -27
  13. package/dist/{embedding-backfill-cwnyr4fj.js → embedding-backfill-mms9nb0h.js} +2 -2
  14. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  20. package/dist/features/magic-context/git-commits/indexer.d.ts +2 -0
  21. package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
  22. package/dist/features/magic-context/project-embedding-registry.d.ts +3 -1
  23. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  24. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  25. package/dist/features/magic-context/work-metrics.d.ts +5 -3
  26. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/apply-operations.d.ts +2 -0
  28. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -1
  30. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +3 -1
  32. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/lkg-slot.d.ts +7 -0
  35. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/read-session-db.d.ts +3 -2
  38. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/replay-committed-drops.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  41. package/dist/{index-n8qmmd7f.js → index-093c4g1c.js} +3 -3
  42. package/dist/{index-89k1w6x2.js → index-a1h316s9.js} +2 -2
  43. package/dist/{index-fq2p158w.js → index-bps8vn9r.js} +13 -7
  44. package/dist/{index-tvzvjggy.js → index-gkee6p4g.js} +1 -1
  45. package/dist/{index-a2fz9hpa.js → index-gxkhzmjq.js} +4 -2
  46. package/dist/{index-4cpc59jv.js → index-h4s5jk7f.js} +1 -1
  47. package/dist/{index-t3rbb8wr.js → index-pe6b4bhs.js} +1 -1
  48. package/dist/{index-dcqk0y27.js → index-q9zvphbq.js} +104 -24
  49. package/dist/{index-7gbcg8xv.js → index-tx68g9nc.js} +1 -1
  50. package/dist/{index-4ypqzvhf.js → index-vnn5405n.js} +1 -1
  51. package/dist/{index-077ht5cq.js → index-waktfbjy.js} +1 -1
  52. package/dist/{index-k9jx7ma9.js → index-wpcdk76y.js} +325 -249
  53. package/dist/{index-dzfevacx.js → index-wvffze8y.js} +10 -6
  54. package/dist/{index-31y1mznz.js → index-wyt5m81v.js} +4 -4
  55. package/dist/{index-v0dhhxsq.js → index-y1qwzvxn.js} +5 -5
  56. package/dist/{index-yzjhx10y.js → index-zhgqgxtt.js} +5 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +26 -30
  59. package/dist/{message-index-bounded-7ayzb2ah.js → message-index-bounded-7qsn4fd3.js} +3 -3
  60. package/dist/migration-worker.js +1 -1
  61. package/dist/plugin/dream-timer.d.ts.map +1 -1
  62. package/dist/plugin/embedding-bootstrap-helpers.d.ts +5 -1
  63. package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
  64. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  65. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  66. package/dist/{project-embedding-registry-94nncs5g.js → project-embedding-registry-cg281d9j.js} +1 -1
  67. package/dist/raw-ordinal-warmup-worker.js +2 -2
  68. package/dist/{read-session-chunk-jzb6w9w5.js → read-session-chunk-a5d0q3m0.js} +2 -2
  69. package/dist/{recomp-orchestrator-02sa2ngr.js → recomp-orchestrator-na34m4c5.js} +10 -10
  70. package/dist/{search-spq6nfyf.js → search-0cd6hx7a.js} +7 -7
  71. package/dist/{search-archive-q1vw7zg4.js → search-archive-66pm17gm.js} +3 -3
  72. package/dist/{send-session-notification-afzzqy5b.js → send-session-notification-vz86nt79.js} +2 -2
  73. package/dist/{transform-postprocess-phase-6menyyxc.js → transform-postprocess-phase-27we33af.js} +10 -10
  74. package/dist/v2/auto-search-worker.js +3 -3
  75. package/dist/v2/chunk-embedding-write-worker.js +1 -1
  76. package/dist/v2/{context-0qhz1nnj.js → context-g4ptae4x.js} +197 -80
  77. package/dist/v2/{embedding-backfill-rh5dgsva.js → embedding-backfill-35k6czrr.js} +2 -2
  78. package/dist/v2/hidden-completion.d.ts.map +1 -1
  79. package/dist/v2/hooks/dream-timer.d.ts.map +1 -1
  80. package/dist/v2/hooks/store.d.ts.map +1 -1
  81. package/dist/v2/{message-index-bounded-n09b74vr.js → message-index-bounded-7363502m.js} +3 -3
  82. package/dist/v2/migration-worker.js +1 -1
  83. package/dist/v2/{project-embedding-registry-6pak822h.js → project-embedding-registry-p1p10ks7.js} +1 -1
  84. package/dist/v2/raw-ordinal-warmup-worker.js +2 -2
  85. package/dist/v2/{read-session-chunk-vpt69tw9.js → read-session-chunk-kkppfhft.js} +2 -2
  86. package/dist/v2/{recomp-orchestrator-txsmbnvw.js → recomp-orchestrator-f1df1gb8.js} +10 -10
  87. package/dist/v2/{search-dcy9b6gd.js → search-2v3xyzse.js} +7 -7
  88. package/dist/v2/{search-archive-t9bcz4ht.js → search-archive-v07jpw25.js} +3 -3
  89. package/dist/v2/{send-session-notification-vdsgnf4c.js → send-session-notification-x5nzy34v.js} +2 -2
  90. package/dist/v2/{server-e3hc93wk.js → server-77gznpmf.js} +1 -1
  91. package/dist/v2/{server-94wk1ey3.js → server-835q39kd.js} +45 -5
  92. package/dist/v2/{server-txbc3czn.js → server-976dxaxh.js} +10 -6
  93. package/dist/v2/{server-3vhxy2k5.js → server-a7x00tw8.js} +5 -5
  94. package/dist/v2/{server-z2n2xaeh.js → server-k9je35ev.js} +1 -1
  95. package/dist/v2/{server-xkvxss5g.js → server-mxzj3r0y.js} +5 -1
  96. package/dist/v2/{server-6avtp9jp.js → server-p0snacz3.js} +1 -1
  97. package/dist/v2/{server-z3mk9jdc.js → server-ph300d6m.js} +1 -1
  98. package/dist/v2/{server-jj9qye8y.js → server-q50ttc2g.js} +62 -21
  99. package/dist/v2/{server-6v3cdzsx.js → server-qc915kw2.js} +1 -1
  100. package/dist/v2/{server-zmcrvpm3.js → server-w79f8ejn.js} +2 -2
  101. package/dist/v2/{server-yqek3s4k.js → server-zh3esb0z.js} +4 -2
  102. package/dist/v2/server.js +3 -3
  103. package/dist/v2/{wrapup-orchestrator-gjkszser.js → wrapup-orchestrator-jq33fda8.js} +10 -10
  104. package/dist/{wrapup-orchestrator-n5h5sr1x.js → wrapup-orchestrator-f0xr7pft.js} +11 -11
  105. package/package.json +1 -1
  106. package/src/config/commit-retention-recovery.ts +34 -0
  107. package/src/config/index.ts +22 -2
  108. package/src/config/live-child-output-cap.test.ts +28 -0
  109. package/src/config/live-child-output-cap.ts +2 -0
  110. package/src/config/live-run-config.test.ts +44 -1
  111. package/src/config/live-run-config.ts +8 -1
  112. package/src/config/project-config-trust-boundary.test.ts +57 -0
  113. package/src/config/project-security.ts +6 -2
  114. package/src/shared/windows-hidden-spawns.test.ts +133 -23
@@ -9,10 +9,53 @@ import {
9
9
  import { producerInputTokenLimit } from "../hooks/magic-context/producer-window-guard";
10
10
  import { resolveDreamerTaskModel, resolveHistorianModel } from "../shared/model-resolution";
11
11
  import { createTestTempDirFromPath } from "../shared/test-temp-dir";
12
+ import { isCommitRetentionPaused } from "./commit-retention-recovery";
12
13
  import { loadPluginConfigDetailed } from "./index";
13
- import { dreamerRunConfig, historianRunConfig } from "./live-run-config";
14
+ import { changedLiveKeys, dreamerRunConfig, historianRunConfig } from "./live-run-config";
14
15
  import { LiveConfigReader } from "./live-snapshot";
15
16
 
17
+ test("dreamer retains the recovered-cap pause through live copies and clears it when the same cap is fixed", () => {
18
+ const root = createTestTempDirFromPath(join(tmpdir(), "mc-live-retention-"));
19
+ const previous = process.env.XDG_CONFIG_HOME;
20
+ process.env.XDG_CONFIG_HOME = root;
21
+ const directory = join(root, "project");
22
+ const file = join(root, "cortexkit", "magic-context.jsonc");
23
+ mkdirSync(join(root, "cortexkit"), { recursive: true });
24
+ mkdirSync(directory, { recursive: true });
25
+ const writeCap = (max_commits: number) =>
26
+ writeFileSync(
27
+ file,
28
+ JSON.stringify({ memory: { git_commit_indexing: { enabled: true, max_commits } } }),
29
+ );
30
+ try {
31
+ writeCap(0);
32
+ const boot = loadPluginConfigDetailed(directory, false).config;
33
+ const reader = new LiveConfigReader(
34
+ directory,
35
+ boot,
36
+ () => {
37
+ const loaded = loadPluginConfigDetailed(directory, false);
38
+ if (loaded.loadOutcome === "schema-recovery") throw new Error("invalid config");
39
+ return loaded.config;
40
+ },
41
+ () => {},
42
+ changedLiveKeys,
43
+ );
44
+ const initial = dreamerRunConfig(boot, reader.poll().effective);
45
+ expect(initial.memory.git_commit_indexing.max_commits).toBe(2000);
46
+ expect(isCommitRetentionPaused(initial.memory.git_commit_indexing)).toBe(true);
47
+ writeCap(2000);
48
+ const fixed = dreamerRunConfig(boot, reader.poll().effective);
49
+ expect(fixed.memory.git_commit_indexing.max_commits).toBe(2000);
50
+ expect(isCommitRetentionPaused(fixed.memory.git_commit_indexing)).toBe(false);
51
+ expect(isCommitRetentionPaused(initial.memory.git_commit_indexing)).toBe(true);
52
+ } finally {
53
+ if (previous === undefined) delete process.env.XDG_CONFIG_HOME;
54
+ else process.env.XDG_CONFIG_HOME = previous;
55
+ rmSync(root, { recursive: true, force: true });
56
+ }
57
+ });
58
+
16
59
  for (const host of ["OC1", "OC2", "Pi", "OMP"] as const) {
17
60
  test(`${host} historian samples the next generation while an existing run keeps its model and fallback`, () => {
18
61
  const root = createTestTempDirFromPath(join(tmpdir(), "mc-live-historian-"));
@@ -1,4 +1,5 @@
1
1
  import { log } from "../shared/logger";
2
+ import { copyCommitRetentionRecovery } from "./commit-retention-recovery";
2
3
  import { loadPluginConfigDetailed, type MagicContextPluginConfig } from "./index";
3
4
  import { LiveConfigReader } from "./live-snapshot";
4
5
  import { LIVE_RELOAD_CONFIG_PATHS, type MagicContextConfig } from "./schema/magic-context";
@@ -32,7 +33,13 @@ export function sampleLiveConfig<T extends MagicContextConfig>(boot: T, fresh: T
32
33
  const leaf = parts.at(-1);
33
34
  if (leaf !== undefined) target[leaf] = value;
34
35
  }
35
- return result as T;
36
+ const sampled = result as T;
37
+ const freshCommitIndexing = fresh.memory?.git_commit_indexing;
38
+ const sampledCommitIndexing = sampled.memory?.git_commit_indexing;
39
+ if (freshCommitIndexing && sampledCommitIndexing) {
40
+ copyCommitRetentionRecovery(freshCommitIndexing, sampledCommitIndexing);
41
+ }
42
+ return sampled;
36
43
  }
37
44
 
38
45
  function removeLeaf(root: Record<string, unknown>, parts: string[]): void {
@@ -2,7 +2,9 @@ import { describe, expect, it } from "bun:test";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { isConfigLoadUntrusted } from "../plugin/embedding-bootstrap-helpers";
5
6
  import { createTestTempDirFromPath } from "../shared/test-temp-dir";
7
+ import { isCommitRetentionPaused } from "./commit-retention-recovery";
6
8
  import { loadPluginConfigDetailed } from "./index";
7
9
 
8
10
  /**
@@ -38,6 +40,61 @@ function loadUserAndProject(user: unknown, project: unknown) {
38
40
  }
39
41
 
40
42
  describe("project config cannot reset user settings with a wrong-typed parent block", () => {
43
+ it("pauses retention eviction for a recovered commit cap without making memory untrusted", () => {
44
+ const loaded = loadUserAndProject(
45
+ { memory: { git_commit_indexing: { enabled: true, max_commits: 0 } } },
46
+ {},
47
+ );
48
+ expect(loaded.config.memory.git_commit_indexing.max_commits).toBe(2000);
49
+ expect(isCommitRetentionPaused(loaded.config.memory.git_commit_indexing)).toBe(true);
50
+ expect(isConfigLoadUntrusted(loaded)).toBe(false);
51
+ expect(
52
+ loaded.config.configWarnings?.filter((warning) =>
53
+ warning.includes("retention eviction is paused"),
54
+ ),
55
+ ).toHaveLength(1);
56
+ expect(loaded.config.configWarnings?.join("\n")).toContain(
57
+ "memory.git_commit_indexing.max_commits",
58
+ );
59
+ expect(
60
+ loaded.warningDetails.filter((warning) =>
61
+ warning.message.includes("retention eviction is paused"),
62
+ ),
63
+ ).toHaveLength(1);
64
+ const valid = loadUserAndProject(
65
+ { memory: { git_commit_indexing: { enabled: true, max_commits: 100 } } },
66
+ {},
67
+ );
68
+ expect(isCommitRetentionPaused(valid.config.memory.git_commit_indexing)).toBe(false);
69
+ });
70
+
71
+ it("recovers an out-of-range injection budget without making memory untrusted", () => {
72
+ const loaded = loadUserAndProject({}, { memory: { injection_budget_tokens: 20001 } });
73
+ expect(loaded.config.memory.enabled).toBe(true);
74
+ expect(loaded.config.memory.injection_budget_tokens).toBe(4000);
75
+ expect(loaded.recoveredFieldPaths).toEqual(["memory.injection_budget_tokens"]);
76
+ expect(loaded.config.configWarnings?.join("\n")).toContain(
77
+ "memory.injection_budget_tokens",
78
+ );
79
+ expect(isConfigLoadUntrusted(loaded)).toBe(false);
80
+ });
81
+
82
+ it("records the exact restored project leaf without distrusting a plain threshold", () => {
83
+ const loaded = loadUserAndProject(
84
+ { memory: { auto_search: { score_threshold: 0.7 } } },
85
+ { memory: { auto_search: { score_threshold: "invalid" } } },
86
+ );
87
+ expect(loaded.config.memory.auto_search.score_threshold).toBe(0.7);
88
+ expect(loaded.recoveredFieldPaths).toEqual(["memory.auto_search.score_threshold"]);
89
+ expect(isConfigLoadUntrusted(loaded)).toBe(false);
90
+ });
91
+
92
+ it("keeps an invalid embedding model untrusted after field recovery", () => {
93
+ const loaded = loadUserAndProject({ embedding: { model: 123 } }, {});
94
+ expect(loaded.recoveredFieldPaths).toEqual(["embedding.model"]);
95
+ expect(isConfigLoadUntrusted(loaded)).toBe(true);
96
+ });
97
+
41
98
  it("keeps the user's compaction block when the project sets compaction:false", () => {
42
99
  const loaded = loadUserAndProject(
43
100
  { compaction: { enabled: false } },
@@ -973,7 +973,7 @@ export function restoreTrustedValuesOverInvalidProjectValues(args: {
973
973
  trustedRaw: Record<string, unknown>;
974
974
  projectRaw: Record<string, unknown>;
975
975
  collectIssuePaths: (raw: Record<string, unknown>) => readonly ConfigIssuePath[];
976
- }): { warnings: string[]; restoredTopLevelKeys: string[] } {
976
+ }): { warnings: string[]; restoredTopLevelKeys: string[]; restoredFieldPaths: string[] } {
977
977
  const warnings: string[] = [];
978
978
  const restoredTopLevelKeys = new Set<string>();
979
979
  const restored = new Set<string>();
@@ -1000,7 +1000,11 @@ export function restoreTrustedValuesOverInvalidProjectValues(args: {
1000
1000
  if (!changed) break;
1001
1001
  }
1002
1002
 
1003
- return { warnings, restoredTopLevelKeys: [...restoredTopLevelKeys] };
1003
+ return {
1004
+ warnings,
1005
+ restoredTopLevelKeys: [...restoredTopLevelKeys],
1006
+ restoredFieldPaths: [...restored],
1007
+ };
1004
1008
  }
1005
1009
 
1006
1010
  const PROJECT_COMMAND_STRING_FIELDS = ["description", "agent", "model"] as const;
@@ -30,21 +30,28 @@ function sourceFiles(dir: string): string[] {
30
30
  function missingHiddenOptions(path: string, text = readFileSync(path, "utf8")): string[] {
31
31
  if (!/node:child_process|Bun\.spawn/.test(text)) return [];
32
32
  const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
33
- const imports = new Set<string>();
33
+ const imports = new Map<string, string>();
34
+ const promisifyImports = new Set<string>();
35
+ const utilImports = new Set<string>();
34
36
  const variables = new Map<string, ts.Expression>();
35
37
  for (const statement of source.statements) {
36
- if (
37
- ts.isImportDeclaration(statement) &&
38
- statement.moduleSpecifier.getText(source).includes("node:child_process")
39
- ) {
40
- const bindings = statement.importClause?.namedBindings;
41
- if (bindings && ts.isNamedImports(bindings)) {
42
- for (const element of bindings.elements) {
43
- if (subprocessNames.has(element.propertyName?.text ?? element.name.text))
44
- imports.add(element.name.text);
45
- }
38
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
39
+ continue;
40
+ const module = statement.moduleSpecifier.text;
41
+ const bindings = statement.importClause?.namedBindings;
42
+ if (bindings && ts.isNamedImports(bindings)) {
43
+ for (const element of bindings.elements) {
44
+ const imported = element.propertyName?.text ?? element.name.text;
45
+ if (module === "node:child_process" && subprocessNames.has(imported))
46
+ imports.set(element.name.text, imported);
47
+ if ((module === "node:util" || module === "util") && imported === "promisify")
48
+ promisifyImports.add(element.name.text);
46
49
  }
47
50
  }
51
+ if (module === "node:util" || module === "util") {
52
+ if (bindings && ts.isNamespaceImport(bindings)) utilImports.add(bindings.name.text);
53
+ if (statement.importClause?.name) utilImports.add(statement.importClause.name.text);
54
+ }
48
55
  }
49
56
  function collect(node: ts.Node): void {
50
57
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer)
@@ -52,6 +59,44 @@ function missingHiddenOptions(path: string, text = readFileSync(path, "utf8")):
52
59
  ts.forEachChild(node, collect);
53
60
  }
54
61
  collect(source);
62
+ function isUtil(node: ts.Expression, seen = new Set<string>()): boolean {
63
+ if (ts.isParenthesizedExpression(node)) return isUtil(node.expression, seen);
64
+ if (!ts.isIdentifier(node)) return false;
65
+ if (utilImports.has(node.text)) return true;
66
+ if (seen.has(node.text)) return false;
67
+ seen.add(node.text);
68
+ const initializer = variables.get(node.text);
69
+ return initializer ? isUtil(initializer, seen) : false;
70
+ }
71
+ function isPromisify(node: ts.Expression, seen = new Set<string>()): boolean {
72
+ if (ts.isParenthesizedExpression(node)) return isPromisify(node.expression, seen);
73
+ if (ts.isIdentifier(node)) {
74
+ if (promisifyImports.has(node.text)) return true;
75
+ if (seen.has(node.text)) return false;
76
+ seen.add(node.text);
77
+ const initializer = variables.get(node.text);
78
+ return initializer ? isPromisify(initializer, seen) : false;
79
+ }
80
+ return (
81
+ ts.isPropertyAccessExpression(node) &&
82
+ isUtil(node.expression) &&
83
+ node.name.text === "promisify"
84
+ );
85
+ }
86
+ function subprocess(node: ts.Expression, seen = new Set<string>()): string | undefined {
87
+ if (ts.isParenthesizedExpression(node)) return subprocess(node.expression, seen);
88
+ if (ts.isIdentifier(node)) {
89
+ const imported = imports.get(node.text);
90
+ if (imported) return imported;
91
+ if (seen.has(node.text)) return undefined;
92
+ seen.add(node.text);
93
+ const initializer = variables.get(node.text);
94
+ return initializer ? subprocess(initializer, seen) : undefined;
95
+ }
96
+ if (ts.isCallExpression(node) && isPromisify(node.expression) && node.arguments[0])
97
+ return subprocess(node.arguments[0], seen);
98
+ return undefined;
99
+ }
55
100
  function hidden(node: ts.Expression | undefined, seen = new Set<string>()): boolean {
56
101
  if (!node) return false;
57
102
  if (ts.isIdentifier(node)) {
@@ -90,39 +135,49 @@ function missingHiddenOptions(path: string, text = readFileSync(path, "utf8")):
90
135
  if (ts.isCallExpression(node)) {
91
136
  const expression = node.expression;
92
137
  let name: string | undefined;
138
+ // Track the original API, not the alias name: exec takes options at index 1,
139
+ // while execFile and spawn take them at index 2, even after promisify.
140
+ let kind = subprocess(expression);
141
+ if (kind) name = expression.getText(source);
93
142
  if (
143
+ !kind &&
94
144
  ts.isIdentifier(expression) &&
95
- (imports.has(expression.text) ||
96
- [
97
- "execFileAsync",
98
- "execFileSyncForIdentity",
99
- "rpcIdentityExecFileSync",
100
- "execFileForVerificationPaths",
101
- ].includes(expression.text) ||
145
+ ([
146
+ "execFileAsync",
147
+ "execFileSyncForIdentity",
148
+ "rpcIdentityExecFileSync",
149
+ "execFileForVerificationPaths",
150
+ ].includes(expression.text) ||
102
151
  (expression.text === "exec" && path.endsWith("rpc-utils.ts")))
103
- )
152
+ ) {
104
153
  name = expression.text;
154
+ kind = "execFile";
155
+ }
105
156
  if (ts.isPropertyAccessExpression(expression)) {
106
157
  const owner = expression.expression.getText(source);
107
158
  if (
108
159
  owner === "Bun" &&
109
160
  (expression.name.text === "spawn" || expression.name.text === "spawnSync")
110
- )
161
+ ) {
111
162
  name = "Bun.spawn";
163
+ kind = "Bun.spawn";
164
+ }
112
165
  if (
113
166
  (owner === "deps" || owner === "this") &&
114
167
  (subprocessNames.has(expression.name.text) ||
115
168
  expression.name.text === "spawnImpl")
116
- )
169
+ ) {
117
170
  name = expression.name.text;
171
+ kind = expression.name.text;
172
+ }
118
173
  }
119
174
  if (name) {
120
175
  const index =
121
- name === "Bun.spawn"
176
+ kind === "Bun.spawn"
122
177
  ? node.arguments.length === 1
123
178
  ? 0
124
179
  : 1
125
- : name === "execSync" || (name === "exec" && !path.endsWith("rpc-utils.ts"))
180
+ : kind === "execSync" || kind === "exec"
126
181
  ? 1
127
182
  : 2;
128
183
  // A single-argument Bun.spawnSync({cmd, ...}) passes options as its first argument.
@@ -158,6 +213,61 @@ describe("Windows child processes", () => {
158
213
  expect(failures).toEqual([]);
159
214
  });
160
215
 
216
+ test.each([
217
+ ["execFile", "[],"],
218
+ ["exec", ""],
219
+ ["spawn", "[],"],
220
+ ])("follows promisified %s aliases and checks the original options position", (api, args) => {
221
+ const path = join(root, "packages/plugin/src/shared/promisified-fixture.ts");
222
+ const text = `import { ${api} as childProcess } from "node:child_process";
223
+ import { promisify as toPromise } from "node:util";
224
+ const rawAlias = childProcess;
225
+ const promiseAlias = toPromise;
226
+ const exec = promiseAlias(rawAlias);
227
+ const run = exec;
228
+ run("git", ${args} { windowsHide: true });
229
+ run("git", ${args} { windowsHide: false });
230
+ run("git", ${args} {});`;
231
+ expect(missingHiddenOptions(path, text)).toEqual([
232
+ "packages/plugin/src/shared/promisified-fixture.ts:8 run",
233
+ "packages/plugin/src/shared/promisified-fixture.ts:9 run",
234
+ ]);
235
+ });
236
+
237
+ test.each([
238
+ 'import { promisify } from "node:util";',
239
+ 'import * as util from "node:util"; const promisify = util.promisify;',
240
+ 'import util from "util"; const promisify = util.promisify;',
241
+ 'import * as util from "node:util"; const u = util; const promisify = u.promisify;',
242
+ ])("recognizes promisify bindings: %s", (utilImport) => {
243
+ const path = join(root, "packages/plugin/src/shared/promisified-fixture.ts");
244
+ const text = `import { execFile } from "node:child_process";
245
+ ${utilImport}
246
+ const exec = promisify(execFile);
247
+ exec("git", [], { windowsHide: true });
248
+ exec("git", [], {});`;
249
+ expect(missingHiddenOptions(path, text)).toEqual([
250
+ "packages/plugin/src/shared/promisified-fixture.ts:5 exec",
251
+ ]);
252
+ });
253
+
254
+ test("recognizes direct util.promisify calls and does not trust options in the wrong slot", () => {
255
+ const path = join(root, "packages/plugin/src/shared/promisified-fixture.ts");
256
+ const text = `import { execFile, exec, spawn } from "node:child_process";
257
+ import * as util from "node:util";
258
+ const file = util.promisify(execFile);
259
+ const shell = util.promisify(exec);
260
+ const child = util.promisify(spawn);
261
+ file("git", { windowsHide: true }, {});
262
+ shell("git", {}, { windowsHide: true });
263
+ child("git", { windowsHide: true }, {});`;
264
+ expect(missingHiddenOptions(path, text)).toEqual([
265
+ "packages/plugin/src/shared/promisified-fixture.ts:6 file",
266
+ "packages/plugin/src/shared/promisified-fixture.ts:7 shell",
267
+ "packages/plugin/src/shared/promisified-fixture.ts:8 child",
268
+ ]);
269
+ });
270
+
161
271
  test("another dashboard subprocess still requires windowsHide true", () => {
162
272
  const path = join(root, "packages/cli/src/lib/dashboard-install.ts");
163
273
  const text = `import { execFile } from "node:child_process";