@opencode-ai/util 0.0.0-beta-18219 → 0.0.0-beta-18269

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 CHANGED
@@ -16,6 +16,7 @@ export interface EntryPoint {
16
16
  export interface Interface {
17
17
  readonly add: (pkg: string, options?: {
18
18
  readonly subpaths?: readonly string[];
19
+ readonly refresh?: boolean;
19
20
  }) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>;
20
21
  readonly resolve: (pkg: string, options?: {
21
22
  readonly subpaths?: readonly string[];
@@ -27,6 +28,8 @@ export declare class Service extends Service_base {
27
28
  }
28
29
  export declare function sanitize(pkg: string): string;
29
30
  export declare function isRegistryPackage(pkg: string): Promise<boolean>;
31
+ export declare function isInstallablePackage(pkg: string): Promise<boolean>;
32
+ export declare function cacheKey(pkg: string): Promise<string>;
30
33
  export declare const node: LayerNode.Node<Service, never, LayerNode.Tag<"global">>;
31
34
  export declare function add(...args: Parameters<Interface["add"]>): Promise<EntryPoint>;
32
35
  export declare function resolve(...args: Parameters<Interface["resolve"]>): Promise<EntryPoint>;
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";
3
4
  import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect";
4
- import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
5
5
  import { FSUtil } from "./fs-util.js";
6
6
  import { Global } from "./global.js";
7
7
  import { EffectFlock } from "./effect-flock.js";
@@ -35,6 +35,27 @@ export async function isRegistryPackage(pkg) {
35
35
  return false;
36
36
  }
37
37
  }
38
+ export async function isInstallablePackage(pkg) {
39
+ const { default: npa } = await import("npm-package-arg");
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
+ }
47
+ }
48
+ export async function cacheKey(pkg) {
49
+ const { default: npa } = await import("npm-package-arg");
50
+ try {
51
+ if (npa(pkg).type === "git")
52
+ return `git-${createHash("sha256").update(pkg).digest("hex")}`;
53
+ }
54
+ catch {
55
+ // Preserve the existing fallback for invalid and non-registry package strings.
56
+ }
57
+ return sanitize(pkg);
58
+ }
38
59
  const resolveEntryPoint = (name, dir, subpaths = [""]) => {
39
60
  const entrypoint = subpaths
40
61
  .map((subpath) => {
@@ -51,19 +72,37 @@ const resolveEntryPoint = (name, dir, subpaths = [""]) => {
51
72
  entrypoint,
52
73
  };
53
74
  };
75
+ const PackageJson = Schema.Struct({
76
+ dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
77
+ });
54
78
  const layer = Layer.effect(Service, Effect.gen(function* () {
55
79
  const afs = yield* FSUtil.Service;
56
80
  const global = yield* Global.Service;
57
81
  const fs = yield* FileSystem.FileSystem;
58
82
  const flock = yield* EffectFlock.Service;
59
- const directory = (pkg) => path.join(global.cache, "packages", sanitize(pkg));
83
+ const directory = (pkg) => Effect.map(Effect.promise(() => cacheKey(pkg)), (key) => path.join(global.cache, "packages", key));
84
+ const installedName = Effect.fnUntraced(function* (pkg, dir, parsedName) {
85
+ if (parsedName)
86
+ return parsedName;
87
+ const manifest = yield* afs
88
+ .readJson(path.join(dir, "package.json"))
89
+ .pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
90
+ if (Option.isSome(manifest)) {
91
+ const name = Object.keys(manifest.value.dependencies ?? {})[0];
92
+ if (name)
93
+ return name;
94
+ }
95
+ return pkg;
96
+ });
97
+ const refreshed = new Set();
60
98
  const reify = (input) => Effect.gen(function* () {
61
99
  yield* flock.acquire(`npm-install:${input.dir}`);
62
100
  const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
63
101
  const add = input.add ?? [];
64
102
  const npmOptions = yield* NpmConfig.load(input.dir);
103
+ const options = input.update ? { ...npmOptions, preferOnline: true, noGitRevCache: true } : npmOptions;
65
104
  const arborist = new Arborist({
66
- ...npmOptions,
105
+ ...options,
67
106
  path: input.dir,
68
107
  binLinks: true,
69
108
  progress: false,
@@ -72,8 +111,9 @@ const layer = Layer.effect(Service, Effect.gen(function* () {
72
111
  });
73
112
  return yield* Effect.tryPromise({
74
113
  try: () => arborist.reify({
75
- ...npmOptions,
114
+ ...options,
76
115
  add,
116
+ update: input.update,
77
117
  save: true,
78
118
  saveType: "prod",
79
119
  }),
@@ -88,22 +128,32 @@ const layer = Layer.effect(Service, Effect.gen(function* () {
88
128
  }));
89
129
  const add = Effect.fn("Npm.add")(function* (pkg, options) {
90
130
  const { default: npa } = yield* Effect.promise(() => import("npm-package-arg"));
91
- const dir = directory(pkg);
92
- const name = (() => {
131
+ const parsed = (() => {
93
132
  try {
94
- return npa(pkg).name ?? pkg;
133
+ return npa(pkg);
95
134
  }
96
135
  catch {
97
- return pkg;
136
+ return undefined;
98
137
  }
99
138
  })();
100
- if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
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")));
148
+ }
149
+ if (cached) {
101
150
  return resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths);
102
151
  }
103
152
  const tree = yield* reify({ dir, add: [pkg] });
104
153
  const first = tree.edgesOut.values().next().value?.to;
105
154
  if (!first) {
106
- const result = resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths);
155
+ const installed = yield* installedName(pkg, dir, parsedName);
156
+ const result = resolveEntryPoint(installed, path.join(dir, "node_modules", installed), options?.subpaths);
107
157
  if (result.entrypoint)
108
158
  return result;
109
159
  return yield* new InstallFailedError({ add: [pkg], dir });
@@ -112,21 +162,23 @@ const layer = Layer.effect(Service, Effect.gen(function* () {
112
162
  }, Effect.scoped);
113
163
  const resolve = Effect.fn("Npm.resolve")(function* (pkg, options) {
114
164
  const { default: npa } = yield* Effect.promise(() => import("npm-package-arg"));
115
- const name = (() => {
165
+ const parsedName = (() => {
116
166
  try {
117
- return npa(pkg).name ?? pkg;
167
+ return npa(pkg).name ?? undefined;
118
168
  }
119
169
  catch {
120
- return pkg;
170
+ return undefined;
121
171
  }
122
172
  })();
123
- const dir = path.join(directory(pkg), "node_modules", name);
173
+ const root = yield* directory(pkg);
174
+ const name = yield* installedName(pkg, root, parsedName);
175
+ const dir = path.join(root, "node_modules", name);
124
176
  if (!(yield* afs.existsSafe(dir)))
125
177
  return { directory: dir };
126
178
  return resolveEntryPoint(name, dir, options?.subpaths);
127
179
  });
128
180
  const which = Effect.fn("Npm.which")(function* (pkg, bin) {
129
- const dir = directory(pkg);
181
+ const dir = yield* directory(pkg);
130
182
  const binDir = path.join(dir, "node_modules", ".bin");
131
183
  const pick = Effect.fnUntraced(function* () {
132
184
  const files = yield* fs.readDirectory(binDir).pipe(Effect.orElseSucceed(() => []));
@@ -188,3 +240,12 @@ export async function resolve(...args) {
188
240
  export async function which(...args) {
189
241
  return runPromise((svc) => svc.which(...args));
190
242
  }
243
+ function isMutable(parsed) {
244
+ if (!parsed)
245
+ return false;
246
+ if (["tag", "range"].includes(parsed.type))
247
+ return true;
248
+ if (parsed.type !== "git")
249
+ return false;
250
+ return !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(parsed.gitCommittish ?? "");
251
+ }
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-beta-18219",
4
+ "version": "0.0.0-beta-18269",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "dependencies": {
46
46
  "@effect/opentelemetry": "4.0.0-rc.111",
47
47
  "@effect/platform-node": "4.0.0-rc.111",
48
+ "@effect/platform-node-shared": "4.0.0-rc.111",
48
49
  "@npmcli/arborist": "9.4.0",
49
50
  "@npmcli/config": "10.8.1",
50
51
  "@opentelemetry/api": "1.9.0",