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.
- package/README.md +26 -12
- package/deploy/bootstrap-vps.sh +184 -0
- package/dist/approvalHttp.js +6 -1
- package/dist/approvalHttp.js.map +1 -1
- package/dist/automation.d.ts +20 -0
- package/dist/automation.js +35 -0
- package/dist/automation.js.map +1 -1
- package/dist/bridge.js +22 -4
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeToken.js +57 -19
- package/dist/bridgeToken.js.map +1 -1
- package/dist/commands/recipe.d.ts +256 -0
- package/dist/commands/recipe.js +1313 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/connectors/baseConnector.d.ts +117 -0
- package/dist/connectors/baseConnector.js +213 -0
- package/dist/connectors/baseConnector.js.map +1 -0
- package/dist/connectors/confluence.d.ts +111 -0
- package/dist/connectors/confluence.js +406 -0
- package/dist/connectors/confluence.js.map +1 -0
- package/dist/connectors/fixtureLibrary.d.ts +21 -0
- package/dist/connectors/fixtureLibrary.js +70 -0
- package/dist/connectors/fixtureLibrary.js.map +1 -0
- package/dist/connectors/fixtureRecorder.d.ts +1 -0
- package/dist/connectors/fixtureRecorder.js +35 -0
- package/dist/connectors/fixtureRecorder.js.map +1 -0
- package/dist/connectors/github.js +2 -11
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gmail.js +23 -7
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.js +23 -7
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/connectors/jira.d.ts +98 -0
- package/dist/connectors/jira.js +379 -0
- package/dist/connectors/jira.js.map +1 -0
- package/dist/connectors/linear.js +2 -11
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpOAuth.d.ts +1 -0
- package/dist/connectors/mcpOAuth.js +30 -4
- package/dist/connectors/mcpOAuth.js.map +1 -1
- package/dist/connectors/mockConnector.d.ts +28 -0
- package/dist/connectors/mockConnector.js +81 -0
- package/dist/connectors/mockConnector.js.map +1 -0
- package/dist/connectors/notion.d.ts +143 -0
- package/dist/connectors/notion.js +424 -0
- package/dist/connectors/notion.js.map +1 -0
- package/dist/connectors/sentry.js +2 -11
- package/dist/connectors/sentry.js.map +1 -1
- package/dist/connectors/slack.js +50 -15
- package/dist/connectors/slack.js.map +1 -1
- package/dist/connectors/tokenStorage.d.ts +35 -0
- package/dist/connectors/tokenStorage.js +394 -0
- package/dist/connectors/tokenStorage.js.map +1 -0
- package/dist/connectors/zendesk.d.ts +104 -0
- package/dist/connectors/zendesk.js +424 -0
- package/dist/connectors/zendesk.js.map +1 -0
- package/dist/featureFlags.d.ts +73 -0
- package/dist/featureFlags.js +203 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/fp/automationInterpreter.js +1 -0
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +1 -1
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/policyParser.js +17 -0
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/index.js +508 -36
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +4 -1
- package/dist/oauth.js +50 -14
- package/dist/oauth.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +104 -0
- package/dist/recipes/chainedRunner.js +359 -0
- package/dist/recipes/chainedRunner.js.map +1 -0
- package/dist/recipes/dependencyGraph.d.ts +39 -0
- package/dist/recipes/dependencyGraph.js +199 -0
- package/dist/recipes/dependencyGraph.js.map +1 -0
- package/dist/recipes/legacyRecipeCompat.d.ts +1 -0
- package/dist/recipes/legacyRecipeCompat.js +97 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -0
- package/dist/recipes/nestedRecipeStep.d.ts +58 -0
- package/dist/recipes/nestedRecipeStep.js +95 -0
- package/dist/recipes/nestedRecipeStep.js.map +1 -0
- package/dist/recipes/outputRegistry.d.ts +28 -0
- package/dist/recipes/outputRegistry.js +52 -0
- package/dist/recipes/outputRegistry.js.map +1 -0
- package/dist/recipes/schemaGenerator.d.ts +28 -0
- package/dist/recipes/schemaGenerator.js +484 -0
- package/dist/recipes/schemaGenerator.js.map +1 -0
- package/dist/recipes/templateEngine.d.ts +62 -0
- package/dist/recipes/templateEngine.js +182 -0
- package/dist/recipes/templateEngine.js.map +1 -0
- package/dist/recipes/toolRegistry.d.ts +181 -0
- package/dist/recipes/toolRegistry.js +300 -0
- package/dist/recipes/toolRegistry.js.map +1 -0
- package/dist/recipes/tools/calendar.d.ts +6 -0
- package/dist/recipes/tools/calendar.js +61 -0
- package/dist/recipes/tools/calendar.js.map +1 -0
- package/dist/recipes/tools/confluence.d.ts +6 -0
- package/dist/recipes/tools/confluence.js +254 -0
- package/dist/recipes/tools/confluence.js.map +1 -0
- package/dist/recipes/tools/diagnostics.d.ts +6 -0
- package/dist/recipes/tools/diagnostics.js +36 -0
- package/dist/recipes/tools/diagnostics.js.map +1 -0
- package/dist/recipes/tools/file.d.ts +6 -0
- package/dist/recipes/tools/file.js +170 -0
- package/dist/recipes/tools/file.js.map +1 -0
- package/dist/recipes/tools/git.d.ts +6 -0
- package/dist/recipes/tools/git.js +63 -0
- package/dist/recipes/tools/git.js.map +1 -0
- package/dist/recipes/tools/github.d.ts +6 -0
- package/dist/recipes/tools/github.js +91 -0
- package/dist/recipes/tools/github.js.map +1 -0
- package/dist/recipes/tools/gmail.d.ts +6 -0
- package/dist/recipes/tools/gmail.js +210 -0
- package/dist/recipes/tools/gmail.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +18 -0
- package/dist/recipes/tools/index.js +21 -0
- package/dist/recipes/tools/index.js.map +1 -0
- package/dist/recipes/tools/linear.d.ts +6 -0
- package/dist/recipes/tools/linear.js +83 -0
- package/dist/recipes/tools/linear.js.map +1 -0
- package/dist/recipes/tools/notion.d.ts +6 -0
- package/dist/recipes/tools/notion.js +278 -0
- package/dist/recipes/tools/notion.js.map +1 -0
- package/dist/recipes/tools/slack.d.ts +6 -0
- package/dist/recipes/tools/slack.js +72 -0
- package/dist/recipes/tools/slack.js.map +1 -0
- package/dist/recipes/tools/zendesk.d.ts +6 -0
- package/dist/recipes/tools/zendesk.js +245 -0
- package/dist/recipes/tools/zendesk.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +71 -7
- package/dist/recipes/yamlRunner.js +406 -439
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/riskTier.js +1 -0
- package/dist/riskTier.js.map +1 -1
- package/dist/runLog.d.ts +18 -0
- package/dist/runLog.js +5 -0
- package/dist/runLog.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +224 -0
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.js +2 -0
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/github/actions.js +4 -2
- package/dist/tools/github/actions.js.map +1 -1
- package/dist/tools/github/composite.d.ts +339 -0
- package/dist/tools/github/composite.js +343 -0
- package/dist/tools/github/composite.js.map +1 -0
- package/dist/tools/github/index.d.ts +1 -0
- package/dist/tools/github/index.js +1 -0
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/issues.js +8 -4
- package/dist/tools/github/issues.js.map +1 -1
- package/dist/tools/github/pr.js +14 -7
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/searchTools.js +1 -1
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/transport.d.ts +7 -1
- package/dist/transport.js +85 -11
- package/dist/transport.js.map +1 -1
- package/package.json +1 -1
- package/templates/automation-policies/recipe-authoring.json +25 -0
- package/templates/automation-policy.example.json +6 -0
- 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
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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({
|
|
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 "${
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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(
|
|
672
|
-
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|