@opencode-ai/util 0.0.0-beta-17492

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.
Files changed (57) hide show
  1. package/dist/bom.d.ts +22 -0
  2. package/dist/bom.js +37 -0
  3. package/dist/cross-spawn-spawner.d.ts +3 -0
  4. package/dist/cross-spawn-spawner.js +406 -0
  5. package/dist/effect/app-node-platform.d.ts +6 -0
  6. package/dist/effect/app-node-platform.js +11 -0
  7. package/dist/effect/app-node.d.ts +50 -0
  8. package/dist/effect/app-node.js +8 -0
  9. package/dist/effect/layer-node.d.ts +79 -0
  10. package/dist/effect/layer-node.js +181 -0
  11. package/dist/effect/memo-map.d.ts +2 -0
  12. package/dist/effect/memo-map.js +2 -0
  13. package/dist/effect/runtime.d.ts +8 -0
  14. package/dist/effect/runtime.js +16 -0
  15. package/dist/effect/service-use.d.ts +7 -0
  16. package/dist/effect/service-use.js +27 -0
  17. package/dist/effect-flock.d.ts +31 -0
  18. package/dist/effect-flock.js +185 -0
  19. package/dist/flock.d.ts +30 -0
  20. package/dist/flock.js +273 -0
  21. package/dist/fs-util.d.ts +137 -0
  22. package/dist/fs-util.js +212 -0
  23. package/dist/glob.d.ts +12 -0
  24. package/dist/glob.js +26 -0
  25. package/dist/global-roots.d.ts +8 -0
  26. package/dist/global-roots.js +17 -0
  27. package/dist/global-roots.workerd.d.ts +7 -0
  28. package/dist/global-roots.workerd.js +15 -0
  29. package/dist/global.d.ts +30 -0
  30. package/dist/global.js +54 -0
  31. package/dist/hash.d.ts +4 -0
  32. package/dist/hash.js +12 -0
  33. package/dist/npm-config.d.ts +4 -0
  34. package/dist/npm-config.js +34 -0
  35. package/dist/npm.d.ts +28 -0
  36. package/dist/npm.js +161 -0
  37. package/dist/observability/logging.d.ts +6 -0
  38. package/dist/observability/logging.js +71 -0
  39. package/dist/observability/otlp.d.ts +18 -0
  40. package/dist/observability/otlp.js +76 -0
  41. package/dist/observability/shared.d.ts +1 -0
  42. package/dist/observability/shared.js +7 -0
  43. package/dist/observability.d.ts +13 -0
  44. package/dist/observability.js +37 -0
  45. package/dist/patch.d.ts +43 -0
  46. package/dist/patch.js +332 -0
  47. package/dist/process.d.ts +54 -0
  48. package/dist/process.js +162 -0
  49. package/dist/runtime/import.bun.d.ts +2 -0
  50. package/dist/runtime/import.bun.js +6 -0
  51. package/dist/runtime/import.node.d.ts +2 -0
  52. package/dist/runtime/import.node.js +36 -0
  53. package/dist/runtime-import.d.ts +1 -0
  54. package/dist/runtime-import.js +1 -0
  55. package/dist/session-title-fallback.d.ts +16 -0
  56. package/dist/session-title-fallback.js +23 -0
  57. package/package.json +72 -0
@@ -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,7 @@
1
+ export declare function roots(app: string): {
2
+ data: string;
3
+ cache: string;
4
+ config: string;
5
+ state: string;
6
+ tmp: string;
7
+ };
@@ -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
+ }
@@ -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,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
@@ -0,0 +1,4 @@
1
+ export declare namespace Hash {
2
+ function fast(input: string | Buffer): string;
3
+ function sha256(input: string | Buffer): string;
4
+ }
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,4 @@
1
+ export * as NpmConfig from "./npm-config.js";
2
+ import { Effect } from "effect";
3
+ export declare const load: (dir: string) => Effect.Effect<Record<string, unknown>, never, never>;
4
+ export declare const registry: (dir: string) => Effect.Effect<string, never, never>;
@@ -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,28 @@
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 which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>;
21
+ }
22
+ declare const Service_base: Context.ServiceClass<Service, "@opencode/Npm", Interface>;
23
+ export declare class Service extends Service_base {
24
+ }
25
+ export declare function sanitize(pkg: string): string;
26
+ export declare const node: LayerNode.Node<Service, never, LayerNode.Tag<"global">>;
27
+ export declare function add(...args: Parameters<Interface["add"]>): Promise<EntryPoint>;
28
+ export declare function which(...args: Parameters<Interface["which"]>): Promise<string | undefined>;
package/dist/npm.js ADDED
@@ -0,0 +1,161 @@
1
+ export * as Npm from "./npm.js";
2
+ import path from "path";
3
+ import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect";
4
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
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
+ import { resolveModule } from "#runtime-import";
14
+ export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", {
15
+ add: Schema.Array(Schema.String).pipe(Schema.optional),
16
+ dir: Schema.String,
17
+ cause: Schema.optional(Schema.Defect()),
18
+ }) {
19
+ }
20
+ export class Service extends Context.Service()("@opencode/Npm") {
21
+ }
22
+ const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined;
23
+ export function sanitize(pkg) {
24
+ if (!illegal)
25
+ return pkg;
26
+ return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("");
27
+ }
28
+ const resolveEntryPoint = (name, dir, subpaths = [""]) => {
29
+ const entrypoint = subpaths
30
+ .map((subpath) => {
31
+ try {
32
+ return resolveModule([name, subpath].filter(Boolean).join("/"), dir);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ })
38
+ .find((entrypoint) => entrypoint !== undefined);
39
+ return {
40
+ directory: dir,
41
+ entrypoint,
42
+ };
43
+ };
44
+ const layer = Layer.effect(Service, Effect.gen(function* () {
45
+ const afs = yield* FSUtil.Service;
46
+ const global = yield* Global.Service;
47
+ const fs = yield* FileSystem.FileSystem;
48
+ const flock = yield* EffectFlock.Service;
49
+ const directory = (pkg) => path.join(global.cache, "packages", sanitize(pkg));
50
+ const reify = (input) => Effect.gen(function* () {
51
+ yield* flock.acquire(`npm-install:${input.dir}`);
52
+ const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
53
+ const add = input.add ?? [];
54
+ const npmOptions = yield* NpmConfig.load(input.dir);
55
+ const arborist = new Arborist({
56
+ ...npmOptions,
57
+ path: input.dir,
58
+ binLinks: true,
59
+ progress: false,
60
+ savePrefix: "",
61
+ ignoreScripts: true,
62
+ });
63
+ return yield* Effect.tryPromise({
64
+ try: () => arborist.reify({
65
+ ...npmOptions,
66
+ add,
67
+ save: true,
68
+ saveType: "prod",
69
+ }),
70
+ catch: (cause) => new InstallFailedError({
71
+ cause,
72
+ add,
73
+ dir: input.dir,
74
+ }),
75
+ });
76
+ }).pipe(Effect.withSpan("Npm.reify", {
77
+ attributes: input,
78
+ }));
79
+ const add = Effect.fn("Npm.add")(function* (pkg, options) {
80
+ const { default: npa } = yield* Effect.promise(() => import("npm-package-arg"));
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 which = Effect.fn("Npm.which")(function* (pkg, bin) {
104
+ const dir = directory(pkg);
105
+ const binDir = path.join(dir, "node_modules", ".bin");
106
+ const pick = Effect.fnUntraced(function* () {
107
+ const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([])));
108
+ if (files.length === 0)
109
+ return Option.none();
110
+ // Caller picked a specific bin (e.g. pyright exposes both `pyright` and
111
+ // `pyright-langserver`); trust the hint if the package provides it.
112
+ if (bin)
113
+ return files.includes(bin) ? Option.some(bin) : Option.none();
114
+ if (files.length === 1)
115
+ return Option.some(files[0]);
116
+ const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option);
117
+ if (Option.isSome(pkgJson)) {
118
+ const parsed = pkgJson.value;
119
+ if (parsed?.bin) {
120
+ const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg;
121
+ const parsedBin = parsed.bin;
122
+ if (typeof parsedBin === "string")
123
+ return Option.some(unscoped);
124
+ const keys = Object.keys(parsedBin);
125
+ if (keys.length === 1)
126
+ return Option.some(keys[0]);
127
+ return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]);
128
+ }
129
+ }
130
+ return Option.some(files[0]);
131
+ });
132
+ return Option.getOrUndefined(yield* Effect.gen(function* () {
133
+ const bin = yield* pick();
134
+ if (Option.isSome(bin)) {
135
+ return Option.some(path.join(binDir, bin.value));
136
+ }
137
+ yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => { }));
138
+ yield* add(pkg);
139
+ const resolved = yield* pick();
140
+ if (Option.isNone(resolved))
141
+ return Option.none();
142
+ return Option.some(path.join(binDir, resolved.value));
143
+ }).pipe(Effect.scoped, Effect.orElseSucceed(() => Option.none())));
144
+ });
145
+ return Service.of({
146
+ add,
147
+ which,
148
+ });
149
+ }));
150
+ export const node = makeGlobalNode({
151
+ service: Service,
152
+ layer: layer,
153
+ deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node],
154
+ });
155
+ const { runPromise } = makeRuntime(Service, LayerNode.compile(node));
156
+ export async function add(...args) {
157
+ return runPromise((svc) => svc.add(...args));
158
+ }
159
+ export async function which(...args) {
160
+ return runPromise((svc) => svc.which(...args));
161
+ }
@@ -0,0 +1,6 @@
1
+ import { Effect, FileSystem, Logger } from "effect";
2
+ export declare function file(local?: boolean, channel?: string): string;
3
+ export declare function fileLogger(target?: string, id?: string): Effect.Effect<Logger.Logger<unknown, void>, import("effect/PlatformError").PlatformError, FileSystem.FileSystem | import("effect/Scope").Scope>;
4
+ export declare function minimumLogLevel(): "Error" | "Warn" | "Info" | "Debug";
5
+ export declare function loggers(local?: boolean, channel?: string): (Effect.Effect<Logger.Logger<unknown, void>, import("effect/PlatformError").PlatformError, FileSystem.FileSystem | import("effect/Scope").Scope> | Logger.Logger<unknown, boolean>)[];
6
+ export * as Logging from "./logging.js";
@@ -0,0 +1,71 @@
1
+ import { Effect, FileSystem, 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 Effect.gen(function* () {
51
+ const fs = yield* FileSystem.FileSystem;
52
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true });
53
+ return yield* Logger.toFile(formatter(id), target, { flag: "a" });
54
+ });
55
+ }
56
+ const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n"));
57
+ export function minimumLogLevel() {
58
+ const value = process.env.OPENCODE_LOG_LEVEL?.toUpperCase();
59
+ const levels = {
60
+ DEBUG: "Debug",
61
+ INFO: "Info",
62
+ WARN: "Warn",
63
+ ERROR: "Error",
64
+ };
65
+ return value && value in levels ? levels[value] : levels.INFO;
66
+ }
67
+ export function loggers(local = true, channel = "local") {
68
+ const logger = fileLogger(file(local, channel));
69
+ return process.env.OPENCODE_PRINT_LOGS === "1" ? [logger, stderrLogger] : [logger];
70
+ }
71
+ export * as Logging from "./logging.js";
@@ -0,0 +1,18 @@
1
+ import { Effect, Layer, Scope } 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): Effect.Effect<import("effect/Logger").Logger<unknown, void>, never, import("effect/unstable/http/HttpClient").HttpClient | Scope.Scope | import("effect/unstable/observability/OtlpSerialization").OtlpSerialization>[];
17
+ export declare const tracingLayer: (options: Options | undefined, app: App) => Effect.Effect<Layer.Layer<never, never, never>, never, never>;
18
+ export * as Otlp from "./otlp.js";
@@ -0,0 +1,76 @@
1
+ import { Effect, Layer, Scope } 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 const tracingLayer = Effect.fnUntraced(function* (options, app) {
54
+ if (!options?.endpoint)
55
+ return Layer.empty;
56
+ const [{ layer }, { OTLPTraceExporter }, { BatchSpanProcessor }, { AsyncLocalStorageContextManager }, { context }] = yield* Effect.all([
57
+ Effect.promise(() => import("@effect/opentelemetry/NodeSdk")),
58
+ Effect.promise(() => import("@opentelemetry/exporter-trace-otlp-http")),
59
+ Effect.promise(() => import("@opentelemetry/sdk-trace-base")),
60
+ Effect.promise(() => import("@opentelemetry/context-async-hooks")),
61
+ Effect.promise(() => import("@opentelemetry/api")),
62
+ ], { concurrency: "unbounded" });
63
+ // The Effect Node SDK does not register a global context manager, but the AI SDK uses it to parent spans.
64
+ const manager = new AsyncLocalStorageContextManager();
65
+ manager.enable();
66
+ context.setGlobalContextManager(manager);
67
+ const tracing = layer(() => ({
68
+ resource: resource(app),
69
+ spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter({
70
+ url: `${options.endpoint}/v1/traces`,
71
+ headers: parseHeaders(options.headers),
72
+ })),
73
+ }));
74
+ return Layer.effectContext(Effect.acquireRelease(Scope.make(), (scope, exit) => Scope.close(scope, exit).pipe(Effect.ignoreCause)).pipe(Effect.flatMap((scope) => Layer.buildWithScope(tracing, scope))));
75
+ });
76
+ export * as Otlp from "./otlp.js";
@@ -0,0 +1 @@
1
+ export declare function runID(): string;
@@ -0,0 +1,7 @@
1
+ // Lazy: workerd forbids generating random values in global scope, so the id
2
+ // materializes on first call (inside a handler) and stays stable afterwards.
3
+ let generated;
4
+ export function runID() {
5
+ generated ??= crypto.randomUUID().slice(0, 8);
6
+ return generated;
7
+ }
@@ -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,37 @@
1
+ export * as Observability from "./observability.js";
2
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
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* Otlp.tracingLayer(options, app));
29
+ })).pipe(Layer.catchCause(() => local));
30
+ }
31
+ // Layer.suspend: constructing the loggers eagerly at module scope performs
32
+ // I/O (file logger, run id) that workerd forbids in global scope.
33
+ export const node = LayerNode.make({
34
+ name: "observability",
35
+ layer: Layer.suspend(() => layer()),
36
+ deps: [],
37
+ });
@@ -0,0 +1,43 @@
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
+ readonly reason: Schema.optional<Schema.String>;
13
+ }>, import("effect/Cause").YieldableError>;
14
+ export declare class InvalidHunkError extends InvalidHunkError_base {
15
+ get message(): string;
16
+ }
17
+ export type ParseError = BoundaryError | InvalidHunkError;
18
+ export type Hunk = {
19
+ readonly type: "add";
20
+ readonly path: string;
21
+ readonly contents: string;
22
+ } | {
23
+ readonly type: "delete";
24
+ readonly path: string;
25
+ } | {
26
+ readonly type: "update";
27
+ readonly path: string;
28
+ readonly movePath?: string;
29
+ readonly chunks: ReadonlyArray<UpdateFileChunk>;
30
+ };
31
+ export interface UpdateFileChunk {
32
+ readonly oldLines: ReadonlyArray<string>;
33
+ readonly newLines: ReadonlyArray<string>;
34
+ readonly changeContext?: string;
35
+ readonly endOfFile?: boolean;
36
+ }
37
+ export interface FileUpdate {
38
+ readonly content: string;
39
+ readonly bom: boolean;
40
+ }
41
+ export declare function parse(patchText: string): Result.Result<ReadonlyArray<Hunk>, ParseError>;
42
+ export declare function derive(path: string, chunks: ReadonlyArray<UpdateFileChunk>, original: string): FileUpdate;
43
+ export declare function joinBom(text: string, bom: boolean): string;