@prover-coder-ai/context-doc 1.0.12 → 1.0.13

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 (49) hide show
  1. package/.jscpd.json +16 -0
  2. package/CHANGELOG.md +13 -0
  3. package/bin/context-doc.js +12 -0
  4. package/biome.json +37 -0
  5. package/eslint.config.mts +305 -0
  6. package/eslint.effect-ts-check.config.mjs +220 -0
  7. package/linter.config.json +33 -0
  8. package/package.json +72 -41
  9. package/src/app/main.ts +29 -0
  10. package/src/app/program.ts +32 -0
  11. package/src/core/knowledge.ts +93 -117
  12. package/src/shell/cli.ts +73 -5
  13. package/src/shell/services/crypto.ts +50 -0
  14. package/src/shell/services/file-system.ts +142 -0
  15. package/src/shell/services/runtime-env.ts +54 -0
  16. package/src/shell/sync/index.ts +35 -0
  17. package/src/shell/sync/shared.ts +229 -0
  18. package/src/shell/sync/sources/claude.ts +54 -0
  19. package/src/shell/sync/sources/codex.ts +261 -0
  20. package/src/shell/sync/sources/qwen.ts +97 -0
  21. package/src/shell/sync/types.ts +47 -0
  22. package/tests/core/knowledge.test.ts +95 -0
  23. package/tests/shell/cli-pack.test.ts +176 -0
  24. package/tests/shell/sync-knowledge.test.ts +203 -0
  25. package/tests/support/fs-helpers.ts +50 -0
  26. package/tsconfig.json +19 -0
  27. package/vite.config.ts +32 -0
  28. package/vitest.config.ts +70 -66
  29. package/README.md +0 -42
  30. package/dist/core/greeting.js +0 -25
  31. package/dist/core/knowledge.js +0 -49
  32. package/dist/main.js +0 -27
  33. package/dist/shell/claudeSync.js +0 -23
  34. package/dist/shell/cli.js +0 -5
  35. package/dist/shell/codexSync.js +0 -129
  36. package/dist/shell/qwenSync.js +0 -41
  37. package/dist/shell/syncKnowledge.js +0 -39
  38. package/dist/shell/syncShared.js +0 -49
  39. package/dist/shell/syncTypes.js +0 -1
  40. package/src/core/greeting.ts +0 -39
  41. package/src/main.ts +0 -49
  42. package/src/shell/claudeSync.ts +0 -55
  43. package/src/shell/codexSync.ts +0 -236
  44. package/src/shell/qwenSync.ts +0 -73
  45. package/src/shell/syncKnowledge.ts +0 -49
  46. package/src/shell/syncShared.ts +0 -94
  47. package/src/shell/syncTypes.ts +0 -30
  48. package/src/types/env.d.ts +0 -10
  49. package/tsconfig.build.json +0 -18
@@ -0,0 +1,142 @@
1
+ import * as FileSystem from "@effect/platform/FileSystem"
2
+ import * as Path from "@effect/platform/Path"
3
+ import { Context, Effect, Layer, pipe } from "effect"
4
+
5
+ import { type SyncError, syncError } from "../sync/types.js"
6
+
7
+ export type DirectoryEntryKind = "file" | "directory" | "other"
8
+
9
+ export interface DirectoryEntry {
10
+ readonly name: string
11
+ readonly path: string
12
+ readonly kind: DirectoryEntryKind
13
+ }
14
+
15
+ export class FileSystemService extends Context.Tag("FileSystemService")<
16
+ FileSystemService,
17
+ {
18
+ readonly readFileString: (pathValue: string) => Effect.Effect<string, SyncError>
19
+ readonly readDirectory: (
20
+ pathValue: string
21
+ ) => Effect.Effect<ReadonlyArray<DirectoryEntry>, SyncError>
22
+ readonly makeDirectory: (pathValue: string) => Effect.Effect<void, SyncError>
23
+ readonly copyFile: (
24
+ sourcePath: string,
25
+ destinationPath: string
26
+ ) => Effect.Effect<void, SyncError>
27
+ readonly exists: (pathValue: string) => Effect.Effect<boolean, SyncError>
28
+ }
29
+ >() {}
30
+
31
+ const forEach = Effect.forEach
32
+
33
+ const resolveEntryPath = (
34
+ path: Path.Path,
35
+ root: string,
36
+ entry: string
37
+ ): string => (path.isAbsolute(entry) ? entry : path.join(root, entry))
38
+
39
+ const entryKindFromInfo = (
40
+ info: FileSystem.File.Info
41
+ ): DirectoryEntryKind => {
42
+ if (info.type === "Directory") {
43
+ return "directory"
44
+ }
45
+ if (info.type === "File") {
46
+ return "file"
47
+ }
48
+ return "other"
49
+ }
50
+
51
+ const toDirectoryEntry = (
52
+ path: Path.Path,
53
+ entryPath: string,
54
+ info: FileSystem.File.Info
55
+ ): DirectoryEntry => ({
56
+ name: path.basename(entryPath),
57
+ path: entryPath,
58
+ kind: entryKindFromInfo(info)
59
+ })
60
+
61
+ const readEntry = (
62
+ fs: FileSystem.FileSystem,
63
+ path: Path.Path,
64
+ entryPath: string
65
+ ): Effect.Effect<DirectoryEntry, SyncError> =>
66
+ pipe(
67
+ fs.stat(entryPath),
68
+ Effect.map((info) => toDirectoryEntry(path, entryPath, info)),
69
+ Effect.mapError(() => syncError(entryPath, "Cannot read directory entry"))
70
+ )
71
+
72
+ const resolveEntry = (
73
+ fs: FileSystem.FileSystem,
74
+ path: Path.Path,
75
+ root: string,
76
+ entry: string
77
+ ): Effect.Effect<DirectoryEntry, SyncError> => {
78
+ const entryPath = resolveEntryPath(path, root, entry)
79
+ return readEntry(fs, path, entryPath)
80
+ }
81
+
82
+ // CHANGE: wrap filesystem access behind a service for typed errors and testing
83
+ // WHY: enforce shell boundary and avoid raw fs usage in logic
84
+ // QUOTE(TZ): "Внешние зависимости: только через типизированные интерфейсы"
85
+ // REF: user-2026-01-19-sync-rewrite
86
+ // SOURCE: n/a
87
+ // FORMAT THEOREM: forall p: exists(p) -> readable(p)
88
+ // PURITY: SHELL
89
+ // EFFECT: Effect<FileSystemService, SyncError, never>
90
+ // INVARIANT: readDirectory returns absolute entry paths
91
+ // COMPLEXITY: O(n)/O(n)
92
+ export const FileSystemLive = Layer.effect(
93
+ FileSystemService,
94
+ Effect.gen(function*(_) {
95
+ const fs = yield* _(FileSystem.FileSystem)
96
+ const path = yield* _(Path.Path)
97
+
98
+ const readFileString = (pathValue: string): Effect.Effect<string, SyncError> =>
99
+ pipe(
100
+ fs.readFileString(pathValue, "utf8"),
101
+ Effect.mapError(() => syncError(pathValue, "Cannot read file"))
102
+ )
103
+
104
+ const readDirectory = (
105
+ pathValue: string
106
+ ): Effect.Effect<ReadonlyArray<DirectoryEntry>, SyncError> =>
107
+ pipe(
108
+ fs.readDirectory(pathValue),
109
+ Effect.mapError(() => syncError(pathValue, "Cannot read directory")),
110
+ Effect.flatMap((entries) => forEach(entries, (entry) => resolveEntry(fs, path, pathValue, entry)))
111
+ )
112
+
113
+ const makeDirectory = (pathValue: string): Effect.Effect<void, SyncError> =>
114
+ pipe(
115
+ fs.makeDirectory(pathValue, { recursive: true }),
116
+ Effect.mapError(() => syncError(pathValue, "Cannot create destination directory structure"))
117
+ )
118
+
119
+ const copyFile = (
120
+ sourcePath: string,
121
+ destinationPath: string
122
+ ): Effect.Effect<void, SyncError> =>
123
+ pipe(
124
+ fs.copyFile(sourcePath, destinationPath),
125
+ Effect.mapError(() => syncError(sourcePath, "Cannot copy file into destination"))
126
+ )
127
+
128
+ const exists = (pathValue: string): Effect.Effect<boolean, SyncError> =>
129
+ pipe(
130
+ fs.exists(pathValue),
131
+ Effect.mapError(() => syncError(pathValue, "Cannot check path existence"))
132
+ )
133
+
134
+ return {
135
+ readFileString,
136
+ readDirectory,
137
+ makeDirectory,
138
+ copyFile,
139
+ exists
140
+ }
141
+ })
142
+ )
@@ -0,0 +1,54 @@
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
+ })
@@ -0,0 +1,35 @@
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
+ })
@@ -0,0 +1,229 @@
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
+ )
@@ -0,0 +1,54 @@
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)