@opencode/util 0.0.0-reserved → 2.0.1
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/activity-calendar.d.ts +21 -0
- package/dist/activity-calendar.js +42 -0
- package/dist/activity-calendar.test.d.ts +1 -0
- package/dist/activity-calendar.test.js +119 -0
- package/dist/binary.d.ts +7 -0
- package/dist/binary.js +34 -0
- package/dist/binary.test.d.ts +1 -0
- package/dist/binary.test.js +16 -0
- package/dist/bom.d.ts +22 -0
- package/dist/bom.js +36 -0
- package/dist/bom.test.d.ts +1 -0
- package/dist/bom.test.js +19 -0
- package/dist/cross-spawn-spawner.d.ts +3 -0
- package/dist/cross-spawn-spawner.js +438 -0
- package/dist/effect/app-node-platform.d.ts +6 -0
- package/dist/effect/app-node-platform.js +11 -0
- package/dist/effect/app-node.d.ts +62 -0
- package/dist/effect/app-node.js +8 -0
- package/dist/effect/layer-node.d.ts +135 -0
- package/dist/effect/layer-node.js +156 -0
- package/dist/effect/memo-map.d.ts +2 -0
- package/dist/effect/memo-map.js +2 -0
- package/dist/effect/runtime.d.ts +8 -0
- package/dist/effect/runtime.js +16 -0
- package/dist/effect/service-use.d.ts +7 -0
- package/dist/effect/service-use.js +27 -0
- package/dist/effect-flock.d.ts +31 -0
- package/dist/effect-flock.js +186 -0
- package/dist/encode.d.ts +4 -0
- package/dist/encode.js +38 -0
- package/dist/encode.test.d.ts +1 -0
- package/dist/encode.test.js +23 -0
- package/dist/flock.d.ts +30 -0
- package/dist/flock.js +273 -0
- package/dist/fs-util.d.ts +137 -0
- package/dist/fs-util.js +211 -0
- package/dist/glob.d.ts +12 -0
- package/dist/glob.js +26 -0
- package/dist/global-roots.d.ts +8 -0
- package/dist/global-roots.js +17 -0
- package/dist/global-roots.workerd.d.ts +7 -0
- package/dist/global-roots.workerd.js +15 -0
- package/dist/global.d.ts +30 -0
- package/dist/global.js +54 -0
- package/dist/hash.d.ts +4 -0
- package/dist/hash.js +12 -0
- package/dist/npm-config.d.ts +4 -0
- package/dist/npm-config.js +34 -0
- package/dist/npm.d.ts +37 -0
- package/dist/npm.js +386 -0
- package/dist/observability/logging.d.ts +14 -0
- package/dist/observability/logging.js +151 -0
- package/dist/observability/otlp.d.ts +18 -0
- package/dist/observability/otlp.js +76 -0
- package/dist/observability/shared.d.ts +1 -0
- package/dist/observability/shared.js +7 -0
- package/dist/observability.d.ts +13 -0
- package/dist/observability.js +37 -0
- package/dist/patch.d.ts +43 -0
- package/dist/patch.js +331 -0
- package/dist/path.d.ts +4 -0
- package/dist/path.js +33 -0
- package/dist/path.test.d.ts +1 -0
- package/dist/path.test.js +36 -0
- package/dist/process.d.ts +54 -0
- package/dist/process.js +167 -0
- package/dist/retry.d.ts +8 -0
- package/dist/retry.js +37 -0
- package/dist/retry.test.d.ts +1 -0
- package/dist/retry.test.js +31 -0
- package/dist/runtime/import.bun.d.ts +2 -0
- package/dist/runtime/import.bun.js +8 -0
- package/dist/runtime/import.node.d.ts +2 -0
- package/dist/runtime/import.node.js +62 -0
- package/dist/runtime/import.workerd.d.ts +2 -0
- package/dist/runtime/import.workerd.js +7 -0
- package/dist/runtime-import.d.ts +1 -0
- package/dist/runtime-import.js +1 -0
- package/dist/session-title-fallback.d.ts +16 -0
- package/dist/session-title-fallback.js +23 -0
- package/package.json +66 -6
- package/README.md +0 -5
package/dist/glob.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { glob, globSync } from "glob";
|
|
2
|
+
import { minimatch } from "minimatch";
|
|
3
|
+
export var Glob;
|
|
4
|
+
(function (Glob) {
|
|
5
|
+
function toGlobOptions(options) {
|
|
6
|
+
return {
|
|
7
|
+
cwd: options.cwd,
|
|
8
|
+
absolute: options.absolute,
|
|
9
|
+
dot: options.dot,
|
|
10
|
+
follow: options.symlink ?? false,
|
|
11
|
+
nodir: options.include !== "all",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function scan(pattern, options = {}) {
|
|
15
|
+
return glob(pattern, toGlobOptions(options));
|
|
16
|
+
}
|
|
17
|
+
Glob.scan = scan;
|
|
18
|
+
function scanSync(pattern, options = {}) {
|
|
19
|
+
return globSync(pattern, toGlobOptions(options));
|
|
20
|
+
}
|
|
21
|
+
Glob.scanSync = scanSync;
|
|
22
|
+
function match(pattern, filepath) {
|
|
23
|
+
return minimatch(filepath, pattern, { dot: true });
|
|
24
|
+
}
|
|
25
|
+
Glob.match = match;
|
|
26
|
+
})(Glob || (Glob = {}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const home = os.homedir();
|
|
4
|
+
const data = process.env.XDG_DATA_HOME || (home ? path.join(home, ".local", "share") : undefined);
|
|
5
|
+
const cache = process.env.XDG_CACHE_HOME || (home ? path.join(home, ".cache") : undefined);
|
|
6
|
+
const config = process.env.XDG_CONFIG_HOME || (home ? path.join(home, ".config") : undefined);
|
|
7
|
+
const state = process.env.XDG_STATE_HOME || (home ? path.join(home, ".local", "state") : undefined);
|
|
8
|
+
/** The XDG base directories that root opencode's global paths. */
|
|
9
|
+
export function roots(app) {
|
|
10
|
+
return {
|
|
11
|
+
data: path.join(data, app),
|
|
12
|
+
cache: path.join(cache, app),
|
|
13
|
+
config: path.join(config, app),
|
|
14
|
+
state: path.join(state, app),
|
|
15
|
+
tmp: path.join(os.tmpdir(), app),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// workerd has no home directory or XDG base dirs and only tmp is writable, so
|
|
4
|
+
// every global path roots under one directory there. Nothing durable lives in
|
|
5
|
+
// these: on workerd the database is on Durable Object storage.
|
|
6
|
+
export function roots(app) {
|
|
7
|
+
const root = path.join(os.tmpdir(), app);
|
|
8
|
+
return {
|
|
9
|
+
data: path.join(root, "data"),
|
|
10
|
+
cache: path.join(root, "cache"),
|
|
11
|
+
config: path.join(root, "config"),
|
|
12
|
+
state: path.join(root, "state"),
|
|
13
|
+
tmp: path.join(root, "tmp"),
|
|
14
|
+
};
|
|
15
|
+
}
|
package/dist/global.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
2
|
+
export declare const Path: {
|
|
3
|
+
readonly home: string;
|
|
4
|
+
data: string;
|
|
5
|
+
bin: string;
|
|
6
|
+
log: string;
|
|
7
|
+
repos: string;
|
|
8
|
+
cache: string;
|
|
9
|
+
config: string;
|
|
10
|
+
state: string;
|
|
11
|
+
tmp: string;
|
|
12
|
+
};
|
|
13
|
+
declare const Service_base: Context.ServiceClass<Service, "@opencode/Global", Interface>;
|
|
14
|
+
export declare class Service extends Service_base {
|
|
15
|
+
}
|
|
16
|
+
export interface Interface {
|
|
17
|
+
readonly home: string;
|
|
18
|
+
readonly data: string;
|
|
19
|
+
readonly cache: string;
|
|
20
|
+
readonly config: string;
|
|
21
|
+
readonly state: string;
|
|
22
|
+
readonly tmp: string;
|
|
23
|
+
readonly bin: string;
|
|
24
|
+
readonly log: string;
|
|
25
|
+
readonly repos: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function make(input?: Partial<Interface>): Interface;
|
|
28
|
+
export declare const node: import("./effect/layer-node.js").Provider<Service, never, import("./effect/layer-node.js").Tag<"global">>;
|
|
29
|
+
export declare const layerWith: (input: Partial<Interface>) => Layer.Layer<Service, never, never>;
|
|
30
|
+
export * as Global from "./global.js";
|
package/dist/global.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { Context, Effect, Layer } from "effect";
|
|
5
|
+
// XDG on runtimes with a home directory; one tmp-rooted directory on workerd.
|
|
6
|
+
// The variants resolve through the `workerd` bundle condition, like the
|
|
7
|
+
// native-module stubs, so no runtime sniffing happens here.
|
|
8
|
+
import { roots } from "#global-roots";
|
|
9
|
+
import { Flock } from "./flock.js";
|
|
10
|
+
import { makeGlobalNode } from "./effect/app-node.js";
|
|
11
|
+
const app = "opencode";
|
|
12
|
+
const { data, cache, config, state, tmp } = roots(app);
|
|
13
|
+
const paths = {
|
|
14
|
+
get home() {
|
|
15
|
+
return process.env.OPENCODE_TEST_HOME ?? os.homedir();
|
|
16
|
+
},
|
|
17
|
+
data,
|
|
18
|
+
bin: path.join(cache, "bin"),
|
|
19
|
+
log: path.join(data, "log"),
|
|
20
|
+
repos: path.join(data, "repos"),
|
|
21
|
+
cache,
|
|
22
|
+
config,
|
|
23
|
+
state,
|
|
24
|
+
tmp,
|
|
25
|
+
};
|
|
26
|
+
export const Path = paths;
|
|
27
|
+
Flock.setGlobal({ state });
|
|
28
|
+
export class Service extends Context.Service()("@opencode/Global") {
|
|
29
|
+
}
|
|
30
|
+
export function make(input = {}) {
|
|
31
|
+
// The acquired service canonicalizes default tmp; use it instead of Path.tmp for path comparisons.
|
|
32
|
+
return {
|
|
33
|
+
home: Path.home,
|
|
34
|
+
data: Path.data,
|
|
35
|
+
cache: Path.cache,
|
|
36
|
+
config: Path.config,
|
|
37
|
+
state: Path.state,
|
|
38
|
+
tmp: input.tmp ?? Path.tmp,
|
|
39
|
+
bin: Path.bin,
|
|
40
|
+
log: Path.log,
|
|
41
|
+
repos: Path.repos,
|
|
42
|
+
...input,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const acquire = (input) => Effect.gen(function* () {
|
|
46
|
+
const service = Service.of(make(input));
|
|
47
|
+
yield* Effect.promise(() => Promise.all([service.data, service.config, service.state, service.log, service.bin, service.repos, service.tmp].map((directory) => fs.promises.mkdir(directory, { recursive: true }))));
|
|
48
|
+
const canonicalTmp = yield* Effect.promise(() => fs.promises.realpath(service.tmp));
|
|
49
|
+
return Service.of({ ...service, tmp: input.tmp ?? canonicalTmp });
|
|
50
|
+
});
|
|
51
|
+
const layer = Layer.effect(Service, Effect.suspend(() => acquire({ config: process.env.OPENCODE_CONFIG_DIR ?? Path.config })));
|
|
52
|
+
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] });
|
|
53
|
+
export const layerWith = (input) => Layer.effect(Service, acquire(input));
|
|
54
|
+
export * as Global from "./global.js";
|
package/dist/hash.d.ts
ADDED
package/dist/hash.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
export var Hash;
|
|
3
|
+
(function (Hash) {
|
|
4
|
+
function fast(input) {
|
|
5
|
+
return createHash("sha1").update(input).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
Hash.fast = fast;
|
|
8
|
+
function sha256(input) {
|
|
9
|
+
return createHash("sha256").update(input).digest("hex");
|
|
10
|
+
}
|
|
11
|
+
Hash.sha256 = sha256;
|
|
12
|
+
})(Hash || (Hash = {}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export * as NpmConfig from "./npm-config.js";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
export const load = (dir) => Effect.tryPromise({
|
|
5
|
+
try: async () => {
|
|
6
|
+
// @ts-expect-error npm does not publish types for this internal config API.
|
|
7
|
+
const { default: Config } = await import("@npmcli/config");
|
|
8
|
+
// @ts-expect-error npm does not publish types for this internal config API.
|
|
9
|
+
const { default: npmDefinitions } = await import("@npmcli/config/lib/definitions/index.js");
|
|
10
|
+
const { definitions, flatten, nerfDarts, shorthands } = npmDefinitions;
|
|
11
|
+
const config = new Config({
|
|
12
|
+
// Resolved per call: on workerd import.meta.url is undefined and building
|
|
13
|
+
// this URL at module scope fails startup validation; npm config never runs there.
|
|
14
|
+
npmPath: fileURLToPath(new URL("..", import.meta.url)),
|
|
15
|
+
cwd: dir,
|
|
16
|
+
env: { ...process.env },
|
|
17
|
+
argv: [process.execPath, process.execPath, "--prefix", dir],
|
|
18
|
+
execPath: process.execPath,
|
|
19
|
+
platform: process.platform,
|
|
20
|
+
definitions,
|
|
21
|
+
flatten,
|
|
22
|
+
nerfDarts,
|
|
23
|
+
shorthands,
|
|
24
|
+
warn: false,
|
|
25
|
+
});
|
|
26
|
+
await config.load();
|
|
27
|
+
return config.flat;
|
|
28
|
+
},
|
|
29
|
+
catch: (cause) => cause,
|
|
30
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
31
|
+
export const registry = (dir) => load(dir).pipe(Effect.map((config) => {
|
|
32
|
+
const registry = typeof config.registry === "string" ? config.registry : "https://registry.npmjs.org";
|
|
33
|
+
return registry.endsWith("/") ? registry.slice(0, -1) : registry;
|
|
34
|
+
}));
|
package/dist/npm.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export * as Npm from "./npm.js";
|
|
2
|
+
import { Effect, Schema, Context } from "effect";
|
|
3
|
+
import { EffectFlock } from "./effect-flock.js";
|
|
4
|
+
import { LayerNode } from "./effect/layer-node.js";
|
|
5
|
+
declare const InstallFailedError_base: Schema.Class<InstallFailedError, Schema.TaggedStruct<"NpmInstallFailedError", {
|
|
6
|
+
readonly add: Schema.optional<Schema.$Array<Schema.String>>;
|
|
7
|
+
readonly dir: Schema.String;
|
|
8
|
+
readonly cause: Schema.optional<Schema.Defect>;
|
|
9
|
+
}>, import("effect/Cause").YieldableError>;
|
|
10
|
+
export declare class InstallFailedError extends InstallFailedError_base {
|
|
11
|
+
}
|
|
12
|
+
export interface Package {
|
|
13
|
+
readonly directory: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly version?: string;
|
|
16
|
+
readonly revision?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Interface {
|
|
19
|
+
readonly add: (pkg: string) => Effect.Effect<Package, InstallFailedError | EffectFlock.LockError>;
|
|
20
|
+
readonly resolve: (pkg: string) => Effect.Effect<Package>;
|
|
21
|
+
readonly check: (pkg: string) => Effect.Effect<boolean, InstallFailedError>;
|
|
22
|
+
readonly update: (pkg: string) => Effect.Effect<Package, InstallFailedError | EffectFlock.LockError>;
|
|
23
|
+
readonly which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>;
|
|
24
|
+
}
|
|
25
|
+
declare const Service_base: Context.ServiceClass<Service, "@opencode/Npm", Interface>;
|
|
26
|
+
export declare class Service extends Service_base {
|
|
27
|
+
}
|
|
28
|
+
export declare function sanitize(pkg: string): string;
|
|
29
|
+
export declare function isRegistryPackage(pkg: string): Promise<boolean>;
|
|
30
|
+
export declare function isInstallablePackage(pkg: string): Promise<boolean>;
|
|
31
|
+
export declare function cacheKey(pkg: string): Promise<string>;
|
|
32
|
+
export declare const node: LayerNode.Provider<Service, never, LayerNode.Tag<"global">>;
|
|
33
|
+
export declare function add(...args: Parameters<Interface["add"]>): Promise<Package>;
|
|
34
|
+
export declare function resolve(...args: Parameters<Interface["resolve"]>): Promise<Package>;
|
|
35
|
+
export declare function check(...args: Parameters<Interface["check"]>): Promise<boolean>;
|
|
36
|
+
export declare function update(...args: Parameters<Interface["update"]>): Promise<Package>;
|
|
37
|
+
export declare function which(...args: Parameters<Interface["which"]>): Promise<string | undefined>;
|
package/dist/npm.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
export * as Npm from "./npm.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { Clock, Effect, Schema, Context, Layer, Option, FileSystem } from "effect";
|
|
5
|
+
import { FSUtil } from "./fs-util.js";
|
|
6
|
+
import { Global } from "./global.js";
|
|
7
|
+
import { EffectFlock } from "./effect-flock.js";
|
|
8
|
+
import { makeGlobalNode } from "./effect/app-node.js";
|
|
9
|
+
import { filesystem } from "./effect/app-node-platform.js";
|
|
10
|
+
import { LayerNode } from "./effect/layer-node.js";
|
|
11
|
+
import { makeRuntime } from "./effect/runtime.js";
|
|
12
|
+
import { NpmConfig } from "./npm-config.js";
|
|
13
|
+
export class InstallFailedError extends Schema.TaggedError()("NpmInstallFailedError", {
|
|
14
|
+
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
|
15
|
+
dir: Schema.String,
|
|
16
|
+
cause: Schema.optional(Schema.Defect()),
|
|
17
|
+
}) {
|
|
18
|
+
}
|
|
19
|
+
export class Service extends Context.Service()("@opencode/Npm") {
|
|
20
|
+
}
|
|
21
|
+
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined;
|
|
22
|
+
export function sanitize(pkg) {
|
|
23
|
+
if (!illegal)
|
|
24
|
+
return pkg;
|
|
25
|
+
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("");
|
|
26
|
+
}
|
|
27
|
+
export async function isRegistryPackage(pkg) {
|
|
28
|
+
return (await parse(pkg))?.type === "registry";
|
|
29
|
+
}
|
|
30
|
+
export async function isInstallablePackage(pkg) {
|
|
31
|
+
return (await parse(pkg)) !== undefined;
|
|
32
|
+
}
|
|
33
|
+
export async function cacheKey(pkg) {
|
|
34
|
+
return key(pkg, await parse(pkg));
|
|
35
|
+
}
|
|
36
|
+
async function parse(pkg) {
|
|
37
|
+
const { default: npa } = await import("npm-package-arg");
|
|
38
|
+
try {
|
|
39
|
+
const result = npa(pkg);
|
|
40
|
+
if (result.type === "git") {
|
|
41
|
+
return {
|
|
42
|
+
type: "git",
|
|
43
|
+
...(result.name ? { name: result.name } : {}),
|
|
44
|
+
slug: gitSlug(pkg),
|
|
45
|
+
mutable: !isCommit(result.gitCommittish),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!result.name || !["version", "range", "tag"].includes(result.type))
|
|
49
|
+
return;
|
|
50
|
+
return {
|
|
51
|
+
type: "registry",
|
|
52
|
+
name: result.name,
|
|
53
|
+
spec: result.raw === result.name ? "latest" : result.rawSpec,
|
|
54
|
+
mutable: result.type !== "version",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function key(pkg, target) {
|
|
62
|
+
if (target?.type === "git")
|
|
63
|
+
return `git-${target.slug}-${createHash("sha256").update(pkg).digest("hex").slice(0, 12)}`;
|
|
64
|
+
if (target?.type === "registry")
|
|
65
|
+
return sanitize(`${target.name}@${target.spec}`);
|
|
66
|
+
return sanitize(pkg);
|
|
67
|
+
}
|
|
68
|
+
function gitSlug(pkg) {
|
|
69
|
+
const target = (() => {
|
|
70
|
+
try {
|
|
71
|
+
return decodeURIComponent(pkg.split("#")[0]);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return pkg.split("#")[0];
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
return (target
|
|
78
|
+
.replace(/\.git$/i, "")
|
|
79
|
+
.split(/[/:\\]/)
|
|
80
|
+
.at(-1)
|
|
81
|
+
?.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
82
|
+
.replace(/^-+|-+$/g, "") || "repository");
|
|
83
|
+
}
|
|
84
|
+
const PackageJson = Schema.Struct({
|
|
85
|
+
dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
86
|
+
version: Schema.optional(Schema.String),
|
|
87
|
+
});
|
|
88
|
+
const PackageLock = Schema.Struct({
|
|
89
|
+
packages: Schema.optional(Schema.Record(Schema.String, Schema.Struct({ resolved: Schema.optional(Schema.String) }))),
|
|
90
|
+
});
|
|
91
|
+
const retention = 7 * 24 * 60 * 60 * 1_000;
|
|
92
|
+
const stagingRetention = 60 * 60 * 1_000;
|
|
93
|
+
const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
94
|
+
const afs = yield* FSUtil.Service;
|
|
95
|
+
const global = yield* Global.Service;
|
|
96
|
+
const fs = yield* FileSystem.FileSystem;
|
|
97
|
+
const flock = yield* EffectFlock.Service;
|
|
98
|
+
const directory = (pkg, target) => path.join(global.cache, "npm", key(pkg, target));
|
|
99
|
+
const generations = Effect.fnUntraced(function* (dir) {
|
|
100
|
+
return yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []), Effect.map((entries) => entries
|
|
101
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
102
|
+
.toSorted((a, b) => Number(a) - Number(b))));
|
|
103
|
+
});
|
|
104
|
+
const current = Effect.fnUntraced(function* (dir) {
|
|
105
|
+
const latest = (yield* generations(dir)).at(-1);
|
|
106
|
+
return latest ? path.join(dir, latest) : undefined;
|
|
107
|
+
});
|
|
108
|
+
const mkdir = (dir) => fs.makeDirectory(dir, { recursive: true }).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
109
|
+
const remove = (target, dir) => fs.remove(target, { recursive: true, force: true }).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
110
|
+
const rename = (from, to, dir) => fs.rename(from, to).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
111
|
+
const installedName = Effect.fnUntraced(function* (pkg, dir, target) {
|
|
112
|
+
if (target?.name)
|
|
113
|
+
return target.name;
|
|
114
|
+
const manifest = yield* afs
|
|
115
|
+
.readJson(path.join(dir, "package.json"))
|
|
116
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
|
|
117
|
+
if (Option.isSome(manifest)) {
|
|
118
|
+
const name = Object.keys(manifest.value.dependencies ?? {})[0];
|
|
119
|
+
if (name)
|
|
120
|
+
return name;
|
|
121
|
+
}
|
|
122
|
+
return pkg;
|
|
123
|
+
});
|
|
124
|
+
const installedRevision = Effect.fnUntraced(function* (root, name, target) {
|
|
125
|
+
const dir = path.join(root, "node_modules", name);
|
|
126
|
+
if (target.type === "registry") {
|
|
127
|
+
const manifest = yield* afs
|
|
128
|
+
.readJson(path.join(dir, "package.json"))
|
|
129
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
|
|
130
|
+
return Option.isSome(manifest) ? manifest.value.version : undefined;
|
|
131
|
+
}
|
|
132
|
+
for (const file of [path.join(root, "package-lock.json"), path.join(root, "node_modules", ".package-lock.json")]) {
|
|
133
|
+
const lock = yield* afs
|
|
134
|
+
.readJson(file)
|
|
135
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageLock)), Effect.option);
|
|
136
|
+
const revision = gitRevision(Option.isSome(lock) ? lock.value.packages?.[`node_modules/${name}`]?.resolved : undefined);
|
|
137
|
+
if (revision)
|
|
138
|
+
return revision;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const metadata = Effect.fnUntraced(function* (root, name, dir, target) {
|
|
142
|
+
const manifest = yield* afs
|
|
143
|
+
.readJson(path.join(dir, "package.json"))
|
|
144
|
+
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(PackageJson)), Effect.option);
|
|
145
|
+
const manifestVersion = Option.isSome(manifest) ? manifest.value.version : undefined;
|
|
146
|
+
const revision = target ? (yield* installedRevision(root, name, target)) ?? manifestVersion : undefined;
|
|
147
|
+
const version = target?.type === "git" ? revision : manifestVersion;
|
|
148
|
+
return {
|
|
149
|
+
directory: dir,
|
|
150
|
+
name,
|
|
151
|
+
...(version ? { version } : {}),
|
|
152
|
+
...(revision ? { revision } : {}),
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
const reify = (input) => Effect.gen(function* () {
|
|
156
|
+
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
|
|
157
|
+
const add = input.add ?? [];
|
|
158
|
+
const options = {
|
|
159
|
+
...(yield* NpmConfig.load(input.config ?? input.dir)),
|
|
160
|
+
...(input.update ? { preferOnline: true, noGitRevCache: true } : {}),
|
|
161
|
+
// Audit reports are unused here, but Arborist waits for them before completing an install.
|
|
162
|
+
audit: false,
|
|
163
|
+
};
|
|
164
|
+
const arborist = new Arborist({
|
|
165
|
+
...options,
|
|
166
|
+
path: input.dir,
|
|
167
|
+
binLinks: true,
|
|
168
|
+
progress: false,
|
|
169
|
+
savePrefix: "",
|
|
170
|
+
ignoreScripts: true,
|
|
171
|
+
});
|
|
172
|
+
return yield* Effect.tryPromise({
|
|
173
|
+
try: () => arborist.reify({
|
|
174
|
+
...options,
|
|
175
|
+
add,
|
|
176
|
+
update: input.update,
|
|
177
|
+
save: true,
|
|
178
|
+
saveType: "prod",
|
|
179
|
+
}),
|
|
180
|
+
catch: (cause) => new InstallFailedError({
|
|
181
|
+
cause,
|
|
182
|
+
add,
|
|
183
|
+
dir: input.dir,
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
}).pipe(Effect.withSpan("Npm.reify", {
|
|
187
|
+
attributes: input,
|
|
188
|
+
}));
|
|
189
|
+
const install = Effect.fnUntraced(function* (pkg, target, dir, update) {
|
|
190
|
+
yield* flock.acquire(`npm-install:${dir}`);
|
|
191
|
+
const active = yield* current(dir);
|
|
192
|
+
const name = yield* installedName(pkg, active ?? dir, target);
|
|
193
|
+
if (active && !update && (yield* afs.existsSafe(path.join(active, "node_modules", name)))) {
|
|
194
|
+
return yield* metadata(active, name, path.join(active, "node_modules", name), target);
|
|
195
|
+
}
|
|
196
|
+
yield* mkdir(dir);
|
|
197
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
198
|
+
// Arborist keys lockfile entries relative to the root's real path. When the cache
|
|
199
|
+
// directory is reached through a symlink (macOS `/var` → `/private/var`, a linked
|
|
200
|
+
// XDG cache), the keys become `../../…` paths that installedRevision never finds,
|
|
201
|
+
// so Git checks report "not installed" and updates go undetected. Stage under the
|
|
202
|
+
// resolved directory so the root path and real path agree.
|
|
203
|
+
const root = yield* fs.realPath(dir).pipe(Effect.mapError((cause) => new InstallFailedError({ dir, cause })));
|
|
204
|
+
const staging = path.join(root, `.staging-${startedAt}-${randomUUID()}`);
|
|
205
|
+
const staged = yield* Effect.gen(function* () {
|
|
206
|
+
const tree = yield* reify({ dir: staging, config: dir, add: [pkg], update });
|
|
207
|
+
const installed = tree.edgesOut.values().next().value?.to;
|
|
208
|
+
const installedNameValue = installed?.name ?? (yield* installedName(pkg, staging, target));
|
|
209
|
+
const result = yield* metadata(staging, installedNameValue, installed?.path ?? path.join(staging, "node_modules", installedNameValue), target);
|
|
210
|
+
if (!installed && !(yield* afs.isDir(result.directory)))
|
|
211
|
+
return yield* new InstallFailedError({ add: [pkg], dir: staging });
|
|
212
|
+
const links = process.platform === "win32"
|
|
213
|
+
? Array.from(tree.inventory.values()).filter((node) => node.isLink && FSUtil.contains(staging, node.path) && FSUtil.contains(staging, node.realpath))
|
|
214
|
+
: [];
|
|
215
|
+
return { result, links };
|
|
216
|
+
}).pipe(Effect.onError(() => remove(staging, dir).pipe(Effect.ignore)));
|
|
217
|
+
if (active) {
|
|
218
|
+
const activeEntry = yield* metadata(active, name, path.join(active, "node_modules", name), target);
|
|
219
|
+
if (activeEntry.revision && activeEntry.revision === staged.result.revision) {
|
|
220
|
+
yield* remove(staging, dir);
|
|
221
|
+
return activeEntry;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const completedAt = yield* Clock.currentTimeMillis;
|
|
225
|
+
const newest = Number((yield* generations(dir)).at(-1) ?? 0);
|
|
226
|
+
const generation = path.join(dir, String(Math.max(completedAt, newest + 1)));
|
|
227
|
+
// Windows junctions use absolute targets, so rebase internal links before publishing the generation.
|
|
228
|
+
if (staged.links.length > 0) {
|
|
229
|
+
const { unlink, symlink } = yield* Effect.promise(() => import("node:fs/promises"));
|
|
230
|
+
yield* Effect.forEach(staged.links, (link) => Effect.tryPromise({
|
|
231
|
+
try: async () => {
|
|
232
|
+
await unlink(link.path);
|
|
233
|
+
await symlink(path.join(generation, path.relative(staging, link.realpath)), link.path, "junction");
|
|
234
|
+
},
|
|
235
|
+
catch: (cause) => new InstallFailedError({ dir, cause }),
|
|
236
|
+
}), { discard: true }).pipe(Effect.onError(() => remove(staging, dir).pipe(Effect.ignore)));
|
|
237
|
+
}
|
|
238
|
+
yield* rename(staging, generation, dir);
|
|
239
|
+
return { ...staged.result, directory: path.join(generation, "node_modules", staged.result.name) };
|
|
240
|
+
});
|
|
241
|
+
const collect = Effect.fnUntraced(function* (dir) {
|
|
242
|
+
const now = yield* Clock.currentTimeMillis;
|
|
243
|
+
const completed = yield* generations(dir);
|
|
244
|
+
const keep = new Set(completed.slice(-2));
|
|
245
|
+
const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
|
|
246
|
+
yield* Effect.forEach(entries, (name) => {
|
|
247
|
+
const timestamp = /^\d+$/.test(name)
|
|
248
|
+
? Number(name)
|
|
249
|
+
: Number(name.match(/^\.staging-(\d+)-/)?.[1] ?? Number.NaN);
|
|
250
|
+
const maximumAge = name.startsWith(".staging-") ? stagingRetention : retention;
|
|
251
|
+
if (!Number.isFinite(timestamp) || keep.has(name) || now - timestamp <= maximumAge)
|
|
252
|
+
return Effect.void;
|
|
253
|
+
return remove(path.join(dir, name), dir).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to remove stale npm generation", { dir, name, cause })));
|
|
254
|
+
}, { concurrency: "unbounded", discard: true });
|
|
255
|
+
});
|
|
256
|
+
const add = Effect.fn("Npm.add")(function* (pkg) {
|
|
257
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
258
|
+
const dir = directory(pkg, target);
|
|
259
|
+
return yield* install(pkg, target, dir, false);
|
|
260
|
+
}, Effect.scoped);
|
|
261
|
+
const resolve = Effect.fn("Npm.resolve")(function* (pkg) {
|
|
262
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
263
|
+
const root = directory(pkg, target);
|
|
264
|
+
const generation = yield* current(root);
|
|
265
|
+
const name = yield* installedName(pkg, generation ?? root, target);
|
|
266
|
+
const dir = path.join(generation ?? root, "node_modules", name);
|
|
267
|
+
if (!(yield* afs.existsSafe(dir)))
|
|
268
|
+
return { directory: dir, name };
|
|
269
|
+
return yield* metadata(generation ?? root, name, dir, target);
|
|
270
|
+
});
|
|
271
|
+
const check = Effect.fn("Npm.check")(function* (pkg) {
|
|
272
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
273
|
+
const root = directory(pkg, target);
|
|
274
|
+
if (!target)
|
|
275
|
+
return yield* new InstallFailedError({
|
|
276
|
+
dir: root,
|
|
277
|
+
cause: new Error("Package checks only support registry and Git package specs"),
|
|
278
|
+
});
|
|
279
|
+
if (!target.mutable)
|
|
280
|
+
return false;
|
|
281
|
+
const generation = yield* current(root);
|
|
282
|
+
const name = yield* installedName(pkg, generation ?? root, target);
|
|
283
|
+
const installed = generation ? yield* installedRevision(generation, name, target) : undefined;
|
|
284
|
+
if (!installed)
|
|
285
|
+
return yield* new InstallFailedError({ dir: root, cause: new Error(`Package is not installed: ${pkg}`) });
|
|
286
|
+
const { manifest, resolve } = yield* Effect.promise(() => import("pacote"));
|
|
287
|
+
const options = { ...(yield* NpmConfig.load(root)), preferOnline: true, noGitRevCache: true, ignoreScripts: true };
|
|
288
|
+
const available = yield* Effect.tryPromise({
|
|
289
|
+
try: async () => target.type === "git" ? gitRevision(await resolve(pkg, options)) : (await manifest(pkg, options)).version,
|
|
290
|
+
catch: (cause) => new InstallFailedError({ dir: root, cause }),
|
|
291
|
+
});
|
|
292
|
+
if (!available)
|
|
293
|
+
return yield* new InstallFailedError({ dir: root, cause: new Error(`Package revision not found: ${pkg}`) });
|
|
294
|
+
return installed !== available;
|
|
295
|
+
});
|
|
296
|
+
const update = Effect.fn("Npm.update")(function* (pkg) {
|
|
297
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
298
|
+
const dir = directory(pkg, target);
|
|
299
|
+
if (!target)
|
|
300
|
+
return yield* new InstallFailedError({
|
|
301
|
+
dir,
|
|
302
|
+
cause: new Error("Package updates only support registry and Git package specs"),
|
|
303
|
+
});
|
|
304
|
+
if (!target.mutable)
|
|
305
|
+
return yield* add(pkg);
|
|
306
|
+
const installed = yield* install(pkg, target, dir, true);
|
|
307
|
+
yield* collect(dir);
|
|
308
|
+
return installed;
|
|
309
|
+
}, Effect.scoped);
|
|
310
|
+
const which = Effect.fn("Npm.which")(function* (pkg, bin) {
|
|
311
|
+
const target = yield* Effect.promise(() => parse(pkg));
|
|
312
|
+
const root = directory(pkg, target);
|
|
313
|
+
const pick = Effect.fnUntraced(function* (dir) {
|
|
314
|
+
const binDir = path.join(dir, "node_modules", ".bin");
|
|
315
|
+
const files = yield* fs.readDirectory(binDir).pipe(Effect.orElseSucceed(() => []));
|
|
316
|
+
if (files.length === 0)
|
|
317
|
+
return Option.none();
|
|
318
|
+
// Caller picked a specific bin (e.g. pyright exposes both `pyright` and
|
|
319
|
+
// `pyright-langserver`); trust the hint if the package provides it.
|
|
320
|
+
if (bin)
|
|
321
|
+
return files.includes(bin) ? Option.some(path.join(binDir, bin)) : Option.none();
|
|
322
|
+
if (files.length === 1)
|
|
323
|
+
return Option.some(path.join(binDir, files[0]));
|
|
324
|
+
const packageName = target?.name ?? pkg;
|
|
325
|
+
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", packageName, "package.json")).pipe(Effect.option);
|
|
326
|
+
if (Option.isSome(pkgJson)) {
|
|
327
|
+
const parsed = pkgJson.value;
|
|
328
|
+
if (parsed?.bin) {
|
|
329
|
+
const unscoped = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
|
|
330
|
+
const parsedBin = parsed.bin;
|
|
331
|
+
if (typeof parsedBin === "string")
|
|
332
|
+
return Option.some(path.join(binDir, unscoped));
|
|
333
|
+
const keys = Object.keys(parsedBin);
|
|
334
|
+
const selected = parsedBin[unscoped] ? unscoped : keys[0];
|
|
335
|
+
return selected ? Option.some(path.join(binDir, selected)) : Option.none();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return Option.some(path.join(binDir, files[0]));
|
|
339
|
+
});
|
|
340
|
+
return Option.getOrUndefined(yield* Effect.gen(function* () {
|
|
341
|
+
const generation = yield* current(root);
|
|
342
|
+
const selected = generation ? yield* pick(generation) : Option.none();
|
|
343
|
+
if (Option.isSome(selected))
|
|
344
|
+
return selected;
|
|
345
|
+
yield* add(pkg);
|
|
346
|
+
const installed = yield* current(root);
|
|
347
|
+
if (!installed)
|
|
348
|
+
return Option.none();
|
|
349
|
+
return yield* pick(installed);
|
|
350
|
+
}).pipe(Effect.scoped, Effect.orElseSucceed(() => Option.none())));
|
|
351
|
+
});
|
|
352
|
+
return Service.of({
|
|
353
|
+
add,
|
|
354
|
+
resolve,
|
|
355
|
+
check,
|
|
356
|
+
update,
|
|
357
|
+
which,
|
|
358
|
+
});
|
|
359
|
+
}));
|
|
360
|
+
export const node = makeGlobalNode({
|
|
361
|
+
service: Service,
|
|
362
|
+
layer: layer,
|
|
363
|
+
deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node],
|
|
364
|
+
});
|
|
365
|
+
const { runPromise } = makeRuntime(Service, LayerNode.compile(node));
|
|
366
|
+
export async function add(...args) {
|
|
367
|
+
return runPromise((svc) => svc.add(...args));
|
|
368
|
+
}
|
|
369
|
+
export async function resolve(...args) {
|
|
370
|
+
return runPromise((svc) => svc.resolve(...args));
|
|
371
|
+
}
|
|
372
|
+
export async function check(...args) {
|
|
373
|
+
return runPromise((svc) => svc.check(...args));
|
|
374
|
+
}
|
|
375
|
+
export async function update(...args) {
|
|
376
|
+
return runPromise((svc) => svc.update(...args));
|
|
377
|
+
}
|
|
378
|
+
export async function which(...args) {
|
|
379
|
+
return runPromise((svc) => svc.which(...args));
|
|
380
|
+
}
|
|
381
|
+
function gitRevision(resolved) {
|
|
382
|
+
return resolved?.match(/#([a-f0-9]{40}|[a-f0-9]{64})(?=::|$)/i)?.[1];
|
|
383
|
+
}
|
|
384
|
+
function isCommit(value) {
|
|
385
|
+
return /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(value ?? "");
|
|
386
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect, FileSystem, Logger } from "effect";
|
|
2
|
+
export declare const LOG_MAX_BYTES: number;
|
|
3
|
+
export declare const LOG_KEEP_BYTES: number;
|
|
4
|
+
export declare const LOG_TRIM_INTERVAL = "1 hour";
|
|
5
|
+
export declare const LOG_TRIM_LOCK_STALE_MS: number;
|
|
6
|
+
export declare function file(local?: boolean, channel?: string): string;
|
|
7
|
+
export declare function fileLogger(target?: string, id?: string): Effect.Effect<Logger.Logger<unknown, void>, import("effect/PlatformError").PlatformError, import("effect/Scope").Scope | FileSystem.FileSystem>;
|
|
8
|
+
export declare const trim: (target: string, options?: {
|
|
9
|
+
max?: number;
|
|
10
|
+
keep?: number;
|
|
11
|
+
} | undefined) => Effect.Effect<void, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
|
|
12
|
+
export declare function minimumLogLevel(): "Error" | "Warn" | "Info" | "Debug";
|
|
13
|
+
export declare function loggers(local?: boolean, channel?: string): (Logger.Logger<unknown, void> | Effect.Effect<Logger.Logger<unknown, void>, import("effect/PlatformError").PlatformError, import("effect/Scope").Scope | FileSystem.FileSystem>)[];
|
|
14
|
+
export * as Logging from "./logging.js";
|