@renxqoo/renx-code 0.0.4 → 0.0.6
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 +82 -51
- package/bin/renx.cjs +16 -0
- package/package.json +2 -45
- package/src/agent/runtime/runtime.context-usage.test.ts +4 -5
- package/src/agent/runtime/runtime.error-handling.test.ts +4 -5
- package/src/agent/runtime/runtime.test.ts +7 -4
- package/src/agent/runtime/runtime.ts +3 -9
- package/src/agent/runtime/runtime.usage-forwarding.test.ts +4 -5
- package/src/agent/runtime/source-modules.test.ts +16 -35
- package/src/agent/runtime/source-modules.ts +17 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_ACCEPTANCE_CHECKLIST.md +95 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.html +1345 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.md +1353 -0
- package/vendor/agent-root/src/agent/ERROR_CONTRACT.md +60 -0
- package/vendor/agent-root/src/agent/TEST_COVERAGE_ANALYSIS.md +278 -0
- package/vendor/agent-root/src/agent/__test__/error-contract.test.ts +72 -0
- package/vendor/agent-root/src/agent/__test__/types.test.ts +137 -0
- package/vendor/agent-root/src/agent/agent/__test__/abort-runtime.test.ts +83 -0
- package/vendor/agent-root/src/agent/agent/__test__/callback-safety.test.ts +34 -0
- package/vendor/agent-root/src/agent/agent/__test__/compaction.test.ts +323 -0
- package/vendor/agent-root/src/agent/agent/__test__/concurrency.test.ts +290 -0
- package/vendor/agent-root/src/agent/agent/__test__/error-normalizer.test.ts +377 -0
- package/vendor/agent-root/src/agent/agent/__test__/error.test.ts +212 -0
- package/vendor/agent-root/src/agent/agent/__test__/fault-injection.test.ts +295 -0
- package/vendor/agent-root/src/agent/agent/__test__/index.test.ts +3607 -0
- package/vendor/agent-root/src/agent/agent/__test__/logger.test.ts +35 -0
- package/vendor/agent-root/src/agent/agent/__test__/message-utils.test.ts +517 -0
- package/vendor/agent-root/src/agent/agent/__test__/telemetry.test.ts +97 -0
- package/vendor/agent-root/src/agent/agent/__test__/timeout-budget.test.ts +479 -0
- package/vendor/agent-root/src/agent/agent/__test__/tool-call-merge.test.ts +80 -0
- package/vendor/agent-root/src/agent/agent/__test__/tool-execution-ledger.test.ts +76 -0
- package/vendor/agent-root/src/agent/agent/__test__/write-buffer.test.ts +173 -0
- package/vendor/agent-root/src/agent/agent/__test__/write-file-session.test.ts +109 -0
- package/vendor/agent-root/src/agent/agent/abort-runtime.ts +71 -0
- package/vendor/agent-root/src/agent/agent/callback-safety.ts +33 -0
- package/vendor/agent-root/src/agent/agent/compaction.ts +291 -0
- package/vendor/agent-root/src/agent/agent/concurrency.ts +103 -0
- package/vendor/agent-root/src/agent/agent/error-normalizer.ts +190 -0
- package/vendor/agent-root/src/agent/agent/error.ts +198 -0
- package/vendor/agent-root/src/agent/agent/index.ts +1772 -0
- package/vendor/agent-root/src/agent/agent/logger.ts +65 -0
- package/vendor/agent-root/src/agent/agent/message-utils.ts +101 -0
- package/vendor/agent-root/src/agent/agent/stream-events.ts +61 -0
- package/vendor/agent-root/src/agent/agent/telemetry.ts +123 -0
- package/vendor/agent-root/src/agent/agent/timeout-budget.ts +227 -0
- package/vendor/agent-root/src/agent/agent/tool-call-merge.ts +111 -0
- package/vendor/agent-root/src/agent/agent/tool-execution-ledger.ts +164 -0
- package/vendor/agent-root/src/agent/agent/write-buffer.ts +188 -0
- package/vendor/agent-root/src/agent/agent/write-file-session.ts +238 -0
- package/vendor/agent-root/src/agent/app/__test__/agent-app-service.test.ts +1053 -0
- package/vendor/agent-root/src/agent/app/__test__/minimal-agent-application.test.ts +158 -0
- package/vendor/agent-root/src/agent/app/__test__/sqlite-agent-app-store.test.ts +437 -0
- package/vendor/agent-root/src/agent/app/agent-app-service.ts +748 -0
- package/vendor/agent-root/src/agent/app/contracts.ts +109 -0
- package/vendor/agent-root/src/agent/app/index.ts +5 -0
- package/vendor/agent-root/src/agent/app/minimal-agent-application.ts +151 -0
- package/vendor/agent-root/src/agent/app/ports.ts +72 -0
- package/vendor/agent-root/src/agent/app/sqlite-agent-app-store.ts +1182 -0
- package/vendor/agent-root/src/agent/app/sqlite-client.ts +177 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/00-README.md +36 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/01-scope-and-goals.md +33 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/02-architecture-overview.md +40 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/03-domain-model-and-contracts.md +91 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/04-ports-and-interfaces.md +116 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/05-run-orchestration-and-state-machine.md +52 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/06-cli-commands-and-ux.md +53 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/07-storage-design-local.md +52 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/08-error-and-observability.md +40 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/09-security-and-policy-boundary.md +19 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/10-test-plan-and-acceptance.md +28 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/11-implementation-phases.md +26 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/12-open-questions-and-risks.md +30 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/13-sqlite-schema-fields-and-rationale.md +567 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/14-project-flow-mermaid.md +583 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/15-openclaw-style-project-blueprint.md +972 -0
- package/vendor/agent-root/src/agent/error-contract.ts +154 -0
- package/vendor/agent-root/src/agent/prompts/system.ts +246 -0
- package/vendor/agent-root/src/agent/prompts/system1.ts +208 -0
- package/vendor/agent-root/src/agent/storage/__test__/file-history-store.test.ts +98 -0
- package/vendor/agent-root/src/agent/storage/file-history-store.ts +313 -0
- package/vendor/agent-root/src/agent/storage/file-storage-config.ts +94 -0
- package/vendor/agent-root/src/agent/storage/file-system.ts +31 -0
- package/vendor/agent-root/src/agent/storage/file-write-service.ts +21 -0
- package/vendor/agent-root/src/agent/tool/__test__/base-tool.test.ts +413 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash-policy.test.ts +356 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash.mocked-coverage.test.ts +375 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash.test.ts +372 -0
- package/vendor/agent-root/src/agent/tool/__test__/error.test.ts +108 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-edit-tool.test.ts +258 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-history-tools.test.ts +121 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-read-tool.test.ts +210 -0
- package/vendor/agent-root/src/agent/tool/__test__/glob.test.ts +139 -0
- package/vendor/agent-root/src/agent/tool/__test__/grep.mocked-coverage.test.ts +456 -0
- package/vendor/agent-root/src/agent/tool/__test__/grep.test.ts +192 -0
- package/vendor/agent-root/src/agent/tool/__test__/lsp.test.ts +300 -0
- package/vendor/agent-root/src/agent/tool/__test__/outside-workspace-confirmation.test.ts +214 -0
- package/vendor/agent-root/src/agent/tool/__test__/path-security.test.ts +336 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-loader.test.ts +494 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-parser.test.ts +543 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-tool.test.ts +172 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-concurrency-and-version.test.ts +116 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-create-get-list-update.test.ts +267 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-create.test.ts +519 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-errors.test.ts +225 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-output-blocking.test.ts +223 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-output.test.ts +184 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-parent-abort.test.ts +287 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-real-runner-adapter.test.ts +190 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-run-lifecycle.test.ts +352 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-store-runner-branches.test.ts +395 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-store.test.ts +391 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config-integration.test.ts +176 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config.test.ts +68 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-tools-core-edges.test.ts +630 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-tools-runtime-edges.test.ts +732 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-types.test.ts +494 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-utils-branches.test.ts +175 -0
- package/vendor/agent-root/src/agent/tool/__test__/tool-manager.test.ts +505 -0
- package/vendor/agent-root/src/agent/tool/__test__/types.test.ts +55 -0
- package/vendor/agent-root/src/agent/tool/__test__/web-fetch.test.ts +244 -0
- package/vendor/agent-root/src/agent/tool/__test__/web-search.test.ts +290 -0
- package/vendor/agent-root/src/agent/tool/__test__/write-file.test.ts +368 -0
- package/vendor/agent-root/src/agent/tool/base-tool.ts +345 -0
- package/vendor/agent-root/src/agent/tool/bash-policy.ts +636 -0
- package/vendor/agent-root/src/agent/tool/bash.ts +688 -0
- package/vendor/agent-root/src/agent/tool/error.ts +131 -0
- package/vendor/agent-root/src/agent/tool/file-edit-tool.ts +264 -0
- package/vendor/agent-root/src/agent/tool/file-history-list.ts +103 -0
- package/vendor/agent-root/src/agent/tool/file-history-restore.ts +149 -0
- package/vendor/agent-root/src/agent/tool/file-read-tool.ts +211 -0
- package/vendor/agent-root/src/agent/tool/glob.ts +171 -0
- package/vendor/agent-root/src/agent/tool/grep.ts +496 -0
- package/vendor/agent-root/src/agent/tool/lsp.ts +481 -0
- package/vendor/agent-root/src/agent/tool/path-security.ts +117 -0
- package/vendor/agent-root/src/agent/tool/search/common.ts +153 -0
- package/vendor/agent-root/src/agent/tool/skill/index.ts +13 -0
- package/vendor/agent-root/src/agent/tool/skill/loader.ts +229 -0
- package/vendor/agent-root/src/agent/tool/skill/parser.ts +124 -0
- package/vendor/agent-root/src/agent/tool/skill/types.ts +27 -0
- package/vendor/agent-root/src/agent/tool/skill-tool.ts +143 -0
- package/vendor/agent-root/src/agent/tool/task-create.ts +186 -0
- package/vendor/agent-root/src/agent/tool/task-errors.ts +42 -0
- package/vendor/agent-root/src/agent/tool/task-get.ts +116 -0
- package/vendor/agent-root/src/agent/tool/task-graph.ts +78 -0
- package/vendor/agent-root/src/agent/tool/task-list.ts +141 -0
- package/vendor/agent-root/src/agent/tool/task-mock-runner-adapter.ts +232 -0
- package/vendor/agent-root/src/agent/tool/task-output.ts +223 -0
- package/vendor/agent-root/src/agent/tool/task-parent-abort.ts +115 -0
- package/vendor/agent-root/src/agent/tool/task-real-runner-adapter.ts +336 -0
- package/vendor/agent-root/src/agent/tool/task-runner-adapter.ts +55 -0
- package/vendor/agent-root/src/agent/tool/task-stop.ts +187 -0
- package/vendor/agent-root/src/agent/tool/task-store.ts +217 -0
- package/vendor/agent-root/src/agent/tool/task-subagent-config.ts +149 -0
- package/vendor/agent-root/src/agent/tool/task-types.ts +264 -0
- package/vendor/agent-root/src/agent/tool/task-update.ts +315 -0
- package/vendor/agent-root/src/agent/tool/task.ts +209 -0
- package/vendor/agent-root/src/agent/tool/tool-manager.ts +362 -0
- package/vendor/agent-root/src/agent/tool/tool-prompts.ts +242 -0
- package/vendor/agent-root/src/agent/tool/types.ts +116 -0
- package/vendor/agent-root/src/agent/tool/web-fetch.ts +227 -0
- package/vendor/agent-root/src/agent/tool/web-search.ts +208 -0
- package/vendor/agent-root/src/agent/tool/write-file.ts +497 -0
- package/vendor/agent-root/src/agent/types.ts +232 -0
- package/vendor/agent-root/src/agent/utils/__tests__/index.test.ts +18 -0
- package/vendor/agent-root/src/agent/utils/__tests__/message-utils.test.ts +610 -0
- package/vendor/agent-root/src/agent/utils/__tests__/message.test.ts +223 -0
- package/vendor/agent-root/src/agent/utils/__tests__/token.test.ts +42 -0
- package/vendor/agent-root/src/agent/utils/index.ts +16 -0
- package/vendor/agent-root/src/agent/utils/message.ts +171 -0
- package/vendor/agent-root/src/agent/utils/token.ts +28 -0
- package/vendor/agent-root/src/config/__tests__/load-config-to-env.test.ts +129 -0
- package/vendor/agent-root/src/config/__tests__/loader.test.ts +247 -0
- package/vendor/agent-root/src/config/__tests__/runtime.test.ts +88 -0
- package/vendor/agent-root/src/config/index.ts +54 -0
- package/vendor/agent-root/src/config/loader.ts +431 -0
- package/vendor/agent-root/src/config/paths.ts +30 -0
- package/vendor/agent-root/src/config/runtime.ts +163 -0
- package/vendor/agent-root/src/config/types.ts +70 -0
- package/vendor/agent-root/src/logger/index.ts +57 -0
- package/vendor/agent-root/src/logger/logger.ts +819 -0
- package/vendor/agent-root/src/logger/types.ts +150 -0
- package/vendor/agent-root/src/providers/__tests__/errors.test.ts +441 -0
- package/vendor/agent-root/src/providers/__tests__/index.test.ts +16 -0
- package/vendor/agent-root/src/providers/__tests__/openai-compatible.options.test.ts +318 -0
- package/vendor/agent-root/src/providers/__tests__/openai-compatible.test.ts +600 -0
- package/vendor/agent-root/src/providers/__tests__/registry.test.ts +449 -0
- package/vendor/agent-root/src/providers/__tests__/responses-adapter.test.ts +298 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/anthropic.test.ts +354 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/kimi.test.ts +58 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/standard.test.ts +261 -0
- package/vendor/agent-root/src/providers/adapters/anthropic.ts +572 -0
- package/vendor/agent-root/src/providers/adapters/base.ts +131 -0
- package/vendor/agent-root/src/providers/adapters/kimi.ts +48 -0
- package/vendor/agent-root/src/providers/adapters/responses.ts +732 -0
- package/vendor/agent-root/src/providers/adapters/standard.ts +120 -0
- package/vendor/agent-root/src/providers/http/__tests__/client.timeout.test.ts +313 -0
- package/vendor/agent-root/src/providers/http/client.ts +289 -0
- package/vendor/agent-root/src/providers/http/stream-parser.ts +109 -0
- package/vendor/agent-root/src/providers/index.ts +76 -0
- package/vendor/agent-root/src/providers/kimi-headers.ts +177 -0
- package/vendor/agent-root/src/providers/openai-compatible.ts +387 -0
- package/vendor/agent-root/src/providers/registry/model-config.ts +230 -0
- package/vendor/agent-root/src/providers/registry/provider-factory.ts +123 -0
- package/vendor/agent-root/src/providers/registry.ts +135 -0
- package/vendor/agent-root/src/providers/types/api.ts +284 -0
- package/vendor/agent-root/src/providers/types/config.ts +58 -0
- package/vendor/agent-root/src/providers/types/errors.ts +323 -0
- package/vendor/agent-root/src/providers/types/index.ts +72 -0
- package/vendor/agent-root/src/providers/types/provider.ts +45 -0
- package/vendor/agent-root/src/providers/types/registry.ts +88 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { FileHistoryStore } from '../file-history-store';
|
|
6
|
+
import type { FileStorageConfig } from '../file-storage-config';
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = [];
|
|
9
|
+
|
|
10
|
+
async function createTempDir(): Promise<string> {
|
|
11
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'renx-file-history-'));
|
|
12
|
+
tempDirs.push(dir);
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createHistoryStore(
|
|
17
|
+
rootDir: string,
|
|
18
|
+
historyDir: string,
|
|
19
|
+
overrides: Partial<FileStorageConfig> = {}
|
|
20
|
+
) {
|
|
21
|
+
return new FileHistoryStore({
|
|
22
|
+
config: {
|
|
23
|
+
rootDir,
|
|
24
|
+
writeBufferDir: path.join(rootDir, 'cache', 'write-buffer'),
|
|
25
|
+
historyDir,
|
|
26
|
+
historyEnabled: true,
|
|
27
|
+
historyMaxPerFile: 20,
|
|
28
|
+
historyMaxAgeDays: 14,
|
|
29
|
+
historyMaxTotalBytes: 1024 * 1024,
|
|
30
|
+
...overrides,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('FileHistoryStore', () => {
|
|
40
|
+
it('stores previous versions and restores a chosen snapshot', async () => {
|
|
41
|
+
const rootDir = await createTempDir();
|
|
42
|
+
const historyDir = path.join(rootDir, 'history');
|
|
43
|
+
const store = createHistoryStore(rootDir, historyDir);
|
|
44
|
+
const targetPath = path.join(rootDir, 'demo.txt');
|
|
45
|
+
|
|
46
|
+
await fs.writeFile(targetPath, 'v1', 'utf8');
|
|
47
|
+
await store.snapshotBeforeWrite({
|
|
48
|
+
targetPath,
|
|
49
|
+
nextContent: 'v2',
|
|
50
|
+
source: 'write_file',
|
|
51
|
+
});
|
|
52
|
+
await fs.writeFile(targetPath, 'v2', 'utf8');
|
|
53
|
+
|
|
54
|
+
await store.snapshotBeforeWrite({
|
|
55
|
+
targetPath,
|
|
56
|
+
nextContent: 'v3',
|
|
57
|
+
source: 'file_edit',
|
|
58
|
+
});
|
|
59
|
+
await fs.writeFile(targetPath, 'v3', 'utf8');
|
|
60
|
+
|
|
61
|
+
const versions = await store.listVersions(targetPath);
|
|
62
|
+
expect(versions).toHaveLength(2);
|
|
63
|
+
expect(versions.map((version) => version.source).sort()).toEqual(['file_edit', 'write_file']);
|
|
64
|
+
|
|
65
|
+
const originalVersion = versions.find((version) => version.source === 'write_file');
|
|
66
|
+
expect(originalVersion).toBeDefined();
|
|
67
|
+
|
|
68
|
+
const restored = await store.restoreVersion(targetPath, originalVersion!.versionId);
|
|
69
|
+
expect(restored).toBe(true);
|
|
70
|
+
expect(await fs.readFile(targetPath, 'utf8')).toBe('v1');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('prunes older snapshots when per-file retention is exceeded', async () => {
|
|
74
|
+
const rootDir = await createTempDir();
|
|
75
|
+
const historyDir = path.join(rootDir, 'history');
|
|
76
|
+
const store = createHistoryStore(rootDir, historyDir, {
|
|
77
|
+
historyMaxPerFile: 1,
|
|
78
|
+
});
|
|
79
|
+
const targetPath = path.join(rootDir, 'retained.txt');
|
|
80
|
+
|
|
81
|
+
await fs.writeFile(targetPath, 'old-1', 'utf8');
|
|
82
|
+
await store.snapshotBeforeWrite({
|
|
83
|
+
targetPath,
|
|
84
|
+
nextContent: 'old-2',
|
|
85
|
+
source: 'write_file',
|
|
86
|
+
});
|
|
87
|
+
await fs.writeFile(targetPath, 'old-2', 'utf8');
|
|
88
|
+
await store.snapshotBeforeWrite({
|
|
89
|
+
targetPath,
|
|
90
|
+
nextContent: 'old-3',
|
|
91
|
+
source: 'write_file',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const versions = await store.listVersions(targetPath);
|
|
95
|
+
expect(versions).toHaveLength(1);
|
|
96
|
+
expect(versions[0].contentHash).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import * as syncFs from 'node:fs';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { getFileStorageConfig, type FileStorageConfig } from './file-storage-config';
|
|
6
|
+
import {
|
|
7
|
+
readJsonFileIfExists,
|
|
8
|
+
writeJsonFileAtomically,
|
|
9
|
+
writeTextFileAtomically,
|
|
10
|
+
} from './file-system';
|
|
11
|
+
|
|
12
|
+
interface FileHistoryManifest {
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
targetPath: string;
|
|
15
|
+
pathKey: string;
|
|
16
|
+
versions: FileHistoryVersion[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FileHistoryVersion {
|
|
20
|
+
versionId: string;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
byteSize: number;
|
|
23
|
+
contentHash: string;
|
|
24
|
+
source: string;
|
|
25
|
+
snapshotFile: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SnapshotBeforeWriteInput {
|
|
29
|
+
targetPath: string;
|
|
30
|
+
nextContent: string;
|
|
31
|
+
source: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FileHistoryStoreOptions {
|
|
35
|
+
config?: FileStorageConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class FileHistoryStore {
|
|
39
|
+
private readonly config: FileStorageConfig;
|
|
40
|
+
|
|
41
|
+
constructor(options: FileHistoryStoreOptions = {}) {
|
|
42
|
+
this.config = options.config ?? getFileStorageConfig();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async snapshotBeforeWrite(input: SnapshotBeforeWriteInput): Promise<FileHistoryVersion | null> {
|
|
46
|
+
if (!this.config.historyEnabled) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const targetPath = this.normalizeTargetPath(input.targetPath);
|
|
51
|
+
let currentContent: string;
|
|
52
|
+
try {
|
|
53
|
+
currentContent = await fs.readFile(targetPath, 'utf8');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (this.isNotFoundError(error)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (currentContent === input.nextContent) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const manifest = (await this.loadManifest(targetPath)) || this.createManifest(targetPath);
|
|
66
|
+
const createdAt = Date.now();
|
|
67
|
+
const versionId = `v_${createdAt}_${randomUUID().slice(0, 8)}`;
|
|
68
|
+
const version: FileHistoryVersion = {
|
|
69
|
+
versionId,
|
|
70
|
+
createdAt,
|
|
71
|
+
byteSize: Buffer.byteLength(currentContent, 'utf8'),
|
|
72
|
+
contentHash: createHash('sha256').update(currentContent).digest('hex'),
|
|
73
|
+
source: input.source,
|
|
74
|
+
snapshotFile: path.join(manifest.pathKey, `${versionId}.snapshot`),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await this.writeSnapshot(version.snapshotFile, currentContent);
|
|
78
|
+
manifest.versions.push(version);
|
|
79
|
+
|
|
80
|
+
await this.pruneManifest(manifest);
|
|
81
|
+
await this.saveManifest(manifest);
|
|
82
|
+
await this.pruneTotalSize();
|
|
83
|
+
|
|
84
|
+
return version;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async listVersions(targetPath: string): Promise<FileHistoryVersion[]> {
|
|
88
|
+
const manifest = await this.loadManifest(targetPath);
|
|
89
|
+
return manifest
|
|
90
|
+
? [...manifest.versions].sort((left, right) => right.createdAt - left.createdAt)
|
|
91
|
+
: [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async restoreVersion(targetPath: string, versionId: string): Promise<boolean> {
|
|
95
|
+
const normalizedTargetPath = this.normalizeTargetPath(targetPath);
|
|
96
|
+
const manifest = await this.loadManifest(normalizedTargetPath);
|
|
97
|
+
if (!manifest) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const version = manifest.versions.find((entry) => entry.versionId === versionId);
|
|
102
|
+
if (!version) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const snapshotPath = this.resolveSnapshotPath(version.snapshotFile);
|
|
107
|
+
const content = await fs.readFile(snapshotPath, 'utf8');
|
|
108
|
+
await this.snapshotBeforeWrite({
|
|
109
|
+
targetPath: normalizedTargetPath,
|
|
110
|
+
nextContent: content,
|
|
111
|
+
source: 'file_history_restore',
|
|
112
|
+
});
|
|
113
|
+
await writeTextFileAtomically(normalizedTargetPath, content);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private createManifest(targetPath: string): FileHistoryManifest {
|
|
118
|
+
return {
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
targetPath,
|
|
121
|
+
pathKey: this.pathKey(targetPath),
|
|
122
|
+
versions: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async loadManifest(targetPath: string): Promise<FileHistoryManifest | null> {
|
|
127
|
+
const resolvedTargetPath = this.normalizeTargetPath(targetPath);
|
|
128
|
+
const manifestPath = this.manifestPathByTarget(resolvedTargetPath);
|
|
129
|
+
return readJsonFileIfExists<FileHistoryManifest>(manifestPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async saveManifest(manifest: FileHistoryManifest): Promise<void> {
|
|
133
|
+
await writeJsonFileAtomically(this.manifestPath(manifest.pathKey), manifest);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async writeSnapshot(snapshotFile: string, content: string): Promise<void> {
|
|
137
|
+
const snapshotPath = this.resolveSnapshotPath(snapshotFile);
|
|
138
|
+
await writeTextFileAtomically(snapshotPath, content);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async pruneManifest(manifest: FileHistoryManifest): Promise<void> {
|
|
142
|
+
let versions = [...manifest.versions].sort((left, right) => left.createdAt - right.createdAt);
|
|
143
|
+
const maxPerFile = this.config.historyMaxPerFile;
|
|
144
|
+
const maxAgeDays = this.config.historyMaxAgeDays;
|
|
145
|
+
|
|
146
|
+
if (maxAgeDays > 0) {
|
|
147
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
148
|
+
const removed = versions.filter((entry) => entry.createdAt < cutoff);
|
|
149
|
+
versions = versions.filter((entry) => entry.createdAt >= cutoff);
|
|
150
|
+
await this.deleteSnapshots(removed);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (maxPerFile > 0 && versions.length > maxPerFile) {
|
|
154
|
+
const overflow = versions.length - maxPerFile;
|
|
155
|
+
const removed = versions.slice(0, overflow);
|
|
156
|
+
versions = versions.slice(overflow);
|
|
157
|
+
await this.deleteSnapshots(removed);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
manifest.versions = versions;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async pruneTotalSize(): Promise<void> {
|
|
164
|
+
const maxTotalBytes = this.config.historyMaxTotalBytes;
|
|
165
|
+
if (maxTotalBytes <= 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const manifestsDir = this.manifestsDir();
|
|
170
|
+
let manifestFiles: string[] = [];
|
|
171
|
+
try {
|
|
172
|
+
manifestFiles = await fs.readdir(manifestsDir);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (this.isNotFoundError(error)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const manifests = (
|
|
181
|
+
await Promise.all(
|
|
182
|
+
manifestFiles
|
|
183
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
184
|
+
.map(async (entry) =>
|
|
185
|
+
readJsonFileIfExists<FileHistoryManifest>(path.join(manifestsDir, entry))
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
).filter((manifest): manifest is FileHistoryManifest => manifest !== null);
|
|
189
|
+
|
|
190
|
+
const allVersions = manifests
|
|
191
|
+
.flatMap((manifest) =>
|
|
192
|
+
manifest.versions.map((version) => ({
|
|
193
|
+
manifest,
|
|
194
|
+
version,
|
|
195
|
+
}))
|
|
196
|
+
)
|
|
197
|
+
.sort((left, right) => left.version.createdAt - right.version.createdAt);
|
|
198
|
+
|
|
199
|
+
let totalBytes = allVersions.reduce((sum, item) => sum + item.version.byteSize, 0);
|
|
200
|
+
if (totalBytes <= maxTotalBytes) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const removals = new Map<string, Set<string>>();
|
|
205
|
+
const snapshotsToDelete: FileHistoryVersion[] = [];
|
|
206
|
+
|
|
207
|
+
for (const item of allVersions) {
|
|
208
|
+
if (totalBytes <= maxTotalBytes) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
totalBytes -= item.version.byteSize;
|
|
212
|
+
snapshotsToDelete.push(item.version);
|
|
213
|
+
const manifestRemovals = removals.get(item.manifest.pathKey) || new Set<string>();
|
|
214
|
+
manifestRemovals.add(item.version.versionId);
|
|
215
|
+
removals.set(item.manifest.pathKey, manifestRemovals);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await this.deleteSnapshots(snapshotsToDelete);
|
|
219
|
+
|
|
220
|
+
await Promise.all(
|
|
221
|
+
manifests.map(async (manifest) => {
|
|
222
|
+
const manifestRemovals = removals.get(manifest.pathKey);
|
|
223
|
+
if (!manifestRemovals || manifestRemovals.size === 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
manifest.versions = manifest.versions.filter(
|
|
227
|
+
(version) => !manifestRemovals.has(version.versionId)
|
|
228
|
+
);
|
|
229
|
+
await this.saveManifest(manifest);
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async deleteSnapshots(versions: FileHistoryVersion[]): Promise<void> {
|
|
235
|
+
await Promise.all(
|
|
236
|
+
versions.map((version) =>
|
|
237
|
+
fs.rm(this.resolveSnapshotPath(version.snapshotFile), { force: true })
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private manifestPathByTarget(targetPath: string): string {
|
|
243
|
+
return this.manifestPath(this.pathKey(targetPath));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private manifestPath(pathKey: string): string {
|
|
247
|
+
return path.join(this.manifestsDir(), `${pathKey}.json`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private manifestsDir(): string {
|
|
251
|
+
return path.join(this.config.historyDir, 'manifests');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private resolveSnapshotPath(snapshotFile: string): string {
|
|
255
|
+
return path.join(this.config.historyDir, 'snapshots', snapshotFile);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private pathKey(targetPath: string): string {
|
|
259
|
+
return createHash('sha256')
|
|
260
|
+
.update(this.normalizeTargetPath(targetPath))
|
|
261
|
+
.digest('hex')
|
|
262
|
+
.slice(0, 24);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private normalizeTargetPath(targetPath: string): string {
|
|
266
|
+
const absolute = path.resolve(targetPath);
|
|
267
|
+
try {
|
|
268
|
+
return syncFs.realpathSync(absolute);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
271
|
+
if (nodeError.code !== 'ENOENT' && nodeError.code !== 'ENOTDIR') {
|
|
272
|
+
return absolute;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let current = absolute;
|
|
277
|
+
const trailingSegments: string[] = [];
|
|
278
|
+
|
|
279
|
+
for (;;) {
|
|
280
|
+
try {
|
|
281
|
+
const realCurrent = syncFs.realpathSync(current);
|
|
282
|
+
if (trailingSegments.length === 0) {
|
|
283
|
+
return realCurrent;
|
|
284
|
+
}
|
|
285
|
+
return path.join(realCurrent, ...trailingSegments.reverse());
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
288
|
+
if (nodeError.code !== 'ENOENT' && nodeError.code !== 'ENOTDIR') {
|
|
289
|
+
return absolute;
|
|
290
|
+
}
|
|
291
|
+
const parent = path.dirname(current);
|
|
292
|
+
if (parent === current) {
|
|
293
|
+
return absolute;
|
|
294
|
+
}
|
|
295
|
+
trailingSegments.push(path.basename(current));
|
|
296
|
+
current = parent;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private isNotFoundError(error: unknown): boolean {
|
|
302
|
+
return (
|
|
303
|
+
typeof error === 'object' &&
|
|
304
|
+
error !== null &&
|
|
305
|
+
'code' in error &&
|
|
306
|
+
(error as { code?: string }).code === 'ENOENT'
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function createConfiguredFileHistoryStore(): FileHistoryStore {
|
|
312
|
+
return new FileHistoryStore();
|
|
313
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { resolveRenxStorageRoot } from '../../config/paths';
|
|
3
|
+
|
|
4
|
+
export const AGENT_FILE_HISTORY_ENABLED_ENV = 'AGENT_FILE_HISTORY_ENABLED';
|
|
5
|
+
export const AGENT_FILE_HISTORY_MAX_PER_FILE_ENV = 'AGENT_FILE_HISTORY_MAX_PER_FILE';
|
|
6
|
+
export const AGENT_FILE_HISTORY_MAX_AGE_DAYS_ENV = 'AGENT_FILE_HISTORY_MAX_AGE_DAYS';
|
|
7
|
+
export const AGENT_FILE_HISTORY_MAX_TOTAL_MB_ENV = 'AGENT_FILE_HISTORY_MAX_TOTAL_MB';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_WRITE_BUFFER_SUBDIR = path.join('cache', 'write-buffer');
|
|
10
|
+
const DEFAULT_HISTORY_SUBDIR = path.join('history', 'file-versions');
|
|
11
|
+
const DEFAULT_HISTORY_ENABLED = true;
|
|
12
|
+
const DEFAULT_HISTORY_MAX_PER_FILE = 20;
|
|
13
|
+
const DEFAULT_HISTORY_MAX_AGE_DAYS = 14;
|
|
14
|
+
const DEFAULT_HISTORY_MAX_TOTAL_MB = 500;
|
|
15
|
+
|
|
16
|
+
export interface FileStorageConfig {
|
|
17
|
+
rootDir: string;
|
|
18
|
+
writeBufferDir: string;
|
|
19
|
+
historyDir: string;
|
|
20
|
+
historyEnabled: boolean;
|
|
21
|
+
historyMaxPerFile: number;
|
|
22
|
+
historyMaxAgeDays: number;
|
|
23
|
+
historyMaxTotalBytes: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseBoolean(raw: string | undefined, fallback: boolean): boolean {
|
|
27
|
+
if (!raw || raw.trim().length === 0) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalized = raw.trim().toLowerCase();
|
|
32
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseNonNegativeInteger(raw: string | undefined, fallback: number): number {
|
|
42
|
+
if (!raw || raw.trim().length === 0) {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
47
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getFileStorageConfig(): FileStorageConfig {
|
|
54
|
+
const rootDir = resolveRenxStorageRoot(process.env);
|
|
55
|
+
const writeBufferDir = path.join(rootDir, DEFAULT_WRITE_BUFFER_SUBDIR);
|
|
56
|
+
const historyDir = path.join(rootDir, DEFAULT_HISTORY_SUBDIR);
|
|
57
|
+
const historyEnabled = parseBoolean(
|
|
58
|
+
process.env[AGENT_FILE_HISTORY_ENABLED_ENV],
|
|
59
|
+
DEFAULT_HISTORY_ENABLED
|
|
60
|
+
);
|
|
61
|
+
const historyMaxPerFile = parseNonNegativeInteger(
|
|
62
|
+
process.env[AGENT_FILE_HISTORY_MAX_PER_FILE_ENV],
|
|
63
|
+
DEFAULT_HISTORY_MAX_PER_FILE
|
|
64
|
+
);
|
|
65
|
+
const historyMaxAgeDays = parseNonNegativeInteger(
|
|
66
|
+
process.env[AGENT_FILE_HISTORY_MAX_AGE_DAYS_ENV],
|
|
67
|
+
DEFAULT_HISTORY_MAX_AGE_DAYS
|
|
68
|
+
);
|
|
69
|
+
const historyMaxTotalMb = parseNonNegativeInteger(
|
|
70
|
+
process.env[AGENT_FILE_HISTORY_MAX_TOTAL_MB_ENV],
|
|
71
|
+
DEFAULT_HISTORY_MAX_TOTAL_MB
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
rootDir,
|
|
76
|
+
writeBufferDir,
|
|
77
|
+
historyDir,
|
|
78
|
+
historyEnabled,
|
|
79
|
+
historyMaxPerFile,
|
|
80
|
+
historyMaxAgeDays,
|
|
81
|
+
historyMaxTotalBytes: historyMaxTotalMb * 1024 * 1024,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resolveWriteBufferBaseDir(override?: string): string {
|
|
86
|
+
if (override && override.trim().length > 0) {
|
|
87
|
+
return path.resolve(override);
|
|
88
|
+
}
|
|
89
|
+
return getFileStorageConfig().writeBufferDir;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getWriteBufferCandidateDirs(primaryDir?: string): string[] {
|
|
93
|
+
return [resolveWriteBufferBaseDir(primaryDir)];
|
|
94
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
export async function writeTextFileAtomically(targetPath: string, content: string): Promise<void> {
|
|
6
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
7
|
+
const tempPath = `${targetPath}.tmp.${randomUUID().slice(0, 8)}`;
|
|
8
|
+
await fs.writeFile(tempPath, content, 'utf8');
|
|
9
|
+
await fs.rename(tempPath, targetPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function writeJsonFileAtomically(targetPath: string, value: unknown): Promise<void> {
|
|
13
|
+
await writeTextFileAtomically(targetPath, JSON.stringify(value, null, 2));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readJsonFileIfExists<T>(targetPath: string): Promise<T | null> {
|
|
17
|
+
try {
|
|
18
|
+
const content = await fs.readFile(targetPath, 'utf8');
|
|
19
|
+
return JSON.parse(content) as T;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (
|
|
22
|
+
typeof error === 'object' &&
|
|
23
|
+
error !== null &&
|
|
24
|
+
'code' in error &&
|
|
25
|
+
(error as { code?: string }).code === 'ENOENT'
|
|
26
|
+
) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createConfiguredFileHistoryStore, FileHistoryStore } from './file-history-store';
|
|
2
|
+
import { writeTextFileAtomically } from './file-system';
|
|
3
|
+
|
|
4
|
+
export interface WriteTextFileWithHistoryOptions {
|
|
5
|
+
source: string;
|
|
6
|
+
historyStore?: FileHistoryStore;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function writeTextFileWithHistory(
|
|
10
|
+
targetPath: string,
|
|
11
|
+
content: string,
|
|
12
|
+
options: WriteTextFileWithHistoryOptions
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const historyStore = options.historyStore ?? createConfiguredFileHistoryStore();
|
|
15
|
+
await historyStore.snapshotBeforeWrite({
|
|
16
|
+
targetPath,
|
|
17
|
+
nextContent: content,
|
|
18
|
+
source: options.source,
|
|
19
|
+
});
|
|
20
|
+
await writeTextFileAtomically(targetPath, content);
|
|
21
|
+
}
|