@prover-coder-ai/context-doc 1.0.18 → 1.0.19
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/README.md +18 -0
- package/dist/main.js +553 -0
- package/dist/main.js.map +1 -0
- package/package.json +6 -1
- package/.jscpd.json +0 -16
- package/CHANGELOG.md +0 -43
- package/biome.json +0 -37
- package/eslint.config.mts +0 -305
- package/eslint.effect-ts-check.config.mjs +0 -220
- package/linter.config.json +0 -33
- package/src/app/main.ts +0 -29
- package/src/app/program.ts +0 -32
- package/src/core/knowledge.ts +0 -115
- package/src/shell/cli.ts +0 -75
- package/src/shell/services/crypto.ts +0 -50
- package/src/shell/services/file-system.ts +0 -142
- package/src/shell/services/runtime-env.ts +0 -54
- package/src/shell/sync/index.ts +0 -35
- package/src/shell/sync/shared.ts +0 -229
- package/src/shell/sync/sources/claude.ts +0 -54
- package/src/shell/sync/sources/codex.ts +0 -261
- package/src/shell/sync/sources/qwen.ts +0 -97
- package/src/shell/sync/types.ts +0 -47
- package/tests/core/knowledge.test.ts +0 -95
- package/tests/shell/cli-pack.test.ts +0 -176
- package/tests/shell/sync-knowledge.test.ts +0 -203
- package/tests/support/fs-helpers.ts +0 -50
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -32
- package/vitest.config.ts +0 -85
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Option } from "effect"
|
|
2
|
-
|
|
3
|
-
export class RuntimeEnv extends Context.Tag("RuntimeEnv")<
|
|
4
|
-
RuntimeEnv,
|
|
5
|
-
{
|
|
6
|
-
readonly argv: Effect.Effect<ReadonlyArray<string>>
|
|
7
|
-
readonly cwd: Effect.Effect<string>
|
|
8
|
-
readonly homedir: Effect.Effect<string>
|
|
9
|
-
readonly envVar: (key: string) => Effect.Effect<Option.Option<string>>
|
|
10
|
-
}
|
|
11
|
-
>() {}
|
|
12
|
-
|
|
13
|
-
const readProcess = (): NodeJS.Process | undefined => typeof process === "undefined" ? undefined : process
|
|
14
|
-
|
|
15
|
-
const readEnv = (): NodeJS.ProcessEnv => readProcess()?.env ?? {}
|
|
16
|
-
|
|
17
|
-
const resolveHomeDir = (env: NodeJS.ProcessEnv, cwdFallback: string): string => {
|
|
18
|
-
const direct = env["HOME"] ?? env["USERPROFILE"]
|
|
19
|
-
if (direct !== undefined) {
|
|
20
|
-
return direct
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const drive = env["HOMEDRIVE"]
|
|
24
|
-
const path = env["HOMEPATH"]
|
|
25
|
-
if (drive !== undefined && path !== undefined) {
|
|
26
|
-
return `${drive}${path}`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return cwdFallback
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// CHANGE: wrap process/os access behind a typed Effect service
|
|
33
|
-
// WHY: keep shell dependencies injectable and testable
|
|
34
|
-
// QUOTE(TZ): "Внешние зависимости: только через типизированные интерфейсы"
|
|
35
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
36
|
-
// SOURCE: n/a
|
|
37
|
-
// FORMAT THEOREM: forall k: env(k) -> Option<string>
|
|
38
|
-
// PURITY: SHELL
|
|
39
|
-
// EFFECT: Effect<RuntimeEnv, never, never>
|
|
40
|
-
// INVARIANT: argv/cwd/homedir are read once per effect
|
|
41
|
-
// COMPLEXITY: O(1)/O(1)
|
|
42
|
-
export const RuntimeEnvLive = Layer.succeed(RuntimeEnv, {
|
|
43
|
-
argv: Effect.sync(() => {
|
|
44
|
-
const proc = readProcess()
|
|
45
|
-
return proc === undefined ? [] : [...proc.argv]
|
|
46
|
-
}),
|
|
47
|
-
cwd: Effect.sync(() => readProcess()?.cwd() ?? "."),
|
|
48
|
-
homedir: Effect.sync(() => {
|
|
49
|
-
const proc = readProcess()
|
|
50
|
-
const cwdFallback = proc?.cwd() ?? "."
|
|
51
|
-
return resolveHomeDir(readEnv(), cwdFallback)
|
|
52
|
-
}),
|
|
53
|
-
envVar: (key) => Effect.sync(() => Option.fromNullable(readEnv()[key]))
|
|
54
|
-
})
|
package/src/shell/sync/index.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type * as Path from "@effect/platform/Path"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
|
|
4
|
-
import type { CryptoService } from "../services/crypto.js"
|
|
5
|
-
import type { FileSystemService } from "../services/file-system.js"
|
|
6
|
-
import type { RuntimeEnv } from "../services/runtime-env.js"
|
|
7
|
-
import { syncClaude } from "./sources/claude.js"
|
|
8
|
-
import { syncCodex } from "./sources/codex.js"
|
|
9
|
-
import { syncQwen } from "./sources/qwen.js"
|
|
10
|
-
import type { SyncOptions } from "./types.js"
|
|
11
|
-
|
|
12
|
-
type SyncProgramEnv = RuntimeEnv | FileSystemService | CryptoService | Path.Path
|
|
13
|
-
|
|
14
|
-
// CHANGE: compose multi-source sync into a single Effect program
|
|
15
|
-
// WHY: centralize orchestration while keeping each source isolated
|
|
16
|
-
// QUOTE(TZ): "монодическая композиция"
|
|
17
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
18
|
-
// SOURCE: n/a
|
|
19
|
-
// FORMAT THEOREM: forall s in Sources: run(s) -> synced(s)
|
|
20
|
-
// PURITY: SHELL
|
|
21
|
-
// EFFECT: Effect<void, never, FileSystemService | RuntimeEnv | CryptoService | Path>
|
|
22
|
-
// INVARIANT: sources run sequentially in fixed order
|
|
23
|
-
// COMPLEXITY: O(n)/O(n)
|
|
24
|
-
export const buildSyncProgram = (
|
|
25
|
-
options: SyncOptions
|
|
26
|
-
): Effect.Effect<
|
|
27
|
-
void,
|
|
28
|
-
never,
|
|
29
|
-
SyncProgramEnv
|
|
30
|
-
> =>
|
|
31
|
-
Effect.gen(function*(_) {
|
|
32
|
-
yield* _(syncClaude(options))
|
|
33
|
-
yield* _(syncCodex(options))
|
|
34
|
-
yield* _(syncQwen(options))
|
|
35
|
-
})
|
package/src/shell/sync/shared.ts
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import * as Path from "@effect/platform/Path"
|
|
2
|
-
import { Console, Effect, Match, pipe } from "effect"
|
|
3
|
-
|
|
4
|
-
import { type DirectoryEntry, FileSystemService } from "../services/file-system.js"
|
|
5
|
-
import { type SyncError, syncError, type SyncOptions, type SyncSource } from "./types.js"
|
|
6
|
-
|
|
7
|
-
const forEach = Effect.forEach
|
|
8
|
-
|
|
9
|
-
type FilteredSourceEnv<R> = R | FileSystemService | Path.Path
|
|
10
|
-
|
|
11
|
-
const ensureDirectory = (
|
|
12
|
-
directory: string
|
|
13
|
-
): Effect.Effect<void, SyncError, FileSystemService> =>
|
|
14
|
-
Effect.gen(function*(_) {
|
|
15
|
-
const fs = yield* _(FileSystemService)
|
|
16
|
-
yield* _(fs.makeDirectory(directory))
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
// CHANGE: expose recursive traversal for callers that filter by entry
|
|
20
|
-
// WHY: reuse traversal logic across sources without duplicating Match logic
|
|
21
|
-
// QUOTE(TZ): "минимальный корректный diff"
|
|
22
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
23
|
-
// SOURCE: n/a
|
|
24
|
-
// FORMAT THEOREM: forall f: collectFiles(root, p) -> p(f) => exists(f)
|
|
25
|
-
// PURITY: SHELL
|
|
26
|
-
// EFFECT: Effect<ReadonlyArray<string>, SyncError, FileSystemService>
|
|
27
|
-
// INVARIANT: returned paths are absolute and exist in traversal
|
|
28
|
-
// COMPLEXITY: O(n)/O(n)
|
|
29
|
-
export const collectFiles = (
|
|
30
|
-
root: string,
|
|
31
|
-
isRelevant: (entry: DirectoryEntry) => boolean
|
|
32
|
-
): Effect.Effect<ReadonlyArray<string>, SyncError, FileSystemService> =>
|
|
33
|
-
Effect.gen(function*(_) {
|
|
34
|
-
const fs = yield* _(FileSystemService)
|
|
35
|
-
const entries = yield* _(fs.readDirectory(root))
|
|
36
|
-
const chunks = yield* _(
|
|
37
|
-
forEach(entries, (entry) =>
|
|
38
|
-
Match.value(entry.kind).pipe(
|
|
39
|
-
Match.when("directory", () => collectFiles(entry.path, isRelevant)),
|
|
40
|
-
Match.when("file", () => isRelevant(entry) ? Effect.succeed([entry.path]) : Effect.succeed([])),
|
|
41
|
-
Match.when("other", () => Effect.succeed([])),
|
|
42
|
-
Match.exhaustive
|
|
43
|
-
))
|
|
44
|
-
)
|
|
45
|
-
return chunks.flat()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// CHANGE: share relative path copy for multiple sync sources
|
|
49
|
-
// WHY: avoid repeating path math in each source
|
|
50
|
-
// QUOTE(TZ): "SHELL → CORE (but not наоборот)"
|
|
51
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
52
|
-
// SOURCE: n/a
|
|
53
|
-
// FORMAT THEOREM: forall f: copy(f) -> relative(f) preserved
|
|
54
|
-
// PURITY: SHELL
|
|
55
|
-
// EFFECT: Effect<void, SyncError, FileSystemService | Path>
|
|
56
|
-
// INVARIANT: destination preserves source-relative path
|
|
57
|
-
// COMPLEXITY: O(1)/O(1)
|
|
58
|
-
export const copyFilePreservingRelativePath = (
|
|
59
|
-
sourceRoot: string,
|
|
60
|
-
destinationRoot: string,
|
|
61
|
-
filePath: string
|
|
62
|
-
): Effect.Effect<void, SyncError, FileSystemService | Path.Path> =>
|
|
63
|
-
Effect.gen(function*(_) {
|
|
64
|
-
const fs = yield* _(FileSystemService)
|
|
65
|
-
const path = yield* _(Path.Path)
|
|
66
|
-
const relative = path.relative(sourceRoot, filePath)
|
|
67
|
-
const targetPath = path.join(destinationRoot, relative)
|
|
68
|
-
yield* _(fs.makeDirectory(path.dirname(targetPath)))
|
|
69
|
-
yield* _(fs.copyFile(filePath, targetPath))
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
// CHANGE: expose reusable first-match search with effectful predicate
|
|
73
|
-
// WHY: remove duplicated recursive search logic across sources
|
|
74
|
-
// QUOTE(TZ): "минимальный корректный diff"
|
|
75
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
76
|
-
// SOURCE: n/a
|
|
77
|
-
// FORMAT THEOREM: forall c: match(c) -> returns first c
|
|
78
|
-
// PURITY: SHELL
|
|
79
|
-
// EFFECT: Effect<string | undefined, SyncError, R>
|
|
80
|
-
// INVARIANT: result is undefined iff no candidate matches
|
|
81
|
-
// COMPLEXITY: O(n)/O(1)
|
|
82
|
-
export const findFirstMatching = <R>(
|
|
83
|
-
candidates: ReadonlyArray<string>,
|
|
84
|
-
matches: (candidate: string) => Effect.Effect<boolean, SyncError, R>
|
|
85
|
-
): Effect.Effect<string | undefined, SyncError, R> => {
|
|
86
|
-
const loop = (
|
|
87
|
-
remaining: ReadonlyArray<string>
|
|
88
|
-
): Effect.Effect<string | undefined, SyncError, R> =>
|
|
89
|
-
Effect.gen(function*(_) {
|
|
90
|
-
const [candidate, ...rest] = remaining
|
|
91
|
-
if (candidate === undefined) {
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
const matched = yield* _(matches(candidate))
|
|
95
|
-
if (!matched) {
|
|
96
|
-
return yield* _(loop(rest))
|
|
97
|
-
}
|
|
98
|
-
return candidate
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
return loop(candidates)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// CHANGE: ensure destination directory is created through Effect-typed fs
|
|
105
|
-
// WHY: keep IO effects inside SHELL and reuse in sync flows
|
|
106
|
-
// QUOTE(TZ): "SHELL: Все эффекты изолированы в тонкой оболочке"
|
|
107
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
108
|
-
// SOURCE: n/a
|
|
109
|
-
// FORMAT THEOREM: forall d: ensureDirectory(d) -> exists(d)
|
|
110
|
-
// PURITY: SHELL
|
|
111
|
-
// EFFECT: Effect<void, SyncError, FileSystemService>
|
|
112
|
-
// INVARIANT: directory exists after successful effect
|
|
113
|
-
// COMPLEXITY: O(1)/O(1)
|
|
114
|
-
export const ensureDestination = ensureDirectory
|
|
115
|
-
|
|
116
|
-
// CHANGE: resolve project root for cwd-based matching and destination paths
|
|
117
|
-
// WHY: allow running from sub-packages while targeting the repo root
|
|
118
|
-
// QUOTE(TZ): "передай root-path на основную папку"
|
|
119
|
-
// REF: user-2026-01-19-project-root
|
|
120
|
-
// SOURCE: n/a
|
|
121
|
-
// FORMAT THEOREM: forall o: root(o) = resolve(o.projectRoot ?? o.cwd)
|
|
122
|
-
// PURITY: SHELL
|
|
123
|
-
// EFFECT: n/a
|
|
124
|
-
// INVARIANT: resolved root is absolute
|
|
125
|
-
// COMPLEXITY: O(1)/O(1)
|
|
126
|
-
export const resolveProjectRoot = (
|
|
127
|
-
path: Path.Path,
|
|
128
|
-
options: SyncOptions
|
|
129
|
-
): string => path.resolve(options.projectRoot ?? options.cwd)
|
|
130
|
-
|
|
131
|
-
// CHANGE: copy filtered files with typed errors and deterministic traversal
|
|
132
|
-
// WHY: reuse shared traversal logic for Qwen/Claude syncs
|
|
133
|
-
// QUOTE(TZ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
|
|
134
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
135
|
-
// SOURCE: n/a
|
|
136
|
-
// FORMAT THEOREM: forall f: relevant(f) -> copied(f)
|
|
137
|
-
// PURITY: SHELL
|
|
138
|
-
// EFFECT: Effect<number, SyncError, FileSystemService | Path>
|
|
139
|
-
// INVARIANT: copied count equals length of relevant files
|
|
140
|
-
// COMPLEXITY: O(n)/O(n)
|
|
141
|
-
export const copyFilteredFiles = (
|
|
142
|
-
sourceRoot: string,
|
|
143
|
-
destinationRoot: string,
|
|
144
|
-
isRelevant: (entry: DirectoryEntry, fullPath: string) => boolean,
|
|
145
|
-
errorReason: string
|
|
146
|
-
): Effect.Effect<number, SyncError, FileSystemService | Path.Path> =>
|
|
147
|
-
pipe(
|
|
148
|
-
Effect.gen(function*(_) {
|
|
149
|
-
const files = yield* _(collectFiles(sourceRoot, (entry) => isRelevant(entry, entry.path)))
|
|
150
|
-
yield* _(
|
|
151
|
-
forEach(files, (filePath) => copyFilePreservingRelativePath(sourceRoot, destinationRoot, filePath))
|
|
152
|
-
)
|
|
153
|
-
return files.length
|
|
154
|
-
}),
|
|
155
|
-
Effect.mapError(() => syncError(sourceRoot, errorReason))
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
// CHANGE: build a SyncSource using shared filtered copy logic
|
|
159
|
-
// WHY: eliminate repeated source definitions for file-extension based syncs
|
|
160
|
-
// QUOTE(TZ): "минимальный корректный diff"
|
|
161
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
162
|
-
// SOURCE: n/a
|
|
163
|
-
// FORMAT THEOREM: forall s: createFilteredSource(s) -> copyFilteredFiles(s)
|
|
164
|
-
// PURITY: SHELL
|
|
165
|
-
// EFFECT: Effect<number, SyncError, R>
|
|
166
|
-
// INVARIANT: copy uses shared filter predicate
|
|
167
|
-
// COMPLEXITY: O(n)/O(n)
|
|
168
|
-
export const createFilteredSource = <R>(params: {
|
|
169
|
-
readonly name: SyncSource<FilteredSourceEnv<R>>["name"]
|
|
170
|
-
readonly destSubdir: SyncSource<FilteredSourceEnv<R>>["destSubdir"]
|
|
171
|
-
readonly resolveSource: SyncSource<FilteredSourceEnv<R>>["resolveSource"]
|
|
172
|
-
readonly filter: (entry: DirectoryEntry, fullPath: string) => boolean
|
|
173
|
-
readonly errorReason: string
|
|
174
|
-
}): SyncSource<FilteredSourceEnv<R>> => ({
|
|
175
|
-
name: params.name,
|
|
176
|
-
destSubdir: params.destSubdir,
|
|
177
|
-
resolveSource: params.resolveSource,
|
|
178
|
-
copy: (sourceDir, destinationDir) => copyFilteredFiles(sourceDir, destinationDir, params.filter, params.errorReason)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
// CHANGE: standardize per-source sync orchestration with skip-on-error logging
|
|
182
|
-
// WHY: keep the shell thin and consistent for all sources
|
|
183
|
-
// QUOTE(TZ): "SHELL → CORE (but not наоборот)"
|
|
184
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
185
|
-
// SOURCE: n/a
|
|
186
|
-
// FORMAT THEOREM: forall s: runSyncSource(s) -> logs(s)
|
|
187
|
-
// PURITY: SHELL
|
|
188
|
-
// EFFECT: Effect<void, never, FileSystemService | Path>
|
|
189
|
-
// INVARIANT: source==destination implies no copy
|
|
190
|
-
// COMPLEXITY: O(n)/O(n)
|
|
191
|
-
export const runSyncSource = <R>(
|
|
192
|
-
source: SyncSource<R>,
|
|
193
|
-
options: SyncOptions
|
|
194
|
-
): Effect.Effect<void, never, R | FileSystemService | Path.Path> =>
|
|
195
|
-
pipe(
|
|
196
|
-
Effect.gen(function*(_) {
|
|
197
|
-
const path = yield* _(Path.Path)
|
|
198
|
-
const resolvedSource = yield* _(source.resolveSource(options))
|
|
199
|
-
const destination = path.join(
|
|
200
|
-
resolveProjectRoot(path, options),
|
|
201
|
-
".knowledge",
|
|
202
|
-
source.destSubdir
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
if (path.resolve(resolvedSource) === path.resolve(destination)) {
|
|
206
|
-
yield* _(
|
|
207
|
-
Console.log(
|
|
208
|
-
`${source.name}: source equals destination; skipping copy to avoid duplicates`
|
|
209
|
-
)
|
|
210
|
-
)
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
yield* _(ensureDirectory(destination))
|
|
215
|
-
const copied = yield* _(source.copy(resolvedSource, destination, options))
|
|
216
|
-
yield* _(
|
|
217
|
-
Console.log(
|
|
218
|
-
`${source.name}: copied ${copied} files from ${resolvedSource} to ${destination}`
|
|
219
|
-
)
|
|
220
|
-
)
|
|
221
|
-
}),
|
|
222
|
-
Effect.matchEffect({
|
|
223
|
-
onFailure: (error: SyncError) =>
|
|
224
|
-
Console.log(
|
|
225
|
-
`${source.name}: source not found; skipped syncing (${error.reason})`
|
|
226
|
-
),
|
|
227
|
-
onSuccess: () => Effect.void
|
|
228
|
-
})
|
|
229
|
-
)
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import * as Path from "@effect/platform/Path"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
|
|
4
|
-
import { FileSystemService } from "../../services/file-system.js"
|
|
5
|
-
import { RuntimeEnv } from "../../services/runtime-env.js"
|
|
6
|
-
import { createFilteredSource, resolveProjectRoot, runSyncSource } from "../shared.js"
|
|
7
|
-
import { type SyncError, syncError, type SyncOptions, type SyncSource } from "../types.js"
|
|
8
|
-
|
|
9
|
-
type ClaudeEnv = RuntimeEnv | FileSystemService | Path.Path
|
|
10
|
-
|
|
11
|
-
const slugFromCwd = (cwd: string): string => `-${cwd.replace(/^\/+/, "").replaceAll("\\", "-").replaceAll("/", "-")}`
|
|
12
|
-
|
|
13
|
-
const resolveClaudeProjectDir = (
|
|
14
|
-
options: SyncOptions
|
|
15
|
-
): Effect.Effect<string, SyncError, ClaudeEnv> =>
|
|
16
|
-
Effect.gen(function*(_) {
|
|
17
|
-
const env = yield* _(RuntimeEnv)
|
|
18
|
-
const homeDir = yield* _(env.homedir)
|
|
19
|
-
const path = yield* _(Path.Path)
|
|
20
|
-
const base = options.claudeProjectsRoot ??
|
|
21
|
-
path.join(homeDir, ".claude", "projects")
|
|
22
|
-
const candidate = path.join(base, slugFromCwd(resolveProjectRoot(path, options)))
|
|
23
|
-
const fs = yield* _(FileSystemService)
|
|
24
|
-
const exists = yield* _(fs.exists(candidate))
|
|
25
|
-
if (!exists) {
|
|
26
|
-
return yield* _(
|
|
27
|
-
Effect.fail(syncError(".claude", "Claude project directory is missing"))
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return candidate
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const claudeSource: SyncSource<ClaudeEnv> = createFilteredSource({
|
|
35
|
-
name: "Claude",
|
|
36
|
-
destSubdir: ".claude",
|
|
37
|
-
resolveSource: resolveClaudeProjectDir,
|
|
38
|
-
filter: (entry, fullPath) => entry.kind === "file" && fullPath.endsWith(".jsonl"),
|
|
39
|
-
errorReason: "Cannot traverse Claude project"
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// CHANGE: sync Claude dialog files through shared sync runner
|
|
43
|
-
// WHY: keep Claude-specific path resolution isolated from other sources
|
|
44
|
-
// QUOTE(TZ): "SHELL: Все эффекты (IO, сеть, БД, env/process) изолированы"
|
|
45
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
46
|
-
// SOURCE: n/a
|
|
47
|
-
// FORMAT THEOREM: forall f: jsonl(f) -> copied(f)
|
|
48
|
-
// PURITY: SHELL
|
|
49
|
-
// EFFECT: Effect<void, never, FileSystemService | RuntimeEnv | Path>
|
|
50
|
-
// INVARIANT: only .jsonl files are copied
|
|
51
|
-
// COMPLEXITY: O(n)/O(n)
|
|
52
|
-
export const syncClaude = (
|
|
53
|
-
options: SyncOptions
|
|
54
|
-
): Effect.Effect<void, never, ClaudeEnv> => runSyncSource(claudeSource, options)
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import * as Path from "@effect/platform/Path"
|
|
2
|
-
import * as Schema from "@effect/schema/Schema"
|
|
3
|
-
import { Console, Effect, Option, pipe } from "effect"
|
|
4
|
-
|
|
5
|
-
import type { JsonValue, ProjectLocator } from "../../../core/knowledge.js"
|
|
6
|
-
import { buildProjectLocator, valueMatchesProject } from "../../../core/knowledge.js"
|
|
7
|
-
import { FileSystemService } from "../../services/file-system.js"
|
|
8
|
-
import { RuntimeEnv } from "../../services/runtime-env.js"
|
|
9
|
-
import {
|
|
10
|
-
collectFiles,
|
|
11
|
-
copyFilePreservingRelativePath,
|
|
12
|
-
ensureDestination,
|
|
13
|
-
findFirstMatching,
|
|
14
|
-
resolveProjectRoot
|
|
15
|
-
} from "../shared.js"
|
|
16
|
-
import { type SyncError, syncError, type SyncOptions } from "../types.js"
|
|
17
|
-
|
|
18
|
-
type CodexEnv = RuntimeEnv | FileSystemService | Path.Path
|
|
19
|
-
type CodexFsEnv = FileSystemService | Path.Path
|
|
20
|
-
|
|
21
|
-
const some = Option.some
|
|
22
|
-
const forEach = Effect.forEach
|
|
23
|
-
|
|
24
|
-
const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
|
|
25
|
-
Schema.Union(
|
|
26
|
-
Schema.String,
|
|
27
|
-
Schema.Number,
|
|
28
|
-
Schema.Boolean,
|
|
29
|
-
Schema.Null,
|
|
30
|
-
Schema.Array(JsonValueSchema),
|
|
31
|
-
Schema.Record({ key: Schema.String, value: JsonValueSchema })
|
|
32
|
-
)
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
const parseJsonLine = (
|
|
36
|
-
line: string
|
|
37
|
-
): Effect.Effect<Option.Option<JsonValue>> =>
|
|
38
|
-
pipe(
|
|
39
|
-
Schema.decode(Schema.parseJson(JsonValueSchema))(line),
|
|
40
|
-
Effect.match({
|
|
41
|
-
onFailure: () => Option.none(),
|
|
42
|
-
onSuccess: (value) => some(value)
|
|
43
|
-
})
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
const resolveEnvValue = (envValue: Option.Option<string>): string | undefined => Option.getOrUndefined(envValue)
|
|
47
|
-
|
|
48
|
-
const buildLocator = (path: Path.Path, projectRoot: string): ProjectLocator => {
|
|
49
|
-
const normalizedRoot = path.resolve(projectRoot)
|
|
50
|
-
const isWithinRoot = (candidate: string): boolean => {
|
|
51
|
-
const normalizedCandidate = path.resolve(candidate)
|
|
52
|
-
const relative = path.relative(normalizedRoot, normalizedCandidate)
|
|
53
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
54
|
-
}
|
|
55
|
-
return buildProjectLocator(normalizedRoot, isWithinRoot)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const containsJsonl = (root: string): Effect.Effect<boolean, SyncError, FileSystemService> =>
|
|
59
|
-
Effect.gen(function*(_) {
|
|
60
|
-
const fs = yield* _(FileSystemService)
|
|
61
|
-
const entries = yield* _(fs.readDirectory(root))
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
if (entry.kind === "file" && entry.path.endsWith(".jsonl")) {
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
if (entry.kind === "directory") {
|
|
67
|
-
const found = yield* _(containsJsonl(entry.path))
|
|
68
|
-
if (found) {
|
|
69
|
-
return true
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return false
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const hasJsonlInCandidate = (
|
|
77
|
-
candidate: string
|
|
78
|
-
): Effect.Effect<boolean, SyncError, FileSystemService> =>
|
|
79
|
-
Effect.gen(function*(_) {
|
|
80
|
-
const fs = yield* _(FileSystemService)
|
|
81
|
-
const exists = yield* _(fs.exists(candidate))
|
|
82
|
-
if (!exists) {
|
|
83
|
-
return false
|
|
84
|
-
}
|
|
85
|
-
return yield* _(containsJsonl(candidate))
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const findFirstExistingWithJsonl = (
|
|
89
|
-
candidates: ReadonlyArray<string>
|
|
90
|
-
): Effect.Effect<string | undefined, SyncError, FileSystemService> => findFirstMatching(candidates, hasJsonlInCandidate)
|
|
91
|
-
|
|
92
|
-
const resolveSourceDir = (
|
|
93
|
-
options: SyncOptions
|
|
94
|
-
): Effect.Effect<string, SyncError, CodexEnv> =>
|
|
95
|
-
Effect.gen(function*(_) {
|
|
96
|
-
const env = yield* _(RuntimeEnv)
|
|
97
|
-
const path = yield* _(Path.Path)
|
|
98
|
-
const envSource = resolveEnvValue(yield* _(env.envVar("CODEX_SOURCE_DIR")))
|
|
99
|
-
const homeDir = yield* _(env.homedir)
|
|
100
|
-
const projectRoot = resolveProjectRoot(path, options)
|
|
101
|
-
let metaCandidate: string | undefined
|
|
102
|
-
if (options.metaRoot !== undefined) {
|
|
103
|
-
metaCandidate = options.metaRoot.endsWith(".codex")
|
|
104
|
-
? options.metaRoot
|
|
105
|
-
: path.join(options.metaRoot, ".codex")
|
|
106
|
-
}
|
|
107
|
-
const localSource = path.join(projectRoot, ".codex")
|
|
108
|
-
const localKnowledge = path.join(projectRoot, ".knowledge", ".codex")
|
|
109
|
-
const homeSource = path.join(homeDir, ".codex")
|
|
110
|
-
const homeKnowledge = path.join(homeDir, ".knowledge", ".codex")
|
|
111
|
-
|
|
112
|
-
const candidates = [
|
|
113
|
-
options.sourceDir,
|
|
114
|
-
envSource,
|
|
115
|
-
metaCandidate,
|
|
116
|
-
localSource,
|
|
117
|
-
homeSource,
|
|
118
|
-
localKnowledge,
|
|
119
|
-
homeKnowledge
|
|
120
|
-
].filter((candidate): candidate is string => candidate !== undefined)
|
|
121
|
-
|
|
122
|
-
const existing = yield* _(findFirstExistingWithJsonl(candidates))
|
|
123
|
-
if (existing === undefined) {
|
|
124
|
-
return yield* _(
|
|
125
|
-
Effect.fail(
|
|
126
|
-
syncError(
|
|
127
|
-
".codex",
|
|
128
|
-
`No .jsonl files found in .codex candidates; checked: ${candidates.join(", ")}`
|
|
129
|
-
)
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return existing
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const resolveLocator = (
|
|
138
|
-
options: SyncOptions
|
|
139
|
-
): Effect.Effect<ProjectLocator, never, Path.Path> =>
|
|
140
|
-
Effect.gen(function*(_) {
|
|
141
|
-
const path = yield* _(Path.Path)
|
|
142
|
-
const projectRoot = resolveProjectRoot(path, options)
|
|
143
|
-
return buildLocator(path, projectRoot)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
const lineMatchesProject = (
|
|
147
|
-
line: string,
|
|
148
|
-
locator: ProjectLocator
|
|
149
|
-
): Effect.Effect<boolean> =>
|
|
150
|
-
pipe(
|
|
151
|
-
parseJsonLine(line),
|
|
152
|
-
Effect.map((parsed) => Option.exists(parsed, (value) => valueMatchesProject(value, locator)))
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
const fileMatchesProject = (
|
|
156
|
-
filePath: string,
|
|
157
|
-
locator: ProjectLocator
|
|
158
|
-
): Effect.Effect<boolean, SyncError, FileSystemService> =>
|
|
159
|
-
Effect.gen(function*(_) {
|
|
160
|
-
const fs = yield* _(FileSystemService)
|
|
161
|
-
const content = yield* _(fs.readFileString(filePath))
|
|
162
|
-
const lines = content.split("\n")
|
|
163
|
-
const matches = yield* _(
|
|
164
|
-
forEach(lines, (line) => {
|
|
165
|
-
const trimmed = line.trim()
|
|
166
|
-
return trimmed.length === 0
|
|
167
|
-
? Effect.succeed(false)
|
|
168
|
-
: lineMatchesProject(trimmed, locator)
|
|
169
|
-
})
|
|
170
|
-
)
|
|
171
|
-
return matches.some(Boolean)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const selectRelevantFiles = (
|
|
175
|
-
files: ReadonlyArray<string>,
|
|
176
|
-
locator: ProjectLocator
|
|
177
|
-
): Effect.Effect<ReadonlyArray<string>, SyncError, FileSystemService> =>
|
|
178
|
-
pipe(
|
|
179
|
-
forEach(files, (filePath) =>
|
|
180
|
-
pipe(
|
|
181
|
-
fileMatchesProject(filePath, locator),
|
|
182
|
-
Effect.map((matches) => ({ filePath, matches }))
|
|
183
|
-
)),
|
|
184
|
-
Effect.map((results) =>
|
|
185
|
-
results
|
|
186
|
-
.filter((result) => result.matches)
|
|
187
|
-
.map((result) => result.filePath)
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
const copyCodexFiles = (
|
|
192
|
-
sourceDir: string,
|
|
193
|
-
destinationDir: string,
|
|
194
|
-
locator: ProjectLocator
|
|
195
|
-
): Effect.Effect<void, SyncError, CodexFsEnv> =>
|
|
196
|
-
Effect.gen(function*(_) {
|
|
197
|
-
yield* _(ensureDestination(destinationDir))
|
|
198
|
-
const allJsonlFiles = yield* _(
|
|
199
|
-
collectFiles(sourceDir, (entry) => entry.kind === "file" && entry.path.endsWith(".jsonl"))
|
|
200
|
-
)
|
|
201
|
-
const relevantFiles = yield* _(selectRelevantFiles(allJsonlFiles, locator))
|
|
202
|
-
yield* _(
|
|
203
|
-
forEach(relevantFiles, (filePath) => copyFilePreservingRelativePath(sourceDir, destinationDir, filePath))
|
|
204
|
-
)
|
|
205
|
-
yield* _(
|
|
206
|
-
Console.log(
|
|
207
|
-
`Codex: copied ${relevantFiles.length} files from ${sourceDir} to ${destinationDir}`
|
|
208
|
-
)
|
|
209
|
-
)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// CHANGE: extract Codex dialog sync into dedicated module
|
|
213
|
-
// WHY: keep Codex-specific shell effects isolated from other sources
|
|
214
|
-
// QUOTE(TZ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
|
|
215
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
216
|
-
// SOURCE: n/a
|
|
217
|
-
// FORMAT THEOREM: forall f: relevant(f, locator) -> copied(f)
|
|
218
|
-
// PURITY: SHELL
|
|
219
|
-
// EFFECT: Effect<void, never, FileSystemService | RuntimeEnv | Path>
|
|
220
|
-
// INVARIANT: copied files contain at least one project-matching line by projectRoot
|
|
221
|
-
// COMPLEXITY: O(n)/O(n)
|
|
222
|
-
export const syncCodex = (
|
|
223
|
-
options: SyncOptions
|
|
224
|
-
): Effect.Effect<void, never, CodexEnv> =>
|
|
225
|
-
Effect.gen(function*(_) {
|
|
226
|
-
const locator = yield* _(resolveLocator(options))
|
|
227
|
-
const sourceDir = yield* _(resolveSourceDir(options))
|
|
228
|
-
const path = yield* _(Path.Path)
|
|
229
|
-
const destinationDir = options.destinationDir ??
|
|
230
|
-
path.join(resolveProjectRoot(path, options), ".knowledge", ".codex")
|
|
231
|
-
|
|
232
|
-
if (path.resolve(sourceDir) === path.resolve(destinationDir)) {
|
|
233
|
-
yield* _(
|
|
234
|
-
Console.log(
|
|
235
|
-
"Codex source equals destination; skipping copy to avoid duplicates"
|
|
236
|
-
)
|
|
237
|
-
)
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
yield* _(copyCodexFiles(sourceDir, destinationDir, locator))
|
|
242
|
-
}).pipe(
|
|
243
|
-
Effect.matchEffect({
|
|
244
|
-
onFailure: (error: SyncError) =>
|
|
245
|
-
Console.log(
|
|
246
|
-
`Codex source not found; skipped syncing Codex dialog files (${error.reason})`
|
|
247
|
-
),
|
|
248
|
-
onSuccess: () => Effect.void
|
|
249
|
-
})
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
// CHANGE: expose DirectoryEntry type for Codex traversal helpers
|
|
253
|
-
// WHY: reuse typed filesystem entries without leaking Node types
|
|
254
|
-
// QUOTE(TZ): "CORE never calls SHELL"
|
|
255
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
256
|
-
// SOURCE: n/a
|
|
257
|
-
// FORMAT THEOREM: forall e: DirectoryEntry -> shellOnly(e)
|
|
258
|
-
// PURITY: SHELL
|
|
259
|
-
// EFFECT: n/a
|
|
260
|
-
// INVARIANT: entry.kind is one of file|directory|other
|
|
261
|
-
// COMPLEXITY: O(1)/O(1)
|