mova-claude-import 0.1.1 → 0.1.2
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 +68 -4
- package/control_surface_exclusions_v0.json +8 -0
- package/dist/anthropic_profile_v0.d.ts +1 -1
- package/dist/anthropic_profile_v0.js +1 -0
- package/dist/cli.js +206 -23
- package/dist/control_apply_v0.d.ts +5 -1
- package/dist/control_apply_v0.js +125 -8
- package/dist/control_check_v0.d.ts +1 -0
- package/dist/control_check_v0.js +194 -23
- package/dist/control_prefill_v0.js +128 -9
- package/dist/control_surface_coverage_v0.d.ts +22 -0
- package/dist/control_surface_coverage_v0.js +128 -0
- package/dist/control_v0.d.ts +149 -0
- package/dist/control_v0.js +360 -0
- package/dist/control_v0_schema.d.ts +6 -0
- package/dist/control_v0_schema.js +19 -0
- package/dist/init_v0.d.ts +6 -1
- package/dist/init_v0.js +41 -1
- package/dist/observability_writer_v0.d.ts +1 -0
- package/dist/observability_writer_v0.js +157 -0
- package/dist/observe_v0.d.ts +8 -0
- package/dist/observe_v0.js +57 -0
- package/dist/presets_v0.d.ts +11 -0
- package/dist/presets_v0.js +49 -0
- package/dist/redaction.js +5 -1
- package/dist/run_import.js +111 -26
- package/docs/CLAUDE_CONTROL_SURFACE_MAP_v0.md +78 -0
- package/docs/OPERATOR_GUIDE_v0.md +11 -0
- package/fixtures/pos/basic/mova/control_v0.json +93 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/code-reviewer.md +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/github-workflow.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/code-quality.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/docs-sync.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/onboard.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-review.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-summary.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/ticket.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.js +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.sh +15 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.json +21 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.schema.json +24 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/code-style.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/security.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.json +102 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.md +6 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/core-components/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/formik-patterns/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/graphql-schema/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/react-ui-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/testing-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/pr-claude-code-review.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-dependency-audit.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-docs-sync.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-quality.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.mcp.json +44 -0
- package/fixtures/pos/claude_code_demo_full/CLAUDE.md +28 -0
- package/fixtures/pos/control_basic_project/mova/control_v0.json +93 -0
- package/fixtures/pos/observability_basic/CLAUDE.md +3 -0
- package/{.tmp_test_zip/out1 → fixtures/pos/preset_safe_observable_v0}/.mcp.json +3 -3
- package/fixtures/pos/preset_safe_observable_v0/CLAUDE.md +3 -0
- package/package.json +2 -1
- package/presets/safe_observable_v0/assets/.claude/agents/code-reviewer.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/finish.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/start.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.js +15 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.sh +17 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-rules.json +26 -0
- package/presets/safe_observable_v0/assets/.claude/rules/code-style.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/rules/security.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/skills/git-workflow/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/security-basics/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/testing-patterns/SKILL.md +9 -0
- package/presets/safe_observable_v0/control_v0.json +214 -0
- package/schemas/mova.control_v0.schema.json +252 -0
- package/src/anthropic_profile_v0.ts +1 -0
- package/src/cli.ts +194 -23
- package/src/control_apply_v0.ts +131 -8
- package/src/control_check_v0.ts +203 -23
- package/src/control_prefill_v0.ts +136 -8
- package/src/control_surface_coverage_v0.ts +164 -0
- package/src/control_v0.ts +808 -0
- package/src/control_v0_schema.ts +26 -0
- package/src/init_v0.ts +48 -1
- package/src/observability_writer_v0.ts +157 -0
- package/src/observe_v0.ts +64 -0
- package/src/presets_v0.ts +55 -0
- package/src/redaction.ts +6 -1
- package/src/run_import.ts +132 -26
- package/test/control_demo_full_roundtrip.test.js +92 -0
- package/test/control_surface_coverage_v0.test.js +36 -0
- package/test/control_v0_schema_validation.test.js +69 -0
- package/test/init_v0.test.js +9 -0
- package/test/observability_writer_v0.test.js +59 -0
- package/test/preset_safe_observable_v0.test.js +55 -0
- package/test/profile_v0_output.test.js +1 -0
- package/test/scaffold_v0_output.test.js +1 -0
- package/tools/control_surface_coverage_v0.mjs +27 -0
- package/tools/smoke_v0.mjs +33 -0
- package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_control_apply/proj/.mcp.json +0 -3
- package/.tmp_test_control_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_control_apply/proj/MOVA.md +0 -3
- package/.tmp_test_control_check/proj/.mcp.json +0 -1
- package/.tmp_test_control_check/proj/CLAUDE.md +0 -1
- package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out1/prefill_report_v0.json +0 -13
- package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out2/prefill_report_v0.json +0 -13
- package/.tmp_test_overlay/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_overlay/proj/.mcp.json +0 -1
- package/.tmp_test_overlay/proj/CLAUDE.md +0 -1
- package/.tmp_test_profile/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_profile/proj/.mcp.json +0 -1
- package/.tmp_test_profile/proj/CLAUDE.md +0 -1
- package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_scaffold_apply/proj/.mcp.json +0 -3
- package/.tmp_test_scaffold_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_scaffold_apply/proj/MOVA.md +0 -3
- package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +0 -32
- package/.tmp_test_zip/out1/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out1/.claude/settings.json +0 -30
- package/.tmp_test_zip/out1/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out1/CLAUDE.md +0 -4
- package/.tmp_test_zip/out1/MOVA.md +0 -10
- package/.tmp_test_zip/out1/export.zip +0 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/out2/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out2/.claude/settings.json +0 -30
- package/.tmp_test_zip/out2/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out2/.mcp.json +0 -3
- package/.tmp_test_zip/out2/CLAUDE.md +0 -4
- package/.tmp_test_zip/out2/MOVA.md +0 -10
- package/.tmp_test_zip/out2/export.zip +0 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_zip/proj/.mcp.json +0 -1
- package/.tmp_test_zip/proj/CLAUDE.md +0 -1
package/src/control_apply_v0.ts
CHANGED
|
@@ -4,10 +4,18 @@ import { stableStringify } from "./stable_json.js";
|
|
|
4
4
|
import { stableSha256 } from "./redaction.js";
|
|
5
5
|
import { buildMovaControlEntryV0, MOVA_CONTROL_ENTRY_MARKER } from "./mova_overlay_v0.js";
|
|
6
6
|
import { ensureClaudeControlSurfacesV0 } from "./claude_profile_scaffold_v0.js";
|
|
7
|
+
import { controlToMcpJson, controlToSettingsV0, normalizeControlV0 } from "./control_v0.js";
|
|
8
|
+
import { validateControlV0Schema } from "./control_v0_schema.js";
|
|
9
|
+
import { getMovaObserveScriptV0 } from "./observability_writer_v0.js";
|
|
7
10
|
|
|
8
11
|
type ApplyResult = {
|
|
9
12
|
run_id: string;
|
|
10
13
|
report_path: string;
|
|
14
|
+
exit_code?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ApplyOptions = {
|
|
18
|
+
assetSourceRoot?: string;
|
|
11
19
|
};
|
|
12
20
|
|
|
13
21
|
async function exists(p: string): Promise<boolean> {
|
|
@@ -33,6 +41,37 @@ function computeRunId(parts: string[]): string {
|
|
|
33
41
|
return stableSha256(parts.join("|")).slice(0, 16);
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
function mergeOverlayValue(existing: any, incoming: any): any {
|
|
45
|
+
if (existing === undefined) return incoming;
|
|
46
|
+
const existingArr = Array.isArray(existing) ? existing : null;
|
|
47
|
+
const incomingArr = Array.isArray(incoming) ? incoming : null;
|
|
48
|
+
if (existingArr || incomingArr) {
|
|
49
|
+
const left = existingArr ?? (existing === undefined ? [] : [existing]);
|
|
50
|
+
const right = incomingArr ?? (incoming === undefined ? [] : [incoming]);
|
|
51
|
+
const seen = new Set(left.map((item) => stableStringify(item)));
|
|
52
|
+
const merged = left.slice();
|
|
53
|
+
for (const item of right) {
|
|
54
|
+
const key = stableStringify(item);
|
|
55
|
+
if (seen.has(key)) continue;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
merged.push(item);
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
61
|
+
if (existing && typeof existing === "object" && incoming && typeof incoming === "object") {
|
|
62
|
+
const out: Record<string, any> = { ...existing };
|
|
63
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
64
|
+
out[key] = mergeOverlayValue(out[key], value);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
return existing;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergeSettingsOverlay(existing: any, incoming: any) {
|
|
72
|
+
return mergeOverlayValue(existing ?? {}, incoming ?? {});
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
function updateClaude(content: string, marker: string, block: string): string {
|
|
37
76
|
if (content.includes(marker)) {
|
|
38
77
|
const idx = content.indexOf(marker);
|
|
@@ -48,17 +87,41 @@ export async function controlApplyV0(
|
|
|
48
87
|
projectDir: string,
|
|
49
88
|
profilePath: string,
|
|
50
89
|
outDir: string,
|
|
51
|
-
mode?: string
|
|
90
|
+
mode?: string,
|
|
91
|
+
options?: ApplyOptions
|
|
52
92
|
): Promise<ApplyResult> {
|
|
53
93
|
await ensureClaudeControlSurfacesV0(projectDir);
|
|
54
94
|
const profile = await readJson(profilePath);
|
|
55
|
-
const
|
|
95
|
+
const isControlV0 = profile?.version === "control_v0";
|
|
96
|
+
const control = isControlV0 ? normalizeControlV0(profile).control : null;
|
|
97
|
+
if (isControlV0) {
|
|
98
|
+
const validation = await validateControlV0Schema(profile);
|
|
99
|
+
if (!validation.ok) {
|
|
100
|
+
const runId = computeRunId([profilePath, "invalid_schema"]);
|
|
101
|
+
const runBase = path.join(outDir, "mova", "claude_control", "v0", "runs", runId);
|
|
102
|
+
const reportPath = path.join(runBase, "control_apply_report_v0.json");
|
|
103
|
+
const report = {
|
|
104
|
+
profile_version: "v0",
|
|
105
|
+
run_id: runId,
|
|
106
|
+
project_dir: projectDir,
|
|
107
|
+
profile_path: profilePath,
|
|
108
|
+
mode: mode ?? "preview",
|
|
109
|
+
outcome_code: "INVALID_SCHEMA",
|
|
110
|
+
errors: validation.errors ?? [],
|
|
111
|
+
};
|
|
112
|
+
await writeJson(reportPath, report);
|
|
113
|
+
return { run_id: runId, report_path: reportPath, exit_code: 2 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const applyMode = mode ?? (isControlV0 && control?.policy?.mode === "report_only" ? "preview" : profile?.apply?.default_apply_mode) ?? "preview";
|
|
117
|
+
const isOverlay = applyMode === "overlay";
|
|
118
|
+
const assetSourceRoot = options?.assetSourceRoot ?? projectDir;
|
|
56
119
|
|
|
57
120
|
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
58
121
|
const mcpPath = path.join(projectDir, ".mcp.json");
|
|
59
122
|
const claudeExists = await exists(claudePath);
|
|
60
123
|
const mcpExists = await exists(mcpPath);
|
|
61
|
-
const marker = profile?.anthropic?.claude_md?.marker ?? MOVA_CONTROL_ENTRY_MARKER;
|
|
124
|
+
const marker = control?.claude_md?.marker ?? profile?.anthropic?.claude_md?.marker ?? MOVA_CONTROL_ENTRY_MARKER;
|
|
62
125
|
|
|
63
126
|
const runId = computeRunId([profilePath, claudeExists ? "claude" : "", mcpExists ? "mcp" : "", marker, applyMode]);
|
|
64
127
|
const runBase = path.join(outDir, "mova", "claude_control", "v0", "runs", runId);
|
|
@@ -75,15 +138,75 @@ export async function controlApplyV0(
|
|
|
75
138
|
};
|
|
76
139
|
const controlEntry = buildMovaControlEntryV0(overlayParams);
|
|
77
140
|
|
|
78
|
-
const applied = { claude_md: false, mcp_json: false, settings: false };
|
|
79
|
-
if (applyMode === "apply") {
|
|
80
|
-
if (profile?.anthropic?.claude_md?.inject_control_entry && claudeExists) {
|
|
141
|
+
const applied = { claude_md: false, mcp_json: false, settings: false, assets: false, lsp: false };
|
|
142
|
+
if (applyMode === "apply" || applyMode === "overlay") {
|
|
143
|
+
if ((control?.claude_md?.inject_control_entry ?? profile?.anthropic?.claude_md?.inject_control_entry) && claudeExists) {
|
|
81
144
|
const raw = await fs.readFile(claudePath, "utf8");
|
|
82
145
|
const updated = updateClaude(raw, marker, controlEntry);
|
|
83
146
|
await fs.writeFile(claudePath, updated, "utf8");
|
|
84
147
|
applied.claude_md = true;
|
|
85
148
|
}
|
|
86
|
-
if (
|
|
149
|
+
if (control) {
|
|
150
|
+
const settingsPath = path.join(projectDir, ".claude", "settings.json");
|
|
151
|
+
const settingsGenerated = controlToSettingsV0(control);
|
|
152
|
+
if (isOverlay && (await exists(settingsPath))) {
|
|
153
|
+
const current = await readJson(settingsPath);
|
|
154
|
+
const merged = mergeSettingsOverlay(current, settingsGenerated);
|
|
155
|
+
await writeJson(settingsPath, merged);
|
|
156
|
+
} else {
|
|
157
|
+
await writeJson(settingsPath, settingsGenerated);
|
|
158
|
+
}
|
|
159
|
+
applied.settings = true;
|
|
160
|
+
|
|
161
|
+
if (!(isOverlay && (await exists(mcpPath)))) {
|
|
162
|
+
await writeJson(path.join(projectDir, ".mcp.json"), controlToMcpJson(control));
|
|
163
|
+
applied.mcp_json = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const assets = [
|
|
167
|
+
...control.assets.skills,
|
|
168
|
+
...control.assets.agents,
|
|
169
|
+
...control.assets.commands,
|
|
170
|
+
...control.assets.rules,
|
|
171
|
+
...control.assets.hooks,
|
|
172
|
+
...control.assets.workflows,
|
|
173
|
+
...control.assets.docs,
|
|
174
|
+
...control.assets.dotfiles,
|
|
175
|
+
...control.assets.schemas,
|
|
176
|
+
];
|
|
177
|
+
for (const asset of assets) {
|
|
178
|
+
const target = path.join(projectDir, asset.path);
|
|
179
|
+
if (isOverlay && (await exists(target))) continue;
|
|
180
|
+
const sourceRel = asset.source_path ?? asset.path;
|
|
181
|
+
const source = path.isAbsolute(sourceRel) ? sourceRel : path.join(assetSourceRoot, sourceRel);
|
|
182
|
+
try {
|
|
183
|
+
await fs.stat(source);
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
188
|
+
if (source !== target) {
|
|
189
|
+
await fs.copyFile(source, target);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
applied.assets = assets.length > 0;
|
|
193
|
+
|
|
194
|
+
if (control.lsp.managed && Array.isArray(control.lsp.enabled_plugins)) {
|
|
195
|
+
const lspPath = path.join(projectDir, control.lsp.config_path);
|
|
196
|
+
if (!(isOverlay && (await exists(lspPath)))) {
|
|
197
|
+
await writeJson(lspPath, { enabled_plugins: control.lsp.enabled_plugins });
|
|
198
|
+
}
|
|
199
|
+
applied.lsp = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (control.observability.enable && control.observability.writer?.script_path) {
|
|
203
|
+
const scriptPath = path.join(projectDir, control.observability.writer.script_path);
|
|
204
|
+
if (!(isOverlay && (await exists(scriptPath)))) {
|
|
205
|
+
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
206
|
+
await fs.writeFile(scriptPath, getMovaObserveScriptV0(), "utf8");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else if (profile?.anthropic?.mcp?.servers && mcpExists) {
|
|
87
210
|
const mcp = await readJson(mcpPath);
|
|
88
211
|
const merged = { ...mcp, servers: profile.anthropic.mcp.servers };
|
|
89
212
|
await fs.writeFile(mcpPath, stableStringify(merged) + "\n", "utf8");
|
|
@@ -97,7 +220,7 @@ export async function controlApplyV0(
|
|
|
97
220
|
project_dir: projectDir,
|
|
98
221
|
profile_path: profilePath,
|
|
99
222
|
mode: applyMode,
|
|
100
|
-
outcome_code: applyMode === "apply" ? "APPLIED" : "PREVIEW",
|
|
223
|
+
outcome_code: applyMode === "apply" || applyMode === "overlay" ? "APPLIED" : "PREVIEW",
|
|
101
224
|
applied,
|
|
102
225
|
};
|
|
103
226
|
|
package/src/control_check_v0.ts
CHANGED
|
@@ -3,11 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import { stableStringify } from "./stable_json.js";
|
|
4
4
|
import { stableSha256 } from "./redaction.js";
|
|
5
5
|
import { buildMovaControlEntryV0, MOVA_CONTROL_ENTRY_MARKER } from "./mova_overlay_v0.js";
|
|
6
|
+
import { controlToMcpJson, controlToSettingsV0, normalizeControlV0 } from "./control_v0.js";
|
|
7
|
+
import { validateControlV0Schema } from "./control_v0_schema.js";
|
|
6
8
|
|
|
7
9
|
type CheckResult = {
|
|
8
10
|
run_id: string;
|
|
9
11
|
plan_path: string;
|
|
10
12
|
summary_path: string;
|
|
13
|
+
exit_code?: number;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
async function exists(p: string): Promise<boolean> {
|
|
@@ -33,14 +36,48 @@ function computeRunId(parts: string[]): string {
|
|
|
33
36
|
return stableSha256(parts.join("|")).slice(0, 16);
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
function normalizeJson(obj: any): string {
|
|
40
|
+
return stableStringify(obj);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPlaceholder(value: string): boolean {
|
|
44
|
+
return /^\$\{[A-Z0-9_]+(?::-?[^}]+)?\}$/.test(value.trim());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateMcpEnvValues(servers: any): Array<{ server: string; key: string; value: string }> {
|
|
48
|
+
if (!servers || typeof servers !== "object") return [];
|
|
49
|
+
const issues: Array<{ server: string; key: string; value: string }> = [];
|
|
50
|
+
const entries = Array.isArray(servers)
|
|
51
|
+
? servers.map((server, idx) => [server?.name ?? `index_${idx}`, server])
|
|
52
|
+
: Object.entries(servers);
|
|
53
|
+
for (const [name, server] of entries) {
|
|
54
|
+
if (!server || typeof server !== "object") continue;
|
|
55
|
+
const env = (server as any).env;
|
|
56
|
+
if (!env || typeof env !== "object") continue;
|
|
57
|
+
for (const [key, value] of Object.entries(env)) {
|
|
58
|
+
if (typeof value !== "string") continue;
|
|
59
|
+
if (!value.includes("${")) continue;
|
|
60
|
+
if (!isPlaceholder(value)) {
|
|
61
|
+
issues.push({ server: String(name), key, value });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
export async function controlCheckV0(projectDir: string, profilePath: string, outDir: string): Promise<CheckResult> {
|
|
37
69
|
const profile = await readJson(profilePath);
|
|
38
70
|
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
39
71
|
const mcpPath = path.join(projectDir, ".mcp.json");
|
|
72
|
+
const settingsPath = path.join(projectDir, ".claude", "settings.json");
|
|
40
73
|
|
|
41
74
|
const claudeExists = await exists(claudePath);
|
|
42
75
|
const mcpExists = await exists(mcpPath);
|
|
43
|
-
const
|
|
76
|
+
const settingsExists = await exists(settingsPath);
|
|
77
|
+
const isControlV0 = profile?.version === "control_v0";
|
|
78
|
+
const marker = isControlV0
|
|
79
|
+
? profile?.claude_md?.marker ?? MOVA_CONTROL_ENTRY_MARKER
|
|
80
|
+
: profile?.anthropic?.claude_md?.marker ?? MOVA_CONTROL_ENTRY_MARKER;
|
|
44
81
|
|
|
45
82
|
const runId = computeRunId([profilePath, claudeExists ? "claude" : "", mcpExists ? "mcp" : "", marker]);
|
|
46
83
|
const runBase = path.join(outDir, "mova", "claude_control", "v0", "runs", runId);
|
|
@@ -56,20 +93,148 @@ export async function controlCheckV0(projectDir: string, profilePath: string, ou
|
|
|
56
93
|
exportManifestFile: "export_manifest_v0.json",
|
|
57
94
|
};
|
|
58
95
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
if (!isControlV0) {
|
|
97
|
+
const actions = [];
|
|
98
|
+
if (profile?.anthropic?.claude_md?.inject_control_entry) {
|
|
99
|
+
actions.push({
|
|
100
|
+
target: "CLAUDE.md",
|
|
101
|
+
action: "insert_or_update_control_entry",
|
|
102
|
+
marker,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (profile?.anthropic?.mcp?.servers && mcpExists) {
|
|
106
|
+
actions.push({
|
|
107
|
+
target: ".mcp.json",
|
|
108
|
+
action: "merge_servers",
|
|
109
|
+
summary: "merge profile servers with project mcp.json",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const plan = {
|
|
114
|
+
profile_version: "v0",
|
|
115
|
+
run_id: runId,
|
|
116
|
+
project_dir: projectDir,
|
|
117
|
+
profile_path: profilePath,
|
|
118
|
+
actions,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const summary = {
|
|
122
|
+
run_id: runId,
|
|
123
|
+
outcome_code: "PREVIEW",
|
|
124
|
+
actions_count: actions.length,
|
|
125
|
+
control_entry_preview: profile?.anthropic?.claude_md?.inject_control_entry
|
|
126
|
+
? buildMovaControlEntryV0(overlayParams)
|
|
127
|
+
: null,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const planPath = path.join(runBase, "control_plan_v0.json");
|
|
131
|
+
const summaryPath = path.join(runBase, "control_summary_v0.json");
|
|
132
|
+
await writeJson(planPath, plan);
|
|
133
|
+
await writeJson(summaryPath, summary);
|
|
134
|
+
|
|
135
|
+
return { run_id: runId, plan_path: planPath, summary_path: summaryPath };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const schemaValidation = await validateControlV0Schema(profile);
|
|
139
|
+
const control = normalizeControlV0(profile).control;
|
|
140
|
+
const planPath = path.join(runBase, "control_plan_v0.json");
|
|
141
|
+
const summaryPath = path.join(runBase, "control_summary_v0.json");
|
|
142
|
+
|
|
143
|
+
if (!schemaValidation.ok) {
|
|
144
|
+
const summary = {
|
|
145
|
+
run_id: runId,
|
|
146
|
+
outcome_code: "INVALID_SCHEMA",
|
|
147
|
+
errors: schemaValidation.errors ?? [],
|
|
148
|
+
};
|
|
149
|
+
await writeJson(planPath, { profile_version: "v0", run_id: runId, project_dir: projectDir, profile_path: profilePath, actions: [] });
|
|
150
|
+
await writeJson(summaryPath, summary);
|
|
151
|
+
return { run_id: runId, plan_path: planPath, summary_path: summaryPath, exit_code: 2 };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const mcpJsonExpected = controlToMcpJson(control);
|
|
155
|
+
const settingsExpected = controlToSettingsV0(control);
|
|
156
|
+
const missing: string[] = [];
|
|
157
|
+
const drift: Array<{ path: string; expected: string; actual: string }> = [];
|
|
158
|
+
|
|
159
|
+
if (!claudeExists) missing.push("CLAUDE.md");
|
|
160
|
+
if (!settingsExists) missing.push(".claude/settings.json");
|
|
161
|
+
if (!mcpExists) missing.push(".mcp.json");
|
|
162
|
+
|
|
163
|
+
if (claudeExists && control.claude_md.inject_control_entry) {
|
|
164
|
+
const claude = await fs.readFile(claudePath, "utf8");
|
|
165
|
+
if (!claude.includes(control.claude_md.marker)) {
|
|
166
|
+
drift.push({ path: "CLAUDE.md", expected: `marker:${control.claude_md.marker}`, actual: "missing_marker" });
|
|
167
|
+
}
|
|
66
168
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
169
|
+
|
|
170
|
+
if (settingsExists) {
|
|
171
|
+
const actual = normalizeJson(await readJson(settingsPath));
|
|
172
|
+
const expected = normalizeJson(settingsExpected);
|
|
173
|
+
if (actual !== expected) {
|
|
174
|
+
drift.push({ path: ".claude/settings.json", expected, actual });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (mcpExists) {
|
|
179
|
+
const actual = normalizeJson(await readJson(mcpPath));
|
|
180
|
+
const expected = normalizeJson(mcpJsonExpected);
|
|
181
|
+
if (actual !== expected) {
|
|
182
|
+
drift.push({ path: ".mcp.json", expected, actual });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (control.lsp.managed) {
|
|
187
|
+
const lspPath = control.lsp.config_path;
|
|
188
|
+
const abs = path.join(projectDir, lspPath);
|
|
189
|
+
if (!(await exists(abs))) {
|
|
190
|
+
missing.push(lspPath);
|
|
191
|
+
} else {
|
|
192
|
+
const actual = normalizeJson(await readJson(abs));
|
|
193
|
+
const expected = normalizeJson({ enabled_plugins: control.lsp.enabled_plugins });
|
|
194
|
+
if (actual !== expected) {
|
|
195
|
+
drift.push({ path: lspPath, expected, actual });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (control.observability.enable && control.observability.writer?.script_path) {
|
|
201
|
+
const obsPath = path.join(projectDir, control.observability.writer.script_path);
|
|
202
|
+
if (!(await exists(obsPath))) {
|
|
203
|
+
missing.push(control.observability.writer.script_path);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const assets = [
|
|
208
|
+
...control.assets.skills,
|
|
209
|
+
...control.assets.agents,
|
|
210
|
+
...control.assets.commands,
|
|
211
|
+
...control.assets.rules,
|
|
212
|
+
...control.assets.hooks,
|
|
213
|
+
...control.assets.workflows,
|
|
214
|
+
...control.assets.docs,
|
|
215
|
+
...control.assets.dotfiles,
|
|
216
|
+
...control.assets.schemas,
|
|
217
|
+
];
|
|
218
|
+
for (const asset of assets) {
|
|
219
|
+
const abs = path.join(projectDir, asset.path);
|
|
220
|
+
if (!(await exists(abs))) {
|
|
221
|
+
missing.push(asset.path);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const envIssues = validateMcpEnvValues(control.mcp.servers);
|
|
226
|
+
|
|
227
|
+
let outcome = "OK";
|
|
228
|
+
let exitCode = 0;
|
|
229
|
+
if (envIssues.length > 0) {
|
|
230
|
+
outcome = "INVALID_CONTROL";
|
|
231
|
+
exitCode = 2;
|
|
232
|
+
} else if (missing.length > 0) {
|
|
233
|
+
outcome = "MISSING_REQUIRED_FILES";
|
|
234
|
+
exitCode = 4;
|
|
235
|
+
} else if (drift.length > 0) {
|
|
236
|
+
outcome = "DRIFT";
|
|
237
|
+
exitCode = 3;
|
|
73
238
|
}
|
|
74
239
|
|
|
75
240
|
const plan = {
|
|
@@ -77,22 +242,37 @@ export async function controlCheckV0(projectDir: string, profilePath: string, ou
|
|
|
77
242
|
run_id: runId,
|
|
78
243
|
project_dir: projectDir,
|
|
79
244
|
profile_path: profilePath,
|
|
80
|
-
actions
|
|
245
|
+
actions: [
|
|
246
|
+
{
|
|
247
|
+
target: "CLAUDE.md",
|
|
248
|
+
action: "ensure_control_entry",
|
|
249
|
+
marker: control.claude_md.marker,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
target: ".claude/settings.json",
|
|
253
|
+
action: "overwrite",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
target: ".mcp.json",
|
|
257
|
+
action: "overwrite",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
81
260
|
};
|
|
82
261
|
|
|
83
262
|
const summary = {
|
|
84
263
|
run_id: runId,
|
|
85
|
-
outcome_code:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
264
|
+
outcome_code: outcome,
|
|
265
|
+
exit_code: exitCode,
|
|
266
|
+
missing,
|
|
267
|
+
drift_count: drift.length,
|
|
268
|
+
invalid_mcp_env: envIssues,
|
|
269
|
+
control_entry_preview: control.claude_md.inject_control_entry ? buildMovaControlEntryV0(overlayParams) : null,
|
|
90
270
|
};
|
|
91
271
|
|
|
92
|
-
const
|
|
93
|
-
const summaryPath = path.join(runBase, "control_summary_v0.json");
|
|
272
|
+
const reportPath = path.join(runBase, "control_check_report_v0.json");
|
|
94
273
|
await writeJson(planPath, plan);
|
|
95
274
|
await writeJson(summaryPath, summary);
|
|
275
|
+
await writeJson(reportPath, { summary, drift, missing, invalid_mcp_env: envIssues });
|
|
96
276
|
|
|
97
|
-
return { run_id: runId, plan_path: planPath, summary_path: summaryPath };
|
|
277
|
+
return { run_id: runId, plan_path: planPath, summary_path: summaryPath, exit_code: exitCode };
|
|
98
278
|
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { stableStringify } from "./stable_json.js";
|
|
4
4
|
import { loadControlContractsV0 } from "./control_contracts_v0.js";
|
|
5
|
+
import { controlFromSettingsV0, normalizeControlV0 } from "./control_v0.js";
|
|
5
6
|
|
|
6
7
|
type PrefillResult = {
|
|
7
8
|
profile_path: string;
|
|
@@ -17,6 +18,21 @@ async function exists(p: string): Promise<boolean> {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
async function listFilesRec(dir: string): Promise<string[]> {
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
const stack = [dir];
|
|
24
|
+
while (stack.length) {
|
|
25
|
+
const current = stack.pop()!;
|
|
26
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const abs = path.join(current, e.name);
|
|
29
|
+
if (e.isDirectory()) stack.push(abs);
|
|
30
|
+
else if (e.isFile()) out.push(abs);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
async function readJson(p: string) {
|
|
21
37
|
const raw = await fs.readFile(p, "utf8");
|
|
22
38
|
return JSON.parse(raw);
|
|
@@ -34,29 +50,129 @@ export async function controlPrefillV0(projectDir: string, outDir: string): Prom
|
|
|
34
50
|
const settingsPath = path.join(projectDir, ".claude", "settings.json");
|
|
35
51
|
const settingsLocalPath = path.join(projectDir, ".claude", "settings.local.json");
|
|
36
52
|
|
|
37
|
-
let
|
|
53
|
+
let mcpParsed: any | undefined;
|
|
38
54
|
let mcpFound = false;
|
|
39
55
|
if (await exists(mcpPath)) {
|
|
40
|
-
|
|
41
|
-
if (Array.isArray(parsed?.servers)) {
|
|
42
|
-
mcpServers = { servers: parsed.servers };
|
|
43
|
-
} else if (parsed?.servers && typeof parsed.servers === "object") {
|
|
44
|
-
mcpServers = { servers: parsed.servers };
|
|
45
|
-
}
|
|
56
|
+
mcpParsed = await readJson(mcpPath);
|
|
46
57
|
mcpFound = true;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
if (template?.anthropic?.mcp) {
|
|
50
|
-
|
|
61
|
+
const servers = mcpParsed?.mcpServers ?? mcpParsed?.servers ?? {};
|
|
62
|
+
template.anthropic.mcp.servers = servers;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let settingsParsed: any | undefined;
|
|
66
|
+
if (await exists(settingsPath)) {
|
|
67
|
+
settingsParsed = await readJson(settingsPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const controlDerived = controlFromSettingsV0(settingsParsed, mcpParsed);
|
|
71
|
+
const control = normalizeControlV0(controlDerived.control).control;
|
|
72
|
+
|
|
73
|
+
const skills: Array<{ path: string; mode: "copy_through"; source_path: string }> = [];
|
|
74
|
+
const skillsRoot = path.join(projectDir, ".claude", "skills");
|
|
75
|
+
if (await exists(skillsRoot)) {
|
|
76
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry.isDirectory()) continue;
|
|
79
|
+
const rel = `.claude/skills/${entry.name}/SKILL.md`;
|
|
80
|
+
const abs = path.join(skillsRoot, entry.name, "SKILL.md");
|
|
81
|
+
if (await exists(abs)) {
|
|
82
|
+
skills.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const assetDirs = [
|
|
88
|
+
{ key: "agents", root: path.join(projectDir, ".claude", "agents"), prefix: ".claude/agents" },
|
|
89
|
+
{ key: "commands", root: path.join(projectDir, ".claude", "commands"), prefix: ".claude/commands" },
|
|
90
|
+
{ key: "rules", root: path.join(projectDir, ".claude", "rules"), prefix: ".claude/rules" },
|
|
91
|
+
{ key: "hooks", root: path.join(projectDir, ".claude", "hooks"), prefix: ".claude/hooks" },
|
|
92
|
+
] as const;
|
|
93
|
+
|
|
94
|
+
const assetMap: Record<string, Array<{ path: string; mode: "copy_through"; source_path: string }>> = {
|
|
95
|
+
agents: [],
|
|
96
|
+
commands: [],
|
|
97
|
+
rules: [],
|
|
98
|
+
hooks: [],
|
|
99
|
+
workflows: [],
|
|
100
|
+
docs: [],
|
|
101
|
+
dotfiles: [],
|
|
102
|
+
schemas: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
for (const dir of assetDirs) {
|
|
106
|
+
if (!(await exists(dir.root))) continue;
|
|
107
|
+
const files = await listFilesRec(dir.root);
|
|
108
|
+
for (const abs of files) {
|
|
109
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
110
|
+
assetMap[dir.key].push({ path: rel, mode: "copy_through", source_path: rel });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const docsCandidates = [
|
|
115
|
+
path.join(projectDir, ".claude", "settings.md"),
|
|
116
|
+
path.join(projectDir, ".claude", "skills", "README.md"),
|
|
117
|
+
];
|
|
118
|
+
for (const abs of docsCandidates) {
|
|
119
|
+
if (await exists(abs)) {
|
|
120
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
121
|
+
assetMap.docs.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const dotfileCandidates = [path.join(projectDir, ".claude", ".gitignore")];
|
|
126
|
+
for (const abs of dotfileCandidates) {
|
|
127
|
+
if (await exists(abs)) {
|
|
128
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
129
|
+
assetMap.dotfiles.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const schemasRoot = path.join(projectDir, ".claude");
|
|
134
|
+
if (await exists(schemasRoot)) {
|
|
135
|
+
const files = await listFilesRec(schemasRoot);
|
|
136
|
+
for (const abs of files) {
|
|
137
|
+
if (!abs.endsWith(".schema.json")) continue;
|
|
138
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
139
|
+
assetMap.schemas.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const workflowsRoot = path.join(projectDir, ".github", "workflows");
|
|
144
|
+
if (await exists(workflowsRoot)) {
|
|
145
|
+
const files = await listFilesRec(workflowsRoot);
|
|
146
|
+
for (const abs of files) {
|
|
147
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
148
|
+
assetMap.workflows.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
control.assets.skills = skills.sort((a, b) => a.path.localeCompare(b.path));
|
|
153
|
+
control.assets.agents = assetMap.agents.sort((a, b) => a.path.localeCompare(b.path));
|
|
154
|
+
control.assets.commands = assetMap.commands.sort((a, b) => a.path.localeCompare(b.path));
|
|
155
|
+
control.assets.rules = assetMap.rules.sort((a, b) => a.path.localeCompare(b.path));
|
|
156
|
+
control.assets.hooks = assetMap.hooks.sort((a, b) => a.path.localeCompare(b.path));
|
|
157
|
+
control.assets.workflows = assetMap.workflows.sort((a, b) => a.path.localeCompare(b.path));
|
|
158
|
+
control.assets.docs = assetMap.docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
159
|
+
control.assets.dotfiles = assetMap.dotfiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
160
|
+
control.assets.schemas = assetMap.schemas.sort((a, b) => a.path.localeCompare(b.path));
|
|
161
|
+
|
|
162
|
+
const hasSkillEval = control.assets.hooks.some((h) => h.path.endsWith("skill-eval.sh") || h.path.endsWith("skill-eval.js"));
|
|
163
|
+
if (hasSkillEval) {
|
|
164
|
+
control.skill_eval.enable = true;
|
|
51
165
|
}
|
|
52
166
|
|
|
53
167
|
const profilePath = path.join(outDir, "claude_control_profile_v0.json");
|
|
54
168
|
const reportPath = path.join(outDir, "prefill_report_v0.json");
|
|
169
|
+
const controlPath = path.join(outDir, "mova", "control_v0.json");
|
|
55
170
|
|
|
56
171
|
const report = {
|
|
57
172
|
profile_version: "v0",
|
|
58
173
|
project_dir: projectDir,
|
|
59
174
|
profile_path: profilePath,
|
|
175
|
+
control_path: controlPath,
|
|
60
176
|
found: {
|
|
61
177
|
mcp_json: mcpFound,
|
|
62
178
|
settings_json: await exists(settingsPath),
|
|
@@ -64,10 +180,22 @@ export async function controlPrefillV0(projectDir: string, outDir: string): Prom
|
|
|
64
180
|
},
|
|
65
181
|
applied: {
|
|
66
182
|
mcp_servers: mcpFound,
|
|
183
|
+
assets: {
|
|
184
|
+
skills: control.assets.skills.length,
|
|
185
|
+
agents: control.assets.agents.length,
|
|
186
|
+
commands: control.assets.commands.length,
|
|
187
|
+
rules: control.assets.rules.length,
|
|
188
|
+
hooks: control.assets.hooks.length,
|
|
189
|
+
workflows: control.assets.workflows.length,
|
|
190
|
+
docs: control.assets.docs.length,
|
|
191
|
+
dotfiles: control.assets.dotfiles.length,
|
|
192
|
+
schemas: control.assets.schemas.length,
|
|
193
|
+
},
|
|
67
194
|
},
|
|
68
195
|
};
|
|
69
196
|
|
|
70
197
|
await writeJson(profilePath, template);
|
|
198
|
+
await writeJson(controlPath, control);
|
|
71
199
|
await writeJson(reportPath, report);
|
|
72
200
|
|
|
73
201
|
return { profile_path: profilePath, report_path: reportPath };
|