mova-claude-import 0.1.1
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/.github/workflows/ci.yml +42 -0
- package/.github/workflows/release-dry-run.yml +40 -0
- package/.github/workflows/release-publish.yml +43 -0
- package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +3 -0
- package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_control_apply/proj/.claude/settings.json +30 -0
- package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +3 -0
- package/.tmp_test_control_apply/proj/.mcp.json +3 -0
- package/.tmp_test_control_apply/proj/CLAUDE.md +13 -0
- package/.tmp_test_control_apply/proj/MOVA.md +3 -0
- package/.tmp_test_control_check/proj/.mcp.json +1 -0
- package/.tmp_test_control_check/proj/CLAUDE.md +1 -0
- package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +114 -0
- package/.tmp_test_control_prefill/out1/prefill_report_v0.json +13 -0
- package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +114 -0
- package/.tmp_test_control_prefill/out2/prefill_report_v0.json +13 -0
- package/.tmp_test_overlay/proj/.claude/skills/a.md +1 -0
- package/.tmp_test_overlay/proj/.mcp.json +1 -0
- package/.tmp_test_overlay/proj/CLAUDE.md +1 -0
- package/.tmp_test_profile/proj/.claude/skills/a.md +1 -0
- package/.tmp_test_profile/proj/.mcp.json +1 -0
- package/.tmp_test_profile/proj/CLAUDE.md +1 -0
- package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +3 -0
- package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_scaffold_apply/proj/.claude/settings.json +30 -0
- package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +3 -0
- package/.tmp_test_scaffold_apply/proj/.mcp.json +3 -0
- package/.tmp_test_scaffold_apply/proj/CLAUDE.md +13 -0
- package/.tmp_test_scaffold_apply/proj/MOVA.md +3 -0
- package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +10 -0
- package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +20 -0
- package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +20 -0
- package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +32 -0
- package/.tmp_test_zip/out1/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_zip/out1/.claude/commands/example_command.md +3 -0
- package/.tmp_test_zip/out1/.claude/commands/mova_context.md +4 -0
- package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +4 -0
- package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +6 -0
- package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_zip/out1/.claude/settings.json +30 -0
- package/.tmp_test_zip/out1/.claude/settings.local.example.json +3 -0
- package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +1 -0
- package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +11 -0
- package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +8 -0
- package/.tmp_test_zip/out1/.mcp.json +3 -0
- package/.tmp_test_zip/out1/CLAUDE.md +4 -0
- package/.tmp_test_zip/out1/MOVA.md +10 -0
- package/.tmp_test_zip/out1/export.zip +0 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +10 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +8 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +4 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +11 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +80 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +32 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +33 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +38 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +6 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +4 -0
- package/.tmp_test_zip/out2/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_zip/out2/.claude/commands/example_command.md +3 -0
- package/.tmp_test_zip/out2/.claude/commands/mova_context.md +4 -0
- package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +4 -0
- package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +6 -0
- package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_zip/out2/.claude/settings.json +30 -0
- package/.tmp_test_zip/out2/.claude/settings.local.example.json +3 -0
- package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +1 -0
- package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +11 -0
- package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +8 -0
- package/.tmp_test_zip/out2/.mcp.json +3 -0
- package/.tmp_test_zip/out2/CLAUDE.md +4 -0
- package/.tmp_test_zip/out2/MOVA.md +10 -0
- package/.tmp_test_zip/out2/export.zip +0 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +10 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +8 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +4 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +11 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +80 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +32 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +33 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +38 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +6 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +4 -0
- package/.tmp_test_zip/proj/.claude/skills/a.md +1 -0
- package/.tmp_test_zip/proj/.mcp.json +1 -0
- package/.tmp_test_zip/proj/CLAUDE.md +1 -0
- package/README.md +86 -0
- package/create_files.js +52 -0
- package/dist/anthropic_profile_v0.d.ts +2 -0
- package/dist/anthropic_profile_v0.js +66 -0
- package/dist/claude_profile_scaffold_v0.d.ts +2 -0
- package/dist/claude_profile_scaffold_v0.js +110 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +163 -0
- package/dist/cli_entry.d.ts +1 -0
- package/dist/cli_entry.js +1 -0
- package/dist/control_apply_v0.d.ts +6 -0
- package/dist/control_apply_v0.js +86 -0
- package/dist/control_check_v0.d.ts +7 -0
- package/dist/control_check_v0.js +80 -0
- package/dist/control_contracts_v0.d.ts +8 -0
- package/dist/control_contracts_v0.js +17 -0
- package/dist/control_prefill_v0.d.ts +6 -0
- package/dist/control_prefill_v0.js +61 -0
- package/dist/export_zip_v0.d.ts +8 -0
- package/dist/export_zip_v0.js +79 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +2 -0
- package/dist/init_v0.d.ts +7 -0
- package/dist/init_v0.js +47 -0
- package/dist/input_policy_v0.d.ts +26 -0
- package/dist/input_policy_v0.js +76 -0
- package/dist/lint_v0.d.ts +18 -0
- package/dist/lint_v0.js +131 -0
- package/dist/mova_overlay_v0.d.ts +14 -0
- package/dist/mova_overlay_v0.js +65 -0
- package/dist/mova_spec_bindings_v0.d.ts +5 -0
- package/dist/mova_spec_bindings_v0.js +5 -0
- package/dist/quality_v0.d.ts +1 -0
- package/dist/quality_v0.js +223 -0
- package/dist/redaction.d.ts +14 -0
- package/dist/redaction.js +52 -0
- package/dist/run_import.d.ts +2 -0
- package/dist/run_import.js +479 -0
- package/dist/stable_json.d.ts +1 -0
- package/dist/stable_json.js +15 -0
- package/docs/ANTHROPIC_PROFILE_v0.md +38 -0
- package/docs/COMPATIBILITY_MATRIX.md +25 -0
- package/docs/CONTROL_PROFILE_GUIDE_v0.md +40 -0
- package/docs/IMPORT_SPEC_v0.md +30 -0
- package/docs/MOVA_SPEC_BINDINGS.json +21 -0
- package/docs/MOVA_SPEC_BINDINGS.md +11 -0
- package/docs/OPERATOR_GUIDE_v0.md +43 -0
- package/docs/SECURITY_MODEL_v0.md +20 -0
- package/examples/control_profile_min.json +37 -0
- package/examples/control_profile_standard.json +81 -0
- package/examples/control_profile_strict.json +68 -0
- package/fixtures/neg/bad_skill_structure/.claude/skills/bad/README.md +3 -0
- package/fixtures/neg/bad_skill_structure/CLAUDE.md +3 -0
- package/fixtures/neg/local_settings_present/.claude/settings.local.json +3 -0
- package/fixtures/neg/local_settings_present/.claude/skills/alpha/SKILL.md +6 -0
- package/fixtures/neg/local_settings_present/CLAUDE.md +3 -0
- package/fixtures/neg/secret_leak/.claude/skills/alpha/SKILL.md +6 -0
- package/fixtures/neg/secret_leak/.mcp.json +3 -0
- package/fixtures/neg/secret_leak/CLAUDE.md +3 -0
- package/fixtures/neg/strict_denied_local/.claude/settings.local.json +3 -0
- package/fixtures/neg/strict_denied_local/CLAUDE.md +3 -0
- package/fixtures/pos/basic/.claude/skills/alpha/SKILL.md +8 -0
- package/fixtures/pos/basic/.mcp.json +3 -0
- package/fixtures/pos/basic/CLAUDE.md +3 -0
- package/fixtures/pos/control_basic_project/.mcp.json +3 -0
- package/fixtures/pos/control_basic_project/CLAUDE.md +3 -0
- package/fixtures/pos/control_profile_filled/claude_control_profile_v0.json +18 -0
- package/fixtures/pos/full_scaffold_roundtrip/README.md +1 -0
- package/package.json +39 -0
- package/schemas/claude_control/v0/ds/ds.claude_control_mapping_v0.json +227 -0
- package/schemas/claude_control/v0/ds/ds.claude_control_profile_v0.json +114 -0
- package/schemas/claude_control/v0/env/env.claude_control_apply_v0.json +170 -0
- package/schemas/claude_control/v0/env/env.claude_control_import_prefill_v0.json +171 -0
- package/schemas/claude_control/v0/global/global.claude_control_precedence_v0.json +58 -0
- package/schemas/claude_control/v0/global/global.claude_control_vocab_v0.json +98 -0
- package/schemas/ds.claude_import.instruction_profile_v0.schema.json +31 -0
- package/schemas/ds.claude_import.mcp_servers_v0.schema.json +47 -0
- package/schemas/ds.claude_import.skills_catalog_v0.schema.json +48 -0
- package/src/anthropic_profile_v0.ts +68 -0
- package/src/claude_profile_scaffold_v0.ts +117 -0
- package/src/cli.ts +160 -0
- package/src/cli_entry.ts +1 -0
- package/src/control_apply_v0.ts +108 -0
- package/src/control_check_v0.ts +98 -0
- package/src/control_contracts_v0.ts +26 -0
- package/src/control_prefill_v0.ts +74 -0
- package/src/export_zip_v0.ts +90 -0
- package/src/index.ts +29 -0
- package/src/init_v0.ts +59 -0
- package/src/input_policy_v0.ts +103 -0
- package/src/lint_v0.ts +151 -0
- package/src/mova_overlay_v0.ts +79 -0
- package/src/mova_spec_bindings_v0.ts +5 -0
- package/src/quality_v0.ts +264 -0
- package/src/redaction.ts +63 -0
- package/src/run_import.ts +526 -0
- package/src/stable_json.ts +15 -0
- package/test/control_apply_apply.test.js +40 -0
- package/test/control_check_preview.test.js +38 -0
- package/test/control_prefill.test.js +30 -0
- package/test/demo_v0_smoke.test.js +37 -0
- package/test/export_zip_determinism.test.js +41 -0
- package/test/import_determinism.test.js +53 -0
- package/test/init_v0.test.js +37 -0
- package/test/overlay_v0_output.test.js +38 -0
- package/test/profile_v0_output.test.js +44 -0
- package/test/scaffold_v0_output.test.js +64 -0
- package/test/strict_input_policy.test.js +45 -0
- package/tools/demo_v0.mjs +98 -0
- package/tools/deps_audit_v0.mjs +123 -0
- package/tools/write_mova_spec_bindings_v0.mjs +122 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import type { ImportOptions, ImportResult } from "./index.js";
|
|
7
|
+
import { redactText, redactJson, RedactionHit } from "./redaction.js";
|
|
8
|
+
import Ajv from "ajv";
|
|
9
|
+
import addFormats from "ajv-formats";
|
|
10
|
+
import { getAnthropicProfileV0Files } from "./anthropic_profile_v0.js";
|
|
11
|
+
import { lintV0, type LintReportV0 } from "./lint_v0.js";
|
|
12
|
+
import { stableStringify } from "./stable_json.js";
|
|
13
|
+
import { createExportZipV0 } from "./export_zip_v0.js";
|
|
14
|
+
import { buildMovaOverlayV0, buildMovaControlEntryV0, MOVA_CONTROL_ENTRY_MARKER } from "./mova_overlay_v0.js";
|
|
15
|
+
import { scanInputPolicyV0 } from "./input_policy_v0.js";
|
|
16
|
+
import { EvidenceWriter } from "@leryk1981/mova-core-engine";
|
|
17
|
+
import { MOVA_SPEC_BINDINGS_V0 } from "./mova_spec_bindings_v0.js";
|
|
18
|
+
import { writeCleanClaudeProfileScaffoldV0 } from "./claude_profile_scaffold_v0.js";
|
|
19
|
+
|
|
20
|
+
type Found = {
|
|
21
|
+
claudeMdPath?: string;
|
|
22
|
+
mcpJsonPath?: string;
|
|
23
|
+
skillFiles: string[];
|
|
24
|
+
skipped: Array<{ path: string; reason: string }>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function exists(p: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
await fs.stat(p);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function sha256File(p: string): Promise<string> {
|
|
37
|
+
const buf = await fs.readFile(p);
|
|
38
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readToolVersion(): Promise<string> {
|
|
42
|
+
try {
|
|
43
|
+
const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
44
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
return typeof parsed.version === "string" ? parsed.version : "0.0.0";
|
|
47
|
+
} catch {
|
|
48
|
+
return "0.0.0";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isLocalExcluded(rel: string, includeLocal: boolean): boolean {
|
|
53
|
+
if (includeLocal) return false;
|
|
54
|
+
const base = path.basename(rel);
|
|
55
|
+
if (base === "CLAUDE.local.md") return true;
|
|
56
|
+
if (base.includes(".local.")) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function scanProject(opts: ImportOptions): Promise<Found> {
|
|
61
|
+
const projectDir = path.resolve(opts.projectDir);
|
|
62
|
+
const skipped: Found["skipped"] = [];
|
|
63
|
+
const found: Found = { skillFiles: [], skipped };
|
|
64
|
+
|
|
65
|
+
const claudeMd = path.join(projectDir, "CLAUDE.md");
|
|
66
|
+
if (await exists(claudeMd)) found.claudeMdPath = claudeMd;
|
|
67
|
+
|
|
68
|
+
const mcpJson = path.join(projectDir, ".mcp.json");
|
|
69
|
+
if (await exists(mcpJson)) found.mcpJsonPath = mcpJson;
|
|
70
|
+
|
|
71
|
+
const skillsRoot = path.join(projectDir, ".claude", "skills");
|
|
72
|
+
if (await exists(skillsRoot)) {
|
|
73
|
+
const stack = [skillsRoot];
|
|
74
|
+
while (stack.length) {
|
|
75
|
+
const dir = stack.pop()!;
|
|
76
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
const abs = path.join(dir, e.name);
|
|
79
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
80
|
+
if (isLocalExcluded(rel, opts.includeLocal)) {
|
|
81
|
+
skipped.push({ path: rel, reason: "excluded_by_default_local" });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (e.isDirectory()) stack.push(abs);
|
|
85
|
+
else if (e.isFile() && e.name.toLowerCase().endsWith(".md"))
|
|
86
|
+
found.skillFiles.push(abs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// placeholder for user settings exclusion
|
|
92
|
+
if (!opts.includeUserSettings) {
|
|
93
|
+
skipped.push({ path: "~/.claude/*", reason: "excluded_by_default_user_settings" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
found.skillFiles.sort((a, b) => a.localeCompare(b));
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function computeRunId(hashes: string[]): string {
|
|
101
|
+
const h = crypto.createHash("sha256");
|
|
102
|
+
for (const x of hashes.sort()) h.update(x);
|
|
103
|
+
return h.digest("hex").slice(0, 16);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeSkillDir(rel: string): string {
|
|
107
|
+
const base = rel.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
108
|
+
const withoutRoot = base.replace(/^\.claude\/skills\//, "");
|
|
109
|
+
return withoutRoot.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function writeJsonFile(absPath: string, obj: any) {
|
|
113
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
114
|
+
await fs.writeFile(absPath, stableStringify(obj) + "\n", "utf8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function writeTextFile(absPath: string, content: string) {
|
|
118
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
119
|
+
await fs.writeFile(absPath, content, "utf8");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Load a JSON file and redact it */
|
|
123
|
+
async function loadAndRedactJson(p: string) {
|
|
124
|
+
const raw = await fs.readFile(p, "utf8");
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
const { redacted, hits } = redactJson(parsed);
|
|
127
|
+
return { raw, parsed, redacted, hits };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function ensureClaudeControlEntry(claudePath: string, block: string, marker: string) {
|
|
131
|
+
const content = await fs.readFile(claudePath, "utf8");
|
|
132
|
+
if (content.includes(marker)) return;
|
|
133
|
+
await fs.writeFile(claudePath, `${block}\n${content}`, "utf8");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function orderedObject(obj: any) {
|
|
137
|
+
return JSON.parse(stableStringify(obj));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function writeEvidenceArtifact(writer: EvidenceWriter, baseDir: string, rel: string, obj: any) {
|
|
141
|
+
const target = await writer.writeArtifact(baseDir, rel, orderedObject(obj));
|
|
142
|
+
await fs.writeFile(target, stableStringify(obj) + "\n", "utf8");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function loadMovaSpecSchema(fileName: string) {
|
|
146
|
+
const require = createRequire(import.meta.url);
|
|
147
|
+
const pkgPath = require.resolve("@leryk1981/mova-spec/package.json");
|
|
148
|
+
const schemaPath = path.join(path.dirname(pkgPath), "schemas", fileName);
|
|
149
|
+
const raw = await fs.readFile(schemaPath, "utf8");
|
|
150
|
+
return JSON.parse(raw);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function loadMovaSpecSchemaById(schemaId: string) {
|
|
154
|
+
const fileName = schemaId.split("/").pop();
|
|
155
|
+
if (!fileName) throw new Error(`Invalid schema id: ${schemaId}`);
|
|
156
|
+
return loadMovaSpecSchema(fileName);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
160
|
+
const projectDir = path.resolve(opts.projectDir);
|
|
161
|
+
const outRoot = path.resolve(opts.outDir);
|
|
162
|
+
const inputPolicy = await scanInputPolicyV0(projectDir, {
|
|
163
|
+
strict: opts.strict,
|
|
164
|
+
include_local: opts.includeLocal,
|
|
165
|
+
});
|
|
166
|
+
const found = await scanProject(opts);
|
|
167
|
+
|
|
168
|
+
const inputs: Array<{ rel: string; sha256: string }> = [];
|
|
169
|
+
const redactionHits: RedactionHit[] = [];
|
|
170
|
+
|
|
171
|
+
/** Process a text file – redact and record */
|
|
172
|
+
async function processTextFile(rel: string, absPath: string) {
|
|
173
|
+
const raw = await fs.readFile(absPath, "utf8");
|
|
174
|
+
const { redacted, hits } = redactText(raw);
|
|
175
|
+
inputs.push({ rel, sha256: await sha256File(absPath) });
|
|
176
|
+
redactionHits.push(...hits);
|
|
177
|
+
return redacted;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// CLAUDE.md
|
|
181
|
+
let claudeMdRedacted = "";
|
|
182
|
+
if (found.claudeMdPath) {
|
|
183
|
+
claudeMdRedacted = await processTextFile("CLAUDE.md", found.claudeMdPath);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// .mcp.json
|
|
187
|
+
let mcpJsonRedacted = "";
|
|
188
|
+
let mcpJsonParsed: any | undefined;
|
|
189
|
+
if (found.mcpJsonPath) {
|
|
190
|
+
const { parsed, redacted, hits } = await loadAndRedactJson(found.mcpJsonPath);
|
|
191
|
+
mcpJsonParsed = parsed;
|
|
192
|
+
mcpJsonRedacted = stableStringify(redacted);
|
|
193
|
+
redactionHits.push(...hits);
|
|
194
|
+
inputs.push({
|
|
195
|
+
rel: ".mcp.json",
|
|
196
|
+
sha256: await sha256File(found.mcpJsonPath),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// skill files
|
|
201
|
+
const skillRedactedMap: Record<string, string> = {};
|
|
202
|
+
for (const f of found.skillFiles) {
|
|
203
|
+
const rel = path.relative(projectDir, f).replace(/\\/g, "/");
|
|
204
|
+
const redacted = await processTextFile(rel, f);
|
|
205
|
+
skillRedactedMap[rel] = redacted;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const runId = computeRunId(inputs.map((x) => `${x.rel}:${x.sha256}`));
|
|
209
|
+
|
|
210
|
+
const movaBase = path.join(outRoot, "mova", "claude_import", "v0");
|
|
211
|
+
const evidenceWriter = new EvidenceWriter();
|
|
212
|
+
const overlayParams = {
|
|
213
|
+
contractsDir: "mova/claude_import/v0/contracts/",
|
|
214
|
+
artifactsDir: "mova/claude_import/v0/",
|
|
215
|
+
instructionProfileFile: "instruction_profile_v0.json",
|
|
216
|
+
skillsCatalogFile: "skills_catalog_v0.json",
|
|
217
|
+
mcpServersFile: "mcp_servers_v0.json",
|
|
218
|
+
lintReportFile: "lint_report_v0.json",
|
|
219
|
+
qualityReportFile: "quality_report_v0.json",
|
|
220
|
+
exportManifestFile: "export_manifest_v0.json",
|
|
221
|
+
};
|
|
222
|
+
const normalizedSkills = Object.entries(skillRedactedMap).map(([rel, body]) => ({
|
|
223
|
+
rel,
|
|
224
|
+
body,
|
|
225
|
+
normDir: normalizeSkillDir(rel),
|
|
226
|
+
title: path.basename(rel, ".md"),
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
if (!opts.dryRun) {
|
|
230
|
+
const toolVersion = await readToolVersion();
|
|
231
|
+
const versionInfo = {
|
|
232
|
+
tool_name: "mova-claude-import",
|
|
233
|
+
tool_version: toolVersion,
|
|
234
|
+
profile_version: "anthropic_profile_v0",
|
|
235
|
+
overlay_version: "mova_control_overlay_v0",
|
|
236
|
+
input_policy_version: "input_policy_v0",
|
|
237
|
+
lint_version: "lint_v0",
|
|
238
|
+
quality_version: "quality_v0",
|
|
239
|
+
export_zip_version: "export_zip_v0",
|
|
240
|
+
};
|
|
241
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "VERSION.json", versionInfo);
|
|
242
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "input_policy_report_v0.json", inputPolicy);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (opts.strict && !inputPolicy.ok) {
|
|
246
|
+
const deniedRunId = computeRunId(
|
|
247
|
+
inputPolicy.denied.map((d) => `${d.path}:${d.kind}:${d.reason}`).sort()
|
|
248
|
+
);
|
|
249
|
+
if (!opts.dryRun) {
|
|
250
|
+
const manifest = {
|
|
251
|
+
tool: "mova-claude-import",
|
|
252
|
+
version: "v0",
|
|
253
|
+
run_id: deniedRunId,
|
|
254
|
+
project_dir: projectDir,
|
|
255
|
+
emit_profile: opts.emitProfile,
|
|
256
|
+
inputs: [],
|
|
257
|
+
imported: {
|
|
258
|
+
claude_md: false,
|
|
259
|
+
mcp_json: false,
|
|
260
|
+
skills_count: 0,
|
|
261
|
+
},
|
|
262
|
+
skipped: found.skipped,
|
|
263
|
+
input_policy_ok: false,
|
|
264
|
+
};
|
|
265
|
+
const episode = {
|
|
266
|
+
episode_id: deniedRunId,
|
|
267
|
+
recorded_at: "1970-01-01T00:00:00.000Z",
|
|
268
|
+
episode_type: "claude_import_run_v0",
|
|
269
|
+
run_id: deniedRunId,
|
|
270
|
+
ok: false,
|
|
271
|
+
result_core: {
|
|
272
|
+
imported: manifest.imported,
|
|
273
|
+
inputs_count: 0,
|
|
274
|
+
validation: null,
|
|
275
|
+
},
|
|
276
|
+
failure: {
|
|
277
|
+
reason: "input_policy_denied",
|
|
278
|
+
denied_count: inputPolicy.denied.length,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "import_manifest.json", manifest);
|
|
282
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "episode_import_run.json", episode);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
exit_code: 2,
|
|
287
|
+
run_id: deniedRunId,
|
|
288
|
+
out_dir: outRoot,
|
|
289
|
+
imported: { claude_md: false, mcp_json: false, skills_count: 0 },
|
|
290
|
+
skipped: found.skipped,
|
|
291
|
+
lint_summary: "lint_v0: skipped",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let movaSpecSchemas: { instruction_profile: any; mcp_servers: any; core: any };
|
|
296
|
+
try {
|
|
297
|
+
movaSpecSchemas = {
|
|
298
|
+
instruction_profile: await loadMovaSpecSchemaById(MOVA_SPEC_BINDINGS_V0.instruction_profile_id),
|
|
299
|
+
mcp_servers: await loadMovaSpecSchemaById(MOVA_SPEC_BINDINGS_V0.mcp_servers_id),
|
|
300
|
+
core: await loadMovaSpecSchemaById(MOVA_SPEC_BINDINGS_V0.core_schema_id),
|
|
301
|
+
};
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
if (!opts.dryRun) {
|
|
304
|
+
const manifest = {
|
|
305
|
+
tool: "mova-claude-import",
|
|
306
|
+
version: "v0",
|
|
307
|
+
run_id: runId,
|
|
308
|
+
project_dir: projectDir,
|
|
309
|
+
emit_profile: opts.emitProfile,
|
|
310
|
+
inputs: [],
|
|
311
|
+
imported: {
|
|
312
|
+
claude_md: false,
|
|
313
|
+
mcp_json: false,
|
|
314
|
+
skills_count: 0,
|
|
315
|
+
},
|
|
316
|
+
skipped: found.skipped,
|
|
317
|
+
input_policy_ok: inputPolicy.ok,
|
|
318
|
+
failure: {
|
|
319
|
+
reason: "mova_spec_schema_missing",
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
const episode = {
|
|
323
|
+
episode_id: runId,
|
|
324
|
+
recorded_at: "1970-01-01T00:00:00.000Z",
|
|
325
|
+
episode_type: "claude_import_run_v0",
|
|
326
|
+
run_id: runId,
|
|
327
|
+
ok: false,
|
|
328
|
+
result_core: {
|
|
329
|
+
imported: manifest.imported,
|
|
330
|
+
inputs_count: 0,
|
|
331
|
+
validation: null,
|
|
332
|
+
},
|
|
333
|
+
failure: {
|
|
334
|
+
reason: "mova_spec_schema_missing",
|
|
335
|
+
detail: String(err?.message ?? err),
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "import_manifest.json", manifest);
|
|
339
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "episode_import_run.json", episode);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
exit_code: 2,
|
|
344
|
+
run_id: runId,
|
|
345
|
+
out_dir: outRoot,
|
|
346
|
+
imported: { claude_md: false, mcp_json: false, skills_count: 0 },
|
|
347
|
+
skipped: found.skipped,
|
|
348
|
+
lint_summary: "lint_v0: skipped",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!opts.dryRun && opts.emitProfile) {
|
|
353
|
+
await writeCleanClaudeProfileScaffoldV0(outRoot);
|
|
354
|
+
if (opts.emitOverlay) {
|
|
355
|
+
const controlEntry = buildMovaControlEntryV0(overlayParams);
|
|
356
|
+
const claudePath = path.join(outRoot, "CLAUDE.md");
|
|
357
|
+
if (await exists(claudePath)) {
|
|
358
|
+
await ensureClaudeControlEntry(claudePath, controlEntry, MOVA_CONTROL_ENTRY_MARKER);
|
|
359
|
+
}
|
|
360
|
+
const overlayFiles = buildMovaOverlayV0(overlayParams);
|
|
361
|
+
for (const [rel, content] of Object.entries(overlayFiles)) {
|
|
362
|
+
await writeTextFile(path.join(outRoot, rel), content);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const profileFiles = getAnthropicProfileV0Files();
|
|
366
|
+
for (const [rel, content] of Object.entries(profileFiles)) {
|
|
367
|
+
if (rel === "CLAUDE.md" || rel === ".claude/settings.json") continue;
|
|
368
|
+
await writeTextFile(path.join(outRoot, rel), content);
|
|
369
|
+
}
|
|
370
|
+
if (mcpJsonParsed) {
|
|
371
|
+
await writeJsonFile(path.join(outRoot, ".mcp.json"), mcpJsonParsed);
|
|
372
|
+
}
|
|
373
|
+
for (const skill of normalizedSkills) {
|
|
374
|
+
const outRel = path.join(".claude", "skills", skill.normDir, "SKILL.md");
|
|
375
|
+
await writeTextFile(path.join(outRoot, outRel), skill.body);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Build contracts
|
|
380
|
+
const instructionProfile = {
|
|
381
|
+
profile_version: "v0",
|
|
382
|
+
claude_md: claudeMdRedacted,
|
|
383
|
+
anchors: {
|
|
384
|
+
mova_entry: "MOVA.md",
|
|
385
|
+
normalized_project: ".",
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const skillsCatalog = {
|
|
390
|
+
profile_version: "v0",
|
|
391
|
+
skills: normalizedSkills.map((skill) => ({
|
|
392
|
+
skill_id: skill.normDir,
|
|
393
|
+
rel_dir: `.claude/skills/${skill.normDir}`,
|
|
394
|
+
title: skill.title,
|
|
395
|
+
skill_md: skill.body,
|
|
396
|
+
})),
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const mcpServers = {
|
|
400
|
+
profile_version: "v0",
|
|
401
|
+
servers: Array.isArray(mcpJsonParsed?.servers) ? mcpJsonParsed?.servers : [],
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
if (!opts.dryRun) {
|
|
405
|
+
await writeJsonFile(path.join(movaBase, "contracts", "instruction_profile_v0.json"), instructionProfile);
|
|
406
|
+
await writeJsonFile(path.join(movaBase, "contracts", "skills_catalog_v0.json"), skillsCatalog);
|
|
407
|
+
await writeJsonFile(path.join(movaBase, "contracts", "mcp_servers_v0.json"), mcpServers);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Validation with Ajv
|
|
411
|
+
const ajv = new (Ajv as any)({ allErrors: true, strict: true, validateSchema: false });
|
|
412
|
+
(addFormats as any)(ajv);
|
|
413
|
+
const coreSchemaId = movaSpecSchemas.core?.$id ?? "https://mova.dev/schemas/ds.mova_schema_core_v1.schema.json";
|
|
414
|
+
ajv.addSchema(movaSpecSchemas.core, coreSchemaId);
|
|
415
|
+
const schemaPath = (name: string) => fileURLToPath(new URL(`../schemas/${name}`, import.meta.url));
|
|
416
|
+
const schemas = {
|
|
417
|
+
instruction_profile: JSON.parse(
|
|
418
|
+
await fs.readFile(schemaPath("ds.claude_import.instruction_profile_v0.schema.json"), "utf8")
|
|
419
|
+
),
|
|
420
|
+
skills_catalog: JSON.parse(
|
|
421
|
+
await fs.readFile(schemaPath("ds.claude_import.skills_catalog_v0.schema.json"), "utf8")
|
|
422
|
+
),
|
|
423
|
+
mcp_servers: JSON.parse(
|
|
424
|
+
await fs.readFile(schemaPath("ds.claude_import.mcp_servers_v0.schema.json"), "utf8")
|
|
425
|
+
),
|
|
426
|
+
};
|
|
427
|
+
const validateInstruction = ajv.compile(schemas.instruction_profile);
|
|
428
|
+
const validateSkills = ajv.compile(schemas.skills_catalog);
|
|
429
|
+
const validateMcp = ajv.compile(schemas.mcp_servers);
|
|
430
|
+
const validateSpecInstruction = ajv.compile(movaSpecSchemas.instruction_profile);
|
|
431
|
+
const validateSpecMcp = ajv.compile(movaSpecSchemas.mcp_servers);
|
|
432
|
+
|
|
433
|
+
const validationReport = {
|
|
434
|
+
instruction_profile: validateInstruction(instructionProfile),
|
|
435
|
+
skills_catalog: validateSkills(skillsCatalog),
|
|
436
|
+
mcp_servers: validateMcp(mcpServers),
|
|
437
|
+
mova_spec: {
|
|
438
|
+
instruction_profile: validateSpecInstruction(instructionProfile),
|
|
439
|
+
mcp_servers: validateSpecMcp(mcpServers),
|
|
440
|
+
errors: {
|
|
441
|
+
instruction_profile: validateSpecInstruction.errors,
|
|
442
|
+
mcp_servers: validateSpecMcp.errors,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
errors: {
|
|
446
|
+
instruction_profile: validateInstruction.errors,
|
|
447
|
+
skills_catalog: validateSkills.errors,
|
|
448
|
+
mcp_servers: validateMcp.errors,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const manifest = {
|
|
453
|
+
tool: "mova-claude-import",
|
|
454
|
+
version: "v0",
|
|
455
|
+
run_id: runId,
|
|
456
|
+
project_dir: projectDir,
|
|
457
|
+
emit_profile: opts.emitProfile,
|
|
458
|
+
inputs: inputs.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
459
|
+
imported: {
|
|
460
|
+
claude_md: Boolean(found.claudeMdPath),
|
|
461
|
+
mcp_json: Boolean(found.mcpJsonPath),
|
|
462
|
+
skills_count: normalizedSkills.length,
|
|
463
|
+
},
|
|
464
|
+
skipped: found.skipped,
|
|
465
|
+
input_policy_ok: inputPolicy.ok,
|
|
466
|
+
};
|
|
467
|
+
const redactionReport = {
|
|
468
|
+
hits: redactionHits,
|
|
469
|
+
note: "Only presence/len recorded, values omitted.",
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const episode = {
|
|
473
|
+
episode_id: runId,
|
|
474
|
+
recorded_at: "1970-01-01T00:00:00.000Z",
|
|
475
|
+
episode_type: "claude_import_run_v0",
|
|
476
|
+
run_id: runId,
|
|
477
|
+
ok: true,
|
|
478
|
+
result_core: {
|
|
479
|
+
imported: manifest.imported,
|
|
480
|
+
inputs_count: manifest.inputs.length,
|
|
481
|
+
validation: validationReport,
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
if (!opts.dryRun) {
|
|
486
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "import_manifest.json", manifest);
|
|
487
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "redaction_report.json", redactionReport);
|
|
488
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "episode_import_run.json", episode);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let lintReport: LintReportV0 = {
|
|
492
|
+
profile_version: "v0" as const,
|
|
493
|
+
ok: true,
|
|
494
|
+
issues: [],
|
|
495
|
+
summary: "lint_v0: ok",
|
|
496
|
+
};
|
|
497
|
+
if (!opts.dryRun) {
|
|
498
|
+
lintReport = await lintV0({
|
|
499
|
+
outRoot,
|
|
500
|
+
emitProfile: opts.emitProfile,
|
|
501
|
+
mcpExpected: Boolean(found.mcpJsonPath),
|
|
502
|
+
});
|
|
503
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "lint_report_v0.json", lintReport);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!opts.dryRun && opts.emitZip) {
|
|
507
|
+
const exportZip = await createExportZipV0(outRoot, opts.zipName);
|
|
508
|
+
const exportManifest = {
|
|
509
|
+
profile_version: "v0",
|
|
510
|
+
zip_rel_path: exportZip.zipRelPath,
|
|
511
|
+
zip_sha256: exportZip.zipSha256,
|
|
512
|
+
files_count: exportZip.files.length,
|
|
513
|
+
files: exportZip.files,
|
|
514
|
+
};
|
|
515
|
+
await writeEvidenceArtifact(evidenceWriter, movaBase, "export_manifest_v0.json", exportManifest);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
ok: true,
|
|
520
|
+
run_id: runId,
|
|
521
|
+
out_dir: outRoot,
|
|
522
|
+
imported: manifest.imported,
|
|
523
|
+
skipped: found.skipped,
|
|
524
|
+
lint_summary: lintReport.summary,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function sortValue(value: any): any {
|
|
2
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
3
|
+
if (value && typeof value === "object") {
|
|
4
|
+
const out: Record<string, any> = {};
|
|
5
|
+
for (const key of Object.keys(value).sort()) {
|
|
6
|
+
out[key] = sortValue(value[key]);
|
|
7
|
+
}
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stableStringify(value: any): string {
|
|
14
|
+
return JSON.stringify(sortValue(value), null, 2);
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
test("control apply writes marker and report", async () => {
|
|
11
|
+
const tmp = path.join(process.cwd(), ".tmp_test_control_apply");
|
|
12
|
+
const proj = path.join(tmp, "proj");
|
|
13
|
+
const out = path.join(tmp, "out");
|
|
14
|
+
const profile = path.join(process.cwd(), "fixtures", "pos", "control_profile_filled", "claude_control_profile_v0.json");
|
|
15
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
16
|
+
await fs.mkdir(proj, { recursive: true });
|
|
17
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Hello\n", "utf8");
|
|
18
|
+
await fs.writeFile(path.join(proj, ".mcp.json"), "{\"servers\":[]}", "utf8");
|
|
19
|
+
|
|
20
|
+
await execFileP("node", [
|
|
21
|
+
"dist/cli.js",
|
|
22
|
+
"control",
|
|
23
|
+
"apply",
|
|
24
|
+
"--project",
|
|
25
|
+
proj,
|
|
26
|
+
"--profile",
|
|
27
|
+
profile,
|
|
28
|
+
"--mode",
|
|
29
|
+
"apply",
|
|
30
|
+
"--out",
|
|
31
|
+
out,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const claude = await fs.readFile(path.join(proj, "CLAUDE.md"), "utf8");
|
|
35
|
+
assert.ok(claude.includes("MOVA_CONTROL_ENTRY_V0"));
|
|
36
|
+
|
|
37
|
+
const runsDir = path.join(out, "mova", "claude_control", "v0", "runs");
|
|
38
|
+
const entries = await fs.readdir(runsDir);
|
|
39
|
+
assert.ok(entries.length > 0);
|
|
40
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function readFile(p) {
|
|
11
|
+
return fs.readFile(p, "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("control check does not modify project files", async () => {
|
|
15
|
+
const tmp = path.join(process.cwd(), ".tmp_test_control_check");
|
|
16
|
+
const proj = path.join(tmp, "proj");
|
|
17
|
+
const out = path.join(tmp, "out");
|
|
18
|
+
const profile = path.join(process.cwd(), "fixtures", "pos", "control_profile_filled", "claude_control_profile_v0.json");
|
|
19
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
20
|
+
await fs.mkdir(proj, { recursive: true });
|
|
21
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Hello\n", "utf8");
|
|
22
|
+
await fs.writeFile(path.join(proj, ".mcp.json"), "{\"servers\":[]}", "utf8");
|
|
23
|
+
|
|
24
|
+
const claudeBefore = await readFile(path.join(proj, "CLAUDE.md"));
|
|
25
|
+
const mcpBefore = await readFile(path.join(proj, ".mcp.json"));
|
|
26
|
+
|
|
27
|
+
await execFileP("node", ["dist/cli.js", "control", "check", "--project", proj, "--profile", profile, "--out", out]);
|
|
28
|
+
|
|
29
|
+
const claudeAfter = await readFile(path.join(proj, "CLAUDE.md"));
|
|
30
|
+
const mcpAfter = await readFile(path.join(proj, ".mcp.json"));
|
|
31
|
+
|
|
32
|
+
assert.equal(claudeBefore, claudeAfter);
|
|
33
|
+
assert.equal(mcpBefore, mcpAfter);
|
|
34
|
+
|
|
35
|
+
const runsDir = path.join(out, "mova", "claude_control", "v0", "runs");
|
|
36
|
+
const entries = await fs.readdir(runsDir);
|
|
37
|
+
assert.ok(entries.length > 0);
|
|
38
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function readFile(p) {
|
|
11
|
+
return fs.readFile(p, "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("control prefill is deterministic", async () => {
|
|
15
|
+
const tmp = path.join(process.cwd(), ".tmp_test_control_prefill");
|
|
16
|
+
const proj = path.join(process.cwd(), "fixtures", "pos", "control_basic_project");
|
|
17
|
+
const out1 = path.join(tmp, "out1");
|
|
18
|
+
const out2 = path.join(tmp, "out2");
|
|
19
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
20
|
+
await fs.mkdir(out1, { recursive: true });
|
|
21
|
+
await fs.mkdir(out2, { recursive: true });
|
|
22
|
+
|
|
23
|
+
await execFileP("node", ["dist/cli.js", "control", "prefill", "--project", proj, "--out", out1]);
|
|
24
|
+
await execFileP("node", ["dist/cli.js", "control", "prefill", "--project", proj, "--out", out2]);
|
|
25
|
+
|
|
26
|
+
const profile1 = await readFile(path.join(out1, "claude_control_profile_v0.json"));
|
|
27
|
+
const profile2 = await readFile(path.join(out2, "claude_control_profile_v0.json"));
|
|
28
|
+
|
|
29
|
+
assert.equal(profile1, profile2);
|
|
30
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function exists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.stat(p);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("demo v0 produces report and referenced files", async () => {
|
|
20
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
21
|
+
await execFileP(npmCmd, ["run", "demo"], { cwd: process.cwd(), shell: true });
|
|
22
|
+
|
|
23
|
+
const demoRoot = path.join(process.cwd(), "artifacts", "demo_v0");
|
|
24
|
+
const dirs = await fs.readdir(demoRoot);
|
|
25
|
+
assert.ok(dirs.length > 0);
|
|
26
|
+
dirs.sort();
|
|
27
|
+
const reportPath = path.join(demoRoot, dirs[dirs.length - 1], "demo_report_v0.json");
|
|
28
|
+
assert.ok(await exists(reportPath));
|
|
29
|
+
|
|
30
|
+
const report = JSON.parse(await fs.readFile(reportPath, "utf8"));
|
|
31
|
+
assert.ok(await exists(report.output_path));
|
|
32
|
+
assert.ok(await exists(report.refs.manifest));
|
|
33
|
+
assert.ok(await exists(report.refs.export_manifest));
|
|
34
|
+
assert.ok(await exists(report.refs.control_runs_dir));
|
|
35
|
+
assert.ok(await exists(report.refs.control_check_summary));
|
|
36
|
+
assert.ok(await exists(report.refs.control_apply_report));
|
|
37
|
+
});
|