@opencode-ai/util 0.0.0-dev-18789 → 0.0.0-dev-18793
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/npm.d.ts +8 -1
- package/dist/npm.js +227 -102
- package/package.json +3 -1
package/dist/npm.d.ts
CHANGED
|
@@ -12,15 +12,20 @@ export declare class InstallFailedError extends InstallFailedError_base {
|
|
|
12
12
|
export interface EntryPoint {
|
|
13
13
|
readonly directory: string;
|
|
14
14
|
readonly entrypoint?: string;
|
|
15
|
+
readonly version?: string;
|
|
16
|
+
readonly revision?: string;
|
|
15
17
|
}
|
|
16
18
|
export interface Interface {
|
|
17
19
|
readonly add: (pkg: string, options?: {
|
|
18
20
|
readonly subpaths?: readonly string[];
|
|
19
|
-
readonly refresh?: boolean;
|
|
20
21
|
}) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>;
|
|
21
22
|
readonly resolve: (pkg: string, options?: {
|
|
22
23
|
readonly subpaths?: readonly string[];
|
|
23
24
|
}) => Effect.Effect<EntryPoint>;
|
|
25
|
+
readonly check: (pkg: string) => Effect.Effect<boolean, InstallFailedError>;
|
|
26
|
+
readonly update: (pkg: string, options?: {
|
|
27
|
+
readonly subpaths?: readonly string[];
|
|
28
|
+
}) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>;
|
|
24
29
|
readonly which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>;
|
|
25
30
|
}
|
|
26
31
|
declare const Service_base: Context.ServiceClass<Service, "@opencode/Npm", Interface>;
|
|
@@ -33,4 +38,6 @@ export declare function cacheKey(pkg: string): Promise<string>;
|
|
|
33
38
|
export declare const node: LayerNode.Provider<Service, never, LayerNode.Tag<"global">>;
|
|
34
39
|
export declare function add(...args: Parameters<Interface["add"]>): Promise<EntryPoint>;
|
|
35
40
|
export declare function resolve(...args: Parameters<Interface["resolve"]>): Promise<EntryPoint>;
|
|
41
|
+
export declare function check(...args: Parameters<Interface["check"]>): Promise<boolean>;
|
|
42
|
+
export declare function update(...args: Parameters<Interface["update"]>): Promise<EntryPoint>;
|
|
36
43
|
export declare function which(...args: Parameters<Interface["which"]>): Promise<string | undefined>;
|
package/dist/npm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as Npm from "./npm.js";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { Clock, Effect, Schema, Context, Layer, Option, FileSystem } from "effect";
|
|
5
5
|
import { FSUtil } from "./fs-util.js";
|
|
6
6
|
import { Global } from "./global.js";
|
|
7
7
|
import { EffectFlock } from "./effect-flock.js";
|
|
@@ -26,36 +26,62 @@ export function sanitize(pkg) {
|
|
|
26
26
|
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("");
|
|
27
27
|
}
|
|
28
28
|
export async function isRegistryPackage(pkg) {
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const result = npa(pkg);
|
|
32
|
-
return result.name !== undefined && ["version", "range", "tag"].includes(result.type);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
29
|
+
return (await parse(pkg))?.type === "registry";
|
|
37
30
|
}
|
|
38
31
|
export async function isInstallablePackage(pkg) {
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const result = npa(pkg);
|
|
42
|
-
return result.type === "git" || (result.name !== undefined && ["version", "range", "tag"].includes(result.type));
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
32
|
+
return (await parse(pkg)) !== undefined;
|
|
47
33
|
}
|
|
48
34
|
export async function cacheKey(pkg) {
|
|
35
|
+
return key(pkg, await parse(pkg));
|
|
36
|
+
}
|
|
37
|
+
async function parse(pkg) {
|
|
49
38
|
const { default: npa } = await import("npm-package-arg");
|
|
50
39
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
const result = npa(pkg);
|
|
41
|
+
if (result.type === "git") {
|
|
42
|
+
return {
|
|
43
|
+
type: "git",
|
|
44
|
+
...(result.name ? { name: result.name } : {}),
|
|
45
|
+
slug: gitSlug(pkg),
|
|
46
|
+
mutable: !isCommit(result.gitCommittish),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!result.name || !["version", "range", "tag"].includes(result.type))
|
|
50
|
+
return;
|
|
51
|
+
return {
|
|
52
|
+
type: "registry",
|
|
53
|
+
name: result.name,
|
|
54
|
+
spec: result.raw === result.name ? "latest" : result.rawSpec,
|
|
55
|
+
mutable: result.type !== "version",
|
|
56
|
+
};
|
|
53
57
|
}
|
|
54
58
|
catch {
|
|
55
|
-
|
|
59
|
+
return;
|
|
56
60
|
}
|
|
61
|
+
}
|
|
62
|
+
function key(pkg, target) {
|
|
63
|
+
if (target?.type === "git")
|
|
64
|
+
return `git-${target.slug}-${createHash("sha256").update(pkg).digest("hex").slice(0, 12)}`;
|
|
65
|
+
if (target?.type === "registry")
|
|
66
|
+
return sanitize(`${target.name}@${target.spec}`);
|
|
57
67
|
return sanitize(pkg);
|
|
58
68
|
}
|
|
69
|
+
function gitSlug(pkg) {
|
|
70
|
+
const target = (() => {
|
|
71
|
+
try {
|
|
72
|
+
return decodeURIComponent(pkg.split("#")[0]);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return pkg.split("#")[0];
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
return (target
|
|
79
|
+
.replace(/\.git$/i, "")
|
|
80
|
+
.split(/[/:\\]/)
|
|
81
|
+
.at(-1)
|
|
82
|
+
?.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
83
|
+
.replace(/^-+|-+$/g, "") || "repository");
|
|
84
|
+
}
|
|
59
85
|
const resolveEntryPoint = (name, dir, subpaths = [""]) => {
|
|
60
86
|
const entrypoint = subpaths
|
|
61
87
|
.map((subpath) => {
|
|
@@ -74,16 +100,34 @@ const resolveEntryPoint = (name, dir, subpaths = [""]) => {
|
|
|
74
100
|
};
|
|
75
101
|
const PackageJson = Schema.Struct({
|
|
76
102
|
dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
103
|
+
version: Schema.optional(Schema.String),
|
|
104
|
+
});
|
|
105
|
+
const PackageLock = Schema.Struct({
|
|
106
|
+
packages: Schema.optional(Schema.Record(Schema.String, Schema.Struct({ resolved: Schema.optional(Schema.String) }))),
|
|
77
107
|
});
|
|
108
|
+
const retention = 7 * 24 * 60 * 60 * 1_000;
|
|
109
|
+
const stagingRetention = 60 * 60 * 1_000;
|
|
78
110
|
const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
79
111
|
const afs = yield* FSUtil.Service;
|
|
80
112
|
const global = yield* Global.Service;
|
|
81
113
|
const fs = yield* FileSystem.FileSystem;
|
|
82
114
|
const flock = yield* EffectFlock.Service;
|
|
83
|
-
const directory = (pkg
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
const directory = (pkg, target) => path.join(global.cache, "npm", key(pkg, target));
|
|
116
|
+
const generations = Effect.fnUntraced(function* (dir) {
|
|
117
|
+
return yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []), Effect.map((entries) => entries
|
|
118
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
119
|
+
.toSorted((a, b) => Number(a) - Number(b))));
|
|
120
|
+
});
|
|
121
|
+
const current = Effect.fnUntraced(function* (dir) {
|
|
122
|
+
const latest = (yield* generations(dir)).at(-1);
|
|
123
|
+
return latest ? path.join(dir, latest) : undefined;
|
|
124
|
+
});
|
|
125
|
+
const mkdir = (dir) => fs.makeDirectory(dir, { recursive: true }).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
126
|
+
const remove = (target, dir) => fs.remove(target, { recursive: true, force: true }).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
127
|
+
const rename = (from, to, dir) => fs.rename(from, to).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
128
|
+
const installedName = Effect.fnUntraced(function* (pkg, dir, target) {
|
|
129
|
+
if (target?.name)
|
|
130
|
+
return target.name;
|
|
87
131
|
const manifest = yield* afs
|
|
88
132
|
.readJson(path.join(dir, "package.json"))
|
|
89
133
|
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
|
|
@@ -94,12 +138,40 @@ const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
|
94
138
|
}
|
|
95
139
|
return pkg;
|
|
96
140
|
});
|
|
97
|
-
const
|
|
141
|
+
const installedRevision = Effect.fnUntraced(function* (root, name, target) {
|
|
142
|
+
const dir = path.join(root, "node_modules", name);
|
|
143
|
+
if (target.type === "registry") {
|
|
144
|
+
const manifest = yield* afs
|
|
145
|
+
.readJson(path.join(dir, "package.json"))
|
|
146
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
|
|
147
|
+
return Option.isSome(manifest) ? manifest.value.version : undefined;
|
|
148
|
+
}
|
|
149
|
+
for (const file of [path.join(root, "package-lock.json"), path.join(root, "node_modules", ".package-lock.json")]) {
|
|
150
|
+
const lock = yield* afs
|
|
151
|
+
.readJson(file)
|
|
152
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageLock)), Effect.option);
|
|
153
|
+
const revision = gitRevision(Option.isSome(lock) ? lock.value.packages?.[`node_modules/${name}`]?.resolved : undefined);
|
|
154
|
+
if (revision)
|
|
155
|
+
return revision;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const entry = Effect.fnUntraced(function* (root, name, dir, target, subpaths) {
|
|
159
|
+
const manifest = yield* afs
|
|
160
|
+
.readJson(path.join(dir, "package.json"))
|
|
161
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
|
|
162
|
+
const manifestVersion = Option.isSome(manifest) ? manifest.value.version : undefined;
|
|
163
|
+
const revision = target ? (yield* installedRevision(root, name, target)) ?? manifestVersion : undefined;
|
|
164
|
+
const version = target?.type === "git" ? revision : manifestVersion;
|
|
165
|
+
return {
|
|
166
|
+
...resolveEntryPoint(name, dir, subpaths),
|
|
167
|
+
...(version ? { version } : {}),
|
|
168
|
+
...(revision ? { revision } : {}),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
98
171
|
const reify = (input) => Effect.gen(function* () {
|
|
99
|
-
yield* flock.acquire(`npm-install:${input.dir}`);
|
|
100
172
|
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
|
|
101
173
|
const add = input.add ?? [];
|
|
102
|
-
const npmOptions = yield* NpmConfig.load(input.dir);
|
|
174
|
+
const npmOptions = yield* NpmConfig.load(input.config ?? input.dir);
|
|
103
175
|
const options = input.update ? { ...npmOptions, preferOnline: true, noGitRevCache: true } : npmOptions;
|
|
104
176
|
const arborist = new Arborist({
|
|
105
177
|
...options,
|
|
@@ -126,104 +198,154 @@ const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
|
126
198
|
}).pipe(Effect.withSpan("Npm.reify", {
|
|
127
199
|
attributes: input,
|
|
128
200
|
}));
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
catch {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
})();
|
|
139
|
-
const parsedName = parsed?.name ?? undefined;
|
|
140
|
-
const dir = yield* directory(pkg);
|
|
141
|
-
const name = yield* installedName(pkg, dir, parsedName);
|
|
142
|
-
const cached = yield* afs.existsSafe(path.join(dir, "node_modules", name));
|
|
143
|
-
const refresh = options?.refresh && isMutable(parsed) && !refreshed.has(pkg);
|
|
144
|
-
if (refresh) {
|
|
145
|
-
refreshed.add(pkg);
|
|
146
|
-
if (cached)
|
|
147
|
-
yield* reify({ dir, add: [pkg], update: true }).pipe(Effect.catchCause(() => Effect.logWarning("failed to refresh cached package; using installed version")));
|
|
201
|
+
const install = Effect.fnUntraced(function* (pkg, target, dir, subpaths, update) {
|
|
202
|
+
yield* flock.acquire(`npm-install:${dir}`);
|
|
203
|
+
const active = yield* current(dir);
|
|
204
|
+
const name = yield* installedName(pkg, active ?? dir, target);
|
|
205
|
+
if (active && !update && (yield* afs.existsSafe(path.join(active, "node_modules", name)))) {
|
|
206
|
+
return yield* entry(active, name, path.join(active, "node_modules", name), target, subpaths);
|
|
148
207
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
208
|
+
yield* mkdir(dir);
|
|
209
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
210
|
+
const staging = path.join(dir, `.staging-${startedAt}-${randomUUID()}`);
|
|
211
|
+
const staged = yield* Effect.gen(function* () {
|
|
212
|
+
const tree = yield* reify({ dir: staging, config: dir, add: [pkg], update });
|
|
213
|
+
const installed = tree.edgesOut.values().next().value?.to;
|
|
214
|
+
const installedNameValue = installed?.name ?? (yield* installedName(pkg, staging, target));
|
|
215
|
+
const result = yield* entry(staging, installedNameValue, installed?.path ?? path.join(staging, "node_modules", installedNameValue), target, subpaths);
|
|
216
|
+
if (!installed && !result.entrypoint)
|
|
217
|
+
return yield* new InstallFailedError({ add: [pkg], dir: staging });
|
|
218
|
+
return { name: installedNameValue, result };
|
|
219
|
+
}).pipe(Effect.onError(() => remove(staging, dir).pipe(Effect.ignore)));
|
|
220
|
+
if (active) {
|
|
221
|
+
const activeEntry = yield* entry(active, name, path.join(active, "node_modules", name), target, subpaths);
|
|
222
|
+
if (activeEntry.revision && activeEntry.revision === staged.result.revision) {
|
|
223
|
+
yield* remove(staging, dir);
|
|
224
|
+
return activeEntry;
|
|
225
|
+
}
|
|
162
226
|
}
|
|
163
|
-
|
|
227
|
+
const completedAt = yield* Clock.currentTimeMillis;
|
|
228
|
+
const newest = Number((yield* generations(dir)).at(-1) ?? 0);
|
|
229
|
+
const generation = path.join(dir, String(Math.max(completedAt, newest + 1)));
|
|
230
|
+
yield* rename(staging, generation, dir);
|
|
231
|
+
return yield* entry(generation, staged.name, path.join(generation, "node_modules", staged.name), target, subpaths);
|
|
232
|
+
});
|
|
233
|
+
const collect = Effect.fnUntraced(function* (dir) {
|
|
234
|
+
const now = yield* Clock.currentTimeMillis;
|
|
235
|
+
const completed = yield* generations(dir);
|
|
236
|
+
const keep = new Set(completed.slice(-2));
|
|
237
|
+
const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
|
|
238
|
+
yield* Effect.forEach(entries, (name) => {
|
|
239
|
+
const timestamp = /^\d+$/.test(name)
|
|
240
|
+
? Number(name)
|
|
241
|
+
: Number(name.match(/^\.staging-(\d+)-/)?.[1] ?? Number.NaN);
|
|
242
|
+
const maximumAge = name.startsWith(".staging-") ? stagingRetention : retention;
|
|
243
|
+
if (!Number.isFinite(timestamp) || keep.has(name) || now - timestamp <= maximumAge)
|
|
244
|
+
return Effect.void;
|
|
245
|
+
return remove(path.join(dir, name), dir).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to remove stale npm generation", { dir, name, cause })));
|
|
246
|
+
}, { concurrency: "unbounded", discard: true });
|
|
247
|
+
});
|
|
248
|
+
const add = Effect.fn("Npm.add")(function* (pkg, options) {
|
|
249
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
250
|
+
const dir = directory(pkg, target);
|
|
251
|
+
return yield* install(pkg, target, dir, options?.subpaths, false);
|
|
164
252
|
}, Effect.scoped);
|
|
165
253
|
const resolve = Effect.fn("Npm.resolve")(function* (pkg, options) {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
catch {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
})();
|
|
175
|
-
const root = yield* directory(pkg);
|
|
176
|
-
const name = yield* installedName(pkg, root, parsedName);
|
|
177
|
-
const dir = path.join(root, "node_modules", name);
|
|
254
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
255
|
+
const root = directory(pkg, target);
|
|
256
|
+
const generation = yield* current(root);
|
|
257
|
+
const name = yield* installedName(pkg, generation ?? root, target);
|
|
258
|
+
const dir = path.join(generation ?? root, "node_modules", name);
|
|
178
259
|
if (!(yield* afs.existsSafe(dir)))
|
|
179
260
|
return { directory: dir };
|
|
180
|
-
return
|
|
261
|
+
return yield* entry(generation ?? root, name, dir, target, options?.subpaths);
|
|
262
|
+
});
|
|
263
|
+
const check = Effect.fn("Npm.check")(function* (pkg) {
|
|
264
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
265
|
+
const root = directory(pkg, target);
|
|
266
|
+
if (!target)
|
|
267
|
+
return yield* new InstallFailedError({
|
|
268
|
+
dir: root,
|
|
269
|
+
cause: new Error("Package checks only support registry and Git package specs"),
|
|
270
|
+
});
|
|
271
|
+
if (!target.mutable)
|
|
272
|
+
return false;
|
|
273
|
+
const generation = yield* current(root);
|
|
274
|
+
const name = yield* installedName(pkg, generation ?? root, target);
|
|
275
|
+
const installed = generation ? yield* installedRevision(generation, name, target) : undefined;
|
|
276
|
+
if (!installed)
|
|
277
|
+
return yield* new InstallFailedError({ dir: root, cause: new Error(`Package is not installed: ${pkg}`) });
|
|
278
|
+
const { manifest, resolve } = yield* Effect.promise(() => import("pacote"));
|
|
279
|
+
const options = { ...(yield* NpmConfig.load(root)), preferOnline: true, noGitRevCache: true, ignoreScripts: true };
|
|
280
|
+
const available = yield* Effect.tryPromise({
|
|
281
|
+
try: async () => target.type === "git" ? gitRevision(await resolve(pkg, options)) : (await manifest(pkg, options)).version,
|
|
282
|
+
catch: (cause) => new InstallFailedError({ dir: root, cause }),
|
|
283
|
+
});
|
|
284
|
+
if (!available)
|
|
285
|
+
return yield* new InstallFailedError({ dir: root, cause: new Error(`Package revision not found: ${pkg}`) });
|
|
286
|
+
return installed !== available;
|
|
181
287
|
});
|
|
288
|
+
const update = Effect.fn("Npm.update")(function* (pkg, options) {
|
|
289
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
290
|
+
const dir = directory(pkg, target);
|
|
291
|
+
if (!target)
|
|
292
|
+
return yield* new InstallFailedError({
|
|
293
|
+
dir,
|
|
294
|
+
cause: new Error("Package updates only support registry and Git package specs"),
|
|
295
|
+
});
|
|
296
|
+
if (!target.mutable)
|
|
297
|
+
return yield* add(pkg, options);
|
|
298
|
+
const installed = yield* install(pkg, target, dir, options?.subpaths, true);
|
|
299
|
+
yield* collect(dir);
|
|
300
|
+
return installed;
|
|
301
|
+
}, Effect.scoped);
|
|
182
302
|
const which = Effect.fn("Npm.which")(function* (pkg, bin) {
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const pick = Effect.fnUntraced(function* () {
|
|
303
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
304
|
+
const root = directory(pkg, target);
|
|
305
|
+
const pick = Effect.fnUntraced(function* (dir) {
|
|
306
|
+
const binDir = path.join(dir, "node_modules", ".bin");
|
|
186
307
|
const files = yield* fs.readDirectory(binDir).pipe(Effect.orElseSucceed(() => []));
|
|
187
308
|
if (files.length === 0)
|
|
188
309
|
return Option.none();
|
|
189
310
|
// Caller picked a specific bin (e.g. pyright exposes both `pyright` and
|
|
190
311
|
// `pyright-langserver`); trust the hint if the package provides it.
|
|
191
312
|
if (bin)
|
|
192
|
-
return files.includes(bin) ? Option.some(bin) : Option.none();
|
|
313
|
+
return files.includes(bin) ? Option.some(path.join(binDir, bin)) : Option.none();
|
|
193
314
|
if (files.length === 1)
|
|
194
|
-
return Option.some(files[0]);
|
|
195
|
-
const
|
|
315
|
+
return Option.some(path.join(binDir, files[0]));
|
|
316
|
+
const packageName = target?.name ?? pkg;
|
|
317
|
+
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", packageName, "package.json")).pipe(Effect.option);
|
|
196
318
|
if (Option.isSome(pkgJson)) {
|
|
197
319
|
const parsed = pkgJson.value;
|
|
198
320
|
if (parsed?.bin) {
|
|
199
|
-
const unscoped =
|
|
321
|
+
const unscoped = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
|
|
200
322
|
const parsedBin = parsed.bin;
|
|
201
323
|
if (typeof parsedBin === "string")
|
|
202
|
-
return Option.some(unscoped);
|
|
324
|
+
return Option.some(path.join(binDir, unscoped));
|
|
203
325
|
const keys = Object.keys(parsedBin);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]);
|
|
326
|
+
const selected = parsedBin[unscoped] ? unscoped : keys[0];
|
|
327
|
+
return selected ? Option.some(path.join(binDir, selected)) : Option.none();
|
|
207
328
|
}
|
|
208
329
|
}
|
|
209
|
-
return Option.some(files[0]);
|
|
330
|
+
return Option.some(path.join(binDir, files[0]));
|
|
210
331
|
});
|
|
211
332
|
return Option.getOrUndefined(yield* Effect.gen(function* () {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => { }));
|
|
333
|
+
const generation = yield* current(root);
|
|
334
|
+
const selected = generation ? yield* pick(generation) : Option.none();
|
|
335
|
+
if (Option.isSome(selected))
|
|
336
|
+
return selected;
|
|
217
337
|
yield* add(pkg);
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
338
|
+
const installed = yield* current(root);
|
|
339
|
+
if (!installed)
|
|
220
340
|
return Option.none();
|
|
221
|
-
return
|
|
341
|
+
return yield* pick(installed);
|
|
222
342
|
}).pipe(Effect.scoped, Effect.orElseSucceed(() => Option.none())));
|
|
223
343
|
});
|
|
224
344
|
return Service.of({
|
|
225
345
|
add,
|
|
226
346
|
resolve,
|
|
347
|
+
check,
|
|
348
|
+
update,
|
|
227
349
|
which,
|
|
228
350
|
});
|
|
229
351
|
}));
|
|
@@ -239,15 +361,18 @@ export async function add(...args) {
|
|
|
239
361
|
export async function resolve(...args) {
|
|
240
362
|
return runPromise((svc) => svc.resolve(...args));
|
|
241
363
|
}
|
|
364
|
+
export async function check(...args) {
|
|
365
|
+
return runPromise((svc) => svc.check(...args));
|
|
366
|
+
}
|
|
367
|
+
export async function update(...args) {
|
|
368
|
+
return runPromise((svc) => svc.update(...args));
|
|
369
|
+
}
|
|
242
370
|
export async function which(...args) {
|
|
243
371
|
return runPromise((svc) => svc.which(...args));
|
|
244
372
|
}
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (parsed.type !== "git")
|
|
251
|
-
return false;
|
|
252
|
-
return !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(parsed.gitCommittish ?? "");
|
|
373
|
+
function gitRevision(resolved) {
|
|
374
|
+
return resolved?.match(/#([a-f0-9]{40}|[a-f0-9]{64})(?=::|$)/i)?.[1];
|
|
375
|
+
}
|
|
376
|
+
function isCommit(value) {
|
|
377
|
+
return /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(value ?? "");
|
|
253
378
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode-ai/util",
|
|
4
|
-
"version": "0.0.0-dev-
|
|
4
|
+
"version": "0.0.0-dev-18793",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"mime-types": "3.0.2",
|
|
61
61
|
"minimatch": "10.2.5",
|
|
62
62
|
"npm-package-arg": "13.0.2",
|
|
63
|
+
"pacote": "21.5.1",
|
|
63
64
|
"resolve.exports": "2.0.3"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"@types/node": "24.12.2",
|
|
70
71
|
"@types/npm-package-arg": "6.1.4",
|
|
71
72
|
"@types/npmcli__arborist": "6.3.3",
|
|
73
|
+
"@types/pacote": "11.1.8",
|
|
72
74
|
"@typescript/native-preview": "7.0.0-dev.20251207.1"
|
|
73
75
|
}
|
|
74
76
|
}
|