patchwork-os 0.2.0-alpha.21 → 0.2.0-alpha.23

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 (169) hide show
  1. package/README.md +26 -12
  2. package/deploy/bootstrap-vps.sh +184 -0
  3. package/dist/approvalHttp.js +6 -1
  4. package/dist/approvalHttp.js.map +1 -1
  5. package/dist/automation.d.ts +20 -0
  6. package/dist/automation.js +35 -0
  7. package/dist/automation.js.map +1 -1
  8. package/dist/bridge.js +22 -4
  9. package/dist/bridge.js.map +1 -1
  10. package/dist/bridgeToken.js +57 -19
  11. package/dist/bridgeToken.js.map +1 -1
  12. package/dist/commands/recipe.d.ts +256 -0
  13. package/dist/commands/recipe.js +1313 -0
  14. package/dist/commands/recipe.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.js +9 -0
  17. package/dist/config.js.map +1 -1
  18. package/dist/connectors/baseConnector.d.ts +117 -0
  19. package/dist/connectors/baseConnector.js +213 -0
  20. package/dist/connectors/baseConnector.js.map +1 -0
  21. package/dist/connectors/confluence.d.ts +111 -0
  22. package/dist/connectors/confluence.js +406 -0
  23. package/dist/connectors/confluence.js.map +1 -0
  24. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  25. package/dist/connectors/fixtureLibrary.js +70 -0
  26. package/dist/connectors/fixtureLibrary.js.map +1 -0
  27. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  28. package/dist/connectors/fixtureRecorder.js +35 -0
  29. package/dist/connectors/fixtureRecorder.js.map +1 -0
  30. package/dist/connectors/github.js +2 -11
  31. package/dist/connectors/github.js.map +1 -1
  32. package/dist/connectors/gmail.js +23 -7
  33. package/dist/connectors/gmail.js.map +1 -1
  34. package/dist/connectors/googleCalendar.js +23 -7
  35. package/dist/connectors/googleCalendar.js.map +1 -1
  36. package/dist/connectors/jira.d.ts +98 -0
  37. package/dist/connectors/jira.js +379 -0
  38. package/dist/connectors/jira.js.map +1 -0
  39. package/dist/connectors/linear.js +2 -11
  40. package/dist/connectors/linear.js.map +1 -1
  41. package/dist/connectors/mcpOAuth.d.ts +1 -0
  42. package/dist/connectors/mcpOAuth.js +30 -4
  43. package/dist/connectors/mcpOAuth.js.map +1 -1
  44. package/dist/connectors/mockConnector.d.ts +28 -0
  45. package/dist/connectors/mockConnector.js +81 -0
  46. package/dist/connectors/mockConnector.js.map +1 -0
  47. package/dist/connectors/notion.d.ts +143 -0
  48. package/dist/connectors/notion.js +424 -0
  49. package/dist/connectors/notion.js.map +1 -0
  50. package/dist/connectors/sentry.js +2 -11
  51. package/dist/connectors/sentry.js.map +1 -1
  52. package/dist/connectors/slack.js +50 -15
  53. package/dist/connectors/slack.js.map +1 -1
  54. package/dist/connectors/tokenStorage.d.ts +35 -0
  55. package/dist/connectors/tokenStorage.js +394 -0
  56. package/dist/connectors/tokenStorage.js.map +1 -0
  57. package/dist/connectors/zendesk.d.ts +104 -0
  58. package/dist/connectors/zendesk.js +424 -0
  59. package/dist/connectors/zendesk.js.map +1 -0
  60. package/dist/featureFlags.d.ts +73 -0
  61. package/dist/featureFlags.js +203 -0
  62. package/dist/featureFlags.js.map +1 -0
  63. package/dist/fp/automationInterpreter.js +1 -0
  64. package/dist/fp/automationInterpreter.js.map +1 -1
  65. package/dist/fp/automationProgram.d.ts +1 -1
  66. package/dist/fp/automationProgram.js.map +1 -1
  67. package/dist/fp/policyParser.js +17 -0
  68. package/dist/fp/policyParser.js.map +1 -1
  69. package/dist/index.js +508 -36
  70. package/dist/index.js.map +1 -1
  71. package/dist/oauth.d.ts +4 -1
  72. package/dist/oauth.js +50 -14
  73. package/dist/oauth.js.map +1 -1
  74. package/dist/recipes/chainedRunner.d.ts +104 -0
  75. package/dist/recipes/chainedRunner.js +359 -0
  76. package/dist/recipes/chainedRunner.js.map +1 -0
  77. package/dist/recipes/dependencyGraph.d.ts +39 -0
  78. package/dist/recipes/dependencyGraph.js +199 -0
  79. package/dist/recipes/dependencyGraph.js.map +1 -0
  80. package/dist/recipes/legacyRecipeCompat.d.ts +1 -0
  81. package/dist/recipes/legacyRecipeCompat.js +97 -0
  82. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  83. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  84. package/dist/recipes/nestedRecipeStep.js +95 -0
  85. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  86. package/dist/recipes/outputRegistry.d.ts +28 -0
  87. package/dist/recipes/outputRegistry.js +52 -0
  88. package/dist/recipes/outputRegistry.js.map +1 -0
  89. package/dist/recipes/schemaGenerator.d.ts +28 -0
  90. package/dist/recipes/schemaGenerator.js +484 -0
  91. package/dist/recipes/schemaGenerator.js.map +1 -0
  92. package/dist/recipes/templateEngine.d.ts +62 -0
  93. package/dist/recipes/templateEngine.js +182 -0
  94. package/dist/recipes/templateEngine.js.map +1 -0
  95. package/dist/recipes/toolRegistry.d.ts +181 -0
  96. package/dist/recipes/toolRegistry.js +300 -0
  97. package/dist/recipes/toolRegistry.js.map +1 -0
  98. package/dist/recipes/tools/calendar.d.ts +6 -0
  99. package/dist/recipes/tools/calendar.js +61 -0
  100. package/dist/recipes/tools/calendar.js.map +1 -0
  101. package/dist/recipes/tools/confluence.d.ts +6 -0
  102. package/dist/recipes/tools/confluence.js +254 -0
  103. package/dist/recipes/tools/confluence.js.map +1 -0
  104. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  105. package/dist/recipes/tools/diagnostics.js +36 -0
  106. package/dist/recipes/tools/diagnostics.js.map +1 -0
  107. package/dist/recipes/tools/file.d.ts +6 -0
  108. package/dist/recipes/tools/file.js +170 -0
  109. package/dist/recipes/tools/file.js.map +1 -0
  110. package/dist/recipes/tools/git.d.ts +6 -0
  111. package/dist/recipes/tools/git.js +63 -0
  112. package/dist/recipes/tools/git.js.map +1 -0
  113. package/dist/recipes/tools/github.d.ts +6 -0
  114. package/dist/recipes/tools/github.js +91 -0
  115. package/dist/recipes/tools/github.js.map +1 -0
  116. package/dist/recipes/tools/gmail.d.ts +6 -0
  117. package/dist/recipes/tools/gmail.js +210 -0
  118. package/dist/recipes/tools/gmail.js.map +1 -0
  119. package/dist/recipes/tools/index.d.ts +18 -0
  120. package/dist/recipes/tools/index.js +21 -0
  121. package/dist/recipes/tools/index.js.map +1 -0
  122. package/dist/recipes/tools/linear.d.ts +6 -0
  123. package/dist/recipes/tools/linear.js +83 -0
  124. package/dist/recipes/tools/linear.js.map +1 -0
  125. package/dist/recipes/tools/notion.d.ts +6 -0
  126. package/dist/recipes/tools/notion.js +278 -0
  127. package/dist/recipes/tools/notion.js.map +1 -0
  128. package/dist/recipes/tools/slack.d.ts +6 -0
  129. package/dist/recipes/tools/slack.js +72 -0
  130. package/dist/recipes/tools/slack.js.map +1 -0
  131. package/dist/recipes/tools/zendesk.d.ts +6 -0
  132. package/dist/recipes/tools/zendesk.js +245 -0
  133. package/dist/recipes/tools/zendesk.js.map +1 -0
  134. package/dist/recipes/yamlRunner.d.ts +71 -7
  135. package/dist/recipes/yamlRunner.js +406 -439
  136. package/dist/recipes/yamlRunner.js.map +1 -1
  137. package/dist/riskTier.js +1 -0
  138. package/dist/riskTier.js.map +1 -1
  139. package/dist/runLog.d.ts +18 -0
  140. package/dist/runLog.js +5 -0
  141. package/dist/runLog.js.map +1 -1
  142. package/dist/server.d.ts +4 -0
  143. package/dist/server.js +224 -0
  144. package/dist/server.js.map +1 -1
  145. package/dist/streamableHttp.js +2 -0
  146. package/dist/streamableHttp.js.map +1 -1
  147. package/dist/tools/github/actions.js +4 -2
  148. package/dist/tools/github/actions.js.map +1 -1
  149. package/dist/tools/github/composite.d.ts +339 -0
  150. package/dist/tools/github/composite.js +343 -0
  151. package/dist/tools/github/composite.js.map +1 -0
  152. package/dist/tools/github/index.d.ts +1 -0
  153. package/dist/tools/github/index.js +1 -0
  154. package/dist/tools/github/index.js.map +1 -1
  155. package/dist/tools/github/issues.js +8 -4
  156. package/dist/tools/github/issues.js.map +1 -1
  157. package/dist/tools/github/pr.js +14 -7
  158. package/dist/tools/github/pr.js.map +1 -1
  159. package/dist/tools/index.js +10 -1
  160. package/dist/tools/index.js.map +1 -1
  161. package/dist/tools/searchTools.js +1 -1
  162. package/dist/tools/searchTools.js.map +1 -1
  163. package/dist/transport.d.ts +7 -1
  164. package/dist/transport.js +85 -11
  165. package/dist/transport.js.map +1 -1
  166. package/package.json +1 -1
  167. package/templates/automation-policies/recipe-authoring.json +25 -0
  168. package/templates/automation-policy.example.json +6 -0
  169. package/templates/recipes/lint-on-save.yaml +1 -2
package/dist/index.js CHANGED
@@ -607,25 +607,94 @@ if (process.argv[2] === "recipe" && process.argv[3] === "list") {
607
607
  // a running bridge's /recipes/run endpoint if one is available.
608
608
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
609
609
  const args = process.argv.slice(4);
610
- const localFlag = args.includes("--local");
611
- const name = args.find((a) => !a.startsWith("--"));
612
- if (!name) {
613
- process.stderr.write("Usage: patchwork recipe run <name> [--local]\n");
610
+ const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
611
+ let localFlag = false;
612
+ let dryRun = false;
613
+ let recipeRef;
614
+ let step;
615
+ const vars = {};
616
+ for (let i = 0; i < args.length; i++) {
617
+ const arg = args[i];
618
+ if (arg === undefined) {
619
+ continue;
620
+ }
621
+ const currentArg = arg;
622
+ if (currentArg === "--local") {
623
+ localFlag = true;
624
+ continue;
625
+ }
626
+ if (currentArg === "--dry-run") {
627
+ dryRun = true;
628
+ continue;
629
+ }
630
+ if (currentArg === "--step" || currentArg.startsWith("--step=")) {
631
+ const value = currentArg === "--step"
632
+ ? args[++i]
633
+ : currentArg.slice("--step=".length);
634
+ if (!value) {
635
+ process.stderr.write(`Error: --step requires a value\n${usage}`);
636
+ process.exit(1);
637
+ }
638
+ step = value;
639
+ continue;
640
+ }
641
+ if (currentArg === "--var" || currentArg.startsWith("--var=")) {
642
+ const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
643
+ if (!assignment) {
644
+ process.stderr.write(`Error: --var requires KEY=VALUE\n${usage}`);
645
+ process.exit(1);
646
+ }
647
+ const eqIndex = assignment.indexOf("=");
648
+ if (eqIndex <= 0) {
649
+ process.stderr.write(`Error: invalid --var assignment "${assignment}" (expected KEY=VALUE)\n${usage}`);
650
+ process.exit(1);
651
+ }
652
+ const key = assignment.slice(0, eqIndex);
653
+ const value = assignment.slice(eqIndex + 1);
654
+ vars[key] = value;
655
+ continue;
656
+ }
657
+ if (currentArg.startsWith("--")) {
658
+ process.stderr.write(`Error: unknown option ${currentArg}\n${usage}`);
659
+ process.exit(1);
660
+ }
661
+ if (!recipeRef) {
662
+ recipeRef = currentArg;
663
+ continue;
664
+ }
665
+ process.stderr.write(`Error: unexpected argument ${currentArg}\n${usage}`);
666
+ process.exit(1);
667
+ }
668
+ if (!recipeRef) {
669
+ process.stderr.write(usage);
614
670
  process.exit(1);
615
671
  }
672
+ const recipeArg = recipeRef;
616
673
  (async () => {
617
674
  try {
618
- // Try bridge first (requires --claude-driver subprocess).
675
+ const seedVars = Object.keys(vars).length > 0 ? vars : undefined;
676
+ const explicitFile = (() => {
677
+ try {
678
+ const resolved = path.resolve(recipeArg);
679
+ return existsSync(resolved) && statSync(resolved).isFile();
680
+ }
681
+ catch {
682
+ return false;
683
+ }
684
+ })();
619
685
  const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
620
686
  const lock = localFlag ? null : findBridgeLock();
621
- if (lock) {
687
+ if (lock && !dryRun && !step && !explicitFile) {
622
688
  const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
623
689
  method: "POST",
624
690
  headers: {
625
691
  Authorization: `Bearer ${lock.authToken}`,
626
692
  "Content-Type": "application/json",
627
693
  },
628
- body: JSON.stringify({ name }),
694
+ body: JSON.stringify({
695
+ name: recipeArg,
696
+ ...(seedVars ? { vars: seedVars } : {}),
697
+ }),
629
698
  });
630
699
  const body = (await res.json());
631
700
  if (!body.ok) {
@@ -638,43 +707,41 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
638
707
  // else: fall through to local runner below
639
708
  }
640
709
  else {
641
- process.stdout.write(` ✓ enqueued recipe "${name}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
710
+ process.stdout.write(` ✓ enqueued recipe "${recipeArg}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
642
711
  " Watch progress on the dashboard Tasks page or via listClaudeTasks.\n");
643
712
  process.exit(0);
644
713
  return;
645
714
  }
646
715
  }
647
- // No bridge run locally using the YAML runner.
648
- const { loadYamlRecipe, runYamlRecipe } = await import("./recipes/yamlRunner.js");
649
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
650
- const bundledDir = fileURLToPath(new URL("../templates/recipes", import.meta.url));
651
- const candidates = [
652
- path.join(recipesDir, `${name}.yaml`),
653
- path.join(recipesDir, `${name}.yml`),
654
- path.join(recipesDir, `${name}.json`),
655
- path.join(bundledDir, `${name}.yaml`),
656
- path.join(bundledDir, `${name}.yml`),
657
- ];
658
- let recipePath;
659
- for (const c of candidates) {
660
- if (existsSync(c)) {
661
- recipePath = c;
662
- break;
663
- }
664
- }
665
- if (!recipePath) {
666
- process.stderr.write(`Error: recipe "${name}" not found in ${recipesDir}\n` +
667
- " Run `patchwork-os recipe list` to see available recipes.\n");
668
- process.exit(1);
716
+ const { runRecipe, runRecipeDryPlan, summarizeRecipeExecution } = await import("./commands/recipe.js");
717
+ if (dryRun) {
718
+ const plan = await runRecipeDryPlan(recipeArg, {
719
+ ...(step ? { step } : {}),
720
+ ...(seedVars ? { vars: seedVars } : {}),
721
+ });
722
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
723
+ process.exit(0);
669
724
  return;
670
725
  }
671
- process.stdout.write(` Running recipe "${name}" locally…\n`);
672
- const recipe = loadYamlRecipe(recipePath);
726
+ process.stdout.write(step
727
+ ? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
728
+ : ` Running recipe "${recipeArg}" locally…\n`);
673
729
  const workdir = lock?.workspace || process.cwd();
674
- const result = await runYamlRecipe(recipe, { workdir });
675
- process.stdout.write(` ✓ ${result.stepsRun} step(s) completed\n`);
676
- if (result.outputs.length > 0) {
677
- process.stdout.write(` Output written to:\n${result.outputs.map((o) => ` ${o}`).join("\n")}\n`);
730
+ const run = await runRecipe(recipeArg, {
731
+ ...(step ? { step } : {}),
732
+ ...(seedVars ? { vars: seedVars } : {}),
733
+ workdir,
734
+ });
735
+ if (run.stepSelection) {
736
+ process.stdout.write(` Selected step via ${run.stepSelection.matchedBy}: ${run.stepSelection.matchedValue}\n`);
737
+ }
738
+ const summary = summarizeRecipeExecution(run.result);
739
+ process.stdout.write(` ${summary.ok ? "✓" : "✗"} ${summary.steps} step(s) completed\n`);
740
+ if (summary.errorMessage) {
741
+ process.stderr.write(` Error: ${summary.errorMessage}\n`);
742
+ }
743
+ if (summary.outputs.length > 0) {
744
+ process.stdout.write(` Output written to:\n${summary.outputs.map((o) => ` ${o}`).join("\n")}\n`);
678
745
  }
679
746
  process.exit(0);
680
747
  }
@@ -710,6 +777,411 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
710
777
  }
711
778
  })();
712
779
  }
780
+ // Patchwork: `patchwork recipe schema [outputDir]` — write generated recipe schemas to disk.
781
+ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
782
+ const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
783
+ (async () => {
784
+ try {
785
+ const { runSchema } = await import("./commands/recipe.js");
786
+ const result = await runSchema(path.resolve(outputDir));
787
+ process.stdout.write(` ✓ Wrote schemas to ${result.outputDir}\n`);
788
+ for (const file of result.filesWritten) {
789
+ process.stdout.write(` ${file}\n`);
790
+ }
791
+ process.exit(0);
792
+ }
793
+ catch (err) {
794
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
795
+ process.exit(1);
796
+ }
797
+ })();
798
+ }
799
+ // Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
800
+ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
801
+ const args = process.argv.slice(4);
802
+ const recipeName = args[0];
803
+ if (!recipeName) {
804
+ process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>]\n");
805
+ process.stderr.write("\nTemplates:\n");
806
+ (async () => {
807
+ const { listTemplates } = await import("./commands/recipe.js");
808
+ for (const t of listTemplates()) {
809
+ process.stderr.write(` ${t}\n`);
810
+ }
811
+ process.exit(1);
812
+ })();
813
+ }
814
+ else {
815
+ (async () => {
816
+ try {
817
+ const { runNew } = await import("./commands/recipe.js");
818
+ const templateIdx = args.indexOf("--template");
819
+ const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
820
+ const descIdx = args.indexOf("--desc");
821
+ const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
822
+ `Recipe: ${recipeName}`;
823
+ const result = runNew({
824
+ name: recipeName,
825
+ description,
826
+ ...(template ? { template } : {}),
827
+ });
828
+ process.stdout.write(` ✓ Created ${result.path}\n`);
829
+ process.exit(0);
830
+ }
831
+ catch (err) {
832
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
833
+ process.exit(1);
834
+ }
835
+ })();
836
+ }
837
+ }
838
+ // Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
839
+ if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
840
+ const file = process.argv[4];
841
+ if (!file) {
842
+ process.stderr.write("Usage: patchwork recipe lint <file.yaml>\n");
843
+ process.exit(1);
844
+ }
845
+ (async () => {
846
+ try {
847
+ const { runLint } = await import("./commands/recipe.js");
848
+ const result = runLint(path.resolve(file));
849
+ for (const issue of result.issues) {
850
+ const prefix = issue.level === "error" ? "✗" : "⚠";
851
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
852
+ }
853
+ if (result.valid) {
854
+ process.stdout.write(` ✓ Valid recipe (${result.warnings} warnings)\n`);
855
+ process.exit(0);
856
+ }
857
+ else {
858
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
859
+ process.exit(1);
860
+ }
861
+ }
862
+ catch (err) {
863
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
864
+ process.exit(1);
865
+ }
866
+ })();
867
+ }
868
+ // Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
869
+ if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
870
+ const args = process.argv.slice(4);
871
+ const usage = "Usage: patchwork recipe preflight <file.yaml> [--json] [--watch] [--require-fixtures] [--no-require-write-ack] [--allow-write <tool-or-ns>]\n";
872
+ let json = false;
873
+ let watchMode = false;
874
+ let requireFixtures = false;
875
+ let requireWriteAck = true;
876
+ const allowWrites = [];
877
+ let file;
878
+ for (let i = 0; i < args.length; i++) {
879
+ const arg = args[i];
880
+ if (arg === undefined)
881
+ continue;
882
+ if (arg === "--json") {
883
+ json = true;
884
+ continue;
885
+ }
886
+ if (arg === "--watch") {
887
+ watchMode = true;
888
+ continue;
889
+ }
890
+ if (arg === "--require-fixtures") {
891
+ requireFixtures = true;
892
+ continue;
893
+ }
894
+ if (arg === "--no-require-write-ack") {
895
+ requireWriteAck = false;
896
+ continue;
897
+ }
898
+ if (arg === "--allow-write" || arg.startsWith("--allow-write=")) {
899
+ const value = arg === "--allow-write"
900
+ ? args[++i]
901
+ : arg.slice("--allow-write=".length);
902
+ if (!value) {
903
+ process.stderr.write(`Error: --allow-write requires a value\n${usage}`);
904
+ process.exit(1);
905
+ }
906
+ allowWrites.push(value);
907
+ continue;
908
+ }
909
+ if (!arg.startsWith("--")) {
910
+ file = arg;
911
+ }
912
+ }
913
+ if (!file) {
914
+ process.stderr.write(usage);
915
+ process.exit(1);
916
+ }
917
+ const renderResult = (result) => {
918
+ if (json) {
919
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
920
+ return;
921
+ }
922
+ for (const issue of result.issues) {
923
+ const prefix = issue.level === "error" ? "✗" : "⚠";
924
+ const where = issue.stepId ? ` [${issue.stepId}]` : "";
925
+ process.stderr.write(` ${prefix} ${issue.code}${where}: ${issue.message}\n`);
926
+ }
927
+ if (result.ok) {
928
+ process.stdout.write(` ✓ Preflight passed for ${result.recipe} (${result.plan.steps.length} steps)\n`);
929
+ }
930
+ else {
931
+ const errorCount = result.issues.filter((i) => i.level === "error").length;
932
+ process.stdout.write(`\n ${errorCount} error(s) — preflight failed\n`);
933
+ }
934
+ };
935
+ (async () => {
936
+ try {
937
+ const { runPreflight, runPreflightWatch } = await import("./commands/recipe.js");
938
+ const resolvedPath = path.resolve(file);
939
+ if (watchMode) {
940
+ process.stdout.write(` Watching ${resolvedPath} — preflight on save…\n`);
941
+ const stop = runPreflightWatch({
942
+ recipePath: resolvedPath,
943
+ requireWriteAck,
944
+ requireFixtures,
945
+ allowWrites,
946
+ onResult: (result) => renderResult(result),
947
+ onError: (err) => {
948
+ process.stderr.write(`Error: ${err.message}\n`);
949
+ },
950
+ });
951
+ process.on("SIGINT", () => {
952
+ stop();
953
+ process.exit(0);
954
+ });
955
+ return;
956
+ }
957
+ const result = await runPreflight(resolvedPath, {
958
+ requireWriteAck,
959
+ requireFixtures,
960
+ allowWrites,
961
+ });
962
+ renderResult(result);
963
+ process.exit(result.ok ? 0 : 1);
964
+ }
965
+ catch (err) {
966
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
967
+ process.exit(1);
968
+ }
969
+ })();
970
+ }
971
+ // Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
972
+ if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
973
+ const args = process.argv.slice(4);
974
+ const check = args.includes("--check");
975
+ const watchMode = args.includes("--watch");
976
+ const file = args.find((arg) => !arg.startsWith("--"));
977
+ if (!file) {
978
+ process.stderr.write("Usage: patchwork recipe fmt <file.yaml> [--check] [--watch]\n");
979
+ process.exit(1);
980
+ }
981
+ const renderResult = (result, filePath) => {
982
+ if (check) {
983
+ process.stdout.write(result.changed
984
+ ? " ✗ File would be reformatted\n"
985
+ : " ✓ File is already formatted\n");
986
+ }
987
+ else {
988
+ process.stdout.write(result.changed
989
+ ? ` ✓ Formatted ${filePath}\n`
990
+ : ` ✓ Already formatted ${filePath}\n`);
991
+ }
992
+ };
993
+ (async () => {
994
+ try {
995
+ const { runFmt, runFmtWatch } = await import("./commands/recipe.js");
996
+ const resolvedPath = path.resolve(file);
997
+ if (watchMode) {
998
+ process.stdout.write(` Watching ${resolvedPath} — fmt on save…\n`);
999
+ const stop = runFmtWatch({
1000
+ recipePath: resolvedPath,
1001
+ check,
1002
+ onResult: (result) => {
1003
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
1004
+ renderResult(result, resolvedPath);
1005
+ },
1006
+ onError: (err) => {
1007
+ process.stderr.write(`Error: ${err.message}\n`);
1008
+ },
1009
+ });
1010
+ process.on("SIGINT", () => {
1011
+ stop();
1012
+ process.exit(0);
1013
+ });
1014
+ return;
1015
+ }
1016
+ const result = runFmt(resolvedPath, { check });
1017
+ renderResult(result, file);
1018
+ process.exit(check && result.changed ? 1 : 0);
1019
+ }
1020
+ catch (err) {
1021
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1022
+ process.exit(1);
1023
+ }
1024
+ })();
1025
+ }
1026
+ // Patchwork: `patchwork recipe record <file.yaml>` — execute live and record connector fixtures.
1027
+ if (process.argv[2] === "recipe" && process.argv[3] === "record") {
1028
+ const args = process.argv.slice(4);
1029
+ const file = args.find((arg) => !arg.startsWith("--"));
1030
+ const fixturesIdx = args.indexOf("--fixtures");
1031
+ const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
1032
+ if (!file) {
1033
+ process.stderr.write("Usage: patchwork recipe record <file.yaml> [--fixtures <dir>]\n");
1034
+ process.exit(1);
1035
+ }
1036
+ (async () => {
1037
+ try {
1038
+ const { runRecord } = await import("./commands/recipe.js");
1039
+ const result = await runRecord(path.resolve(file), {
1040
+ ...(fixturesDir ? { fixturesDir: path.resolve(fixturesDir) } : {}),
1041
+ });
1042
+ for (const issue of result.issues) {
1043
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1044
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1045
+ }
1046
+ if (result.recordedFixtures.length > 0) {
1047
+ process.stdout.write(` ℹ Recorded fixture libraries: ${result.recordedFixtures.join(", ")}\n`);
1048
+ }
1049
+ if (result.valid) {
1050
+ process.stdout.write(" ✓ Recipe fixtures recorded\n");
1051
+ process.exit(0);
1052
+ }
1053
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1054
+ process.exit(1);
1055
+ }
1056
+ catch (err) {
1057
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1058
+ process.exit(1);
1059
+ }
1060
+ })();
1061
+ }
1062
+ // Patchwork: `patchwork recipe test <file.yaml>` — validate fixture coverage for mocked execution.
1063
+ if (process.argv[2] === "recipe" && process.argv[3] === "test") {
1064
+ const args = process.argv.slice(4);
1065
+ const file = args.find((arg) => !arg.startsWith("--"));
1066
+ const fixturesIdx = args.indexOf("--fixtures");
1067
+ const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
1068
+ const watchMode = args.includes("--watch");
1069
+ if (!file) {
1070
+ process.stderr.write("Usage: patchwork recipe test <file.yaml> [--fixtures <dir>] [--watch]\n");
1071
+ process.exit(1);
1072
+ }
1073
+ const renderResult = (result) => {
1074
+ for (const issue of result.issues) {
1075
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1076
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1077
+ }
1078
+ if (result.requiredFixtures.length > 0) {
1079
+ process.stdout.write(` ℹ Required fixtures: ${result.requiredFixtures.join(", ")}\n`);
1080
+ }
1081
+ if (result.valid) {
1082
+ process.stdout.write(" ✓ Test passed\n");
1083
+ }
1084
+ else {
1085
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1086
+ }
1087
+ };
1088
+ (async () => {
1089
+ try {
1090
+ const { runTest, runTestWatch } = await import("./commands/recipe.js");
1091
+ const resolvedPath = path.resolve(file);
1092
+ const resolvedFixtures = fixturesDir
1093
+ ? path.resolve(fixturesDir)
1094
+ : undefined;
1095
+ if (watchMode) {
1096
+ process.stdout.write(` Watching ${resolvedPath} — test on save…\n`);
1097
+ const stop = runTestWatch({
1098
+ recipePath: resolvedPath,
1099
+ ...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
1100
+ onResult: (result) => {
1101
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
1102
+ renderResult(result);
1103
+ },
1104
+ onError: (err) => {
1105
+ process.stderr.write(`Error: ${err.message}\n`);
1106
+ },
1107
+ });
1108
+ process.on("SIGINT", () => {
1109
+ stop();
1110
+ process.exit(0);
1111
+ });
1112
+ return;
1113
+ }
1114
+ const result = await runTest(resolvedPath, {
1115
+ ...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
1116
+ });
1117
+ renderResult(result);
1118
+ process.exit(result.valid ? 0 : 1);
1119
+ }
1120
+ catch (err) {
1121
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1122
+ process.exit(1);
1123
+ }
1124
+ })();
1125
+ }
1126
+ // Patchwork: `patchwork recipe watch <file.yaml>` — watch for changes and validate.
1127
+ if (process.argv[2] === "recipe" && process.argv[3] === "watch") {
1128
+ const file = process.argv[4];
1129
+ if (!file) {
1130
+ process.stderr.write("Usage: patchwork recipe watch <file.yaml>\n");
1131
+ process.exit(1);
1132
+ }
1133
+ (async () => {
1134
+ const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
1135
+ const { runWatch, runLint, runWatchedRecipe } = await import("./commands/recipe.js");
1136
+ const filePath = path.resolve(file);
1137
+ const lock = findBridgeLock();
1138
+ const workdir = lock?.workspace || process.cwd();
1139
+ const initial = runLint(filePath);
1140
+ if (!initial.valid) {
1141
+ process.stderr.write(" ✗ Recipe has errors - fix before watching\n");
1142
+ for (const issue of initial.issues) {
1143
+ process.stderr.write(` ${issue.level}: ${issue.message}\n`);
1144
+ }
1145
+ }
1146
+ else {
1147
+ process.stdout.write(` ✓ Watching ${file} for changes...\n`);
1148
+ }
1149
+ const stop = runWatch({
1150
+ recipePath: filePath,
1151
+ onChange: async () => {
1152
+ process.stdout.write(`\n Change detected, running...\n`);
1153
+ const watched = await runWatchedRecipe(filePath, { workdir });
1154
+ if (!watched.lint.valid) {
1155
+ process.stderr.write(` ✗ Invalid (${watched.lint.errors} errors)\n`);
1156
+ for (const issue of watched.lint.issues) {
1157
+ process.stderr.write(` ${issue.level}: ${issue.message}\n`);
1158
+ }
1159
+ return;
1160
+ }
1161
+ if (watched.run?.stepSelection) {
1162
+ process.stdout.write(` Selected step via ${watched.run.stepSelection.matchedBy}: ${watched.run.stepSelection.matchedValue}\n`);
1163
+ }
1164
+ if (watched.summary) {
1165
+ process.stdout.write(` ${watched.summary.ok ? "✓" : "✗"} ${watched.summary.steps} step(s) completed\n`);
1166
+ if (watched.summary.errorMessage) {
1167
+ process.stderr.write(` Error: ${watched.summary.errorMessage}\n`);
1168
+ }
1169
+ if (watched.summary.outputs.length > 0) {
1170
+ process.stdout.write(` Output written to:\n${watched.summary.outputs.map((outputPath) => ` ${outputPath}`).join("\n")}\n`);
1171
+ }
1172
+ }
1173
+ },
1174
+ onError: (err) => {
1175
+ process.stderr.write(` Error: ${err.message}\n`);
1176
+ },
1177
+ });
1178
+ process.on("SIGINT", () => {
1179
+ process.stdout.write("\n Stopping watch...\n");
1180
+ stop();
1181
+ process.exit(0);
1182
+ });
1183
+ })();
1184
+ }
713
1185
  if (process.argv[2] === "init") {
714
1186
  const argv = process.argv.slice(3);
715
1187
  // Handle init --help