octocode-ai-shared 5.0.0

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/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "version": "5.0.0",
4
+ "name": "octocode-ai-shared",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "private": false,
11
+ "scripts": {
12
+ "test": "bun test",
13
+ "typecheck": "tsgo --noEmit"
14
+ },
15
+ "bin": {
16
+ "opencode": "./bin/opencode"
17
+ },
18
+ "exports": {
19
+ "./*": "./src/*.ts"
20
+ },
21
+ "imports": {},
22
+ "devDependencies": {
23
+ "@tsconfig/bun": "1.0.9",
24
+ "@types/semver": "7.7.1",
25
+ "@types/bun": "1.3.11",
26
+ "@types/npmcli__arborist": "6.3.3"
27
+ },
28
+ "dependencies": {
29
+ "@effect/platform-node": "4.0.0-beta.48",
30
+ "@npmcli/arborist": "9.4.0",
31
+ "effect": "4.0.0-beta.48",
32
+ "glob": "13.0.5",
33
+ "mime-types": "3.0.2",
34
+ "minimatch": "10.2.5",
35
+ "semver": "7.7.4",
36
+ "xdg-basedir": "5.1.0",
37
+ "zod": "4.1.8"
38
+ },
39
+ "overrides": {
40
+ "drizzle-orm": "1.0.0-beta.19-d95b7a4"
41
+ }
42
+ }
@@ -0,0 +1,236 @@
1
+ import { NodeFileSystem } from "@effect/platform-node"
2
+ import { dirname, join, relative, resolve as pathResolve } from "path"
3
+ import { realpathSync } from "fs"
4
+ import * as NFS from "fs/promises"
5
+ import { lookup } from "mime-types"
6
+ import { Effect, FileSystem, Layer, Schema, Context } from "effect"
7
+ import type { PlatformError } from "effect/PlatformError"
8
+ import { Glob } from "./util/glob"
9
+
10
+ export namespace AppFileSystem {
11
+ export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
12
+ method: Schema.String,
13
+ cause: Schema.optional(Schema.Defect),
14
+ }) {}
15
+
16
+ export type Error = PlatformError | FileSystemError
17
+
18
+ export interface DirEntry {
19
+ readonly name: string
20
+ readonly type: "file" | "directory" | "symlink" | "other"
21
+ }
22
+
23
+ export interface Interface extends FileSystem.FileSystem {
24
+ readonly isDir: (path: string) => Effect.Effect<boolean>
25
+ readonly isFile: (path: string) => Effect.Effect<boolean>
26
+ readonly existsSafe: (path: string) => Effect.Effect<boolean>
27
+ readonly readJson: (path: string) => Effect.Effect<unknown, Error>
28
+ readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect<void, Error>
29
+ readonly ensureDir: (path: string) => Effect.Effect<void, Error>
30
+ readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
31
+ readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
32
+ readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
33
+ readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
34
+ readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
35
+ readonly glob: (pattern: string, options?: Glob.Options) => Effect.Effect<string[], Error>
36
+ readonly globMatch: (pattern: string, filepath: string) => boolean
37
+ }
38
+
39
+ export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
40
+
41
+ export const layer = Layer.effect(
42
+ Service,
43
+ Effect.gen(function* () {
44
+ const fs = yield* FileSystem.FileSystem
45
+
46
+ const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) {
47
+ return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))
48
+ })
49
+
50
+ const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) {
51
+ const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
52
+ return info?.type === "Directory"
53
+ })
54
+
55
+ const isFile = Effect.fn("FileSystem.isFile")(function* (path: string) {
56
+ const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
57
+ return info?.type === "File"
58
+ })
59
+
60
+ const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
61
+ return yield* Effect.tryPromise({
62
+ try: async () => {
63
+ const entries = await NFS.readdir(dirPath, { withFileTypes: true })
64
+ return entries.map(
65
+ (e): DirEntry => ({
66
+ name: e.name,
67
+ type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other",
68
+ }),
69
+ )
70
+ },
71
+ catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }),
72
+ })
73
+ })
74
+
75
+ const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
76
+ const text = yield* fs.readFileString(path)
77
+ return JSON.parse(text)
78
+ })
79
+
80
+ const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) {
81
+ const content = JSON.stringify(data, null, 2)
82
+ yield* fs.writeFileString(path, content)
83
+ if (mode) yield* fs.chmod(path, mode)
84
+ })
85
+
86
+ const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) {
87
+ yield* fs.makeDirectory(path, { recursive: true })
88
+ })
89
+
90
+ const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* (
91
+ path: string,
92
+ content: string | Uint8Array,
93
+ mode?: number,
94
+ ) {
95
+ const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content)
96
+
97
+ yield* write.pipe(
98
+ Effect.catchIf(
99
+ (e) => e.reason._tag === "NotFound",
100
+ () =>
101
+ Effect.gen(function* () {
102
+ yield* fs.makeDirectory(dirname(path), { recursive: true })
103
+ yield* write
104
+ }),
105
+ ),
106
+ )
107
+ if (mode) yield* fs.chmod(path, mode)
108
+ })
109
+
110
+ const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) {
111
+ return yield* Effect.tryPromise({
112
+ try: () => Glob.scan(pattern, options),
113
+ catch: (cause) => new FileSystemError({ method: "glob", cause }),
114
+ })
115
+ })
116
+
117
+ const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) {
118
+ const result: string[] = []
119
+ let current = start
120
+ while (true) {
121
+ const search = join(current, target)
122
+ if (yield* fs.exists(search)) result.push(search)
123
+ if (stop === current) break
124
+ const parent = dirname(current)
125
+ if (parent === current) break
126
+ current = parent
127
+ }
128
+ return result
129
+ })
130
+
131
+ const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) {
132
+ const result: string[] = []
133
+ let current = options.start
134
+ while (true) {
135
+ for (const target of options.targets) {
136
+ const search = join(current, target)
137
+ if (yield* fs.exists(search)) result.push(search)
138
+ }
139
+ if (options.stop === current) break
140
+ const parent = dirname(current)
141
+ if (parent === current) break
142
+ current = parent
143
+ }
144
+ return result
145
+ })
146
+
147
+ const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) {
148
+ const result: string[] = []
149
+ let current = start
150
+ while (true) {
151
+ const matches = yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe(
152
+ Effect.catch(() => Effect.succeed([] as string[])),
153
+ )
154
+ result.push(...matches)
155
+ if (stop === current) break
156
+ const parent = dirname(current)
157
+ if (parent === current) break
158
+ current = parent
159
+ }
160
+ return result
161
+ })
162
+
163
+ return Service.of({
164
+ ...fs,
165
+ existsSafe,
166
+ isDir,
167
+ isFile,
168
+ readDirectoryEntries,
169
+ readJson,
170
+ writeJson,
171
+ ensureDir,
172
+ writeWithDirs,
173
+ findUp,
174
+ up,
175
+ globUp,
176
+ glob,
177
+ globMatch: Glob.match,
178
+ })
179
+ }),
180
+ )
181
+
182
+ export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
183
+
184
+ // Pure helpers that don't need Effect (path manipulation, sync operations)
185
+ export function mimeType(p: string): string {
186
+ return lookup(p) || "application/octet-stream"
187
+ }
188
+
189
+ export function normalizePath(p: string): string {
190
+ if (process.platform !== "win32") return p
191
+ const resolved = pathResolve(windowsPath(p))
192
+ try {
193
+ return realpathSync.native(resolved)
194
+ } catch {
195
+ return resolved
196
+ }
197
+ }
198
+
199
+ export function normalizePathPattern(p: string): string {
200
+ if (process.platform !== "win32") return p
201
+ if (p === "*") return p
202
+ const match = p.match(/^(.*)[\\/]\*$/)
203
+ if (!match) return normalizePath(p)
204
+ const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
205
+ return join(normalizePath(dir), "*")
206
+ }
207
+
208
+ export function resolve(p: string): string {
209
+ const resolved = pathResolve(windowsPath(p))
210
+ try {
211
+ return normalizePath(realpathSync(resolved))
212
+ } catch (e: any) {
213
+ if (e?.code === "ENOENT") return normalizePath(resolved)
214
+ throw e
215
+ }
216
+ }
217
+
218
+ export function windowsPath(p: string): string {
219
+ if (process.platform !== "win32") return p
220
+ return p
221
+ .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
222
+ .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
223
+ .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
224
+ .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
225
+ }
226
+
227
+ export function overlaps(a: string, b: string) {
228
+ const relA = relative(a, b)
229
+ const relB = relative(b, a)
230
+ return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
231
+ }
232
+
233
+ export function contains(parent: string, child: string) {
234
+ return !relative(parent, child).startsWith("..")
235
+ }
236
+ }
package/src/global.ts ADDED
@@ -0,0 +1,84 @@
1
+ import path from "path"
2
+ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
3
+ import os from "os"
4
+ import { Context, Effect, Layer } from "effect"
5
+
6
+ const APP = "octocode"
7
+
8
+ export type ResolvedPaths = {
9
+ mode: "octocode_home" | "xdg"
10
+ root?: string
11
+ data: string
12
+ cache: string
13
+ config: string
14
+ state: string
15
+ }
16
+
17
+ /**
18
+ * Resolve octocode's four base directories (config/data/state/cache)
19
+ * from environment variables.
20
+ *
21
+ * If OCTOCODE_HOME is set and non-empty, the four paths are subdirectories
22
+ * of it. Otherwise, falls through to XDG Base Directory defaults.
23
+ *
24
+ * @throws if OCTOCODE_HOME is set but not an absolute path
25
+ */
26
+ export function resolveOctocodeHome(env: NodeJS.ProcessEnv = process.env): ResolvedPaths {
27
+ const home = env.OCTOCODE_HOME
28
+ if (home) {
29
+ if (!path.isAbsolute(home)) {
30
+ throw new Error(
31
+ `OCTOCODE_HOME must be an absolute path, got: ${JSON.stringify(home)}`,
32
+ )
33
+ }
34
+ return {
35
+ mode: "octocode_home",
36
+ root: home,
37
+ data: path.join(home, "data"),
38
+ cache: path.join(home, "cache"),
39
+ config: path.join(home, "config"),
40
+ state: path.join(home, "state"),
41
+ }
42
+ }
43
+ return {
44
+ mode: "xdg",
45
+ data: path.join(xdgData!, APP),
46
+ cache: path.join(xdgCache!, APP),
47
+ config: path.join(xdgConfig!, APP),
48
+ state: path.join(xdgState!, APP),
49
+ }
50
+ }
51
+
52
+ export namespace Global {
53
+ export class Service extends Context.Service<Service, Interface>()("@opencode/Global") {}
54
+
55
+ export interface Interface {
56
+ readonly home: string
57
+ readonly data: string
58
+ readonly cache: string
59
+ readonly config: string
60
+ readonly state: string
61
+ readonly bin: string
62
+ readonly log: string
63
+ }
64
+
65
+ export const layer = Layer.effect(
66
+ Service,
67
+ Effect.gen(function* () {
68
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir()
69
+ const { data, cache, config, state } = yield* Effect.sync(() => resolveOctocodeHome())
70
+ const bin = path.join(cache, "bin")
71
+ const log = path.join(data, "log")
72
+
73
+ return Service.of({
74
+ home,
75
+ data,
76
+ cache,
77
+ config,
78
+ state,
79
+ bin,
80
+ log,
81
+ })
82
+ }),
83
+ )
84
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ declare module "@npmcli/arborist" {
2
+ export interface ArboristOptions {
3
+ path: string
4
+ binLinks?: boolean
5
+ progress?: boolean
6
+ savePrefix?: string
7
+ ignoreScripts?: boolean
8
+ [key: string]: unknown
9
+ }
10
+
11
+ export interface ArboristNode {
12
+ name: string
13
+ path: string
14
+ }
15
+
16
+ export interface ArboristEdge {
17
+ to?: ArboristNode
18
+ }
19
+
20
+ export interface ArboristTree {
21
+ edgesOut: Map<string, ArboristEdge>
22
+ }
23
+
24
+ export interface ReifyOptions {
25
+ add?: string[]
26
+ save?: boolean
27
+ saveType?: "prod" | "dev" | "optional" | "peer"
28
+ [key: string]: unknown
29
+ }
30
+
31
+ export class Arborist {
32
+ constructor(options: ArboristOptions)
33
+ loadVirtual(): Promise<ArboristTree | undefined>
34
+ reify(options?: ReifyOptions): Promise<ArboristTree>
35
+ }
36
+ }
37
+
38
+ declare var Bun:
39
+ | {
40
+ file(path: string): {
41
+ text(): Promise<string>
42
+ json(): Promise<unknown>
43
+ }
44
+ write(path: string, content: string | Uint8Array): Promise<void>
45
+ }
46
+ | undefined
@@ -0,0 +1,10 @@
1
+ export function findLast<T>(
2
+ items: readonly T[],
3
+ predicate: (item: T, index: number, items: readonly T[]) => boolean,
4
+ ): T | undefined {
5
+ for (let i = items.length - 1; i >= 0; i -= 1) {
6
+ const item = items[i]
7
+ if (predicate(item, i, items)) return item
8
+ }
9
+ return undefined
10
+ }
@@ -0,0 +1,41 @@
1
+ export namespace Binary {
2
+ export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
3
+ let left = 0
4
+ let right = array.length - 1
5
+
6
+ while (left <= right) {
7
+ const mid = Math.floor((left + right) / 2)
8
+ const midId = compare(array[mid])
9
+
10
+ if (midId === id) {
11
+ return { found: true, index: mid }
12
+ } else if (midId < id) {
13
+ left = mid + 1
14
+ } else {
15
+ right = mid - 1
16
+ }
17
+ }
18
+
19
+ return { found: false, index: left }
20
+ }
21
+
22
+ export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
23
+ const id = compare(item)
24
+ let left = 0
25
+ let right = array.length
26
+
27
+ while (left < right) {
28
+ const mid = Math.floor((left + right) / 2)
29
+ const midId = compare(array[mid])
30
+
31
+ if (midId < id) {
32
+ left = mid + 1
33
+ } else {
34
+ right = mid
35
+ }
36
+ }
37
+
38
+ array.splice(left, 0, item)
39
+ return array
40
+ }
41
+ }