@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,97 +0,0 @@
|
|
|
1
|
-
import * as Path from "@effect/platform/Path"
|
|
2
|
-
import { Effect, Option, pipe } from "effect"
|
|
3
|
-
|
|
4
|
-
import { CryptoService } from "../../services/crypto.js"
|
|
5
|
-
import { FileSystemService } from "../../services/file-system.js"
|
|
6
|
-
import { RuntimeEnv } from "../../services/runtime-env.js"
|
|
7
|
-
import { createFilteredSource, findFirstMatching, resolveProjectRoot, runSyncSource } from "../shared.js"
|
|
8
|
-
import { type SyncError, syncError, type SyncOptions, type SyncSource } from "../types.js"
|
|
9
|
-
|
|
10
|
-
type QwenEnv = RuntimeEnv | CryptoService | FileSystemService | Path.Path
|
|
11
|
-
|
|
12
|
-
const resolveEnvValue = (envValue: Option.Option<string>): string | undefined => Option.getOrUndefined(envValue)
|
|
13
|
-
|
|
14
|
-
const findFirstExisting = (
|
|
15
|
-
candidates: ReadonlyArray<string>
|
|
16
|
-
): Effect.Effect<string | undefined, SyncError, FileSystemService> =>
|
|
17
|
-
findFirstMatching(candidates, (candidate) =>
|
|
18
|
-
Effect.gen(function*(_) {
|
|
19
|
-
const fs = yield* _(FileSystemService)
|
|
20
|
-
return yield* _(fs.exists(candidate))
|
|
21
|
-
}))
|
|
22
|
-
|
|
23
|
-
const resolveQwenSourceDir = (
|
|
24
|
-
options: SyncOptions
|
|
25
|
-
): Effect.Effect<string, SyncError, QwenEnv> =>
|
|
26
|
-
Effect.gen(function*(_) {
|
|
27
|
-
const env = yield* _(RuntimeEnv)
|
|
28
|
-
const crypto = yield* _(CryptoService)
|
|
29
|
-
const path = yield* _(Path.Path)
|
|
30
|
-
const projectRoot = resolveProjectRoot(path, options)
|
|
31
|
-
const hash = yield* _(
|
|
32
|
-
pipe(
|
|
33
|
-
crypto.sha256(projectRoot),
|
|
34
|
-
Effect.mapError((error) => syncError(".qwen", error.reason))
|
|
35
|
-
)
|
|
36
|
-
)
|
|
37
|
-
const envSource = resolveEnvValue(yield* _(env.envVar("QWEN_SOURCE_DIR")))
|
|
38
|
-
const homeDir = yield* _(env.homedir)
|
|
39
|
-
let baseFromMeta: string | undefined
|
|
40
|
-
if (options.metaRoot !== undefined) {
|
|
41
|
-
baseFromMeta = options.metaRoot.endsWith(".qwen")
|
|
42
|
-
? options.metaRoot
|
|
43
|
-
: path.join(options.metaRoot, ".qwen")
|
|
44
|
-
}
|
|
45
|
-
const metaKnowledge = options.metaRoot === undefined
|
|
46
|
-
? undefined
|
|
47
|
-
: path.join(options.metaRoot, ".knowledge", ".qwen")
|
|
48
|
-
const homeBase = path.join(homeDir, ".qwen")
|
|
49
|
-
const homeKnowledge = path.join(homeDir, ".knowledge", ".qwen")
|
|
50
|
-
|
|
51
|
-
const candidates = [
|
|
52
|
-
options.qwenSourceDir,
|
|
53
|
-
envSource,
|
|
54
|
-
baseFromMeta ? path.join(baseFromMeta, "tmp", hash) : undefined,
|
|
55
|
-
path.join(projectRoot, ".qwen", "tmp", hash),
|
|
56
|
-
path.join(projectRoot, ".knowledge", ".qwen", "tmp", hash),
|
|
57
|
-
metaKnowledge ? path.join(metaKnowledge, "tmp", hash) : undefined,
|
|
58
|
-
path.join(homeBase, "tmp", hash),
|
|
59
|
-
path.join(homeKnowledge, "tmp", hash)
|
|
60
|
-
].filter((candidate): candidate is string => candidate !== undefined)
|
|
61
|
-
|
|
62
|
-
const found = yield* _(findFirstExisting(candidates))
|
|
63
|
-
if (found === undefined) {
|
|
64
|
-
return yield* _(
|
|
65
|
-
Effect.fail(
|
|
66
|
-
syncError(
|
|
67
|
-
".qwen",
|
|
68
|
-
`Qwen source directory is missing for hash ${hash}`
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return found
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const qwenSource: SyncSource<QwenEnv> = createFilteredSource({
|
|
78
|
-
name: "Qwen",
|
|
79
|
-
destSubdir: ".qwen",
|
|
80
|
-
resolveSource: resolveQwenSourceDir,
|
|
81
|
-
filter: (entry, fullPath) => entry.kind === "file" && fullPath.endsWith(".json"),
|
|
82
|
-
errorReason: "Cannot traverse Qwen directory"
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// CHANGE: sync Qwen dialog files through shared sync runner
|
|
86
|
-
// WHY: keep source-specific resolution isolated and reuse copy/traversal logic
|
|
87
|
-
// QUOTE(TZ): "SHELL: Все эффекты (IO, сеть, БД, env/process) изолированы"
|
|
88
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
89
|
-
// SOURCE: n/a
|
|
90
|
-
// FORMAT THEOREM: forall f: json(f) -> copied(f)
|
|
91
|
-
// PURITY: SHELL
|
|
92
|
-
// EFFECT: Effect<void, never, FileSystemService | RuntimeEnv | CryptoService | Path>
|
|
93
|
-
// INVARIANT: only .json files are copied
|
|
94
|
-
// COMPLEXITY: O(n)/O(n)
|
|
95
|
-
export const syncQwen = (
|
|
96
|
-
options: SyncOptions
|
|
97
|
-
): Effect.Effect<void, never, QwenEnv> => runSyncSource(qwenSource, options)
|
package/src/shell/sync/types.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type * as Fx from "effect"
|
|
2
|
-
|
|
3
|
-
export interface SyncError {
|
|
4
|
-
readonly _tag: "SyncError"
|
|
5
|
-
readonly path: string
|
|
6
|
-
readonly reason: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface SyncOptions {
|
|
10
|
-
readonly cwd: string
|
|
11
|
-
readonly projectRoot?: string
|
|
12
|
-
readonly sourceDir?: string
|
|
13
|
-
readonly destinationDir?: string
|
|
14
|
-
readonly repositoryUrlOverride?: string
|
|
15
|
-
readonly metaRoot?: string
|
|
16
|
-
readonly qwenSourceDir?: string
|
|
17
|
-
readonly claudeProjectsRoot?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type SyncEffect<A, R = never> = Fx.Effect.Effect<A, SyncError, R>
|
|
21
|
-
|
|
22
|
-
export interface SyncSource<R> {
|
|
23
|
-
readonly name: string
|
|
24
|
-
readonly destSubdir: ".codex" | ".qwen" | ".claude"
|
|
25
|
-
readonly resolveSource: (options: SyncOptions) => SyncEffect<string, R>
|
|
26
|
-
readonly copy: (
|
|
27
|
-
sourceDir: string,
|
|
28
|
-
destinationDir: string,
|
|
29
|
-
options: SyncOptions
|
|
30
|
-
) => SyncEffect<number, R>
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// CHANGE: centralize sync-specific types to keep shell modules consistent
|
|
34
|
-
// WHY: shared types and error model simplify composition and testing
|
|
35
|
-
// QUOTE(TZ): "Ошибки: типизированы в сигнатурах функций"
|
|
36
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
37
|
-
// SOURCE: n/a
|
|
38
|
-
// FORMAT THEOREM: forall e: SyncError -> typed(e)
|
|
39
|
-
// PURITY: SHELL
|
|
40
|
-
// EFFECT: n/a
|
|
41
|
-
// INVARIANT: SyncError contains path and reason for logging
|
|
42
|
-
// COMPLEXITY: O(1)/O(1)
|
|
43
|
-
export const syncError = (pathValue: string, reason: string): SyncError => ({
|
|
44
|
-
_tag: "SyncError",
|
|
45
|
-
path: pathValue,
|
|
46
|
-
reason
|
|
47
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import * as Path from "@effect/platform/Path"
|
|
2
|
-
import { describe, expect, it } from "@effect/vitest"
|
|
3
|
-
import { Effect } from "effect"
|
|
4
|
-
import fc from "fast-check"
|
|
5
|
-
|
|
6
|
-
import { buildProjectLocator, type JsonValue, valueMatchesProject } from "../../src/core/knowledge.js"
|
|
7
|
-
|
|
8
|
-
const buildLocator = (root: string) =>
|
|
9
|
-
buildProjectLocator(
|
|
10
|
-
root,
|
|
11
|
-
(candidate) => candidate === root || candidate.startsWith(`${root}/`)
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
describe("valueMatchesProject", () => {
|
|
15
|
-
const safeChar: fc.Arbitrary<string> = fc.constantFrom(
|
|
16
|
-
"a",
|
|
17
|
-
"b",
|
|
18
|
-
"c",
|
|
19
|
-
"d",
|
|
20
|
-
"e",
|
|
21
|
-
"f",
|
|
22
|
-
"0",
|
|
23
|
-
"1",
|
|
24
|
-
"2",
|
|
25
|
-
"3"
|
|
26
|
-
)
|
|
27
|
-
const segment: fc.Arbitrary<string> = fc.string({
|
|
28
|
-
minLength: 1,
|
|
29
|
-
maxLength: 8,
|
|
30
|
-
unit: safeChar
|
|
31
|
-
})
|
|
32
|
-
const nonRecordValue: fc.Arbitrary<JsonValue> = fc.oneof(
|
|
33
|
-
fc.string(),
|
|
34
|
-
fc.integer(),
|
|
35
|
-
fc.boolean(),
|
|
36
|
-
fc.constant(null),
|
|
37
|
-
fc.array(fc.string(), { maxLength: 5 }),
|
|
38
|
-
fc.array(fc.integer(), { maxLength: 5 }),
|
|
39
|
-
fc.array(fc.boolean(), { maxLength: 5 }),
|
|
40
|
-
fc.array(fc.constant(null), { maxLength: 5 })
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const projectRoot = Effect.gen(function*(_) {
|
|
44
|
-
const path = yield* _(Path.Path)
|
|
45
|
-
const testFile = yield* _(path.fromFileUrl(new URL(import.meta.url)))
|
|
46
|
-
return path.resolve(path.dirname(testFile), "../../../..")
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it.effect("matches by cwd inside project root", () =>
|
|
50
|
-
Effect.gen(function*(_) {
|
|
51
|
-
const root = yield* _(projectRoot)
|
|
52
|
-
const locator = buildLocator(root)
|
|
53
|
-
const insidePath: fc.Arbitrary<string> = fc
|
|
54
|
-
.array(segment, { minLength: 0, maxLength: 4 })
|
|
55
|
-
.map((segments) => segments.length === 0 ? root : `${root}/${segments.join("/")}`)
|
|
56
|
-
fc.assert(
|
|
57
|
-
fc.property(insidePath, (cwd: string) => {
|
|
58
|
-
const value: JsonValue = { cwd }
|
|
59
|
-
expect(valueMatchesProject(value, locator)).toBe(true)
|
|
60
|
-
})
|
|
61
|
-
)
|
|
62
|
-
}).pipe(Effect.provide(Path.layer)))
|
|
63
|
-
|
|
64
|
-
it.effect("rejects unrelated records", () =>
|
|
65
|
-
Effect.gen(function*(_) {
|
|
66
|
-
const root = yield* _(projectRoot)
|
|
67
|
-
const path = yield* _(Path.Path)
|
|
68
|
-
const locator = buildLocator(root)
|
|
69
|
-
const outsideBase = path.join(path.dirname(root), "outside")
|
|
70
|
-
const outsidePath: fc.Arbitrary<string> = fc
|
|
71
|
-
.array(segment, { minLength: 0, maxLength: 4 })
|
|
72
|
-
.map((segments) =>
|
|
73
|
-
segments.length === 0
|
|
74
|
-
? outsideBase
|
|
75
|
-
: `${outsideBase}/${segments.join("/")}`
|
|
76
|
-
)
|
|
77
|
-
fc.assert(
|
|
78
|
-
fc.property(outsidePath, (cwd: string) => {
|
|
79
|
-
const value: JsonValue = { cwd }
|
|
80
|
-
expect(valueMatchesProject(value, locator)).toBe(false)
|
|
81
|
-
})
|
|
82
|
-
)
|
|
83
|
-
}).pipe(Effect.provide(Path.layer)))
|
|
84
|
-
|
|
85
|
-
it.effect("rejects non-record values", () =>
|
|
86
|
-
Effect.gen(function*(_) {
|
|
87
|
-
const root = yield* _(projectRoot)
|
|
88
|
-
const locator = buildLocator(root)
|
|
89
|
-
fc.assert(
|
|
90
|
-
fc.property(nonRecordValue, (value: JsonValue) => {
|
|
91
|
-
expect(valueMatchesProject(value, locator)).toBe(false)
|
|
92
|
-
})
|
|
93
|
-
)
|
|
94
|
-
}).pipe(Effect.provide(Path.layer)))
|
|
95
|
-
})
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { NodeContext } from "@effect/platform-node"
|
|
2
|
-
import * as Command from "@effect/platform/Command"
|
|
3
|
-
import * as Path from "@effect/platform/Path"
|
|
4
|
-
import { describe, expect, it } from "@effect/vitest"
|
|
5
|
-
import { Effect, Layer, pipe } from "effect"
|
|
6
|
-
|
|
7
|
-
import { buildTestPaths, makeTempDir, withFsPath, writeFile } from "../support/fs-helpers.js"
|
|
8
|
-
|
|
9
|
-
const testPaths = buildTestPaths(
|
|
10
|
-
new URL(import.meta.url),
|
|
11
|
-
"context-doc-cli-tests",
|
|
12
|
-
"context-doc-pack-tests"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
const runCommand = (command: Command.Command) =>
|
|
16
|
-
pipe(
|
|
17
|
-
Command.exitCode(command),
|
|
18
|
-
Effect.flatMap((exitCode) =>
|
|
19
|
-
Number(exitCode) === 0
|
|
20
|
-
? Effect.void
|
|
21
|
-
: Effect.fail(new Error(`Command failed with exit code ${exitCode}`))
|
|
22
|
-
)
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const runCommandOutput = (command: Command.Command) => Command.string(command)
|
|
26
|
-
|
|
27
|
-
const packCli = Effect.gen(function*(_) {
|
|
28
|
-
const { packBase, projectRoot } = yield* _(testPaths)
|
|
29
|
-
const path = yield* _(Path.Path)
|
|
30
|
-
const packDir = yield* _(
|
|
31
|
-
makeTempDir(
|
|
32
|
-
packBase ?? path.join(projectRoot, ".tmp", "context-doc-pack-tests"),
|
|
33
|
-
"pack-"
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
const build = pipe(
|
|
38
|
-
Command.make("pnpm", "--filter", "@prover-coder-ai/context-doc", "build"),
|
|
39
|
-
Command.workingDirectory(projectRoot)
|
|
40
|
-
)
|
|
41
|
-
yield* _(runCommand(build))
|
|
42
|
-
|
|
43
|
-
const appDir = path.join(projectRoot, "packages", "app")
|
|
44
|
-
const pack = pipe(
|
|
45
|
-
Command.make("npm", "pack", "--silent", "--pack-destination", packDir),
|
|
46
|
-
Command.workingDirectory(appDir)
|
|
47
|
-
)
|
|
48
|
-
const tarballName = (yield* _(runCommandOutput(pack))).trim()
|
|
49
|
-
const tarballPath = path.join(packDir, tarballName)
|
|
50
|
-
const tarballSpec = `file:${tarballPath}`
|
|
51
|
-
|
|
52
|
-
const tarballExists = yield* _(
|
|
53
|
-
withFsPath((fs) => fs.exists(tarballPath))
|
|
54
|
-
)
|
|
55
|
-
if (!tarballExists) {
|
|
56
|
-
return yield* _(Effect.fail(new Error("Packed tarball was not created")))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return { projectRoot, tarballSpec }
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const prepareProject = Effect.gen(function*(_) {
|
|
63
|
-
const { tempBase } = yield* _(testPaths)
|
|
64
|
-
const path = yield* _(Path.Path)
|
|
65
|
-
const root = yield* _(
|
|
66
|
-
makeTempDir(tempBase, "project-")
|
|
67
|
-
)
|
|
68
|
-
const codexSource = path.join(root, "codex-src")
|
|
69
|
-
const destDir = path.join(root, ".knowledge", ".codex")
|
|
70
|
-
const qwenSource = path.join(root, "qwen-src")
|
|
71
|
-
|
|
72
|
-
yield* _(
|
|
73
|
-
writeFile(
|
|
74
|
-
path.join(codexSource, "sessions/2025/11/cli.jsonl"),
|
|
75
|
-
JSON.stringify({ cwd: root, message: "ok" })
|
|
76
|
-
)
|
|
77
|
-
)
|
|
78
|
-
yield* _(
|
|
79
|
-
writeFile(
|
|
80
|
-
path.join(qwenSource, "session-1.json"),
|
|
81
|
-
JSON.stringify({ sessionId: "s1" })
|
|
82
|
-
)
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
return { root, codexSource, destDir, qwenSource }
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
type CliRunner = "npx" | "pnpm-dlx"
|
|
89
|
-
|
|
90
|
-
interface CliRunInput {
|
|
91
|
-
readonly runner: CliRunner
|
|
92
|
-
readonly tarballSpec: string
|
|
93
|
-
readonly root: string
|
|
94
|
-
readonly codexSource: string
|
|
95
|
-
readonly destDir: string
|
|
96
|
-
readonly qwenSource: string
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const runCliWithRunner = ({
|
|
100
|
-
codexSource,
|
|
101
|
-
destDir,
|
|
102
|
-
qwenSource,
|
|
103
|
-
root,
|
|
104
|
-
runner,
|
|
105
|
-
tarballSpec
|
|
106
|
-
}: CliRunInput) =>
|
|
107
|
-
Effect.gen(function*(_) {
|
|
108
|
-
const { projectRoot } = yield* _(testPaths)
|
|
109
|
-
const args = [
|
|
110
|
-
"--project-root",
|
|
111
|
-
root,
|
|
112
|
-
"--source",
|
|
113
|
-
codexSource,
|
|
114
|
-
"--dest",
|
|
115
|
-
destDir,
|
|
116
|
-
"--qwen-source",
|
|
117
|
-
qwenSource
|
|
118
|
-
]
|
|
119
|
-
const command = pipe(
|
|
120
|
-
runner === "npx"
|
|
121
|
-
? Command.make("npx", "-y", tarballSpec, ...args)
|
|
122
|
-
: Command.make("pnpm", "dlx", tarballSpec, ...args),
|
|
123
|
-
Command.workingDirectory(projectRoot)
|
|
124
|
-
)
|
|
125
|
-
yield* _(runCommand(command))
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
const assertCopied = (root: string, destDir: string) =>
|
|
129
|
-
withFsPath((fs, path) =>
|
|
130
|
-
Effect.gen(function*(_) {
|
|
131
|
-
const codexFile = path.join(destDir, "sessions/2025/11/cli.jsonl")
|
|
132
|
-
const codexExists = yield* _(fs.exists(codexFile))
|
|
133
|
-
expect(codexExists).toBe(true)
|
|
134
|
-
|
|
135
|
-
const qwenFile = path.join(root, ".knowledge", ".qwen", "session-1.json")
|
|
136
|
-
const qwenExists = yield* _(fs.exists(qwenFile))
|
|
137
|
-
expect(qwenExists).toBe(true)
|
|
138
|
-
})
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
describe("cli package execution", () => {
|
|
142
|
-
it.effect("packs the CLI and executes it via npx", () =>
|
|
143
|
-
Effect.scoped(
|
|
144
|
-
Effect.gen(function*(_) {
|
|
145
|
-
const { tarballSpec } = yield* _(packCli)
|
|
146
|
-
const { codexSource, destDir, qwenSource, root } = yield* _(
|
|
147
|
-
prepareProject
|
|
148
|
-
)
|
|
149
|
-
yield* _(
|
|
150
|
-
runCliWithRunner({
|
|
151
|
-
runner: "npx",
|
|
152
|
-
tarballSpec,
|
|
153
|
-
root,
|
|
154
|
-
codexSource,
|
|
155
|
-
destDir,
|
|
156
|
-
qwenSource
|
|
157
|
-
})
|
|
158
|
-
)
|
|
159
|
-
yield* _(assertCopied(root, destDir))
|
|
160
|
-
const second = yield* _(prepareProject)
|
|
161
|
-
yield* _(
|
|
162
|
-
runCliWithRunner({
|
|
163
|
-
runner: "pnpm-dlx",
|
|
164
|
-
tarballSpec,
|
|
165
|
-
root: second.root,
|
|
166
|
-
codexSource: second.codexSource,
|
|
167
|
-
destDir: second.destDir,
|
|
168
|
-
qwenSource: second.qwenSource
|
|
169
|
-
})
|
|
170
|
-
)
|
|
171
|
-
yield* _(assertCopied(second.root, second.destDir))
|
|
172
|
-
})
|
|
173
|
-
).pipe(
|
|
174
|
-
Effect.provide(Layer.mergeAll(NodeContext.layer))
|
|
175
|
-
), 30_000)
|
|
176
|
-
})
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { NodeContext } from "@effect/platform-node"
|
|
2
|
-
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
3
|
-
import * as Path from "@effect/platform/Path"
|
|
4
|
-
import { describe, expect, it } from "@effect/vitest"
|
|
5
|
-
import { Effect, Layer, Option, pipe } from "effect"
|
|
6
|
-
import fc from "fast-check"
|
|
7
|
-
|
|
8
|
-
import { CryptoService, CryptoServiceLive } from "../../src/shell/services/crypto.js"
|
|
9
|
-
import { FileSystemLive } from "../../src/shell/services/file-system.js"
|
|
10
|
-
import { RuntimeEnv } from "../../src/shell/services/runtime-env.js"
|
|
11
|
-
import { buildSyncProgram } from "../../src/shell/sync/index.js"
|
|
12
|
-
import { buildTestPaths, makeTempDir, withFsPath, writeFile } from "../support/fs-helpers.js"
|
|
13
|
-
|
|
14
|
-
const forEach = Effect.forEach
|
|
15
|
-
const some = Option.some
|
|
16
|
-
const mapFileEntry = (
|
|
17
|
-
sessionDir: string,
|
|
18
|
-
fs: FileSystem.FileSystem,
|
|
19
|
-
path: Path.Path
|
|
20
|
-
) =>
|
|
21
|
-
(entry: string) =>
|
|
22
|
-
pipe(
|
|
23
|
-
fs.stat(path.join(sessionDir, entry)),
|
|
24
|
-
Effect.map((info) => info.type === "File" ? some(entry) : Option.none())
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
const testPaths = buildTestPaths(
|
|
28
|
-
new URL(import.meta.url),
|
|
29
|
-
"context-doc-tests"
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
const withTempDir = Effect.gen(function*(_) {
|
|
33
|
-
const { tempBase } = yield* _(testPaths)
|
|
34
|
-
return yield* _(makeTempDir(tempBase, "context-doc-"))
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const makeRuntimeEnvLayer = (cwd: string): Layer.Layer<RuntimeEnv> =>
|
|
38
|
-
Layer.succeed(RuntimeEnv, {
|
|
39
|
-
argv: Effect.succeed(["node", "main"]),
|
|
40
|
-
cwd: Effect.succeed(cwd),
|
|
41
|
-
homedir: Effect.succeed(cwd),
|
|
42
|
-
envVar: () => Effect.succeed(Option.none())
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const assertSyncOutput = (
|
|
46
|
-
destDir: string,
|
|
47
|
-
qwenHash: string,
|
|
48
|
-
cwd: string,
|
|
49
|
-
expectedMessage: string,
|
|
50
|
-
skippedMessage: string
|
|
51
|
-
) =>
|
|
52
|
-
withFsPath((fs, path) =>
|
|
53
|
-
Effect.gen(function*(_) {
|
|
54
|
-
const sessionDir = path.join(destDir, "sessions/2025/11")
|
|
55
|
-
const entries = yield* _(fs.readDirectory(sessionDir))
|
|
56
|
-
const files = yield* _(
|
|
57
|
-
forEach(entries, mapFileEntry(sessionDir, fs, path))
|
|
58
|
-
)
|
|
59
|
-
const copiedFiles = files.flatMap((entry) => Option.isSome(entry) ? [entry.value] : [])
|
|
60
|
-
|
|
61
|
-
expect(copiedFiles).toEqual(["match.jsonl"])
|
|
62
|
-
|
|
63
|
-
const content = yield* _(
|
|
64
|
-
fs.readFileString(path.join(sessionDir, "match.jsonl"))
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
expect(content).toContain(`"message":"${expectedMessage}"`)
|
|
68
|
-
expect(content).not.toContain(`"message":"${skippedMessage}"`)
|
|
69
|
-
|
|
70
|
-
const qwenCopied = path.join(
|
|
71
|
-
cwd,
|
|
72
|
-
".knowledge",
|
|
73
|
-
".qwen",
|
|
74
|
-
qwenHash,
|
|
75
|
-
"chats",
|
|
76
|
-
"session-1.json"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
const exists = yield* _(fs.exists(qwenCopied))
|
|
80
|
-
expect(exists).toBe(true)
|
|
81
|
-
})
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
const runSyncScenario = (
|
|
85
|
-
matchMessage: string,
|
|
86
|
-
skippedMessage: string
|
|
87
|
-
) =>
|
|
88
|
-
Effect.scoped(
|
|
89
|
-
Effect.gen(function*(_) {
|
|
90
|
-
const path = yield* _(Path.Path)
|
|
91
|
-
const crypto = yield* _(CryptoService)
|
|
92
|
-
const cwd = yield* _(withTempDir)
|
|
93
|
-
const codexDir = path.join(cwd, ".codex")
|
|
94
|
-
const destDir = path.join(cwd, ".knowledge", ".codex")
|
|
95
|
-
const qwenSource = path.join(cwd, ".qwen", "tmp")
|
|
96
|
-
const qwenHash = yield* _(crypto.sha256(cwd))
|
|
97
|
-
yield* _(
|
|
98
|
-
writeFile(
|
|
99
|
-
path.join(codexDir, "sessions/2025/11/match.jsonl"),
|
|
100
|
-
[
|
|
101
|
-
JSON.stringify({ cwd, message: matchMessage }),
|
|
102
|
-
JSON.stringify({
|
|
103
|
-
payload: { cwd: path.join(cwd, "sub") }
|
|
104
|
-
})
|
|
105
|
-
].join("\n")
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
yield* _(
|
|
110
|
-
writeFile(
|
|
111
|
-
path.join(codexDir, "sessions/2025/11/ignore.jsonl"),
|
|
112
|
-
[
|
|
113
|
-
JSON.stringify({
|
|
114
|
-
cwd: "/home/user/other",
|
|
115
|
-
message: skippedMessage
|
|
116
|
-
})
|
|
117
|
-
].join("\n")
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
yield* _(
|
|
122
|
-
writeFile(
|
|
123
|
-
path.join(qwenSource, qwenHash, "chats", "session-1.json"),
|
|
124
|
-
JSON.stringify({ sessionId: "s1", projectHash: qwenHash })
|
|
125
|
-
)
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
yield* _(
|
|
129
|
-
Effect.provide(
|
|
130
|
-
buildSyncProgram({
|
|
131
|
-
cwd,
|
|
132
|
-
sourceDir: codexDir,
|
|
133
|
-
destinationDir: destDir,
|
|
134
|
-
qwenSourceDir: qwenSource
|
|
135
|
-
}),
|
|
136
|
-
Layer.mergeAll(FileSystemLive, makeRuntimeEnvLayer(cwd))
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
yield* _(assertSyncOutput(destDir, qwenHash, cwd, matchMessage, skippedMessage))
|
|
141
|
-
})
|
|
142
|
-
).pipe(
|
|
143
|
-
Effect.provide(Layer.mergeAll(NodeContext.layer, CryptoServiceLive))
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
describe("sync-knowledge end-to-end", () => {
|
|
147
|
-
const safeChar = fc.constantFrom(
|
|
148
|
-
"a",
|
|
149
|
-
"b",
|
|
150
|
-
"c",
|
|
151
|
-
"d",
|
|
152
|
-
"e",
|
|
153
|
-
"f",
|
|
154
|
-
"g",
|
|
155
|
-
"h",
|
|
156
|
-
"i",
|
|
157
|
-
"j",
|
|
158
|
-
"k",
|
|
159
|
-
"l",
|
|
160
|
-
"m",
|
|
161
|
-
"n",
|
|
162
|
-
"o",
|
|
163
|
-
"p",
|
|
164
|
-
"q",
|
|
165
|
-
"r",
|
|
166
|
-
"s",
|
|
167
|
-
"t",
|
|
168
|
-
"u",
|
|
169
|
-
"v",
|
|
170
|
-
"w",
|
|
171
|
-
"x",
|
|
172
|
-
"y",
|
|
173
|
-
"z",
|
|
174
|
-
"0",
|
|
175
|
-
"1",
|
|
176
|
-
"2",
|
|
177
|
-
"3",
|
|
178
|
-
"4",
|
|
179
|
-
"5"
|
|
180
|
-
)
|
|
181
|
-
const message: fc.Arbitrary<string> = fc.string({
|
|
182
|
-
minLength: 1,
|
|
183
|
-
maxLength: 12,
|
|
184
|
-
unit: safeChar
|
|
185
|
-
})
|
|
186
|
-
const messagePair: fc.Arbitrary<[string, string]> = fc
|
|
187
|
-
.tuple(message, message)
|
|
188
|
-
.filter((pair) => pair[0] !== pair[1])
|
|
189
|
-
|
|
190
|
-
it.effect("copies matching Codex files and Qwen json", () =>
|
|
191
|
-
Effect.tryPromise({
|
|
192
|
-
try: () =>
|
|
193
|
-
fc.assert(
|
|
194
|
-
fc.asyncProperty(
|
|
195
|
-
messagePair,
|
|
196
|
-
([matchMessage, skippedMessage]: [string, string]) =>
|
|
197
|
-
Effect.runPromise(runSyncScenario(matchMessage, skippedMessage))
|
|
198
|
-
),
|
|
199
|
-
{ numRuns: 5 }
|
|
200
|
-
),
|
|
201
|
-
catch: (error) => error instanceof Error ? error : new Error(String(error))
|
|
202
|
-
}))
|
|
203
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
-
import * as Path from "@effect/platform/Path"
|
|
3
|
-
import { Effect, pipe } from "effect"
|
|
4
|
-
|
|
5
|
-
const accessFsPath = Effect.gen(function*(_) {
|
|
6
|
-
const fs = yield* _(FileSystem.FileSystem)
|
|
7
|
-
const path = yield* _(Path.Path)
|
|
8
|
-
return { fs, path }
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
export const withFsPath = <A, E, R>(
|
|
12
|
-
fn: (fs: FileSystem.FileSystem, path: Path.Path) => Effect.Effect<A, E, R>
|
|
13
|
-
): Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | R> =>
|
|
14
|
-
pipe(
|
|
15
|
-
accessFsPath,
|
|
16
|
-
Effect.flatMap(({ fs, path }) => fn(fs, path))
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
export const writeFile = (filePath: string, content: string) =>
|
|
20
|
-
withFsPath((fs, path) =>
|
|
21
|
-
Effect.gen(function*(_) {
|
|
22
|
-
yield* _(fs.makeDirectory(path.dirname(filePath), { recursive: true }))
|
|
23
|
-
yield* _(fs.writeFileString(filePath, content))
|
|
24
|
-
})
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
export const makeTempDir = (base: string, prefix: string) =>
|
|
28
|
-
withFsPath((fs, _path) =>
|
|
29
|
-
Effect.gen(function*(_) {
|
|
30
|
-
yield* _(fs.makeDirectory(base, { recursive: true }))
|
|
31
|
-
return yield* _(fs.makeTempDirectoryScoped({ directory: base, prefix }))
|
|
32
|
-
})
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
export const buildTestPaths = (
|
|
36
|
-
testUrl: URL,
|
|
37
|
-
tempName: string,
|
|
38
|
-
packName?: string
|
|
39
|
-
) =>
|
|
40
|
-
Effect.gen(function*(_) {
|
|
41
|
-
const path = yield* _(Path.Path)
|
|
42
|
-
const testFile = yield* _(path.fromFileUrl(testUrl))
|
|
43
|
-
const testRoot = path.dirname(testFile)
|
|
44
|
-
const projectRoot = path.resolve(testRoot, "../../../..")
|
|
45
|
-
const tempBase = path.join(projectRoot, ".tmp", tempName)
|
|
46
|
-
const packBase = packName === undefined
|
|
47
|
-
? undefined
|
|
48
|
-
: path.join(projectRoot, ".tmp", packName)
|
|
49
|
-
return { projectRoot, tempBase, packBase }
|
|
50
|
-
})
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": ".",
|
|
5
|
-
"outDir": "dist",
|
|
6
|
-
"types": ["vitest"],
|
|
7
|
-
"baseUrl": ".",
|
|
8
|
-
"paths": {
|
|
9
|
-
"@/*": ["src/*"]
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
|
-
"include": [
|
|
13
|
-
"src/**/*",
|
|
14
|
-
"tests/**/*",
|
|
15
|
-
"vite.config.ts",
|
|
16
|
-
"vitest.config.ts"
|
|
17
|
-
],
|
|
18
|
-
"exclude": ["dist", "node_modules"]
|
|
19
|
-
}
|