everything-claude-code 1.4.3

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +739 -0
  3. package/README.zh-CN.md +523 -0
  4. package/crates/ecc-kernel/Cargo.lock +160 -0
  5. package/crates/ecc-kernel/Cargo.toml +15 -0
  6. package/crates/ecc-kernel/src/main.rs +710 -0
  7. package/docs/ecc.md +117 -0
  8. package/package.json +45 -0
  9. package/packs/blueprint.json +8 -0
  10. package/packs/forge.json +16 -0
  11. package/packs/instinct.json +16 -0
  12. package/packs/orchestra.json +15 -0
  13. package/packs/proof.json +8 -0
  14. package/packs/sentinel.json +8 -0
  15. package/prompts/ecc/patch.md +25 -0
  16. package/prompts/ecc/plan.md +28 -0
  17. package/schemas/ecc.apply.schema.json +35 -0
  18. package/schemas/ecc.config.schema.json +37 -0
  19. package/schemas/ecc.lock.schema.json +34 -0
  20. package/schemas/ecc.patch.schema.json +25 -0
  21. package/schemas/ecc.plan.schema.json +32 -0
  22. package/schemas/ecc.run.schema.json +67 -0
  23. package/schemas/ecc.verify.schema.json +27 -0
  24. package/schemas/hooks.schema.json +81 -0
  25. package/schemas/package-manager.schema.json +17 -0
  26. package/schemas/plugin.schema.json +13 -0
  27. package/scripts/ecc/catalog.js +82 -0
  28. package/scripts/ecc/config.js +43 -0
  29. package/scripts/ecc/diff.js +113 -0
  30. package/scripts/ecc/exec.js +121 -0
  31. package/scripts/ecc/fixtures/basic/patches/impl-core.diff +8 -0
  32. package/scripts/ecc/fixtures/basic/patches/tests.diff +8 -0
  33. package/scripts/ecc/fixtures/basic/plan.json +23 -0
  34. package/scripts/ecc/fixtures/unauthorized/patches/impl-core.diff +8 -0
  35. package/scripts/ecc/fixtures/unauthorized/plan.json +15 -0
  36. package/scripts/ecc/git.js +139 -0
  37. package/scripts/ecc/id.js +37 -0
  38. package/scripts/ecc/install-kernel.js +344 -0
  39. package/scripts/ecc/json-extract.js +301 -0
  40. package/scripts/ecc/json.js +26 -0
  41. package/scripts/ecc/kernel.js +144 -0
  42. package/scripts/ecc/lock.js +36 -0
  43. package/scripts/ecc/paths.js +28 -0
  44. package/scripts/ecc/plan.js +57 -0
  45. package/scripts/ecc/project.js +37 -0
  46. package/scripts/ecc/providers/codex.js +168 -0
  47. package/scripts/ecc/providers/index.js +23 -0
  48. package/scripts/ecc/providers/mock.js +49 -0
  49. package/scripts/ecc/report.js +127 -0
  50. package/scripts/ecc/run.js +105 -0
  51. package/scripts/ecc/validate.js +325 -0
  52. package/scripts/ecc/verify.js +125 -0
  53. package/scripts/ecc.js +532 -0
  54. package/scripts/lib/package-manager.js +390 -0
  55. package/scripts/lib/session-aliases.js +432 -0
  56. package/scripts/lib/session-manager.js +396 -0
  57. package/scripts/lib/utils.js +426 -0
@@ -0,0 +1,710 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::collections::BTreeSet;
3
+ use std::fs::{self, File};
4
+ use std::io::{self, BufRead, BufReader, Read, Write};
5
+ use std::path::{Component, Path, PathBuf};
6
+ use std::process::{Command, ExitCode, Stdio};
7
+ use time::format_description::well_known::Rfc3339;
8
+ use time::OffsetDateTime;
9
+
10
+ fn now_iso() -> String {
11
+ OffsetDateTime::now_utc()
12
+ .format(&Rfc3339)
13
+ .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
14
+ }
15
+
16
+ fn read_stdin_json<T: for<'de> Deserialize<'de>>() -> Result<T, String> {
17
+ let mut buf = String::new();
18
+ io::stdin()
19
+ .read_to_string(&mut buf)
20
+ .map_err(|e| format!("failed to read stdin: {e}"))?;
21
+ let trimmed = buf.trim();
22
+ if trimmed.is_empty() {
23
+ return Err("missing JSON input on stdin".to_string());
24
+ }
25
+ serde_json::from_str(trimmed).map_err(|e| format!("invalid JSON input: {e}"))
26
+ }
27
+
28
+ fn write_stdout_json<T: Serialize>(value: &T) -> Result<(), String> {
29
+ let out = serde_json::to_string(value).map_err(|e| format!("failed to serialize JSON: {e}"))?;
30
+ print!("{out}");
31
+ Ok(())
32
+ }
33
+
34
+ #[derive(Debug)]
35
+ struct CmdOut {
36
+ ok: bool,
37
+ status: i32,
38
+ stdout: String,
39
+ stderr: String,
40
+ }
41
+
42
+ fn run_cmd(program: &str, args: &[String], cwd: Option<&Path>) -> Result<CmdOut, String> {
43
+ let mut cmd = Command::new(program);
44
+ cmd.args(args);
45
+ if let Some(dir) = cwd {
46
+ cmd.current_dir(dir);
47
+ }
48
+ let output = cmd.output().map_err(|e| format!("{program} failed: {e}"))?;
49
+ let status = output.status.code().unwrap_or(1);
50
+ Ok(CmdOut {
51
+ ok: output.status.success(),
52
+ status,
53
+ stdout: String::from_utf8_lossy(&output.stdout).trim_end().to_string(),
54
+ stderr: String::from_utf8_lossy(&output.stderr).trim_end().to_string(),
55
+ })
56
+ }
57
+
58
+ fn run_git(args: &[String], cwd: Option<&Path>) -> Result<CmdOut, String> {
59
+ run_cmd("git", args, cwd)
60
+ }
61
+
62
+ fn normalize_worktree_path(path: &Path) -> PathBuf {
63
+ // Lexical normalization (no FS access): removes `.` and collapses `..` where possible.
64
+ let mut out: Vec<Component<'_>> = Vec::new();
65
+ for c in path.components() {
66
+ match c {
67
+ Component::CurDir => {}
68
+ Component::ParentDir => {
69
+ if let Some(last) = out.last() {
70
+ if matches!(last, Component::ParentDir) {
71
+ out.push(c);
72
+ } else if !matches!(last, Component::RootDir | Component::Prefix(_)) {
73
+ out.pop();
74
+ } else {
75
+ out.push(c);
76
+ }
77
+ } else {
78
+ out.push(c);
79
+ }
80
+ }
81
+ _ => out.push(c),
82
+ }
83
+ }
84
+
85
+ let mut pb = PathBuf::new();
86
+ for c in out {
87
+ pb.push(c.as_os_str());
88
+ }
89
+ pb
90
+ }
91
+
92
+ fn abs_path(path: &Path) -> Result<PathBuf, String> {
93
+ if path.is_absolute() {
94
+ Ok(normalize_worktree_path(path))
95
+ } else {
96
+ let cwd = std::env::current_dir().map_err(|e| format!("failed to get cwd: {e}"))?;
97
+ Ok(normalize_worktree_path(&cwd.join(path)))
98
+ }
99
+ }
100
+
101
+ fn assert_external_worktree_path(repo_root: &Path, worktree_path: &Path) -> Result<(), String> {
102
+ let repo = abs_path(repo_root)?;
103
+ let wt = abs_path(worktree_path)?;
104
+ if wt.starts_with(&repo) {
105
+ return Err(format!(
106
+ "Refusing to create worktree inside repo root (would recurse): repoRoot={} worktreePath={}",
107
+ repo.display(),
108
+ wt.display()
109
+ ));
110
+ }
111
+ Ok(())
112
+ }
113
+
114
+ fn is_git_worktree(dir: &Path) -> bool {
115
+ if !dir.exists() {
116
+ return false;
117
+ }
118
+ let args = vec![
119
+ "-C".to_string(),
120
+ dir.display().to_string(),
121
+ "rev-parse".to_string(),
122
+ "--is-inside-work-tree".to_string(),
123
+ ];
124
+ match run_git(&args, None) {
125
+ Ok(out) => out.ok && out.stdout.trim() == "true",
126
+ Err(_) => false,
127
+ }
128
+ }
129
+
130
+ fn branch_exists(repo_root: &Path, branch: &str) -> bool {
131
+ let args = vec![
132
+ "-C".to_string(),
133
+ repo_root.display().to_string(),
134
+ "show-ref".to_string(),
135
+ "--verify".to_string(),
136
+ "--quiet".to_string(),
137
+ format!("refs/heads/{branch}"),
138
+ ];
139
+ match run_git(&args, None) {
140
+ Ok(out) => out.status == 0,
141
+ Err(_) => false,
142
+ }
143
+ }
144
+
145
+ fn ensure_branch_at(repo_root: &Path, branch: &str, base_sha: &str) -> Result<(), String> {
146
+ if branch_exists(repo_root, branch) {
147
+ return Ok(());
148
+ }
149
+ let args = vec![
150
+ "-C".to_string(),
151
+ repo_root.display().to_string(),
152
+ "branch".to_string(),
153
+ branch.to_string(),
154
+ base_sha.to_string(),
155
+ ];
156
+ let out = run_git(&args, None)?;
157
+ if !out.ok {
158
+ return Err(out.stderr.is_empty().then(|| format!("git branch failed")).unwrap_or(out.stderr));
159
+ }
160
+ Ok(())
161
+ }
162
+
163
+ #[derive(Deserialize)]
164
+ struct WorktreeEnsureIn {
165
+ repoRoot: String,
166
+ worktreePath: String,
167
+ branch: String,
168
+ baseSha: String,
169
+ }
170
+
171
+ #[derive(Serialize)]
172
+ struct WorktreeEnsureOut {
173
+ worktreePath: String,
174
+ }
175
+
176
+ fn worktree_ensure(input: WorktreeEnsureIn) -> Result<WorktreeEnsureOut, String> {
177
+ let repo_root = PathBuf::from(input.repoRoot);
178
+ let worktree_path = PathBuf::from(input.worktreePath);
179
+ assert_external_worktree_path(&repo_root, &worktree_path)?;
180
+ ensure_branch_at(&repo_root, &input.branch, &input.baseSha)?;
181
+
182
+ if worktree_path.exists() {
183
+ if !is_git_worktree(&worktree_path) {
184
+ return Err(format!(
185
+ "Worktree path exists but is not a git worktree: {}",
186
+ worktree_path.display()
187
+ ));
188
+ }
189
+ return Ok(WorktreeEnsureOut {
190
+ worktreePath: worktree_path.display().to_string(),
191
+ });
192
+ }
193
+
194
+ if let Some(parent) = worktree_path.parent() {
195
+ fs::create_dir_all(parent)
196
+ .map_err(|e| format!("failed to create worktree parent dir: {e}"))?;
197
+ }
198
+
199
+ let args = vec![
200
+ "-C".to_string(),
201
+ repo_root.display().to_string(),
202
+ "worktree".to_string(),
203
+ "add".to_string(),
204
+ worktree_path.display().to_string(),
205
+ input.branch,
206
+ ];
207
+ let out = run_git(&args, None)?;
208
+ if !out.ok {
209
+ return Err(if out.stderr.is_empty() {
210
+ format!("git worktree add failed: {}", worktree_path.display())
211
+ } else {
212
+ out.stderr
213
+ });
214
+ }
215
+ Ok(WorktreeEnsureOut {
216
+ worktreePath: worktree_path.display().to_string(),
217
+ })
218
+ }
219
+
220
+ #[derive(Deserialize)]
221
+ struct WorktreeRemoveIn {
222
+ repoRoot: String,
223
+ worktreePath: String,
224
+ #[serde(default)]
225
+ force: bool,
226
+ }
227
+
228
+ #[derive(Serialize)]
229
+ struct WorktreeRemoveOut {
230
+ ok: bool,
231
+ }
232
+
233
+ fn worktree_remove(input: WorktreeRemoveIn) -> Result<WorktreeRemoveOut, String> {
234
+ let repo_root = PathBuf::from(input.repoRoot);
235
+ let worktree_path = PathBuf::from(input.worktreePath);
236
+ let mut args = vec![
237
+ "-C".to_string(),
238
+ repo_root.display().to_string(),
239
+ "worktree".to_string(),
240
+ "remove".to_string(),
241
+ ];
242
+ if input.force {
243
+ args.push("--force".to_string());
244
+ }
245
+ args.push(worktree_path.display().to_string());
246
+ let out = run_git(&args, None)?;
247
+ if !out.ok {
248
+ return Err(if out.stderr.is_empty() {
249
+ format!("git worktree remove failed: {}", worktree_path.display())
250
+ } else {
251
+ out.stderr
252
+ });
253
+ }
254
+ Ok(WorktreeRemoveOut { ok: true })
255
+ }
256
+
257
+ fn normalize_repo_path(p: &str) -> Option<String> {
258
+ let posix = p.replace('\\', "/");
259
+ if posix.starts_with('/') {
260
+ return None;
261
+ }
262
+ let bytes = posix.as_bytes();
263
+ if bytes.len() >= 3 {
264
+ let c0 = bytes[0];
265
+ let c1 = bytes[1];
266
+ let c2 = bytes[2];
267
+ if (c0 as char).is_ascii_alphabetic() && c1 == b':' && c2 == b'/' {
268
+ return None;
269
+ }
270
+ }
271
+
272
+ let mut stack: Vec<&str> = Vec::new();
273
+ for part in posix.split('/') {
274
+ if part.is_empty() || part == "." {
275
+ continue;
276
+ }
277
+ if part == ".." {
278
+ if stack.is_empty() {
279
+ return None;
280
+ }
281
+ stack.pop();
282
+ continue;
283
+ }
284
+ stack.push(part);
285
+ }
286
+ if stack.is_empty() {
287
+ return None;
288
+ }
289
+ Some(stack.join("/"))
290
+ }
291
+
292
+ fn touched_files_from_unified_diff(patch_path: &Path) -> Result<Vec<(String, bool)>, String> {
293
+ let f = File::open(patch_path)
294
+ .map_err(|e| format!("failed to open patch for parsing: {}: {e}", patch_path.display()))?;
295
+ let reader = BufReader::new(f);
296
+
297
+ let mut files: Vec<(String, bool)> = Vec::new();
298
+ let mut seen: BTreeSet<String> = BTreeSet::new();
299
+
300
+ for line in reader.lines() {
301
+ let line = line.map_err(|e| format!("failed reading patch: {e}"))?;
302
+ if !line.starts_with("diff --git ") {
303
+ continue;
304
+ }
305
+ // Typical: diff --git a/foo/bar b/foo/bar
306
+ let rest = line.trim_start_matches("diff --git ").trim();
307
+ let mut it = rest.split_whitespace();
308
+ let a = it.next();
309
+ let b = it.next();
310
+ if a.is_none() || b.is_none() {
311
+ continue;
312
+ }
313
+ let a_path = a.unwrap().strip_prefix("a/").unwrap_or(a.unwrap());
314
+ let b_path = b.unwrap().strip_prefix("b/").unwrap_or(b.unwrap());
315
+
316
+ let file = if b_path == "/dev/null" { a_path } else { b_path };
317
+ match normalize_repo_path(file) {
318
+ Some(n) => {
319
+ if seen.contains(&n) {
320
+ continue;
321
+ }
322
+ seen.insert(n.clone());
323
+ files.push((n, false));
324
+ }
325
+ None => {
326
+ files.push((file.to_string(), true));
327
+ }
328
+ }
329
+ }
330
+ Ok(files)
331
+ }
332
+
333
+ fn ensure_owned(touched_files: &[(String, bool)], allowed_prefixes: &[String]) -> Result<(), String> {
334
+ let mut allowed: Vec<String> = allowed_prefixes
335
+ .iter()
336
+ .map(|p| p.replace('\\', "/"))
337
+ .filter(|p| !p.trim().is_empty())
338
+ .map(|p| if p.ends_with('/') { p } else { format!("{p}/") })
339
+ .collect();
340
+ allowed.sort();
341
+ allowed.dedup();
342
+
343
+ if allowed.is_empty() {
344
+ return Err("allowedPathPrefixes is empty".to_string());
345
+ }
346
+
347
+ let mut violations: Vec<String> = Vec::new();
348
+ for (path, invalid) in touched_files.iter() {
349
+ if *invalid {
350
+ violations.push(format!("invalid path in patch: {path}"));
351
+ continue;
352
+ }
353
+ let mut ok = false;
354
+ for prefix in allowed.iter() {
355
+ let base = prefix.trim_end_matches('/');
356
+ if path == base || path.starts_with(prefix) {
357
+ ok = true;
358
+ break;
359
+ }
360
+ }
361
+ if !ok {
362
+ violations.push(format!("unauthorized path: {path}"));
363
+ }
364
+ }
365
+
366
+ if !violations.is_empty() {
367
+ return Err(format!(
368
+ "patch ownership check failed:\n- {}",
369
+ violations.join("\n- ")
370
+ ));
371
+ }
372
+ Ok(())
373
+ }
374
+
375
+ #[derive(Deserialize)]
376
+ struct PatchApplyIn {
377
+ worktreePath: String,
378
+ patchPath: String,
379
+ allowedPathPrefixes: Vec<String>,
380
+ }
381
+
382
+ #[derive(Serialize)]
383
+ struct PatchApplyOut {
384
+ touchedFiles: Vec<String>,
385
+ }
386
+
387
+ fn patch_apply(input: PatchApplyIn) -> Result<PatchApplyOut, String> {
388
+ let worktree_path = PathBuf::from(input.worktreePath);
389
+ let patch_path = PathBuf::from(input.patchPath);
390
+
391
+ let patch_text = fs::read_to_string(&patch_path)
392
+ .map_err(|e| format!("failed to read patch file: {}: {e}", patch_path.display()))?;
393
+ let trimmed = patch_text.trim();
394
+ if trimmed.is_empty() {
395
+ return Ok(PatchApplyOut {
396
+ touchedFiles: Vec::new(),
397
+ });
398
+ }
399
+
400
+ let touched = touched_files_from_unified_diff(&patch_path)?;
401
+ if touched.is_empty() {
402
+ return Err("patch has content but no \"diff --git\" headers (not a unified diff?)".to_string());
403
+ }
404
+ ensure_owned(&touched, &input.allowedPathPrefixes)?;
405
+
406
+ let args_check = vec![
407
+ "-C".to_string(),
408
+ worktree_path.display().to_string(),
409
+ "apply".to_string(),
410
+ "--check".to_string(),
411
+ patch_path.display().to_string(),
412
+ ];
413
+ let out = run_git(&args_check, None)?;
414
+ if !out.ok {
415
+ return Err(if out.stderr.is_empty() {
416
+ "git apply --check failed".to_string()
417
+ } else {
418
+ out.stderr
419
+ });
420
+ }
421
+
422
+ let args_apply = vec![
423
+ "-C".to_string(),
424
+ worktree_path.display().to_string(),
425
+ "apply".to_string(),
426
+ patch_path.display().to_string(),
427
+ ];
428
+ let out2 = run_git(&args_apply, None)?;
429
+ if !out2.ok {
430
+ return Err(if out2.stderr.is_empty() {
431
+ "git apply failed".to_string()
432
+ } else {
433
+ out2.stderr
434
+ });
435
+ }
436
+
437
+ Ok(PatchApplyOut {
438
+ touchedFiles: touched
439
+ .into_iter()
440
+ .filter(|(_, invalid)| !*invalid)
441
+ .map(|(p, _)| p)
442
+ .collect(),
443
+ })
444
+ }
445
+
446
+ #[derive(Deserialize)]
447
+ struct CommitAllIn {
448
+ repoRoot: String,
449
+ message: String,
450
+ }
451
+
452
+ #[derive(Serialize)]
453
+ struct CommitAllOut {
454
+ sha: String,
455
+ }
456
+
457
+ fn commit_all(input: CommitAllIn) -> Result<CommitAllOut, String> {
458
+ let repo_root = PathBuf::from(input.repoRoot);
459
+
460
+ let out_add = run_git(
461
+ &vec![
462
+ "-C".to_string(),
463
+ repo_root.display().to_string(),
464
+ "add".to_string(),
465
+ "-A".to_string(),
466
+ ],
467
+ None,
468
+ )?;
469
+ if !out_add.ok {
470
+ return Err(out_add.stderr.is_empty().then(|| "git add failed".to_string()).unwrap_or(out_add.stderr));
471
+ }
472
+
473
+ let out_commit = run_git(
474
+ &vec![
475
+ "-C".to_string(),
476
+ repo_root.display().to_string(),
477
+ "commit".to_string(),
478
+ "-m".to_string(),
479
+ input.message,
480
+ ],
481
+ None,
482
+ )?;
483
+ if !out_commit.ok {
484
+ return Err(out_commit.stderr.is_empty().then(|| "git commit failed".to_string()).unwrap_or(out_commit.stderr));
485
+ }
486
+
487
+ let out_sha = run_git(
488
+ &vec![
489
+ "-C".to_string(),
490
+ repo_root.display().to_string(),
491
+ "rev-parse".to_string(),
492
+ "HEAD".to_string(),
493
+ ],
494
+ None,
495
+ )?;
496
+ if !out_sha.ok {
497
+ return Err(out_sha.stderr.is_empty().then(|| "git rev-parse HEAD failed".to_string()).unwrap_or(out_sha.stderr));
498
+ }
499
+
500
+ Ok(CommitAllOut {
501
+ sha: out_sha.stdout.trim().to_string(),
502
+ })
503
+ }
504
+
505
+ #[derive(Deserialize)]
506
+ struct VerifyCmdIn {
507
+ name: String,
508
+ command: String,
509
+ }
510
+
511
+ #[derive(Deserialize)]
512
+ struct VerifyRunIn {
513
+ worktreePath: String,
514
+ outDir: String,
515
+ commands: Vec<VerifyCmdIn>,
516
+ }
517
+
518
+ #[derive(Serialize)]
519
+ struct VerifyCmdOut {
520
+ name: String,
521
+ command: String,
522
+ ok: bool,
523
+ exitCode: i32,
524
+ outputPath: String,
525
+ }
526
+
527
+ #[derive(Serialize)]
528
+ struct VerifySummaryOut {
529
+ version: i32,
530
+ ranAt: String,
531
+ commands: Vec<VerifyCmdOut>,
532
+ ok: bool,
533
+ }
534
+
535
+ fn safe_name(name: &str) -> String {
536
+ let mut out = String::new();
537
+ let mut last_was_dash = false;
538
+ for ch in name.trim().to_lowercase().chars() {
539
+ let allowed = ch.is_ascii_alphanumeric() || ch == '.' || ch == '_' || ch == '-';
540
+ if allowed {
541
+ out.push(ch);
542
+ last_was_dash = false;
543
+ } else if !last_was_dash {
544
+ out.push('-');
545
+ last_was_dash = true;
546
+ }
547
+ }
548
+ let out = out.trim_matches('-').to_string();
549
+ if out.is_empty() {
550
+ "command".to_string()
551
+ } else {
552
+ out
553
+ }
554
+ }
555
+
556
+ fn run_shell_command_to_file(command: &str, cwd: &Path, output_path: &Path) -> Result<i32, String> {
557
+ let file = File::create(output_path)
558
+ .map_err(|e| format!("failed to create output file {}: {e}", output_path.display()))?;
559
+ let file_err = file
560
+ .try_clone()
561
+ .map_err(|e| format!("failed to clone output file handle: {e}"))?;
562
+
563
+ let mut cmd;
564
+ if cfg!(windows) {
565
+ cmd = Command::new("cmd");
566
+ cmd.arg("/C").arg(command);
567
+ } else {
568
+ cmd = Command::new("sh");
569
+ cmd.arg("-lc").arg(command);
570
+ }
571
+ let status = cmd
572
+ .current_dir(cwd)
573
+ .stdin(Stdio::null())
574
+ .stdout(Stdio::from(file))
575
+ .stderr(Stdio::from(file_err))
576
+ .status()
577
+ .map_err(|e| format!("failed to run command: {e}"))?;
578
+
579
+ Ok(status.code().unwrap_or(1))
580
+ }
581
+
582
+ fn verify_run(input: VerifyRunIn) -> Result<VerifySummaryOut, String> {
583
+ let worktree = PathBuf::from(input.worktreePath);
584
+ let out_dir = PathBuf::from(input.outDir);
585
+ fs::create_dir_all(&out_dir)
586
+ .map_err(|e| format!("failed to create verify outDir {}: {e}", out_dir.display()))?;
587
+
588
+ let mut results: Vec<VerifyCmdOut> = Vec::new();
589
+ let mut all_ok = true;
590
+
591
+ for c in input.commands.iter() {
592
+ let name_safe = safe_name(&c.name);
593
+ let output_path = out_dir.join(format!("{name_safe}.txt"));
594
+
595
+ let exit_code = run_shell_command_to_file(&c.command, &worktree, &output_path)?;
596
+ let ok = exit_code == 0;
597
+ if !ok {
598
+ all_ok = false;
599
+ }
600
+
601
+ results.push(VerifyCmdOut {
602
+ name: c.name.clone(),
603
+ command: c.command.clone(),
604
+ ok,
605
+ exitCode: exit_code,
606
+ outputPath: output_path.display().to_string(),
607
+ });
608
+ }
609
+
610
+ let summary = VerifySummaryOut {
611
+ version: 1,
612
+ ranAt: now_iso(),
613
+ commands: results,
614
+ ok: all_ok,
615
+ };
616
+
617
+ // Write evidence file.
618
+ let summary_path = out_dir.join("summary.json");
619
+ let mut f = File::create(&summary_path)
620
+ .map_err(|e| format!("failed to write {}: {e}", summary_path.display()))?;
621
+ let json = serde_json::to_string_pretty(&summary)
622
+ .map_err(|e| format!("failed to serialize verify summary: {e}"))?;
623
+ f.write_all(json.as_bytes())
624
+ .and_then(|_| f.write_all(b"\n"))
625
+ .map_err(|e| format!("failed to write verify summary: {e}"))?;
626
+
627
+ Ok(summary)
628
+ }
629
+
630
+ fn usage() {
631
+ eprintln!(
632
+ r#"ecc-kernel
633
+
634
+ Usage:
635
+ ecc-kernel <command> (JSON input on stdin; JSON output on stdout)
636
+
637
+ Commands:
638
+ worktree.ensure
639
+ worktree.remove
640
+ patch.apply
641
+ git.commit_all
642
+ verify.run
643
+ "#
644
+ );
645
+ }
646
+
647
+ fn real_main() -> Result<(), String> {
648
+ let mut args = std::env::args();
649
+ let _ = args.next();
650
+ let cmd = match args.next() {
651
+ Some(c) => c,
652
+ None => {
653
+ usage();
654
+ return Err("missing command".to_string());
655
+ }
656
+ };
657
+
658
+ if cmd == "--help" || cmd == "-h" {
659
+ usage();
660
+ return Ok(());
661
+ }
662
+
663
+ if cmd == "--version" || cmd == "-V" {
664
+ println!("ecc-kernel {}", env!("CARGO_PKG_VERSION"));
665
+ return Ok(());
666
+ }
667
+
668
+ let result = match cmd.as_str() {
669
+ "worktree.ensure" => {
670
+ let input: WorktreeEnsureIn = read_stdin_json()?;
671
+ let out = worktree_ensure(input)?;
672
+ write_stdout_json(&out)
673
+ }
674
+ "worktree.remove" => {
675
+ let input: WorktreeRemoveIn = read_stdin_json()?;
676
+ let out = worktree_remove(input)?;
677
+ write_stdout_json(&out)
678
+ }
679
+ "patch.apply" => {
680
+ let input: PatchApplyIn = read_stdin_json()?;
681
+ let out = patch_apply(input)?;
682
+ write_stdout_json(&out)
683
+ }
684
+ "git.commit_all" => {
685
+ let input: CommitAllIn = read_stdin_json()?;
686
+ let out = commit_all(input)?;
687
+ write_stdout_json(&out)
688
+ }
689
+ "verify.run" => {
690
+ let input: VerifyRunIn = read_stdin_json()?;
691
+ let out = verify_run(input)?;
692
+ write_stdout_json(&out)
693
+ }
694
+ _ => Err(format!("unknown command: {cmd}")),
695
+ };
696
+
697
+ result
698
+ }
699
+
700
+ fn main() -> ExitCode {
701
+ match real_main() {
702
+ Ok(()) => ExitCode::from(0),
703
+ Err(err) => {
704
+ if err != "missing command" {
705
+ eprintln!("{err}");
706
+ }
707
+ ExitCode::from(1)
708
+ }
709
+ }
710
+ }