patchwork-os 0.2.0-alpha.2 → 0.2.0-alpha.22

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 (281) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +40 -15
  3. package/deploy/bootstrap-vps.sh +184 -0
  4. package/dist/approvalHttp.d.ts +11 -2
  5. package/dist/approvalHttp.js +98 -10
  6. package/dist/approvalHttp.js.map +1 -1
  7. package/dist/approvalQueue.d.ts +12 -1
  8. package/dist/approvalQueue.js +25 -3
  9. package/dist/approvalQueue.js.map +1 -1
  10. package/dist/automation.d.ts +20 -0
  11. package/dist/automation.js +35 -0
  12. package/dist/automation.js.map +1 -1
  13. package/dist/bridge.js +145 -23
  14. package/dist/bridge.js.map +1 -1
  15. package/dist/bridgeToken.js +57 -19
  16. package/dist/bridgeToken.js.map +1 -1
  17. package/dist/claudeDriver.d.ts +3 -1
  18. package/dist/claudeDriver.js +48 -0
  19. package/dist/claudeDriver.js.map +1 -1
  20. package/dist/claudeOrchestrator.d.ts +1 -1
  21. package/dist/claudeOrchestrator.js +14 -8
  22. package/dist/claudeOrchestrator.js.map +1 -1
  23. package/dist/commands/launchd.d.ts +2 -0
  24. package/dist/commands/launchd.js +94 -0
  25. package/dist/commands/launchd.js.map +1 -0
  26. package/dist/commands/recipe.d.ts +256 -0
  27. package/dist/commands/recipe.js +1313 -0
  28. package/dist/commands/recipe.js.map +1 -0
  29. package/dist/config.d.ts +15 -2
  30. package/dist/config.js +94 -8
  31. package/dist/config.js.map +1 -1
  32. package/dist/connectors/baseConnector.d.ts +117 -0
  33. package/dist/connectors/baseConnector.js +213 -0
  34. package/dist/connectors/baseConnector.js.map +1 -0
  35. package/dist/connectors/confluence.d.ts +111 -0
  36. package/dist/connectors/confluence.js +406 -0
  37. package/dist/connectors/confluence.js.map +1 -0
  38. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  39. package/dist/connectors/fixtureLibrary.js +70 -0
  40. package/dist/connectors/fixtureLibrary.js.map +1 -0
  41. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  42. package/dist/connectors/fixtureRecorder.js +35 -0
  43. package/dist/connectors/fixtureRecorder.js.map +1 -0
  44. package/dist/connectors/github.d.ts +58 -8
  45. package/dist/connectors/github.js +312 -84
  46. package/dist/connectors/github.js.map +1 -1
  47. package/dist/connectors/gmail.d.ts +4 -1
  48. package/dist/connectors/gmail.js +93 -16
  49. package/dist/connectors/gmail.js.map +1 -1
  50. package/dist/connectors/googleCalendar.d.ts +60 -0
  51. package/dist/connectors/googleCalendar.js +345 -0
  52. package/dist/connectors/googleCalendar.js.map +1 -0
  53. package/dist/connectors/jira.d.ts +98 -0
  54. package/dist/connectors/jira.js +379 -0
  55. package/dist/connectors/jira.js.map +1 -0
  56. package/dist/connectors/linear.d.ts +117 -0
  57. package/dist/connectors/linear.js +239 -0
  58. package/dist/connectors/linear.js.map +1 -0
  59. package/dist/connectors/mcpClient.d.ts +56 -0
  60. package/dist/connectors/mcpClient.js +189 -0
  61. package/dist/connectors/mcpClient.js.map +1 -0
  62. package/dist/connectors/mcpOAuth.d.ts +84 -0
  63. package/dist/connectors/mcpOAuth.js +389 -0
  64. package/dist/connectors/mcpOAuth.js.map +1 -0
  65. package/dist/connectors/mockConnector.d.ts +28 -0
  66. package/dist/connectors/mockConnector.js +81 -0
  67. package/dist/connectors/mockConnector.js.map +1 -0
  68. package/dist/connectors/notion.d.ts +143 -0
  69. package/dist/connectors/notion.js +424 -0
  70. package/dist/connectors/notion.js.map +1 -0
  71. package/dist/connectors/sentry.d.ts +43 -0
  72. package/dist/connectors/sentry.js +188 -0
  73. package/dist/connectors/sentry.js.map +1 -0
  74. package/dist/connectors/slack.d.ts +50 -0
  75. package/dist/connectors/slack.js +324 -0
  76. package/dist/connectors/slack.js.map +1 -0
  77. package/dist/connectors/tokenStorage.d.ts +35 -0
  78. package/dist/connectors/tokenStorage.js +394 -0
  79. package/dist/connectors/tokenStorage.js.map +1 -0
  80. package/dist/connectors/zendesk.d.ts +104 -0
  81. package/dist/connectors/zendesk.js +424 -0
  82. package/dist/connectors/zendesk.js.map +1 -0
  83. package/dist/drivers/claude/api.d.ts +11 -0
  84. package/dist/drivers/claude/api.js +54 -0
  85. package/dist/drivers/claude/api.js.map +1 -0
  86. package/dist/drivers/claude/envSanitizer.d.ts +7 -0
  87. package/dist/drivers/claude/envSanitizer.js +18 -0
  88. package/dist/drivers/claude/envSanitizer.js.map +1 -0
  89. package/dist/drivers/claude/streamParser.d.ts +38 -0
  90. package/dist/drivers/claude/streamParser.js +34 -0
  91. package/dist/drivers/claude/streamParser.js.map +1 -0
  92. package/dist/drivers/claude/subprocess.d.ts +19 -0
  93. package/dist/drivers/claude/subprocess.js +216 -0
  94. package/dist/drivers/claude/subprocess.js.map +1 -0
  95. package/dist/drivers/claude/subprocessSettings.d.ts +9 -0
  96. package/dist/drivers/claude/subprocessSettings.js +55 -0
  97. package/dist/drivers/claude/subprocessSettings.js.map +1 -0
  98. package/dist/drivers/gemini/index.d.ts +18 -0
  99. package/dist/drivers/gemini/index.js +210 -0
  100. package/dist/drivers/gemini/index.js.map +1 -0
  101. package/dist/drivers/grok/index.d.ts +11 -0
  102. package/dist/drivers/grok/index.js +22 -0
  103. package/dist/drivers/grok/index.js.map +1 -0
  104. package/dist/drivers/index.d.ts +23 -0
  105. package/dist/drivers/index.js +31 -0
  106. package/dist/drivers/index.js.map +1 -0
  107. package/dist/drivers/openai/index.d.ts +24 -0
  108. package/dist/drivers/openai/index.js +110 -0
  109. package/dist/drivers/openai/index.js.map +1 -0
  110. package/dist/drivers/types.d.ts +72 -0
  111. package/dist/drivers/types.js +30 -0
  112. package/dist/drivers/types.js.map +1 -0
  113. package/dist/featureFlags.d.ts +73 -0
  114. package/dist/featureFlags.js +203 -0
  115. package/dist/featureFlags.js.map +1 -0
  116. package/dist/fp/automationInterpreter.js +1 -0
  117. package/dist/fp/automationInterpreter.js.map +1 -1
  118. package/dist/fp/automationProgram.d.ts +1 -1
  119. package/dist/fp/automationProgram.js.map +1 -1
  120. package/dist/fp/policyParser.js +17 -0
  121. package/dist/fp/policyParser.js.map +1 -1
  122. package/dist/index.js +543 -37
  123. package/dist/index.js.map +1 -1
  124. package/dist/installGuard.d.ts +25 -0
  125. package/dist/installGuard.js +48 -0
  126. package/dist/installGuard.js.map +1 -0
  127. package/dist/oauth.d.ts +4 -1
  128. package/dist/oauth.js +50 -14
  129. package/dist/oauth.js.map +1 -1
  130. package/dist/patchworkConfig.d.ts +9 -0
  131. package/dist/patchworkConfig.js.map +1 -1
  132. package/dist/recipes/chainedRunner.d.ts +104 -0
  133. package/dist/recipes/chainedRunner.js +359 -0
  134. package/dist/recipes/chainedRunner.js.map +1 -0
  135. package/dist/recipes/dependencyGraph.d.ts +39 -0
  136. package/dist/recipes/dependencyGraph.js +199 -0
  137. package/dist/recipes/dependencyGraph.js.map +1 -0
  138. package/dist/recipes/legacyRecipeCompat.d.ts +1 -0
  139. package/dist/recipes/legacyRecipeCompat.js +97 -0
  140. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  141. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  142. package/dist/recipes/nestedRecipeStep.js +95 -0
  143. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  144. package/dist/recipes/outputRegistry.d.ts +28 -0
  145. package/dist/recipes/outputRegistry.js +52 -0
  146. package/dist/recipes/outputRegistry.js.map +1 -0
  147. package/dist/recipes/scheduler.d.ts +23 -7
  148. package/dist/recipes/scheduler.js +135 -41
  149. package/dist/recipes/scheduler.js.map +1 -1
  150. package/dist/recipes/schemaGenerator.d.ts +28 -0
  151. package/dist/recipes/schemaGenerator.js +484 -0
  152. package/dist/recipes/schemaGenerator.js.map +1 -0
  153. package/dist/recipes/templateEngine.d.ts +62 -0
  154. package/dist/recipes/templateEngine.js +182 -0
  155. package/dist/recipes/templateEngine.js.map +1 -0
  156. package/dist/recipes/toolRegistry.d.ts +181 -0
  157. package/dist/recipes/toolRegistry.js +300 -0
  158. package/dist/recipes/toolRegistry.js.map +1 -0
  159. package/dist/recipes/tools/calendar.d.ts +6 -0
  160. package/dist/recipes/tools/calendar.js +61 -0
  161. package/dist/recipes/tools/calendar.js.map +1 -0
  162. package/dist/recipes/tools/confluence.d.ts +6 -0
  163. package/dist/recipes/tools/confluence.js +254 -0
  164. package/dist/recipes/tools/confluence.js.map +1 -0
  165. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  166. package/dist/recipes/tools/diagnostics.js +36 -0
  167. package/dist/recipes/tools/diagnostics.js.map +1 -0
  168. package/dist/recipes/tools/file.d.ts +6 -0
  169. package/dist/recipes/tools/file.js +170 -0
  170. package/dist/recipes/tools/file.js.map +1 -0
  171. package/dist/recipes/tools/git.d.ts +6 -0
  172. package/dist/recipes/tools/git.js +63 -0
  173. package/dist/recipes/tools/git.js.map +1 -0
  174. package/dist/recipes/tools/github.d.ts +6 -0
  175. package/dist/recipes/tools/github.js +91 -0
  176. package/dist/recipes/tools/github.js.map +1 -0
  177. package/dist/recipes/tools/gmail.d.ts +6 -0
  178. package/dist/recipes/tools/gmail.js +210 -0
  179. package/dist/recipes/tools/gmail.js.map +1 -0
  180. package/dist/recipes/tools/index.d.ts +18 -0
  181. package/dist/recipes/tools/index.js +21 -0
  182. package/dist/recipes/tools/index.js.map +1 -0
  183. package/dist/recipes/tools/linear.d.ts +6 -0
  184. package/dist/recipes/tools/linear.js +83 -0
  185. package/dist/recipes/tools/linear.js.map +1 -0
  186. package/dist/recipes/tools/notion.d.ts +6 -0
  187. package/dist/recipes/tools/notion.js +278 -0
  188. package/dist/recipes/tools/notion.js.map +1 -0
  189. package/dist/recipes/tools/slack.d.ts +6 -0
  190. package/dist/recipes/tools/slack.js +72 -0
  191. package/dist/recipes/tools/slack.js.map +1 -0
  192. package/dist/recipes/tools/zendesk.d.ts +6 -0
  193. package/dist/recipes/tools/zendesk.js +245 -0
  194. package/dist/recipes/tools/zendesk.js.map +1 -0
  195. package/dist/recipes/yamlRunner.d.ts +79 -0
  196. package/dist/recipes/yamlRunner.js +612 -346
  197. package/dist/recipes/yamlRunner.js.map +1 -1
  198. package/dist/recipesHttp.d.ts +14 -1
  199. package/dist/recipesHttp.js +21 -4
  200. package/dist/recipesHttp.js.map +1 -1
  201. package/dist/riskTier.js +1 -0
  202. package/dist/riskTier.js.map +1 -1
  203. package/dist/runLog.d.ts +23 -0
  204. package/dist/runLog.js +56 -1
  205. package/dist/runLog.js.map +1 -1
  206. package/dist/server.d.ts +19 -1
  207. package/dist/server.js +682 -31
  208. package/dist/server.js.map +1 -1
  209. package/dist/streamableHttp.js +2 -0
  210. package/dist/streamableHttp.js.map +1 -1
  211. package/dist/tools/addLinearComment.d.ts +55 -0
  212. package/dist/tools/addLinearComment.js +72 -0
  213. package/dist/tools/addLinearComment.js.map +1 -0
  214. package/dist/tools/bridgeDoctor.js +2 -2
  215. package/dist/tools/bridgeDoctor.js.map +1 -1
  216. package/dist/tools/createLinearIssue.d.ts +84 -0
  217. package/dist/tools/createLinearIssue.js +146 -0
  218. package/dist/tools/createLinearIssue.js.map +1 -0
  219. package/dist/tools/ctxGetTaskContext.d.ts +4 -1
  220. package/dist/tools/ctxGetTaskContext.js +45 -2
  221. package/dist/tools/ctxGetTaskContext.js.map +1 -1
  222. package/dist/tools/fetchCalendarEvents.d.ts +94 -0
  223. package/dist/tools/fetchCalendarEvents.js +97 -0
  224. package/dist/tools/fetchCalendarEvents.js.map +1 -0
  225. package/dist/tools/fetchGithubIssue.d.ts +80 -0
  226. package/dist/tools/fetchGithubIssue.js +84 -0
  227. package/dist/tools/fetchGithubIssue.js.map +1 -0
  228. package/dist/tools/fetchGithubPR.d.ts +89 -0
  229. package/dist/tools/fetchGithubPR.js +96 -0
  230. package/dist/tools/fetchGithubPR.js.map +1 -0
  231. package/dist/tools/fetchLinearIssue.d.ts +112 -0
  232. package/dist/tools/fetchLinearIssue.js +129 -0
  233. package/dist/tools/fetchLinearIssue.js.map +1 -0
  234. package/dist/tools/fetchSentryIssue.d.ts +143 -0
  235. package/dist/tools/fetchSentryIssue.js +150 -0
  236. package/dist/tools/fetchSentryIssue.js.map +1 -0
  237. package/dist/tools/fetchSlackProfile.d.ts +43 -0
  238. package/dist/tools/fetchSlackProfile.js +46 -0
  239. package/dist/tools/fetchSlackProfile.js.map +1 -0
  240. package/dist/tools/getConnectorStatus.d.ts +58 -0
  241. package/dist/tools/getConnectorStatus.js +56 -0
  242. package/dist/tools/getConnectorStatus.js.map +1 -0
  243. package/dist/tools/github/actions.js +4 -2
  244. package/dist/tools/github/actions.js.map +1 -1
  245. package/dist/tools/github/composite.d.ts +339 -0
  246. package/dist/tools/github/composite.js +343 -0
  247. package/dist/tools/github/composite.js.map +1 -0
  248. package/dist/tools/github/index.d.ts +2 -1
  249. package/dist/tools/github/index.js +2 -1
  250. package/dist/tools/github/index.js.map +1 -1
  251. package/dist/tools/github/issues.js +8 -4
  252. package/dist/tools/github/issues.js.map +1 -1
  253. package/dist/tools/github/pr.d.ts +122 -0
  254. package/dist/tools/github/pr.js +195 -5
  255. package/dist/tools/github/pr.js.map +1 -1
  256. package/dist/tools/index.js +36 -1
  257. package/dist/tools/index.js.map +1 -1
  258. package/dist/tools/searchTools.js +1 -1
  259. package/dist/tools/searchTools.js.map +1 -1
  260. package/dist/tools/slackListChannels.d.ts +65 -0
  261. package/dist/tools/slackListChannels.js +70 -0
  262. package/dist/tools/slackListChannels.js.map +1 -0
  263. package/dist/tools/slackPostMessage.d.ts +57 -0
  264. package/dist/tools/slackPostMessage.js +77 -0
  265. package/dist/tools/slackPostMessage.js.map +1 -0
  266. package/dist/tools/updateLinearIssue.d.ts +89 -0
  267. package/dist/tools/updateLinearIssue.js +117 -0
  268. package/dist/tools/updateLinearIssue.js.map +1 -0
  269. package/dist/transport.d.ts +7 -1
  270. package/dist/transport.js +85 -11
  271. package/dist/transport.js.map +1 -1
  272. package/package.json +4 -2
  273. package/scripts/start-all.sh +56 -19
  274. package/templates/automation-policies/recipe-authoring.json +25 -0
  275. package/templates/automation-policy.example.json +6 -0
  276. package/templates/co.patchwork-os.bridge.plist +34 -0
  277. package/templates/recipes/ctx-loop-test.yaml +75 -0
  278. package/templates/recipes/lint-on-save.yaml +1 -2
  279. package/templates/recipes/morning-brief-slack.yaml +57 -0
  280. package/templates/recipes/morning-brief.yaml +21 -5
  281. package/templates/recipes/sentry-to-linear.yaml +77 -0
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Uses Node 20.6+ native dotenv loader; falls back to manual parse for older Node.
4
4
  {
5
5
  const { fileURLToPath: _fileURLToPath } = await import("node:url");
6
- const envPath = _fileURLToPath(new URL("../../.env", import.meta.url));
6
+ const envPath = _fileURLToPath(new URL("../.env", import.meta.url));
7
7
  try {
8
8
  const { readFileSync, existsSync } = await import("node:fs");
9
9
  if (existsSync(envPath)) {
@@ -35,8 +35,24 @@ import { getAnalyticsPref, setAnalyticsPref } from "./analyticsPrefs.js";
35
35
  import { Bridge } from "./bridge.js";
36
36
  import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
37
37
  import { findEditor, parseConfig } from "./config.js";
38
+ import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
38
39
  import { PACKAGE_VERSION, semverGt } from "./version.js";
39
40
  const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
41
+ // Warn when a symlinked global install is detected (`npm install -g .`).
42
+ // launchctl / sandbox environments can fail through that link with EPERM.
43
+ // Warn only — do not crash interactive or dev flows.
44
+ {
45
+ const _symlinkInfo = detectWorkspaceSymlinkInstall();
46
+ if (_symlinkInfo) {
47
+ process.stderr.write(`\n⚠️ Detected a symlinked global ${PATCHWORK_PACKAGE_NAME} install.\n` +
48
+ ` Logical root: ${_symlinkInfo.logicalRoot}\n` +
49
+ ` Real path: ${_symlinkInfo.realRoot}\n\n` +
50
+ " LaunchAgent startup can fail with EPERM when the macOS sandbox\n" +
51
+ " cannot access workspace files under ~/Documents through that link.\n\n" +
52
+ SYMLINK_INSTALL_FIX +
53
+ "\n");
54
+ }
55
+ }
40
56
  const OPEN_VSX_PUBLISHER = "oolab-labs";
41
57
  const OPEN_VSX_NAME = "claude-ide-bridge-extension";
42
58
  // CLAUDE.md versioned-block patching moved to ./claudeMdPatch.ts so tests
@@ -591,25 +607,94 @@ if (process.argv[2] === "recipe" && process.argv[3] === "list") {
591
607
  // a running bridge's /recipes/run endpoint if one is available.
592
608
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
593
609
  const args = process.argv.slice(4);
594
- const localFlag = args.includes("--local");
595
- const name = args.find((a) => !a.startsWith("--"));
596
- if (!name) {
597
- 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);
598
670
  process.exit(1);
599
671
  }
672
+ const recipeArg = recipeRef;
600
673
  (async () => {
601
674
  try {
602
- // 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
+ })();
603
685
  const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
604
686
  const lock = localFlag ? null : findBridgeLock();
605
- if (lock) {
687
+ if (lock && !dryRun && !step && !explicitFile) {
606
688
  const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
607
689
  method: "POST",
608
690
  headers: {
609
691
  Authorization: `Bearer ${lock.authToken}`,
610
692
  "Content-Type": "application/json",
611
693
  },
612
- body: JSON.stringify({ name }),
694
+ body: JSON.stringify({
695
+ name: recipeArg,
696
+ ...(seedVars ? { vars: seedVars } : {}),
697
+ }),
613
698
  });
614
699
  const body = (await res.json());
615
700
  if (!body.ok) {
@@ -622,43 +707,41 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
622
707
  // else: fall through to local runner below
623
708
  }
624
709
  else {
625
- 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` +
626
711
  " Watch progress on the dashboard Tasks page or via listClaudeTasks.\n");
627
712
  process.exit(0);
628
713
  return;
629
714
  }
630
715
  }
631
- // No bridge run locally using the YAML runner.
632
- const { loadYamlRecipe, runYamlRecipe } = await import("./recipes/yamlRunner.js");
633
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
634
- const bundledDir = fileURLToPath(new URL("../templates/recipes", import.meta.url));
635
- const candidates = [
636
- path.join(recipesDir, `${name}.yaml`),
637
- path.join(recipesDir, `${name}.yml`),
638
- path.join(recipesDir, `${name}.json`),
639
- path.join(bundledDir, `${name}.yaml`),
640
- path.join(bundledDir, `${name}.yml`),
641
- ];
642
- let recipePath;
643
- for (const c of candidates) {
644
- if (existsSync(c)) {
645
- recipePath = c;
646
- break;
647
- }
648
- }
649
- if (!recipePath) {
650
- process.stderr.write(`Error: recipe "${name}" not found in ${recipesDir}\n` +
651
- " Run `patchwork-os recipe list` to see available recipes.\n");
652
- 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);
653
724
  return;
654
725
  }
655
- process.stdout.write(` Running recipe "${name}" locally…\n`);
656
- const recipe = loadYamlRecipe(recipePath);
726
+ process.stdout.write(step
727
+ ? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
728
+ : ` Running recipe "${recipeArg}" locally…\n`);
657
729
  const workdir = lock?.workspace || process.cwd();
658
- const result = await runYamlRecipe(recipe, { workdir });
659
- process.stdout.write(` ✓ ${result.stepsRun} step(s) completed\n`);
660
- if (result.outputs.length > 0) {
661
- 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`);
662
745
  }
663
746
  process.exit(0);
664
747
  }
@@ -694,6 +777,411 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
694
777
  }
695
778
  })();
696
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
+ }
697
1185
  if (process.argv[2] === "init") {
698
1186
  const argv = process.argv.slice(3);
699
1187
  // Handle init --help
@@ -1383,6 +1871,23 @@ Options:
1383
1871
  }
1384
1872
  process.exit(0);
1385
1873
  }
1874
+ // Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
1875
+ if (process.argv[2] === "launchd") {
1876
+ const sub = process.argv[3];
1877
+ if (sub === "install") {
1878
+ const { runLaunchdInstall } = await import("./commands/launchd.js");
1879
+ await runLaunchdInstall(process.argv.slice(4));
1880
+ }
1881
+ else if (sub === "uninstall") {
1882
+ const { runLaunchdUninstall } = await import("./commands/launchd.js");
1883
+ await runLaunchdUninstall(process.argv.slice(4));
1884
+ }
1885
+ else {
1886
+ process.stderr.write("Usage: patchwork-os launchd install|uninstall\n");
1887
+ process.exit(1);
1888
+ }
1889
+ process.exit(0);
1890
+ }
1386
1891
  // F6: "Did you mean?" for unknown CLI subcommands
1387
1892
  // Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
1388
1893
  {
@@ -1413,6 +1918,7 @@ Options:
1413
1918
  "shim",
1414
1919
  "recipe",
1415
1920
  "dashboard",
1921
+ "launchd",
1416
1922
  ];
1417
1923
  const unknownSub = process.argv[2];
1418
1924
  if (unknownSub &&