@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.
- package/dist/auto-search-worker.js +3 -3
- package/dist/chunk-embedding-write-worker.js +1 -1
- package/dist/config/commit-retention-recovery.d.ts +9 -0
- package/dist/config/commit-retention-recovery.d.ts.map +1 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/live-child-output-cap.d.ts.map +1 -1
- package/dist/config/live-run-config.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +1 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-tgnw019g.js → conflict-warning-hook-xgskea1v.js} +3 -3
- package/dist/{context-dxfztzsd.js → context-836j3vte.js} +63 -27
- package/dist/{embedding-backfill-cwnyr4fj.js → embedding-backfill-mms9nb0h.js} +2 -2
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/indexer.d.ts +2 -0
- package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +3 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/work-metrics.d.ts +5 -3
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +2 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +7 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +3 -2
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/replay-committed-drops.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-n8qmmd7f.js → index-093c4g1c.js} +3 -3
- package/dist/{index-89k1w6x2.js → index-a1h316s9.js} +2 -2
- package/dist/{index-fq2p158w.js → index-bps8vn9r.js} +13 -7
- package/dist/{index-tvzvjggy.js → index-gkee6p4g.js} +1 -1
- package/dist/{index-a2fz9hpa.js → index-gxkhzmjq.js} +4 -2
- package/dist/{index-4cpc59jv.js → index-h4s5jk7f.js} +1 -1
- package/dist/{index-t3rbb8wr.js → index-pe6b4bhs.js} +1 -1
- package/dist/{index-dcqk0y27.js → index-q9zvphbq.js} +104 -24
- package/dist/{index-7gbcg8xv.js → index-tx68g9nc.js} +1 -1
- package/dist/{index-4ypqzvhf.js → index-vnn5405n.js} +1 -1
- package/dist/{index-077ht5cq.js → index-waktfbjy.js} +1 -1
- package/dist/{index-k9jx7ma9.js → index-wpcdk76y.js} +325 -249
- package/dist/{index-dzfevacx.js → index-wvffze8y.js} +10 -6
- package/dist/{index-31y1mznz.js → index-wyt5m81v.js} +4 -4
- package/dist/{index-v0dhhxsq.js → index-y1qwzvxn.js} +5 -5
- package/dist/{index-yzjhx10y.js → index-zhgqgxtt.js} +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -30
- package/dist/{message-index-bounded-7ayzb2ah.js → message-index-bounded-7qsn4fd3.js} +3 -3
- package/dist/migration-worker.js +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap-helpers.d.ts +5 -1
- package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{project-embedding-registry-94nncs5g.js → project-embedding-registry-cg281d9j.js} +1 -1
- package/dist/raw-ordinal-warmup-worker.js +2 -2
- package/dist/{read-session-chunk-jzb6w9w5.js → read-session-chunk-a5d0q3m0.js} +2 -2
- package/dist/{recomp-orchestrator-02sa2ngr.js → recomp-orchestrator-na34m4c5.js} +10 -10
- package/dist/{search-spq6nfyf.js → search-0cd6hx7a.js} +7 -7
- package/dist/{search-archive-q1vw7zg4.js → search-archive-66pm17gm.js} +3 -3
- package/dist/{send-session-notification-afzzqy5b.js → send-session-notification-vz86nt79.js} +2 -2
- package/dist/{transform-postprocess-phase-6menyyxc.js → transform-postprocess-phase-27we33af.js} +10 -10
- package/dist/v2/auto-search-worker.js +3 -3
- package/dist/v2/chunk-embedding-write-worker.js +1 -1
- package/dist/v2/{context-0qhz1nnj.js → context-g4ptae4x.js} +197 -80
- package/dist/v2/{embedding-backfill-rh5dgsva.js → embedding-backfill-35k6czrr.js} +2 -2
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/dream-timer.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/{message-index-bounded-n09b74vr.js → message-index-bounded-7363502m.js} +3 -3
- package/dist/v2/migration-worker.js +1 -1
- package/dist/v2/{project-embedding-registry-6pak822h.js → project-embedding-registry-p1p10ks7.js} +1 -1
- package/dist/v2/raw-ordinal-warmup-worker.js +2 -2
- package/dist/v2/{read-session-chunk-vpt69tw9.js → read-session-chunk-kkppfhft.js} +2 -2
- package/dist/v2/{recomp-orchestrator-txsmbnvw.js → recomp-orchestrator-f1df1gb8.js} +10 -10
- package/dist/v2/{search-dcy9b6gd.js → search-2v3xyzse.js} +7 -7
- package/dist/v2/{search-archive-t9bcz4ht.js → search-archive-v07jpw25.js} +3 -3
- package/dist/v2/{send-session-notification-vdsgnf4c.js → send-session-notification-x5nzy34v.js} +2 -2
- package/dist/v2/{server-e3hc93wk.js → server-77gznpmf.js} +1 -1
- package/dist/v2/{server-94wk1ey3.js → server-835q39kd.js} +45 -5
- package/dist/v2/{server-txbc3czn.js → server-976dxaxh.js} +10 -6
- package/dist/v2/{server-3vhxy2k5.js → server-a7x00tw8.js} +5 -5
- package/dist/v2/{server-z2n2xaeh.js → server-k9je35ev.js} +1 -1
- package/dist/v2/{server-xkvxss5g.js → server-mxzj3r0y.js} +5 -1
- package/dist/v2/{server-6avtp9jp.js → server-p0snacz3.js} +1 -1
- package/dist/v2/{server-z3mk9jdc.js → server-ph300d6m.js} +1 -1
- package/dist/v2/{server-jj9qye8y.js → server-q50ttc2g.js} +62 -21
- package/dist/v2/{server-6v3cdzsx.js → server-qc915kw2.js} +1 -1
- package/dist/v2/{server-zmcrvpm3.js → server-w79f8ejn.js} +2 -2
- package/dist/v2/{server-yqek3s4k.js → server-zh3esb0z.js} +4 -2
- package/dist/v2/server.js +3 -3
- package/dist/v2/{wrapup-orchestrator-gjkszser.js → wrapup-orchestrator-jq33fda8.js} +10 -10
- package/dist/{wrapup-orchestrator-n5h5sr1x.js → wrapup-orchestrator-f0xr7pft.js} +11 -11
- package/package.json +1 -1
- package/src/config/commit-retention-recovery.ts +34 -0
- package/src/config/index.ts +22 -2
- package/src/config/live-child-output-cap.test.ts +28 -0
- package/src/config/live-child-output-cap.ts +2 -0
- package/src/config/live-run-config.test.ts +44 -1
- package/src/config/live-run-config.ts +8 -1
- package/src/config/project-config-trust-boundary.test.ts +57 -0
- package/src/config/project-security.ts +6 -2
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
176
|
+
kind === "Bun.spawn"
|
|
122
177
|
? node.arguments.length === 1
|
|
123
178
|
? 0
|
|
124
179
|
: 1
|
|
125
|
-
:
|
|
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";
|