@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.
- package/.jscpd.json +16 -0
- package/CHANGELOG.md +13 -0
- package/bin/context-doc.js +12 -0
- package/biome.json +37 -0
- package/eslint.config.mts +305 -0
- package/eslint.effect-ts-check.config.mjs +220 -0
- package/linter.config.json +33 -0
- package/package.json +72 -41
- package/src/app/main.ts +29 -0
- package/src/app/program.ts +32 -0
- package/src/core/knowledge.ts +93 -117
- package/src/shell/cli.ts +73 -5
- package/src/shell/services/crypto.ts +50 -0
- package/src/shell/services/file-system.ts +142 -0
- package/src/shell/services/runtime-env.ts +54 -0
- package/src/shell/sync/index.ts +35 -0
- package/src/shell/sync/shared.ts +229 -0
- package/src/shell/sync/sources/claude.ts +54 -0
- package/src/shell/sync/sources/codex.ts +261 -0
- package/src/shell/sync/sources/qwen.ts +97 -0
- package/src/shell/sync/types.ts +47 -0
- package/tests/core/knowledge.test.ts +95 -0
- package/tests/shell/cli-pack.test.ts +176 -0
- package/tests/shell/sync-knowledge.test.ts +203 -0
- package/tests/support/fs-helpers.ts +50 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +32 -0
- package/vitest.config.ts +70 -66
- package/README.md +0 -42
- package/dist/core/greeting.js +0 -25
- package/dist/core/knowledge.js +0 -49
- package/dist/main.js +0 -27
- package/dist/shell/claudeSync.js +0 -23
- package/dist/shell/cli.js +0 -5
- package/dist/shell/codexSync.js +0 -129
- package/dist/shell/qwenSync.js +0 -41
- package/dist/shell/syncKnowledge.js +0 -39
- package/dist/shell/syncShared.js +0 -49
- package/dist/shell/syncTypes.js +0 -1
- package/src/core/greeting.ts +0 -39
- package/src/main.ts +0 -49
- package/src/shell/claudeSync.ts +0 -55
- package/src/shell/codexSync.ts +0 -236
- package/src/shell/qwenSync.ts +0 -73
- package/src/shell/syncKnowledge.ts +0 -49
- package/src/shell/syncShared.ts +0 -94
- package/src/shell/syncTypes.ts +0 -30
- package/src/types/env.d.ts +0 -10
- package/tsconfig.build.json +0 -18
package/dist/shell/codexSync.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import * as S from "@effect/schema/Schema";
|
|
5
|
-
import { Console, Effect, pipe } from "effect";
|
|
6
|
-
import { buildProjectLocator, linesMatchProject } from "../core/knowledge.js";
|
|
7
|
-
import { ensureDirectory, syncError } from "./syncShared.js";
|
|
8
|
-
const PackageRepositorySchema = S.Struct({
|
|
9
|
-
url: S.String,
|
|
10
|
-
});
|
|
11
|
-
const PackageFileSchema = S.Struct({
|
|
12
|
-
repository: S.optional(PackageRepositorySchema),
|
|
13
|
-
});
|
|
14
|
-
const PackageFileFromJson = S.compose(S.parseJson(), PackageFileSchema);
|
|
15
|
-
const decodePackageFile = S.decodeUnknownSync(PackageFileFromJson);
|
|
16
|
-
const readRepositoryUrl = (cwd, repositoryUrlOverride) => Effect.try({
|
|
17
|
-
try: () => {
|
|
18
|
-
if (repositoryUrlOverride !== undefined) {
|
|
19
|
-
return repositoryUrlOverride;
|
|
20
|
-
}
|
|
21
|
-
const raw = fs.readFileSync(path.join(cwd, "package.json"), "utf8");
|
|
22
|
-
const parsed = decodePackageFile(raw);
|
|
23
|
-
const repositoryUrl = parsed.repository?.url;
|
|
24
|
-
if (repositoryUrl === undefined) {
|
|
25
|
-
throw new Error("repository url is missing");
|
|
26
|
-
}
|
|
27
|
-
return repositoryUrl;
|
|
28
|
-
},
|
|
29
|
-
catch: () => syncError("package.json", "Cannot read repository url"),
|
|
30
|
-
});
|
|
31
|
-
const resolveSourceDir = (cwd, override, metaRoot) => pipe(Effect.sync(() => {
|
|
32
|
-
const envSource = process.env.CODEX_SOURCE_DIR;
|
|
33
|
-
const metaCandidate = metaRoot === undefined
|
|
34
|
-
? undefined
|
|
35
|
-
: metaRoot.endsWith(".codex")
|
|
36
|
-
? metaRoot
|
|
37
|
-
: path.join(metaRoot, ".codex");
|
|
38
|
-
const localSource = path.join(cwd, ".codex");
|
|
39
|
-
const homeSource = path.join(os.homedir(), ".codex");
|
|
40
|
-
const localKnowledge = path.join(cwd, ".knowledge", ".codex");
|
|
41
|
-
const homeKnowledge = path.join(os.homedir(), ".knowledge", ".codex");
|
|
42
|
-
const candidates = [
|
|
43
|
-
override,
|
|
44
|
-
envSource,
|
|
45
|
-
metaCandidate,
|
|
46
|
-
localSource,
|
|
47
|
-
homeSource,
|
|
48
|
-
localKnowledge,
|
|
49
|
-
homeKnowledge,
|
|
50
|
-
].filter((candidate) => candidate !== undefined);
|
|
51
|
-
const existing = candidates.find((candidate) => fs.existsSync(candidate));
|
|
52
|
-
return { existing, candidates };
|
|
53
|
-
}), Effect.flatMap(({ existing, candidates }) => existing === undefined
|
|
54
|
-
? Effect.fail(syncError(".codex", `Source .codex directory is missing; checked: ${candidates.join(", ")}`))
|
|
55
|
-
: Effect.succeed(existing)));
|
|
56
|
-
const collectJsonlFiles = (root) => Effect.try({
|
|
57
|
-
try: () => {
|
|
58
|
-
const collected = [];
|
|
59
|
-
const walk = (current) => {
|
|
60
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
61
|
-
for (const entry of entries) {
|
|
62
|
-
const fullPath = path.join(current, entry.name);
|
|
63
|
-
if (entry.isDirectory()) {
|
|
64
|
-
walk(fullPath);
|
|
65
|
-
}
|
|
66
|
-
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
67
|
-
collected.push(fullPath);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
walk(root);
|
|
72
|
-
return collected;
|
|
73
|
-
},
|
|
74
|
-
catch: () => syncError(root, "Cannot traverse .codex"),
|
|
75
|
-
});
|
|
76
|
-
const isFileRelevant = (filePath, locator) => Effect.try({
|
|
77
|
-
try: () => {
|
|
78
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
79
|
-
const lines = content.split("\n");
|
|
80
|
-
return linesMatchProject(lines, locator);
|
|
81
|
-
},
|
|
82
|
-
catch: () => syncError(filePath, "Cannot read jsonl file"),
|
|
83
|
-
});
|
|
84
|
-
const copyRelevantFile = (sourceRoot, destinationRoot, filePath) => Effect.try({
|
|
85
|
-
try: () => {
|
|
86
|
-
const relative = path.relative(sourceRoot, filePath);
|
|
87
|
-
const targetPath = path.join(destinationRoot, relative);
|
|
88
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
89
|
-
fs.copyFileSync(filePath, targetPath);
|
|
90
|
-
},
|
|
91
|
-
catch: () => syncError(filePath, "Cannot copy file into .knowledge/.codex"),
|
|
92
|
-
});
|
|
93
|
-
const selectRelevantFiles = (files, locator) => Effect.reduce(files, [], (acc, filePath) => pipe(isFileRelevant(filePath, locator), Effect.map((isRelevant) => {
|
|
94
|
-
if (isRelevant) {
|
|
95
|
-
acc.push(filePath);
|
|
96
|
-
}
|
|
97
|
-
return acc;
|
|
98
|
-
})));
|
|
99
|
-
const resolveLocator = (options) => pipe(readRepositoryUrl(options.cwd, options.repositoryUrlOverride), Effect.map((repositoryUrl) => buildProjectLocator(repositoryUrl, options.cwd)), Effect.catchAll(() => Effect.gen(function* (__) {
|
|
100
|
-
yield* __(Console.log("Codex repository url missing; falling back to cwd-only match"));
|
|
101
|
-
return buildProjectLocator(options.cwd, options.cwd);
|
|
102
|
-
})));
|
|
103
|
-
const copyCodexFiles = (sourceDir, destinationDir, locator) => Effect.gen(function* (_) {
|
|
104
|
-
yield* _(ensureDirectory(destinationDir));
|
|
105
|
-
const allJsonlFiles = yield* _(collectJsonlFiles(sourceDir));
|
|
106
|
-
const relevantFiles = yield* _(selectRelevantFiles(allJsonlFiles, locator));
|
|
107
|
-
yield* _(Effect.forEach(relevantFiles, (filePath) => copyRelevantFile(sourceDir, destinationDir, filePath)));
|
|
108
|
-
yield* _(Console.log(`Codex: copied ${relevantFiles.length} files from ${sourceDir} to ${destinationDir}`));
|
|
109
|
-
});
|
|
110
|
-
// CHANGE: Extract Codex dialog sync into dedicated module for clarity.
|
|
111
|
-
// WHY: Separate Codex-specific shell effects from other sync flows.
|
|
112
|
-
// QUOTE(ТЗ): "вынеси в отдельный файл"
|
|
113
|
-
// REF: user request 2025-11-26
|
|
114
|
-
// SOURCE: internal requirement
|
|
115
|
-
// FORMAT THEOREM: ∀f ∈ Files: relevant(f, locator) → copied(f, destination)
|
|
116
|
-
// PURITY: SHELL
|
|
117
|
-
// EFFECT: Effect<void, SyncError, never>
|
|
118
|
-
// INVARIANT: ∀f ∈ copiedFiles: linesMatchProject(f, locator)
|
|
119
|
-
// COMPLEXITY: O(n) time / O(n) space, n = |files|
|
|
120
|
-
export const syncCodex = (options) => Effect.gen(function* (_) {
|
|
121
|
-
const locator = yield* _(resolveLocator(options));
|
|
122
|
-
const sourceDir = yield* _(resolveSourceDir(options.cwd, options.sourceDir, options.metaRoot));
|
|
123
|
-
const destinationDir = options.destinationDir ?? path.join(options.cwd, ".knowledge", ".codex");
|
|
124
|
-
if (path.resolve(sourceDir) === path.resolve(destinationDir)) {
|
|
125
|
-
yield* _(Console.log("Codex source equals destination; skipping copy to avoid duplicates"));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
yield* _(copyCodexFiles(sourceDir, destinationDir, locator));
|
|
129
|
-
}).pipe(Effect.catchAll((error) => Console.log(`Codex source not found; skipped syncing Codex dialog files (${error.reason})`)));
|
package/dist/shell/qwenSync.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { Effect } from "effect";
|
|
6
|
-
import { copyFilteredFiles, runSyncSource, syncError } from "./syncShared.js";
|
|
7
|
-
const qwenHashFromPath = (projectPath) => createHash("sha256").update(projectPath).digest("hex");
|
|
8
|
-
const resolveQwenSourceDir = (cwd, override, metaRoot) => Effect.gen(function* (_) {
|
|
9
|
-
const hash = qwenHashFromPath(cwd);
|
|
10
|
-
const envSource = process.env.QWEN_SOURCE_DIR;
|
|
11
|
-
const baseFromMeta = metaRoot === undefined
|
|
12
|
-
? undefined
|
|
13
|
-
: metaRoot.endsWith(".qwen")
|
|
14
|
-
? metaRoot
|
|
15
|
-
: path.join(metaRoot, ".qwen");
|
|
16
|
-
const metaKnowledge = path.join(metaRoot ?? "", ".knowledge", ".qwen");
|
|
17
|
-
const homeBase = path.join(os.homedir(), ".qwen");
|
|
18
|
-
const homeKnowledge = path.join(os.homedir(), ".knowledge", ".qwen");
|
|
19
|
-
const candidates = [
|
|
20
|
-
override,
|
|
21
|
-
envSource,
|
|
22
|
-
baseFromMeta ? path.join(baseFromMeta, "tmp", hash) : undefined,
|
|
23
|
-
path.join(cwd, ".qwen", "tmp", hash),
|
|
24
|
-
path.join(cwd, ".knowledge", ".qwen", "tmp", hash),
|
|
25
|
-
metaKnowledge ? path.join(metaKnowledge, "tmp", hash) : undefined,
|
|
26
|
-
path.join(homeBase, "tmp", hash),
|
|
27
|
-
path.join(homeKnowledge, "tmp", hash),
|
|
28
|
-
].filter((candidate) => candidate !== undefined);
|
|
29
|
-
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
30
|
-
if (found === undefined) {
|
|
31
|
-
return yield* _(Effect.fail(syncError(".qwen", `Qwen source directory is missing for hash ${hash}`)));
|
|
32
|
-
}
|
|
33
|
-
return found;
|
|
34
|
-
});
|
|
35
|
-
export const syncQwen = (options) => runSyncSource(qwenSource, options);
|
|
36
|
-
const qwenSource = {
|
|
37
|
-
name: "Qwen",
|
|
38
|
-
destSubdir: ".qwen",
|
|
39
|
-
resolveSource: (options) => resolveQwenSourceDir(options.cwd, options.qwenSourceDir, options.metaRoot),
|
|
40
|
-
copy: (sourceDir, destinationDir) => copyFilteredFiles(sourceDir, destinationDir, (entry, fullPath) => entry.isFile() && fullPath.endsWith(".json"), "Cannot traverse Qwen directory"),
|
|
41
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import { syncClaude } from "./claudeSync.js";
|
|
4
|
-
import { syncCodex } from "./codexSync.js";
|
|
5
|
-
import { syncQwen } from "./qwenSync.js";
|
|
6
|
-
// PURPOSE: Sync project-scoped dialogs (Codex + Qwen) into .knowledge storage.
|
|
7
|
-
export const buildSyncProgram = (options) => Effect.gen(function* (_) {
|
|
8
|
-
yield* _(syncClaude(options));
|
|
9
|
-
yield* _(syncCodex(options));
|
|
10
|
-
yield* _(syncQwen(options));
|
|
11
|
-
});
|
|
12
|
-
export const parseArgs = () => {
|
|
13
|
-
const argv = process.argv.slice(2);
|
|
14
|
-
let result = { cwd: process.cwd() };
|
|
15
|
-
const mapping = {
|
|
16
|
-
"--source": "sourceDir",
|
|
17
|
-
"-s": "sourceDir",
|
|
18
|
-
"--dest": "destinationDir",
|
|
19
|
-
"-d": "destinationDir",
|
|
20
|
-
"--project-url": "repositoryUrlOverride",
|
|
21
|
-
"--project-name": "repositoryUrlOverride",
|
|
22
|
-
"--meta-root": "metaRoot",
|
|
23
|
-
"--qwen-source": "qwenSourceDir",
|
|
24
|
-
"--claude-projects": "claudeProjectsRoot",
|
|
25
|
-
};
|
|
26
|
-
for (let i = 0; i < argv.length; i++) {
|
|
27
|
-
const arg = argv[i];
|
|
28
|
-
const key = mapping[arg];
|
|
29
|
-
if (key === undefined) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
const value = argv[i + 1];
|
|
33
|
-
if (value !== undefined) {
|
|
34
|
-
result = { ...result, [key]: value };
|
|
35
|
-
i++;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
};
|
package/dist/shell/syncShared.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { Console, Effect } from "effect";
|
|
4
|
-
import { pipe } from "effect/Function";
|
|
5
|
-
export const syncError = (pathValue, reason) => ({
|
|
6
|
-
_tag: "SyncError",
|
|
7
|
-
path: pathValue,
|
|
8
|
-
reason,
|
|
9
|
-
});
|
|
10
|
-
export const ensureDirectory = (directory) => Effect.try({
|
|
11
|
-
try: () => {
|
|
12
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
13
|
-
},
|
|
14
|
-
catch: () => syncError(directory, "Cannot create destination directory structure"),
|
|
15
|
-
});
|
|
16
|
-
export const runSyncSource = (source, options) => pipe(Effect.gen(function* (_) {
|
|
17
|
-
const resolvedSource = yield* _(source.resolveSource(options));
|
|
18
|
-
const destination = path.join(options.cwd, ".knowledge", source.destSubdir);
|
|
19
|
-
if (path.resolve(resolvedSource) === path.resolve(destination)) {
|
|
20
|
-
yield* _(Console.log(`${source.name}: source equals destination; skipping copy to avoid duplicates`));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
yield* _(ensureDirectory(destination));
|
|
24
|
-
const copied = yield* _(source.copy(resolvedSource, destination, options));
|
|
25
|
-
yield* _(Console.log(`${source.name}: copied ${copied} files from ${resolvedSource} to ${destination}`));
|
|
26
|
-
}), Effect.catchAll((error) => Console.log(`${source.name}: source not found; skipped syncing (${error.reason})`)));
|
|
27
|
-
export const copyFilteredFiles = (sourceRoot, destinationRoot, isRelevant, errorReason) => Effect.try({
|
|
28
|
-
try: () => {
|
|
29
|
-
let copied = 0;
|
|
30
|
-
const walk = (current) => {
|
|
31
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
const full = path.join(current, entry.name);
|
|
34
|
-
if (entry.isDirectory()) {
|
|
35
|
-
walk(full);
|
|
36
|
-
}
|
|
37
|
-
else if (isRelevant(entry, full)) {
|
|
38
|
-
const target = path.join(destinationRoot, path.relative(sourceRoot, full));
|
|
39
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
40
|
-
fs.copyFileSync(full, target);
|
|
41
|
-
copied += 1;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
walk(sourceRoot);
|
|
46
|
-
return copied;
|
|
47
|
-
},
|
|
48
|
-
catch: () => syncError(sourceRoot, errorReason),
|
|
49
|
-
});
|
package/dist/shell/syncTypes.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/src/core/greeting.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Option } from "effect";
|
|
2
|
-
|
|
3
|
-
export interface AppProfile {
|
|
4
|
-
readonly name: string;
|
|
5
|
-
readonly mission: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface Greeting {
|
|
9
|
-
readonly message: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const normalize = (value: string): string => value.trim();
|
|
13
|
-
|
|
14
|
-
const isNonEmpty = (value: string): boolean => value.length > 0;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* CHANGE: Introduce pure greeting synthesis for console entrypoint
|
|
18
|
-
* WHY: Provide mathematically checkable construction of startup banner without side effects
|
|
19
|
-
* QUOTE(ТЗ): "Каждая функция — это теорема."
|
|
20
|
-
* REF: REQ-GREETING-CORE
|
|
21
|
-
* SOURCE: AGENTS.md – функциональное ядро без побочных эффектов
|
|
22
|
-
* FORMAT THEOREM: ∀p ∈ AppProfile: valid(p) → nonEmpty(buildGreeting(p).message)
|
|
23
|
-
* PURITY: CORE
|
|
24
|
-
* EFFECT: None (pure)
|
|
25
|
-
* INVARIANT: output.message.length > 0 when Option is Some
|
|
26
|
-
* COMPLEXITY: O(1)/O(1)
|
|
27
|
-
*/
|
|
28
|
-
export const buildGreeting = (profile: AppProfile): Option.Option<Greeting> => {
|
|
29
|
-
const trimmedName = normalize(profile.name);
|
|
30
|
-
const trimmedMission = normalize(profile.mission);
|
|
31
|
-
|
|
32
|
-
if (!isNonEmpty(trimmedName) || !isNonEmpty(trimmedMission)) {
|
|
33
|
-
return Option.none();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return Option.some({
|
|
37
|
-
message: `${trimmedName}: ${trimmedMission}`,
|
|
38
|
-
});
|
|
39
|
-
};
|
package/src/main.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { Console, Effect, Option, pipe } from "effect";
|
|
2
|
-
import { match } from "ts-pattern";
|
|
3
|
-
import type { AppProfile } from "./core/greeting.js";
|
|
4
|
-
import { buildGreeting } from "./core/greeting.js";
|
|
5
|
-
|
|
6
|
-
interface StartupError {
|
|
7
|
-
readonly _tag: "StartupError";
|
|
8
|
-
readonly reason: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const appProfile: AppProfile = {
|
|
12
|
-
name: "Context Console",
|
|
13
|
-
mission: "Functional core ready for verifiable effects",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const toStartupError = (reason: string): StartupError => ({
|
|
17
|
-
_tag: "StartupError",
|
|
18
|
-
reason,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* CHANGE: Compose shell-level startup program around pure greeting builder
|
|
23
|
-
* WHY: Isolate side effects (console IO) while delegating computation to functional core
|
|
24
|
-
* QUOTE(ТЗ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
|
|
25
|
-
* REF: REQ-SHELL-STARTUP
|
|
26
|
-
* SOURCE: AGENTS.md — эффекты только в SHELL
|
|
27
|
-
* FORMAT THEOREM: ∀profile: valid(profile) → logs(buildGreeting(profile))
|
|
28
|
-
* PURITY: SHELL
|
|
29
|
-
* EFFECT: Effect<void, StartupError, Console>
|
|
30
|
-
* INVARIANT: Console side-effects occur once and only after successful greeting synthesis
|
|
31
|
-
* COMPLEXITY: O(1)/O(1)
|
|
32
|
-
*/
|
|
33
|
-
const program = pipe(
|
|
34
|
-
Effect.succeed(buildGreeting(appProfile)),
|
|
35
|
-
Effect.filterOrFail(Option.isSome, () =>
|
|
36
|
-
toStartupError("Profile must not be empty"),
|
|
37
|
-
),
|
|
38
|
-
Effect.map((option) => option.value),
|
|
39
|
-
Effect.flatMap((greeting) => Console.log(greeting.message)),
|
|
40
|
-
Effect.catchAll((error) =>
|
|
41
|
-
match(error)
|
|
42
|
-
.with({ _tag: "StartupError" }, (startupError) =>
|
|
43
|
-
Console.error(`StartupError: ${startupError.reason}`),
|
|
44
|
-
)
|
|
45
|
-
.exhaustive(),
|
|
46
|
-
),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
Effect.runSync(program);
|
package/src/shell/claudeSync.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import * as NodeFs from "node:fs";
|
|
2
|
-
import * as NodeOs from "node:os";
|
|
3
|
-
import * as NodePath from "node:path";
|
|
4
|
-
import { Effect, pipe } from "effect";
|
|
5
|
-
import { copyFilteredFiles, runSyncSource, syncError } from "./syncShared.js";
|
|
6
|
-
import type { SyncError, SyncOptions, SyncSource } from "./syncTypes.js";
|
|
7
|
-
|
|
8
|
-
const slugFromCwd = (cwd: string): string =>
|
|
9
|
-
`-${cwd.replace(/^\/+/, "").replace(/\//g, "-")}`;
|
|
10
|
-
|
|
11
|
-
const resolveClaudeProjectDir = (
|
|
12
|
-
cwd: string,
|
|
13
|
-
overrideProjectsRoot?: string,
|
|
14
|
-
): Effect.Effect<string, SyncError> =>
|
|
15
|
-
pipe(
|
|
16
|
-
Effect.sync(() => {
|
|
17
|
-
const slug = slugFromCwd(cwd);
|
|
18
|
-
const base =
|
|
19
|
-
overrideProjectsRoot ??
|
|
20
|
-
NodePath.join(NodeOs.homedir(), ".claude", "projects");
|
|
21
|
-
const candidate = NodePath.join(base, slug);
|
|
22
|
-
return NodeFs.existsSync(candidate) ? candidate : undefined;
|
|
23
|
-
}),
|
|
24
|
-
Effect.flatMap((found) =>
|
|
25
|
-
found === undefined
|
|
26
|
-
? Effect.fail(
|
|
27
|
-
syncError(".claude", "Claude project directory is missing"),
|
|
28
|
-
)
|
|
29
|
-
: Effect.succeed(found),
|
|
30
|
-
),
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const copyClaudeJsonl = (
|
|
34
|
-
sourceDir: string,
|
|
35
|
-
destinationDir: string,
|
|
36
|
-
): Effect.Effect<number, SyncError> =>
|
|
37
|
-
copyFilteredFiles(
|
|
38
|
-
sourceDir,
|
|
39
|
-
destinationDir,
|
|
40
|
-
(entry, fullPath) => entry.isFile() && fullPath.endsWith(".jsonl"),
|
|
41
|
-
"Cannot traverse Claude project",
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
export const syncClaude = (
|
|
45
|
-
options: SyncOptions,
|
|
46
|
-
): Effect.Effect<void, SyncError> => runSyncSource(claudeSource, options);
|
|
47
|
-
|
|
48
|
-
const claudeSource: SyncSource = {
|
|
49
|
-
name: "Claude",
|
|
50
|
-
destSubdir: ".claude",
|
|
51
|
-
resolveSource: (options) =>
|
|
52
|
-
resolveClaudeProjectDir(options.cwd, options.claudeProjectsRoot),
|
|
53
|
-
copy: (sourceDir, destinationDir) =>
|
|
54
|
-
copyClaudeJsonl(sourceDir, destinationDir),
|
|
55
|
-
};
|
package/src/shell/codexSync.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import * as S from "@effect/schema/Schema";
|
|
5
|
-
import { Console, Effect, pipe } from "effect";
|
|
6
|
-
import type { ProjectLocator } from "../core/knowledge.js";
|
|
7
|
-
import { buildProjectLocator, linesMatchProject } from "../core/knowledge.js";
|
|
8
|
-
import { ensureDirectory, syncError } from "./syncShared.js";
|
|
9
|
-
import type { SyncError, SyncOptions } from "./syncTypes.js";
|
|
10
|
-
|
|
11
|
-
const PackageRepositorySchema = S.Struct({
|
|
12
|
-
url: S.String,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const PackageFileSchema = S.Struct({
|
|
16
|
-
repository: S.optional(PackageRepositorySchema),
|
|
17
|
-
});
|
|
18
|
-
const PackageFileFromJson = S.compose(S.parseJson(), PackageFileSchema);
|
|
19
|
-
const decodePackageFile = S.decodeUnknownSync(PackageFileFromJson);
|
|
20
|
-
|
|
21
|
-
const readRepositoryUrl = (
|
|
22
|
-
cwd: string,
|
|
23
|
-
repositoryUrlOverride?: string,
|
|
24
|
-
): Effect.Effect<string, SyncError> =>
|
|
25
|
-
Effect.try({
|
|
26
|
-
try: () => {
|
|
27
|
-
if (repositoryUrlOverride !== undefined) {
|
|
28
|
-
return repositoryUrlOverride;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const raw = fs.readFileSync(path.join(cwd, "package.json"), "utf8");
|
|
32
|
-
const parsed = decodePackageFile(raw);
|
|
33
|
-
const repositoryUrl = parsed.repository?.url;
|
|
34
|
-
|
|
35
|
-
if (repositoryUrl === undefined) {
|
|
36
|
-
throw new Error("repository url is missing");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return repositoryUrl;
|
|
40
|
-
},
|
|
41
|
-
catch: () => syncError("package.json", "Cannot read repository url"),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const resolveSourceDir = (
|
|
45
|
-
cwd: string,
|
|
46
|
-
override?: string,
|
|
47
|
-
metaRoot?: string,
|
|
48
|
-
): Effect.Effect<string, SyncError> =>
|
|
49
|
-
pipe(
|
|
50
|
-
Effect.sync(() => {
|
|
51
|
-
const envSource = process.env.CODEX_SOURCE_DIR;
|
|
52
|
-
const metaCandidate =
|
|
53
|
-
metaRoot === undefined
|
|
54
|
-
? undefined
|
|
55
|
-
: metaRoot.endsWith(".codex")
|
|
56
|
-
? metaRoot
|
|
57
|
-
: path.join(metaRoot, ".codex");
|
|
58
|
-
const localSource = path.join(cwd, ".codex");
|
|
59
|
-
const homeSource = path.join(os.homedir(), ".codex");
|
|
60
|
-
const localKnowledge = path.join(cwd, ".knowledge", ".codex");
|
|
61
|
-
const homeKnowledge = path.join(os.homedir(), ".knowledge", ".codex");
|
|
62
|
-
|
|
63
|
-
const candidates = [
|
|
64
|
-
override,
|
|
65
|
-
envSource,
|
|
66
|
-
metaCandidate,
|
|
67
|
-
localSource,
|
|
68
|
-
homeSource,
|
|
69
|
-
localKnowledge,
|
|
70
|
-
homeKnowledge,
|
|
71
|
-
].filter((candidate): candidate is string => candidate !== undefined);
|
|
72
|
-
|
|
73
|
-
const existing = candidates.find((candidate) => fs.existsSync(candidate));
|
|
74
|
-
|
|
75
|
-
return { existing, candidates };
|
|
76
|
-
}),
|
|
77
|
-
Effect.flatMap(({ existing, candidates }) =>
|
|
78
|
-
existing === undefined
|
|
79
|
-
? Effect.fail(
|
|
80
|
-
syncError(
|
|
81
|
-
".codex",
|
|
82
|
-
`Source .codex directory is missing; checked: ${candidates.join(", ")}`,
|
|
83
|
-
),
|
|
84
|
-
)
|
|
85
|
-
: Effect.succeed(existing),
|
|
86
|
-
),
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const collectJsonlFiles = (
|
|
90
|
-
root: string,
|
|
91
|
-
): Effect.Effect<ReadonlyArray<string>, SyncError> =>
|
|
92
|
-
Effect.try({
|
|
93
|
-
try: () => {
|
|
94
|
-
const collected: string[] = [];
|
|
95
|
-
const walk = (current: string): void => {
|
|
96
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
97
|
-
for (const entry of entries) {
|
|
98
|
-
const fullPath = path.join(current, entry.name);
|
|
99
|
-
if (entry.isDirectory()) {
|
|
100
|
-
walk(fullPath);
|
|
101
|
-
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
102
|
-
collected.push(fullPath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
walk(root);
|
|
108
|
-
return collected;
|
|
109
|
-
},
|
|
110
|
-
catch: () => syncError(root, "Cannot traverse .codex"),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const isFileRelevant = (
|
|
114
|
-
filePath: string,
|
|
115
|
-
locator: ProjectLocator,
|
|
116
|
-
): Effect.Effect<boolean, SyncError> =>
|
|
117
|
-
Effect.try({
|
|
118
|
-
try: () => {
|
|
119
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
120
|
-
const lines = content.split("\n");
|
|
121
|
-
return linesMatchProject(lines, locator);
|
|
122
|
-
},
|
|
123
|
-
catch: () => syncError(filePath, "Cannot read jsonl file"),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const copyRelevantFile = (
|
|
127
|
-
sourceRoot: string,
|
|
128
|
-
destinationRoot: string,
|
|
129
|
-
filePath: string,
|
|
130
|
-
): Effect.Effect<void, SyncError> =>
|
|
131
|
-
Effect.try({
|
|
132
|
-
try: () => {
|
|
133
|
-
const relative = path.relative(sourceRoot, filePath);
|
|
134
|
-
const targetPath = path.join(destinationRoot, relative);
|
|
135
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
136
|
-
fs.copyFileSync(filePath, targetPath);
|
|
137
|
-
},
|
|
138
|
-
catch: () => syncError(filePath, "Cannot copy file into .knowledge/.codex"),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const selectRelevantFiles = (
|
|
142
|
-
files: ReadonlyArray<string>,
|
|
143
|
-
locator: ProjectLocator,
|
|
144
|
-
): Effect.Effect<ReadonlyArray<string>, SyncError> =>
|
|
145
|
-
Effect.reduce(files, [] as string[], (acc, filePath) =>
|
|
146
|
-
pipe(
|
|
147
|
-
isFileRelevant(filePath, locator),
|
|
148
|
-
Effect.map((isRelevant) => {
|
|
149
|
-
if (isRelevant) {
|
|
150
|
-
acc.push(filePath);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return acc;
|
|
154
|
-
}),
|
|
155
|
-
),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const resolveLocator = (
|
|
159
|
-
options: SyncOptions,
|
|
160
|
-
): Effect.Effect<ProjectLocator, SyncError> =>
|
|
161
|
-
pipe(
|
|
162
|
-
readRepositoryUrl(options.cwd, options.repositoryUrlOverride),
|
|
163
|
-
Effect.map((repositoryUrl) =>
|
|
164
|
-
buildProjectLocator(repositoryUrl, options.cwd),
|
|
165
|
-
),
|
|
166
|
-
Effect.catchAll(() =>
|
|
167
|
-
Effect.gen(function* (__) {
|
|
168
|
-
yield* __(
|
|
169
|
-
Console.log(
|
|
170
|
-
"Codex repository url missing; falling back to cwd-only match",
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
return buildProjectLocator(options.cwd, options.cwd);
|
|
174
|
-
}),
|
|
175
|
-
),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
const copyCodexFiles = (
|
|
179
|
-
sourceDir: string,
|
|
180
|
-
destinationDir: string,
|
|
181
|
-
locator: ProjectLocator,
|
|
182
|
-
): Effect.Effect<void, SyncError> =>
|
|
183
|
-
Effect.gen(function* (_) {
|
|
184
|
-
yield* _(ensureDirectory(destinationDir));
|
|
185
|
-
const allJsonlFiles = yield* _(collectJsonlFiles(sourceDir));
|
|
186
|
-
const relevantFiles = yield* _(selectRelevantFiles(allJsonlFiles, locator));
|
|
187
|
-
yield* _(
|
|
188
|
-
Effect.forEach(relevantFiles, (filePath) =>
|
|
189
|
-
copyRelevantFile(sourceDir, destinationDir, filePath),
|
|
190
|
-
),
|
|
191
|
-
);
|
|
192
|
-
yield* _(
|
|
193
|
-
Console.log(
|
|
194
|
-
`Codex: copied ${relevantFiles.length} files from ${sourceDir} to ${destinationDir}`,
|
|
195
|
-
),
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// CHANGE: Extract Codex dialog sync into dedicated module for clarity.
|
|
200
|
-
// WHY: Separate Codex-specific shell effects from other sync flows.
|
|
201
|
-
// QUOTE(ТЗ): "вынеси в отдельный файл"
|
|
202
|
-
// REF: user request 2025-11-26
|
|
203
|
-
// SOURCE: internal requirement
|
|
204
|
-
// FORMAT THEOREM: ∀f ∈ Files: relevant(f, locator) → copied(f, destination)
|
|
205
|
-
// PURITY: SHELL
|
|
206
|
-
// EFFECT: Effect<void, SyncError, never>
|
|
207
|
-
// INVARIANT: ∀f ∈ copiedFiles: linesMatchProject(f, locator)
|
|
208
|
-
// COMPLEXITY: O(n) time / O(n) space, n = |files|
|
|
209
|
-
export const syncCodex = (
|
|
210
|
-
options: SyncOptions,
|
|
211
|
-
): Effect.Effect<void, SyncError> =>
|
|
212
|
-
Effect.gen(function* (_) {
|
|
213
|
-
const locator = yield* _(resolveLocator(options));
|
|
214
|
-
const sourceDir = yield* _(
|
|
215
|
-
resolveSourceDir(options.cwd, options.sourceDir, options.metaRoot),
|
|
216
|
-
);
|
|
217
|
-
const destinationDir =
|
|
218
|
-
options.destinationDir ?? path.join(options.cwd, ".knowledge", ".codex");
|
|
219
|
-
|
|
220
|
-
if (path.resolve(sourceDir) === path.resolve(destinationDir)) {
|
|
221
|
-
yield* _(
|
|
222
|
-
Console.log(
|
|
223
|
-
"Codex source equals destination; skipping copy to avoid duplicates",
|
|
224
|
-
),
|
|
225
|
-
);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
yield* _(copyCodexFiles(sourceDir, destinationDir, locator));
|
|
230
|
-
}).pipe(
|
|
231
|
-
Effect.catchAll((error) =>
|
|
232
|
-
Console.log(
|
|
233
|
-
`Codex source not found; skipped syncing Codex dialog files (${error.reason})`,
|
|
234
|
-
),
|
|
235
|
-
),
|
|
236
|
-
);
|