authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 22:04:49-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 22:04:49-07:00
logc7dc3cbd068a93c9fb5027df7bea719d16e4ffbe
tree79336261450889d6acdd9ab240b750f428a13227
parentfd773172e7f26f2d98de3eb572f3317b0ad8d443
signature Signed by PGP key B5690EEEBB952194

fix(server): keep mise-owned npm packages manual-only (#9927)


2 files changed, 122 insertions(+), 0 deletions(-)

apps/server/src/provider/Drivers/CodexDriver.test.ts+116
...@@ -102,4 +102,120 @@ it.layer(testLayer)("CodexDriver", (it) => {...@@ -102,4 +102,120 @@ it.layer(testLayer)("CodexDriver", (it) => {
102 expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();102 expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
103 }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), Effect.scoped),103 }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn), Effect.scoped),
104 );104 );
105
106 for (const fixture of [
107 {
108 name: "leaves mise npm-backend installations manual-only",
109 installSegments: ["mise", "installs", "npm-openai-codex", "0.110.0"],
110 npmOwned: false,
111 },
112 {
113 name: "leaves mise tool aliases backed by npm manual-only",
114 installSegments: ["mise", "installs", "codex", "0.110.0"],
115 npmOwned: false,
116 },
117 {
118 name: "keeps npm updates for globals in a mise Node installation",
119 installSegments: ["mise", "installs", "node", "24.0.0"],
120 npmOwned: true,
121 },
122 {
123 name: "keeps npm updates for ordinary global installations",
124 installSegments: ["npm-global"],
125 npmOwned: true,
126 },
127 ] as const) {
128 it.effect.skipIf(windowsHost)(fixture.name, () =>
129 Effect.gen(function* () {
130 const fs = yield* FileSystem.FileSystem;
131 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-installer-" });
132 const installPath = NodePath.join(tempDir, ...fixture.installSegments);
133 const realBinaryPath = NodePath.join(
134 installPath,
135 "lib",
136 "node_modules",
137 "@openai",
138 "codex",
139 "bin",
140 "codex.js",
141 );
142 const binaryPath = NodePath.join(tempDir, "bin", "codex");
143 yield* fs.makeDirectory(NodePath.dirname(realBinaryPath), { recursive: true });
144 yield* fs.makeDirectory(NodePath.dirname(binaryPath), { recursive: true });
145 yield* fs.writeFileString(realBinaryPath, "#!/bin/sh\n");
146 yield* fs.chmod(realBinaryPath, 0o755);
147 yield* fs.symlink(realBinaryPath, binaryPath);
148
149 const instance = yield* CodexDriver.create({
150 instanceId: ProviderInstanceId.make("codex-installer"),
151 displayName: "Codex installer test",
152 enabled: false,
153 environment: [],
154 config: {
155 ...CodexDriver.defaultConfig(),
156 binaryPath,
157 homePath: NodePath.join(tempDir, "codex-home"),
158 },
159 });
160
161 const update = (yield* instance.snapshot.resolveMaintenance()).update;
162 if (fixture.npmOwned) {
163 expect(update).toMatchObject({
164 executable: "npm",
165 args: [
166 "install",
167 "-g",
168 "--prefix",
169 installPath,
170 "--allow-scripts=@openai/codex",
171 "@openai/codex@latest",
172 ],
173 });
174 } else {
175 expect(update).toBeNull();
176 }
177 }).pipe(
178 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn),
179 Effect.scoped,
180 ),
181 );
182 }
183
184 for (const layout of ["direct", "wrapper"] as const) {
185 it.effect.skipIf(windowsHost)(`leaves a mise ${layout} installation manual-only`, () =>
186 Effect.gen(function* () {
187 const fs = yield* FileSystem.FileSystem;
188 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: `t3-codex-mise-${layout}-` });
189 const binaryPath =
190 layout === "direct"
191 ? NodePath.join(tempDir, "mise", "installs", "codex", "0.110.0", "codex")
192 : NodePath.join(tempDir, "omarchy", "bin", "codex");
193 yield* fs.makeDirectory(NodePath.dirname(binaryPath), { recursive: true });
194 yield* fs.writeFileString(
195 binaryPath,
196 layout === "direct"
197 ? "#!/bin/sh\n"
198 : '#!/bin/sh\nmise use -g --quiet "codex" || exit 1\nexec mise x "codex" -- "codex" "$@"\n',
199 );
200 yield* fs.chmod(binaryPath, 0o755);
201
202 const instance = yield* CodexDriver.create({
203 instanceId: ProviderInstanceId.make(`codex-mise-${layout}`),
204 displayName: "Codex mise test",
205 enabled: false,
206 environment: [],
207 config: {
208 ...CodexDriver.defaultConfig(),
209 binaryPath,
210 homePath: NodePath.join(tempDir, "codex-home"),
211 },
212 });
213
214 expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
215 }).pipe(
216 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawn),
217 Effect.scoped,
218 ),
219 );
220 }
105});221});
apps/server/src/provider/providerMaintenance.ts+6
...@@ -240,6 +240,12 @@ export function npmGlobalPrefixFromCommandPath(...@@ -240,6 +240,12 @@ export function npmGlobalPrefixFromCommandPath(
240 if (packageIndex < 0 || normalized.slice(0, packageIndex).includes("/node_modules/")) {240 if (packageIndex < 0 || normalized.slice(0, packageIndex).includes("/node_modules/")) {
241 return null;241 return null;
242 }242 }
243 // Mise's npm backend uses a global-looking layout inside a tool version.
244 // Globals under its Node installation still belong to npm.
245 const miseTool = /\/mise\/installs\/([^/]+)\/[^/]+$/.exec(normalized.slice(0, packageIndex))?.[1];
246 if (miseTool && miseTool !== "node") {
247 return null;
248 }
243 return packageIndex === 0 ? "/" : slashPath.slice(0, packageIndex);249 return packageIndex === 0 ? "/" : slashPath.slice(0, packageIndex);
244}250}
245251