@opencode-ai/util 0.0.0-bootstrap.0 → 0.0.0-next-15994
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/cross-spawn-spawner.d.ts +3 -0
- package/dist/cross-spawn-spawner.js +403 -0
- package/dist/effect/app-node-platform.d.ts +6 -0
- package/dist/effect/app-node-platform.js +8 -0
- package/dist/effect/app-node.d.ts +50 -0
- package/dist/effect/app-node.js +8 -0
- package/dist/effect/layer-node.d.ts +79 -0
- package/dist/effect/layer-node.js +181 -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 +185 -0
- package/dist/flock.d.ts +30 -0
- package/dist/flock.js +273 -0
- package/dist/fs-util.d.ts +139 -0
- package/dist/fs-util.js +224 -0
- package/dist/glob.d.ts +12 -0
- package/dist/glob.js +26 -0
- package/dist/global.d.ts +30 -0
- package/dist/global.js +57 -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 +32 -0
- package/dist/npm.d.ts +35 -0
- package/dist/npm.js +207 -0
- package/dist/observability/logging.d.ts +6 -0
- package/dist/observability/logging.js +67 -0
- package/dist/observability/otlp.d.ts +18 -0
- package/dist/observability/otlp.js +73 -0
- package/dist/observability/shared.d.ts +1 -0
- package/dist/observability/shared.js +1 -0
- package/dist/observability.d.ts +13 -0
- package/dist/observability.js +31 -0
- package/dist/patch.d.ts +42 -0
- package/dist/patch.js +206 -0
- package/dist/process.d.ts +54 -0
- package/dist/process.js +162 -0
- package/dist/runtime/import.bun.d.ts +2 -0
- package/dist/runtime/import.bun.js +6 -0
- package/dist/runtime/import.node.d.ts +2 -0
- package/dist/runtime/import.node.js +36 -0
- package/dist/runtime-import.d.ts +1 -0
- package/dist/runtime-import.js +1 -0
- package/package.json +56 -7
- package/index.js +0 -1
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").Node<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,57 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { Context, Effect, Layer } from "effect";
|
|
6
|
+
import { Flock } from "./flock.js";
|
|
7
|
+
import { makeGlobalNode } from "./effect/app-node.js";
|
|
8
|
+
const app = "opencode";
|
|
9
|
+
const data = path.join(xdgData, app);
|
|
10
|
+
const cache = path.join(xdgCache, app);
|
|
11
|
+
const config = path.join(xdgConfig, app);
|
|
12
|
+
const state = path.join(xdgState, app);
|
|
13
|
+
const tmp = path.join(os.tmpdir(), app);
|
|
14
|
+
const paths = {
|
|
15
|
+
get home() {
|
|
16
|
+
return process.env.OPENCODE_TEST_HOME ?? os.homedir();
|
|
17
|
+
},
|
|
18
|
+
data,
|
|
19
|
+
bin: path.join(cache, "bin"),
|
|
20
|
+
log: path.join(data, "log"),
|
|
21
|
+
repos: path.join(data, "repos"),
|
|
22
|
+
cache,
|
|
23
|
+
config,
|
|
24
|
+
state,
|
|
25
|
+
tmp,
|
|
26
|
+
};
|
|
27
|
+
export const Path = paths;
|
|
28
|
+
Flock.setGlobal({ state });
|
|
29
|
+
await Promise.all([
|
|
30
|
+
fs.mkdir(Path.data, { recursive: true }),
|
|
31
|
+
fs.mkdir(Path.config, { recursive: true }),
|
|
32
|
+
fs.mkdir(Path.state, { recursive: true }),
|
|
33
|
+
fs.mkdir(Path.tmp, { recursive: true }),
|
|
34
|
+
fs.mkdir(Path.log, { recursive: true }),
|
|
35
|
+
fs.mkdir(Path.bin, { recursive: true }),
|
|
36
|
+
fs.mkdir(Path.repos, { recursive: true }),
|
|
37
|
+
]);
|
|
38
|
+
export class Service extends Context.Service()("@opencode/Global") {
|
|
39
|
+
}
|
|
40
|
+
export function make(input = {}) {
|
|
41
|
+
return {
|
|
42
|
+
home: Path.home,
|
|
43
|
+
data: Path.data,
|
|
44
|
+
cache: Path.cache,
|
|
45
|
+
config: Path.config,
|
|
46
|
+
state: Path.state,
|
|
47
|
+
tmp: Path.tmp,
|
|
48
|
+
bin: Path.bin,
|
|
49
|
+
log: Path.log,
|
|
50
|
+
repos: Path.repos,
|
|
51
|
+
...input,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const layer = Layer.effect(Service, Effect.sync(() => Service.of(make({ config: process.env.OPENCODE_CONFIG_DIR ?? Path.config }))));
|
|
55
|
+
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] });
|
|
56
|
+
export const layerWith = (input) => Layer.effect(Service, Effect.sync(() => Service.of(make(input))));
|
|
57
|
+
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,32 @@
|
|
|
1
|
+
export * as NpmConfig from "./npm-config.js";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
// @ts-expect-error npm does not publish types for this internal config API.
|
|
4
|
+
import Config from "@npmcli/config";
|
|
5
|
+
// @ts-expect-error npm does not publish types for this internal config API.
|
|
6
|
+
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js";
|
|
7
|
+
import { Effect } from "effect";
|
|
8
|
+
const npmPath = fileURLToPath(new URL("..", import.meta.url));
|
|
9
|
+
export const load = (dir) => Effect.tryPromise({
|
|
10
|
+
try: async () => {
|
|
11
|
+
const config = new Config({
|
|
12
|
+
npmPath,
|
|
13
|
+
cwd: dir,
|
|
14
|
+
env: { ...process.env },
|
|
15
|
+
argv: [process.execPath, process.execPath, "--prefix", dir],
|
|
16
|
+
execPath: process.execPath,
|
|
17
|
+
platform: process.platform,
|
|
18
|
+
definitions,
|
|
19
|
+
flatten,
|
|
20
|
+
nerfDarts,
|
|
21
|
+
shorthands,
|
|
22
|
+
warn: false,
|
|
23
|
+
});
|
|
24
|
+
await config.load();
|
|
25
|
+
return config.flat;
|
|
26
|
+
},
|
|
27
|
+
catch: (cause) => cause,
|
|
28
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
29
|
+
export const registry = (dir) => load(dir).pipe(Effect.map((config) => {
|
|
30
|
+
const registry = typeof config.registry === "string" ? config.registry : "https://registry.npmjs.org";
|
|
31
|
+
return registry.endsWith("/") ? registry.slice(0, -1) : registry;
|
|
32
|
+
}));
|
package/dist/npm.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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 EntryPoint {
|
|
13
|
+
readonly directory: string;
|
|
14
|
+
readonly entrypoint?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Interface {
|
|
17
|
+
readonly add: (pkg: string, options?: {
|
|
18
|
+
readonly subpaths?: readonly string[];
|
|
19
|
+
}) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>;
|
|
20
|
+
readonly install: (dir: string, input?: {
|
|
21
|
+
add: {
|
|
22
|
+
name: string;
|
|
23
|
+
version?: string;
|
|
24
|
+
}[];
|
|
25
|
+
}) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>;
|
|
26
|
+
readonly which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>;
|
|
27
|
+
}
|
|
28
|
+
declare const Service_base: Context.ServiceClass<Service, "@opencode/Npm", Interface>;
|
|
29
|
+
export declare class Service extends Service_base {
|
|
30
|
+
}
|
|
31
|
+
export declare function sanitize(pkg: string): string;
|
|
32
|
+
export declare const node: LayerNode.Node<Service, never, LayerNode.Tag<"global">>;
|
|
33
|
+
export declare function install(...args: Parameters<Interface["install"]>): Promise<void>;
|
|
34
|
+
export declare function add(...args: Parameters<Interface["add"]>): Promise<EntryPoint>;
|
|
35
|
+
export declare function which(...args: Parameters<Interface["which"]>): Promise<string | undefined>;
|
package/dist/npm.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
export * as Npm from "./npm.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import npa from "npm-package-arg";
|
|
4
|
+
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect";
|
|
5
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
6
|
+
import { FSUtil } from "./fs-util.js";
|
|
7
|
+
import { Global } from "./global.js";
|
|
8
|
+
import { EffectFlock } from "./effect-flock.js";
|
|
9
|
+
import { makeGlobalNode } from "./effect/app-node.js";
|
|
10
|
+
import { filesystem } from "./effect/app-node-platform.js";
|
|
11
|
+
import { LayerNode } from "./effect/layer-node.js";
|
|
12
|
+
import { makeRuntime } from "./effect/runtime.js";
|
|
13
|
+
import { NpmConfig } from "./npm-config.js";
|
|
14
|
+
import { resolveModule } from "#runtime-import";
|
|
15
|
+
export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", {
|
|
16
|
+
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
|
17
|
+
dir: Schema.String,
|
|
18
|
+
cause: Schema.optional(Schema.Defect()),
|
|
19
|
+
}) {
|
|
20
|
+
}
|
|
21
|
+
export class Service extends Context.Service()("@opencode/Npm") {
|
|
22
|
+
}
|
|
23
|
+
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined;
|
|
24
|
+
export function sanitize(pkg) {
|
|
25
|
+
if (!illegal)
|
|
26
|
+
return pkg;
|
|
27
|
+
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("");
|
|
28
|
+
}
|
|
29
|
+
const resolveEntryPoint = (name, dir, subpaths = [""]) => {
|
|
30
|
+
const entrypoint = subpaths
|
|
31
|
+
.map((subpath) => {
|
|
32
|
+
try {
|
|
33
|
+
return resolveModule([name, subpath].filter(Boolean).join("/"), dir);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.find((entrypoint) => entrypoint !== undefined);
|
|
40
|
+
return {
|
|
41
|
+
directory: dir,
|
|
42
|
+
entrypoint,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const layer = Layer.effect(Service, Effect.gen(function* () {
|
|
46
|
+
const afs = yield* FSUtil.Service;
|
|
47
|
+
const global = yield* Global.Service;
|
|
48
|
+
const fs = yield* FileSystem.FileSystem;
|
|
49
|
+
const flock = yield* EffectFlock.Service;
|
|
50
|
+
const directory = (pkg) => path.join(global.cache, "packages", sanitize(pkg));
|
|
51
|
+
const reify = (input) => Effect.gen(function* () {
|
|
52
|
+
yield* flock.acquire(`npm-install:${input.dir}`);
|
|
53
|
+
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
|
|
54
|
+
const add = input.add ?? [];
|
|
55
|
+
const npmOptions = yield* NpmConfig.load(input.dir);
|
|
56
|
+
const arborist = new Arborist({
|
|
57
|
+
...npmOptions,
|
|
58
|
+
path: input.dir,
|
|
59
|
+
binLinks: true,
|
|
60
|
+
progress: false,
|
|
61
|
+
savePrefix: "",
|
|
62
|
+
ignoreScripts: true,
|
|
63
|
+
});
|
|
64
|
+
return yield* Effect.tryPromise({
|
|
65
|
+
try: () => arborist.reify({
|
|
66
|
+
...npmOptions,
|
|
67
|
+
add,
|
|
68
|
+
save: true,
|
|
69
|
+
saveType: "prod",
|
|
70
|
+
}),
|
|
71
|
+
catch: (cause) => new InstallFailedError({
|
|
72
|
+
cause,
|
|
73
|
+
add,
|
|
74
|
+
dir: input.dir,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
}).pipe(Effect.withSpan("Npm.reify", {
|
|
78
|
+
attributes: input,
|
|
79
|
+
}));
|
|
80
|
+
const add = Effect.fn("Npm.add")(function* (pkg, options) {
|
|
81
|
+
const dir = directory(pkg);
|
|
82
|
+
const name = (() => {
|
|
83
|
+
try {
|
|
84
|
+
return npa(pkg).name ?? pkg;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return pkg;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
|
|
91
|
+
return resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths);
|
|
92
|
+
}
|
|
93
|
+
const tree = yield* reify({ dir, add: [pkg] });
|
|
94
|
+
const first = tree.edgesOut.values().next().value?.to;
|
|
95
|
+
if (!first) {
|
|
96
|
+
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths);
|
|
97
|
+
if (result.entrypoint)
|
|
98
|
+
return result;
|
|
99
|
+
return yield* new InstallFailedError({ add: [pkg], dir });
|
|
100
|
+
}
|
|
101
|
+
return resolveEntryPoint(first.name, first.path, options?.subpaths);
|
|
102
|
+
}, Effect.scoped);
|
|
103
|
+
const install = Effect.fn("Npm.install")(function* (dir, input) {
|
|
104
|
+
const canWrite = yield* afs.access(dir, { writable: true }).pipe(Effect.as(true), Effect.orElseSucceed(() => false));
|
|
105
|
+
if (!canWrite)
|
|
106
|
+
return;
|
|
107
|
+
const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? [];
|
|
108
|
+
if (yield* Effect.gen(function* () {
|
|
109
|
+
const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"));
|
|
110
|
+
if (!nodeModulesExists) {
|
|
111
|
+
yield* reify({ add, dir });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}).pipe(Effect.withSpan("Npm.checkNodeModules")))
|
|
116
|
+
return;
|
|
117
|
+
yield* Effect.gen(function* () {
|
|
118
|
+
const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})));
|
|
119
|
+
const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})));
|
|
120
|
+
const pkgAny = pkg;
|
|
121
|
+
const lockAny = lock;
|
|
122
|
+
const declared = new Set([
|
|
123
|
+
...Object.keys(pkgAny?.dependencies || {}),
|
|
124
|
+
...Object.keys(pkgAny?.devDependencies || {}),
|
|
125
|
+
...Object.keys(pkgAny?.peerDependencies || {}),
|
|
126
|
+
...Object.keys(pkgAny?.optionalDependencies || {}),
|
|
127
|
+
...(input?.add || []).map((pkg) => pkg.name),
|
|
128
|
+
]);
|
|
129
|
+
const root = lockAny?.packages?.[""] || {};
|
|
130
|
+
const locked = new Set([
|
|
131
|
+
...Object.keys(root?.dependencies || {}),
|
|
132
|
+
...Object.keys(root?.devDependencies || {}),
|
|
133
|
+
...Object.keys(root?.peerDependencies || {}),
|
|
134
|
+
...Object.keys(root?.optionalDependencies || {}),
|
|
135
|
+
]);
|
|
136
|
+
for (const name of declared) {
|
|
137
|
+
if (!locked.has(name)) {
|
|
138
|
+
yield* reify({ dir, add });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}).pipe(Effect.withSpan("Npm.checkDirty"));
|
|
143
|
+
return;
|
|
144
|
+
}, Effect.scoped);
|
|
145
|
+
const which = Effect.fn("Npm.which")(function* (pkg, bin) {
|
|
146
|
+
const dir = directory(pkg);
|
|
147
|
+
const binDir = path.join(dir, "node_modules", ".bin");
|
|
148
|
+
const pick = Effect.fnUntraced(function* () {
|
|
149
|
+
const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([])));
|
|
150
|
+
if (files.length === 0)
|
|
151
|
+
return Option.none();
|
|
152
|
+
// Caller picked a specific bin (e.g. pyright exposes both `pyright` and
|
|
153
|
+
// `pyright-langserver`); trust the hint if the package provides it.
|
|
154
|
+
if (bin)
|
|
155
|
+
return files.includes(bin) ? Option.some(bin) : Option.none();
|
|
156
|
+
if (files.length === 1)
|
|
157
|
+
return Option.some(files[0]);
|
|
158
|
+
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option);
|
|
159
|
+
if (Option.isSome(pkgJson)) {
|
|
160
|
+
const parsed = pkgJson.value;
|
|
161
|
+
if (parsed?.bin) {
|
|
162
|
+
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg;
|
|
163
|
+
const parsedBin = parsed.bin;
|
|
164
|
+
if (typeof parsedBin === "string")
|
|
165
|
+
return Option.some(unscoped);
|
|
166
|
+
const keys = Object.keys(parsedBin);
|
|
167
|
+
if (keys.length === 1)
|
|
168
|
+
return Option.some(keys[0]);
|
|
169
|
+
return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return Option.some(files[0]);
|
|
173
|
+
});
|
|
174
|
+
return Option.getOrUndefined(yield* Effect.gen(function* () {
|
|
175
|
+
const bin = yield* pick();
|
|
176
|
+
if (Option.isSome(bin)) {
|
|
177
|
+
return Option.some(path.join(binDir, bin.value));
|
|
178
|
+
}
|
|
179
|
+
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => { }));
|
|
180
|
+
yield* add(pkg);
|
|
181
|
+
const resolved = yield* pick();
|
|
182
|
+
if (Option.isNone(resolved))
|
|
183
|
+
return Option.none();
|
|
184
|
+
return Option.some(path.join(binDir, resolved.value));
|
|
185
|
+
}).pipe(Effect.scoped, Effect.orElseSucceed(() => Option.none())));
|
|
186
|
+
});
|
|
187
|
+
return Service.of({
|
|
188
|
+
add,
|
|
189
|
+
install,
|
|
190
|
+
which,
|
|
191
|
+
});
|
|
192
|
+
}));
|
|
193
|
+
export const node = makeGlobalNode({
|
|
194
|
+
service: Service,
|
|
195
|
+
layer: layer,
|
|
196
|
+
deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node],
|
|
197
|
+
});
|
|
198
|
+
const { runPromise } = makeRuntime(Service, LayerNode.compile(node));
|
|
199
|
+
export async function install(...args) {
|
|
200
|
+
return runPromise((svc) => svc.install(...args));
|
|
201
|
+
}
|
|
202
|
+
export async function add(...args) {
|
|
203
|
+
return runPromise((svc) => svc.add(...args));
|
|
204
|
+
}
|
|
205
|
+
export async function which(...args) {
|
|
206
|
+
return runPromise((svc) => svc.which(...args));
|
|
207
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Logger } from "effect";
|
|
2
|
+
export declare function file(local?: boolean, channel?: string): string;
|
|
3
|
+
export declare function fileLogger(target?: string, id?: string): import("effect/Effect").Effect<Logger.Logger<unknown, void>, import("effect/PlatformError").PlatformError, import("effect/FileSystem").FileSystem | import("effect/Scope").Scope>;
|
|
4
|
+
export declare function minimumLogLevel(): "Error" | "Warn" | "Info" | "Debug";
|
|
5
|
+
export declare function loggers(local?: boolean, channel?: string): (import("effect/Effect").Effect<Logger.Logger<unknown, void>, import("effect/PlatformError").PlatformError, import("effect/FileSystem").FileSystem | import("effect/Scope").Scope> | Logger.Logger<unknown, boolean>)[];
|
|
6
|
+
export * as Logging from "./logging.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Formatter, Logger } from "effect";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Global } from "../global.js";
|
|
4
|
+
import { runID } from "./shared.js";
|
|
5
|
+
function formatter(id = runID) {
|
|
6
|
+
return Logger.map(Logger.formatStructured, (output) => {
|
|
7
|
+
const messages = Array.isArray(output.message) ? output.message : [output.message];
|
|
8
|
+
return [
|
|
9
|
+
["timestamp", output.timestamp],
|
|
10
|
+
["level", output.level],
|
|
11
|
+
["run", id],
|
|
12
|
+
...messages.flatMap((value) => (plain(value) ? flatten(value) : [["message", value]])),
|
|
13
|
+
...(output.cause === undefined ? [] : [["cause", output.cause]]),
|
|
14
|
+
...flatten(output.spans),
|
|
15
|
+
...flatten(output.annotations),
|
|
16
|
+
]
|
|
17
|
+
.map(([key, value]) => `${key}=${format(value)}`)
|
|
18
|
+
.join(" ");
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function flatten(input, prefix = "", seen = new WeakSet()) {
|
|
22
|
+
if (seen.has(input))
|
|
23
|
+
return [[prefix, "[Circular]"]];
|
|
24
|
+
seen.add(input);
|
|
25
|
+
const entries = Object.entries(input);
|
|
26
|
+
if (entries.length === 0 && prefix)
|
|
27
|
+
return [[prefix, input]];
|
|
28
|
+
return entries.flatMap(([key, value]) => {
|
|
29
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
return plain(value) ? flatten(value, path, seen) : [[path, value]];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function plain(input) {
|
|
34
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
35
|
+
return false;
|
|
36
|
+
const prototype = Object.getPrototypeOf(input);
|
|
37
|
+
return prototype === Object.prototype || prototype === null;
|
|
38
|
+
}
|
|
39
|
+
function format(input) {
|
|
40
|
+
const value = typeof input === "string" ? input : Formatter.format(input);
|
|
41
|
+
return /^[^\s="\\]+$/.test(value) ? value : JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
export function file(local = true, channel = "local") {
|
|
44
|
+
if (!local)
|
|
45
|
+
return path.join(Global.Path.log, "opencode.log");
|
|
46
|
+
return path.join(Global.Path.log, `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.log`);
|
|
47
|
+
}
|
|
48
|
+
export function fileLogger(target = file(), id = runID) {
|
|
49
|
+
// Do not set batchWindow to 0; it causes high idle CPU usage.
|
|
50
|
+
return Logger.toFile(formatter(id), target, { flag: "a" });
|
|
51
|
+
}
|
|
52
|
+
const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n"));
|
|
53
|
+
export function minimumLogLevel() {
|
|
54
|
+
const value = process.env.OPENCODE_LOG_LEVEL?.toUpperCase();
|
|
55
|
+
const levels = {
|
|
56
|
+
DEBUG: "Debug",
|
|
57
|
+
INFO: "Info",
|
|
58
|
+
WARN: "Warn",
|
|
59
|
+
ERROR: "Error",
|
|
60
|
+
};
|
|
61
|
+
return value && value in levels ? levels[value] : levels.INFO;
|
|
62
|
+
}
|
|
63
|
+
export function loggers(local = true, channel = "local") {
|
|
64
|
+
const logger = fileLogger(file(local, channel));
|
|
65
|
+
return process.env.OPENCODE_PRINT_LOGS === "1" ? [logger, stderrLogger] : [logger];
|
|
66
|
+
}
|
|
67
|
+
export * as Logging from "./logging.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
export interface Options {
|
|
3
|
+
readonly endpoint?: string;
|
|
4
|
+
readonly headers?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface App {
|
|
7
|
+
readonly client: string;
|
|
8
|
+
readonly version: string;
|
|
9
|
+
readonly channel: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function resource(app?: App): {
|
|
12
|
+
serviceName: string;
|
|
13
|
+
serviceVersion: string;
|
|
14
|
+
attributes: Record<string, string>;
|
|
15
|
+
};
|
|
16
|
+
export declare function loggers(options: Options | undefined, app: App): import("effect/Effect").Effect<import("effect/Logger").Logger<unknown, void>, never, import("effect/unstable/http/HttpClient").HttpClient | import("effect/Scope").Scope | import("effect/unstable/observability/OtlpSerialization").OtlpSerialization>[];
|
|
17
|
+
export declare function tracingLayer(options: Options | undefined, app: App): Promise<Layer.Layer<never, never, never>>;
|
|
18
|
+
export * as Otlp from "./otlp.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { OtlpLogger } from "effect/unstable/observability";
|
|
3
|
+
import { runID } from "./shared.js";
|
|
4
|
+
function parseHeaders(value) {
|
|
5
|
+
return value
|
|
6
|
+
? value.split(",").reduce((acc, entry) => {
|
|
7
|
+
const [key, ...value] = entry.split("=");
|
|
8
|
+
acc[key] = value.join("=");
|
|
9
|
+
return acc;
|
|
10
|
+
}, {})
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function resourceAttributes() {
|
|
14
|
+
const value = process.env.OTEL_RESOURCE_ATTRIBUTES;
|
|
15
|
+
if (!value)
|
|
16
|
+
return {};
|
|
17
|
+
try {
|
|
18
|
+
return Object.fromEntries(value.split(",").map((entry) => {
|
|
19
|
+
const index = entry.indexOf("=");
|
|
20
|
+
if (index < 1)
|
|
21
|
+
throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry");
|
|
22
|
+
return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))];
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function resource(app = { client: "opencode", version: "unknown", channel: "local" }) {
|
|
30
|
+
return {
|
|
31
|
+
serviceName: "opencode",
|
|
32
|
+
serviceVersion: app.version,
|
|
33
|
+
attributes: {
|
|
34
|
+
...resourceAttributes(),
|
|
35
|
+
"deployment.environment.name": app.channel,
|
|
36
|
+
"opencode.client": app.client,
|
|
37
|
+
"opencode.run": runID,
|
|
38
|
+
"service.instance.id": runID,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function loggers(options, app) {
|
|
43
|
+
if (!options?.endpoint)
|
|
44
|
+
return [];
|
|
45
|
+
return [
|
|
46
|
+
OtlpLogger.make({
|
|
47
|
+
url: `${options.endpoint}/v1/logs`,
|
|
48
|
+
resource: resource(app),
|
|
49
|
+
headers: parseHeaders(options.headers),
|
|
50
|
+
}),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
export async function tracingLayer(options, app) {
|
|
54
|
+
if (!options?.endpoint)
|
|
55
|
+
return Layer.empty;
|
|
56
|
+
const NodeSdk = await import("@effect/opentelemetry/NodeSdk");
|
|
57
|
+
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http");
|
|
58
|
+
const SdkBase = await import("@opentelemetry/sdk-trace-base");
|
|
59
|
+
const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks");
|
|
60
|
+
const { context } = await import("@opentelemetry/api");
|
|
61
|
+
// The Effect Node SDK does not register a global context manager, but the AI SDK uses it to parent spans.
|
|
62
|
+
const manager = new AsyncLocalStorageContextManager();
|
|
63
|
+
manager.enable();
|
|
64
|
+
context.setGlobalContextManager(manager);
|
|
65
|
+
return NodeSdk.layer(() => ({
|
|
66
|
+
resource: resource(app),
|
|
67
|
+
spanProcessor: new SdkBase.BatchSpanProcessor(new OTLP.OTLPTraceExporter({
|
|
68
|
+
url: `${options.endpoint}/v1/traces`,
|
|
69
|
+
headers: parseHeaders(options.headers),
|
|
70
|
+
})),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export * as Otlp from "./otlp.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const runID: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const runID = crypto.randomUUID().slice(0, 8);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * as Observability from "./observability.js";
|
|
2
|
+
import { LayerNode } from "./effect/layer-node.js";
|
|
3
|
+
import { Layer, Schema } from "effect";
|
|
4
|
+
export declare const Options: Schema.Struct<{
|
|
5
|
+
readonly endpoint: Schema.optional<Schema.String>;
|
|
6
|
+
readonly headers: Schema.optional<Schema.String>;
|
|
7
|
+
readonly client: Schema.optional<Schema.String>;
|
|
8
|
+
readonly version: Schema.optional<Schema.String>;
|
|
9
|
+
readonly channel: Schema.optional<Schema.String>;
|
|
10
|
+
}>;
|
|
11
|
+
export type Options = typeof Options.Type;
|
|
12
|
+
export declare function layer(options?: Options): Layer.Layer<never, never, never>;
|
|
13
|
+
export declare const node: LayerNode.Node<never, never, undefined>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export * as Observability from "./observability.js";
|
|
2
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
3
|
+
import { LayerNode } from "./effect/layer-node.js";
|
|
4
|
+
import { Effect, Layer, Logger, References, Schema } from "effect";
|
|
5
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
6
|
+
import { OtlpSerialization } from "effect/unstable/observability";
|
|
7
|
+
import { Logging } from "./observability/logging.js";
|
|
8
|
+
import { Otlp } from "./observability/otlp.js";
|
|
9
|
+
export const Options = Schema.Struct({
|
|
10
|
+
endpoint: Schema.optional(Schema.String),
|
|
11
|
+
headers: Schema.optional(Schema.String),
|
|
12
|
+
client: Schema.optional(Schema.String),
|
|
13
|
+
version: Schema.optional(Schema.String),
|
|
14
|
+
channel: Schema.optional(Schema.String),
|
|
15
|
+
});
|
|
16
|
+
export function layer(options = {
|
|
17
|
+
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
18
|
+
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
|
|
19
|
+
}) {
|
|
20
|
+
const app = {
|
|
21
|
+
client: options.client ?? "opencode",
|
|
22
|
+
version: options.version ?? "unknown",
|
|
23
|
+
channel: options.channel ?? "local",
|
|
24
|
+
};
|
|
25
|
+
const local = Logger.layer(Logging.loggers(app.channel === "local", app.channel), { mergeWithExisting: false }).pipe(Layer.provide(NodeFileSystem.layer), Layer.orDie, Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())));
|
|
26
|
+
return Layer.unwrap(Effect.gen(function* () {
|
|
27
|
+
const logs = Logger.layer([...Logging.loggers(app.channel === "local", app.channel), ...Otlp.loggers(options, app)], { mergeWithExisting: false }).pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer), Layer.orDie, Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())));
|
|
28
|
+
return Layer.merge(logs, yield* Effect.promise(() => Otlp.tracingLayer(options, app)));
|
|
29
|
+
})).pipe(Layer.catchCause(() => local));
|
|
30
|
+
}
|
|
31
|
+
export const node = LayerNode.make({ name: "observability", layer: layer(), deps: [] });
|
package/dist/patch.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export * as Patch from "./patch.js";
|
|
2
|
+
import { Result, Schema } from "effect";
|
|
3
|
+
declare const BoundaryError_base: Schema.Class<BoundaryError, Schema.TaggedStruct<"Patch.BoundaryError", {
|
|
4
|
+
readonly boundary: Schema.Literals<readonly ["first", "last"]>;
|
|
5
|
+
}>, import("effect/Cause").YieldableError>;
|
|
6
|
+
export declare class BoundaryError extends BoundaryError_base {
|
|
7
|
+
get message(): string;
|
|
8
|
+
}
|
|
9
|
+
declare const InvalidHunkError_base: Schema.Class<InvalidHunkError, Schema.TaggedStruct<"Patch.InvalidHunkError", {
|
|
10
|
+
readonly line: Schema.String;
|
|
11
|
+
readonly lineNumber: Schema.Number;
|
|
12
|
+
}>, import("effect/Cause").YieldableError>;
|
|
13
|
+
export declare class InvalidHunkError extends InvalidHunkError_base {
|
|
14
|
+
get message(): string;
|
|
15
|
+
}
|
|
16
|
+
export type ParseError = BoundaryError | InvalidHunkError;
|
|
17
|
+
export type Hunk = {
|
|
18
|
+
readonly type: "add";
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly contents: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: "delete";
|
|
23
|
+
readonly path: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly type: "update";
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly movePath?: string;
|
|
28
|
+
readonly chunks: ReadonlyArray<UpdateFileChunk>;
|
|
29
|
+
};
|
|
30
|
+
export interface UpdateFileChunk {
|
|
31
|
+
readonly oldLines: ReadonlyArray<string>;
|
|
32
|
+
readonly newLines: ReadonlyArray<string>;
|
|
33
|
+
readonly changeContext?: string;
|
|
34
|
+
readonly endOfFile?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface FileUpdate {
|
|
37
|
+
readonly content: string;
|
|
38
|
+
readonly bom: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function parse(patchText: string): Result.Result<ReadonlyArray<Hunk>, ParseError>;
|
|
41
|
+
export declare function derive(path: string, chunks: ReadonlyArray<UpdateFileChunk>, original: string): FileUpdate;
|
|
42
|
+
export declare function joinBom(text: string, bom: boolean): string;
|