patchwork-os 0.2.0-alpha.3 → 0.2.0-alpha.31

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 (301) 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/deploy/deploy-dashboard.sh +174 -0
  5. package/deploy/deploy-landing.sh +79 -0
  6. package/dist/activationMetrics.d.ts +67 -0
  7. package/dist/activationMetrics.js +255 -0
  8. package/dist/activationMetrics.js.map +1 -0
  9. package/dist/approvalHttp.d.ts +24 -2
  10. package/dist/approvalHttp.js +150 -10
  11. package/dist/approvalHttp.js.map +1 -1
  12. package/dist/approvalQueue.d.ts +16 -1
  13. package/dist/approvalQueue.js +44 -3
  14. package/dist/approvalQueue.js.map +1 -1
  15. package/dist/automation.d.ts +20 -0
  16. package/dist/automation.js +54 -1
  17. package/dist/automation.js.map +1 -1
  18. package/dist/bridge.d.ts +2 -0
  19. package/dist/bridge.js +55 -130
  20. package/dist/bridge.js.map +1 -1
  21. package/dist/bridgeToken.js +57 -19
  22. package/dist/bridgeToken.js.map +1 -1
  23. package/dist/ccPermissions.js +6 -4
  24. package/dist/ccPermissions.js.map +1 -1
  25. package/dist/claudeOrchestrator.d.ts +1 -1
  26. package/dist/claudeOrchestrator.js +14 -8
  27. package/dist/claudeOrchestrator.js.map +1 -1
  28. package/dist/commands/launchd.d.ts +2 -0
  29. package/dist/commands/launchd.js +94 -0
  30. package/dist/commands/launchd.js.map +1 -0
  31. package/dist/commands/recipe.d.ts +258 -0
  32. package/dist/commands/recipe.js +1130 -0
  33. package/dist/commands/recipe.js.map +1 -0
  34. package/dist/commands/recipeInstall.d.ts +72 -0
  35. package/dist/commands/recipeInstall.js +339 -0
  36. package/dist/commands/recipeInstall.js.map +1 -0
  37. package/dist/config.d.ts +14 -1
  38. package/dist/config.js +99 -8
  39. package/dist/config.js.map +1 -1
  40. package/dist/connectors/baseConnector.d.ts +117 -0
  41. package/dist/connectors/baseConnector.js +213 -0
  42. package/dist/connectors/baseConnector.js.map +1 -0
  43. package/dist/connectors/confluence.d.ts +111 -0
  44. package/dist/connectors/confluence.js +406 -0
  45. package/dist/connectors/confluence.js.map +1 -0
  46. package/dist/connectors/datadog.d.ts +116 -0
  47. package/dist/connectors/datadog.js +385 -0
  48. package/dist/connectors/datadog.js.map +1 -0
  49. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  50. package/dist/connectors/fixtureLibrary.js +70 -0
  51. package/dist/connectors/fixtureLibrary.js.map +1 -0
  52. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  53. package/dist/connectors/fixtureRecorder.js +35 -0
  54. package/dist/connectors/fixtureRecorder.js.map +1 -0
  55. package/dist/connectors/github.d.ts +58 -8
  56. package/dist/connectors/github.js +312 -84
  57. package/dist/connectors/github.js.map +1 -1
  58. package/dist/connectors/gmail.d.ts +4 -1
  59. package/dist/connectors/gmail.js +79 -16
  60. package/dist/connectors/gmail.js.map +1 -1
  61. package/dist/connectors/googleCalendar.d.ts +60 -0
  62. package/dist/connectors/googleCalendar.js +345 -0
  63. package/dist/connectors/googleCalendar.js.map +1 -0
  64. package/dist/connectors/hubspot.d.ts +112 -0
  65. package/dist/connectors/hubspot.js +408 -0
  66. package/dist/connectors/hubspot.js.map +1 -0
  67. package/dist/connectors/intercom.d.ts +102 -0
  68. package/dist/connectors/intercom.js +402 -0
  69. package/dist/connectors/intercom.js.map +1 -0
  70. package/dist/connectors/jira.d.ts +98 -0
  71. package/dist/connectors/jira.js +379 -0
  72. package/dist/connectors/jira.js.map +1 -0
  73. package/dist/connectors/linear.d.ts +69 -19
  74. package/dist/connectors/linear.js +170 -129
  75. package/dist/connectors/linear.js.map +1 -1
  76. package/dist/connectors/mcpClient.d.ts +56 -0
  77. package/dist/connectors/mcpClient.js +189 -0
  78. package/dist/connectors/mcpClient.js.map +1 -0
  79. package/dist/connectors/mcpOAuth.d.ts +84 -0
  80. package/dist/connectors/mcpOAuth.js +389 -0
  81. package/dist/connectors/mcpOAuth.js.map +1 -0
  82. package/dist/connectors/mockConnector.d.ts +28 -0
  83. package/dist/connectors/mockConnector.js +81 -0
  84. package/dist/connectors/mockConnector.js.map +1 -0
  85. package/dist/connectors/notion.d.ts +143 -0
  86. package/dist/connectors/notion.js +424 -0
  87. package/dist/connectors/notion.js.map +1 -0
  88. package/dist/connectors/sentry.d.ts +17 -21
  89. package/dist/connectors/sentry.js +115 -131
  90. package/dist/connectors/sentry.js.map +1 -1
  91. package/dist/connectors/slack.d.ts +50 -0
  92. package/dist/connectors/slack.js +324 -0
  93. package/dist/connectors/slack.js.map +1 -0
  94. package/dist/connectors/stripe.d.ts +116 -0
  95. package/dist/connectors/stripe.js +379 -0
  96. package/dist/connectors/stripe.js.map +1 -0
  97. package/dist/connectors/tokenStorage.d.ts +35 -0
  98. package/dist/connectors/tokenStorage.js +459 -0
  99. package/dist/connectors/tokenStorage.js.map +1 -0
  100. package/dist/connectors/zendesk.d.ts +104 -0
  101. package/dist/connectors/zendesk.js +424 -0
  102. package/dist/connectors/zendesk.js.map +1 -0
  103. package/dist/drivers/gemini/index.d.ts +5 -1
  104. package/dist/drivers/gemini/index.js +39 -5
  105. package/dist/drivers/gemini/index.js.map +1 -1
  106. package/dist/drivers/index.d.ts +5 -0
  107. package/dist/drivers/index.js +1 -1
  108. package/dist/drivers/index.js.map +1 -1
  109. package/dist/featureFlags.d.ts +73 -0
  110. package/dist/featureFlags.js +203 -0
  111. package/dist/featureFlags.js.map +1 -0
  112. package/dist/fp/automationInterpreter.js +1 -0
  113. package/dist/fp/automationInterpreter.js.map +1 -1
  114. package/dist/fp/automationProgram.d.ts +1 -1
  115. package/dist/fp/automationProgram.js.map +1 -1
  116. package/dist/fp/policyParser.js +17 -0
  117. package/dist/fp/policyParser.js.map +1 -1
  118. package/dist/index.js +621 -61
  119. package/dist/index.js.map +1 -1
  120. package/dist/installGuard.d.ts +25 -0
  121. package/dist/installGuard.js +48 -0
  122. package/dist/installGuard.js.map +1 -0
  123. package/dist/oauth.d.ts +4 -1
  124. package/dist/oauth.js +50 -14
  125. package/dist/oauth.js.map +1 -1
  126. package/dist/patchworkConfig.d.ts +9 -0
  127. package/dist/patchworkConfig.js.map +1 -1
  128. package/dist/recipeOrchestration.d.ts +53 -0
  129. package/dist/recipeOrchestration.js +272 -0
  130. package/dist/recipeOrchestration.js.map +1 -0
  131. package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
  132. package/dist/recipes/RecipeOrchestrator.js +51 -0
  133. package/dist/recipes/RecipeOrchestrator.js.map +1 -0
  134. package/dist/recipes/agentExecutor.d.ts +28 -0
  135. package/dist/recipes/agentExecutor.js +42 -0
  136. package/dist/recipes/agentExecutor.js.map +1 -0
  137. package/dist/recipes/chainedRunner.d.ts +140 -0
  138. package/dist/recipes/chainedRunner.js +539 -0
  139. package/dist/recipes/chainedRunner.js.map +1 -0
  140. package/dist/recipes/dependencyGraph.d.ts +39 -0
  141. package/dist/recipes/dependencyGraph.js +199 -0
  142. package/dist/recipes/dependencyGraph.js.map +1 -0
  143. package/dist/recipes/legacyRecipeCompat.d.ts +2 -0
  144. package/dist/recipes/legacyRecipeCompat.js +112 -0
  145. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  146. package/dist/recipes/manifest.d.ts +47 -0
  147. package/dist/recipes/manifest.js +141 -0
  148. package/dist/recipes/manifest.js.map +1 -0
  149. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  150. package/dist/recipes/nestedRecipeStep.js +95 -0
  151. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  152. package/dist/recipes/outputRegistry.d.ts +28 -0
  153. package/dist/recipes/outputRegistry.js +52 -0
  154. package/dist/recipes/outputRegistry.js.map +1 -0
  155. package/dist/recipes/scheduler.d.ts +23 -7
  156. package/dist/recipes/scheduler.js +131 -41
  157. package/dist/recipes/scheduler.js.map +1 -1
  158. package/dist/recipes/schema.d.ts +17 -2
  159. package/dist/recipes/schemaGenerator.d.ts +28 -0
  160. package/dist/recipes/schemaGenerator.js +565 -0
  161. package/dist/recipes/schemaGenerator.js.map +1 -0
  162. package/dist/recipes/templateEngine.d.ts +62 -0
  163. package/dist/recipes/templateEngine.js +182 -0
  164. package/dist/recipes/templateEngine.js.map +1 -0
  165. package/dist/recipes/toolRegistry.d.ts +181 -0
  166. package/dist/recipes/toolRegistry.js +300 -0
  167. package/dist/recipes/toolRegistry.js.map +1 -0
  168. package/dist/recipes/tools/calendar.d.ts +6 -0
  169. package/dist/recipes/tools/calendar.js +61 -0
  170. package/dist/recipes/tools/calendar.js.map +1 -0
  171. package/dist/recipes/tools/confluence.d.ts +6 -0
  172. package/dist/recipes/tools/confluence.js +254 -0
  173. package/dist/recipes/tools/confluence.js.map +1 -0
  174. package/dist/recipes/tools/datadog.d.ts +6 -0
  175. package/dist/recipes/tools/datadog.js +239 -0
  176. package/dist/recipes/tools/datadog.js.map +1 -0
  177. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  178. package/dist/recipes/tools/diagnostics.js +36 -0
  179. package/dist/recipes/tools/diagnostics.js.map +1 -0
  180. package/dist/recipes/tools/file.d.ts +6 -0
  181. package/dist/recipes/tools/file.js +170 -0
  182. package/dist/recipes/tools/file.js.map +1 -0
  183. package/dist/recipes/tools/git.d.ts +6 -0
  184. package/dist/recipes/tools/git.js +63 -0
  185. package/dist/recipes/tools/git.js.map +1 -0
  186. package/dist/recipes/tools/github.d.ts +6 -0
  187. package/dist/recipes/tools/github.js +91 -0
  188. package/dist/recipes/tools/github.js.map +1 -0
  189. package/dist/recipes/tools/gmail.d.ts +6 -0
  190. package/dist/recipes/tools/gmail.js +210 -0
  191. package/dist/recipes/tools/gmail.js.map +1 -0
  192. package/dist/recipes/tools/hubspot.d.ts +6 -0
  193. package/dist/recipes/tools/hubspot.js +232 -0
  194. package/dist/recipes/tools/hubspot.js.map +1 -0
  195. package/dist/recipes/tools/index.d.ts +22 -0
  196. package/dist/recipes/tools/index.js +25 -0
  197. package/dist/recipes/tools/index.js.map +1 -0
  198. package/dist/recipes/tools/intercom.d.ts +6 -0
  199. package/dist/recipes/tools/intercom.js +226 -0
  200. package/dist/recipes/tools/intercom.js.map +1 -0
  201. package/dist/recipes/tools/linear.d.ts +6 -0
  202. package/dist/recipes/tools/linear.js +83 -0
  203. package/dist/recipes/tools/linear.js.map +1 -0
  204. package/dist/recipes/tools/notion.d.ts +6 -0
  205. package/dist/recipes/tools/notion.js +278 -0
  206. package/dist/recipes/tools/notion.js.map +1 -0
  207. package/dist/recipes/tools/slack.d.ts +6 -0
  208. package/dist/recipes/tools/slack.js +72 -0
  209. package/dist/recipes/tools/slack.js.map +1 -0
  210. package/dist/recipes/tools/stripe.d.ts +6 -0
  211. package/dist/recipes/tools/stripe.js +265 -0
  212. package/dist/recipes/tools/stripe.js.map +1 -0
  213. package/dist/recipes/tools/zendesk.d.ts +6 -0
  214. package/dist/recipes/tools/zendesk.js +245 -0
  215. package/dist/recipes/tools/zendesk.js.map +1 -0
  216. package/dist/recipes/validation.d.ts +13 -0
  217. package/dist/recipes/validation.js +433 -0
  218. package/dist/recipes/validation.js.map +1 -0
  219. package/dist/recipes/yamlRunner.d.ts +87 -0
  220. package/dist/recipes/yamlRunner.js +693 -409
  221. package/dist/recipes/yamlRunner.js.map +1 -1
  222. package/dist/recipesHttp.d.ts +34 -6
  223. package/dist/recipesHttp.js +285 -15
  224. package/dist/recipesHttp.js.map +1 -1
  225. package/dist/riskTier.js +1 -0
  226. package/dist/riskTier.js.map +1 -1
  227. package/dist/runLog.d.ts +23 -0
  228. package/dist/runLog.js +56 -1
  229. package/dist/runLog.js.map +1 -1
  230. package/dist/schemas/dry-run-plan.v1.json +139 -0
  231. package/dist/schemas/recipe.v1.json +684 -0
  232. package/dist/server.d.ts +32 -1
  233. package/dist/server.js +980 -97
  234. package/dist/server.js.map +1 -1
  235. package/dist/streamableHttp.js +2 -0
  236. package/dist/streamableHttp.js.map +1 -1
  237. package/dist/tools/addLinearComment.d.ts +55 -0
  238. package/dist/tools/addLinearComment.js +72 -0
  239. package/dist/tools/addLinearComment.js.map +1 -0
  240. package/dist/tools/bridgeDoctor.js +2 -2
  241. package/dist/tools/bridgeDoctor.js.map +1 -1
  242. package/dist/tools/createLinearIssue.d.ts +84 -0
  243. package/dist/tools/createLinearIssue.js +146 -0
  244. package/dist/tools/createLinearIssue.js.map +1 -0
  245. package/dist/tools/fetchCalendarEvents.d.ts +94 -0
  246. package/dist/tools/fetchCalendarEvents.js +97 -0
  247. package/dist/tools/fetchCalendarEvents.js.map +1 -0
  248. package/dist/tools/fetchGithubIssue.d.ts +80 -0
  249. package/dist/tools/fetchGithubIssue.js +84 -0
  250. package/dist/tools/fetchGithubIssue.js.map +1 -0
  251. package/dist/tools/fetchGithubPR.d.ts +89 -0
  252. package/dist/tools/fetchGithubPR.js +96 -0
  253. package/dist/tools/fetchGithubPR.js.map +1 -0
  254. package/dist/tools/fetchSlackProfile.d.ts +43 -0
  255. package/dist/tools/fetchSlackProfile.js +46 -0
  256. package/dist/tools/fetchSlackProfile.js.map +1 -0
  257. package/dist/tools/getConnectorStatus.d.ts +58 -0
  258. package/dist/tools/getConnectorStatus.js +56 -0
  259. package/dist/tools/getConnectorStatus.js.map +1 -0
  260. package/dist/tools/github/actions.js +4 -2
  261. package/dist/tools/github/actions.js.map +1 -1
  262. package/dist/tools/github/composite.d.ts +339 -0
  263. package/dist/tools/github/composite.js +343 -0
  264. package/dist/tools/github/composite.js.map +1 -0
  265. package/dist/tools/github/index.d.ts +2 -1
  266. package/dist/tools/github/index.js +2 -1
  267. package/dist/tools/github/index.js.map +1 -1
  268. package/dist/tools/github/issues.js +8 -4
  269. package/dist/tools/github/issues.js.map +1 -1
  270. package/dist/tools/github/pr.d.ts +122 -0
  271. package/dist/tools/github/pr.js +195 -5
  272. package/dist/tools/github/pr.js.map +1 -1
  273. package/dist/tools/index.js +32 -1
  274. package/dist/tools/index.js.map +1 -1
  275. package/dist/tools/searchTools.js +1 -1
  276. package/dist/tools/searchTools.js.map +1 -1
  277. package/dist/tools/slackListChannels.d.ts +65 -0
  278. package/dist/tools/slackListChannels.js +70 -0
  279. package/dist/tools/slackListChannels.js.map +1 -0
  280. package/dist/tools/slackPostMessage.d.ts +57 -0
  281. package/dist/tools/slackPostMessage.js +77 -0
  282. package/dist/tools/slackPostMessage.js.map +1 -0
  283. package/dist/tools/testTraceToSource.js +2 -2
  284. package/dist/tools/testTraceToSource.js.map +1 -1
  285. package/dist/tools/updateLinearIssue.d.ts +89 -0
  286. package/dist/tools/updateLinearIssue.js +117 -0
  287. package/dist/tools/updateLinearIssue.js.map +1 -0
  288. package/dist/transport.d.ts +7 -1
  289. package/dist/transport.js +85 -11
  290. package/dist/transport.js.map +1 -1
  291. package/package.json +5 -2
  292. package/scripts/start-all.sh +56 -19
  293. package/templates/automation-policies/recipe-authoring.json +25 -0
  294. package/templates/automation-policy.example.json +6 -0
  295. package/templates/co.patchwork-os.bridge.plist +34 -0
  296. package/templates/recipes/ctx-loop-test.yaml +75 -0
  297. package/templates/recipes/lint-on-save.yaml +1 -2
  298. package/templates/recipes/morning-brief-slack.yaml +57 -0
  299. package/templates/recipes/morning-brief.yaml +14 -6
  300. package/templates/recipes/project-health-check.yaml +50 -0
  301. 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
@@ -570,20 +586,9 @@ export function register(ctx) {
570
586
  // Patchwork: `patchwork recipe list` — enumerate installed recipes.
571
587
  if (process.argv[2] === "recipe" && process.argv[3] === "list") {
572
588
  (async () => {
573
- const { listYamlRecipes } = await import("./recipes/yamlRunner.js");
574
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
575
- const recipes = listYamlRecipes(recipesDir);
576
- if (recipes.length === 0) {
577
- process.stdout.write("No recipes installed. Run `patchwork-os patchwork-init` to install the starter set.\n");
578
- }
579
- else {
580
- process.stdout.write(`Installed recipes (${recipes.length}):\n\n`);
581
- for (const r of recipes) {
582
- const desc = r.description ? ` ${r.description}` : "";
583
- process.stdout.write(` ${r.name.padEnd(28)} [${r.trigger}]${desc}\n`);
584
- }
585
- process.stdout.write(`\nRun a recipe: patchwork-os recipe run <name>\n`);
586
- }
589
+ const { listInstalledRecipes, printInstalledList } = await import("./commands/recipeInstall.js");
590
+ const entries = listInstalledRecipes();
591
+ printInstalledList(entries);
587
592
  process.exit(0);
588
593
  })();
589
594
  }
@@ -591,25 +596,94 @@ if (process.argv[2] === "recipe" && process.argv[3] === "list") {
591
596
  // a running bridge's /recipes/run endpoint if one is available.
592
597
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
593
598
  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");
599
+ const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
600
+ let localFlag = false;
601
+ let dryRun = false;
602
+ let recipeRef;
603
+ let step;
604
+ const vars = {};
605
+ for (let i = 0; i < args.length; i++) {
606
+ const arg = args[i];
607
+ if (arg === undefined) {
608
+ continue;
609
+ }
610
+ const currentArg = arg;
611
+ if (currentArg === "--local") {
612
+ localFlag = true;
613
+ continue;
614
+ }
615
+ if (currentArg === "--dry-run") {
616
+ dryRun = true;
617
+ continue;
618
+ }
619
+ if (currentArg === "--step" || currentArg.startsWith("--step=")) {
620
+ const value = currentArg === "--step"
621
+ ? args[++i]
622
+ : currentArg.slice("--step=".length);
623
+ if (!value) {
624
+ process.stderr.write(`Error: --step requires a value\n${usage}`);
625
+ process.exit(1);
626
+ }
627
+ step = value;
628
+ continue;
629
+ }
630
+ if (currentArg === "--var" || currentArg.startsWith("--var=")) {
631
+ const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
632
+ if (!assignment) {
633
+ process.stderr.write(`Error: --var requires KEY=VALUE\n${usage}`);
634
+ process.exit(1);
635
+ }
636
+ const eqIndex = assignment.indexOf("=");
637
+ if (eqIndex <= 0) {
638
+ process.stderr.write(`Error: invalid --var assignment "${assignment}" (expected KEY=VALUE)\n${usage}`);
639
+ process.exit(1);
640
+ }
641
+ const key = assignment.slice(0, eqIndex);
642
+ const value = assignment.slice(eqIndex + 1);
643
+ vars[key] = value;
644
+ continue;
645
+ }
646
+ if (currentArg.startsWith("--")) {
647
+ process.stderr.write(`Error: unknown option ${currentArg}\n${usage}`);
648
+ process.exit(1);
649
+ }
650
+ if (!recipeRef) {
651
+ recipeRef = currentArg;
652
+ continue;
653
+ }
654
+ process.stderr.write(`Error: unexpected argument ${currentArg}\n${usage}`);
598
655
  process.exit(1);
599
656
  }
657
+ if (!recipeRef) {
658
+ process.stderr.write(usage);
659
+ process.exit(1);
660
+ }
661
+ const recipeArg = recipeRef;
600
662
  (async () => {
601
663
  try {
602
- // Try bridge first (requires --claude-driver subprocess).
664
+ const seedVars = Object.keys(vars).length > 0 ? vars : undefined;
665
+ const explicitFile = (() => {
666
+ try {
667
+ const resolved = path.resolve(recipeArg);
668
+ return existsSync(resolved) && statSync(resolved).isFile();
669
+ }
670
+ catch {
671
+ return false;
672
+ }
673
+ })();
603
674
  const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
604
675
  const lock = localFlag ? null : findBridgeLock();
605
- if (lock) {
676
+ if (lock && !dryRun && !step && !explicitFile) {
606
677
  const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
607
678
  method: "POST",
608
679
  headers: {
609
680
  Authorization: `Bearer ${lock.authToken}`,
610
681
  "Content-Type": "application/json",
611
682
  },
612
- body: JSON.stringify({ name }),
683
+ body: JSON.stringify({
684
+ name: recipeArg,
685
+ ...(seedVars ? { vars: seedVars } : {}),
686
+ }),
613
687
  });
614
688
  const body = (await res.json());
615
689
  if (!body.ok) {
@@ -622,43 +696,64 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
622
696
  // else: fall through to local runner below
623
697
  }
624
698
  else {
625
- process.stdout.write(` ✓ enqueued recipe "${name}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
699
+ process.stdout.write(` ✓ enqueued recipe "${recipeArg}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
626
700
  " Watch progress on the dashboard Tasks page or via listClaudeTasks.\n");
627
701
  process.exit(0);
628
702
  return;
629
703
  }
630
704
  }
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);
705
+ const { runRecipe, runRecipeDryPlan, summarizeRecipeExecution, formatRunReport, extractRunLogStepResults, } = await import("./commands/recipe.js");
706
+ if (dryRun) {
707
+ const plan = await runRecipeDryPlan(recipeArg, {
708
+ ...(step ? { step } : {}),
709
+ ...(seedVars ? { vars: seedVars } : {}),
710
+ });
711
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
712
+ process.exit(0);
653
713
  return;
654
714
  }
655
- process.stdout.write(` Running recipe "${name}" locally…\n`);
656
- const recipe = loadYamlRecipe(recipePath);
715
+ process.stdout.write(step
716
+ ? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
717
+ : ` Running recipe "${recipeArg}" locally…\n`);
657
718
  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`);
719
+ const run = await runRecipe(recipeArg, {
720
+ ...(step ? { step } : {}),
721
+ ...(seedVars ? { vars: seedVars } : {}),
722
+ workdir,
723
+ });
724
+ if (run.stepSelection) {
725
+ process.stdout.write(` Selected step via ${run.stepSelection.matchedBy}: ${run.stepSelection.matchedValue}\n`);
726
+ }
727
+ const summary = summarizeRecipeExecution(run.result);
728
+ process.stdout.write(`${formatRunReport(run.result, run.recipe.name)}\n`);
729
+ if (summary.errorMessage) {
730
+ process.stderr.write(` Error: ${summary.errorMessage}\n`);
731
+ }
732
+ // Append to run log so CLI runs appear in ctxQueryTraces + dashboard /runs
733
+ try {
734
+ const { RecipeRunLog } = await import("./runLog.js");
735
+ const runLog = new RecipeRunLog({
736
+ dir: path.join(os.homedir(), ".patchwork"),
737
+ });
738
+ const startedAt = Date.now();
739
+ const stepResultsForLog = extractRunLogStepResults(run.result);
740
+ runLog.appendDirect({
741
+ taskId: `cli-${Date.now()}`,
742
+ recipeName: run.recipe.name,
743
+ trigger: "recipe",
744
+ status: summary.ok ? "done" : "error",
745
+ createdAt: startedAt,
746
+ startedAt,
747
+ doneAt: Date.now(),
748
+ durationMs: 0,
749
+ ...(summary.errorMessage
750
+ ? { errorMessage: summary.errorMessage }
751
+ : {}),
752
+ ...(stepResultsForLog ? { stepResults: stepResultsForLog } : {}),
753
+ });
754
+ }
755
+ catch {
756
+ // Non-fatal — run log write failure must not abort the CLI
662
757
  }
663
758
  process.exit(0);
664
759
  }
@@ -669,24 +764,324 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
669
764
  })();
670
765
  }
671
766
  // Handle init subcommand — one-command setup: install extension + write CLAUDE.md + print next steps
672
- // Patchwork: `patchwork recipe install <file.json>` subcommand.
767
+ // Patchwork: `patchwork recipe install <source>` subcommand.
768
+ // Supports: github:owner/repo, github:owner/repo/subdir, https://github.com/owner/repo,
769
+ // ./local/path, or legacy <file.json> (single-recipe install).
673
770
  if (process.argv[2] === "recipe" && process.argv[3] === "install") {
771
+ const source = process.argv[4];
772
+ if (!source) {
773
+ process.stderr.write("Usage: patchwork recipe install <source>\n" +
774
+ " <source> can be:\n" +
775
+ " github:owner/repo\n" +
776
+ " github:owner/repo/subdir\n" +
777
+ " https://github.com/owner/repo\n" +
778
+ " ./local/path\n");
779
+ process.exit(1);
780
+ }
781
+ (async () => {
782
+ try {
783
+ // Legacy path: bare .json file argument → single-file installer
784
+ if (source.endsWith(".json") &&
785
+ !source.startsWith("github:") &&
786
+ !source.startsWith("http")) {
787
+ const { installRecipeFromFile } = await import("./recipes/installer.js");
788
+ const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
789
+ const result = installRecipeFromFile(path.resolve(source), {
790
+ recipesDir,
791
+ });
792
+ process.stdout.write(` ✓ ${result.action} ${result.installedPath}\n` +
793
+ ` ℹ permissions snippet written to ${result.installedPath}.permissions.json\n` +
794
+ ` Review + merge into ~/.claude/settings.json to pre-approve recipe steps.\n`);
795
+ }
796
+ else {
797
+ // Marketplace install: github:, https://, ./local/
798
+ const { runRecipeInstall, printInstallResult } = await import("./commands/recipeInstall.js");
799
+ const result = await runRecipeInstall(source);
800
+ printInstallResult(result);
801
+ }
802
+ process.exit(0);
803
+ }
804
+ catch (err) {
805
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
806
+ process.exit(1);
807
+ }
808
+ })();
809
+ }
810
+ // Patchwork: `patchwork recipe schema [outputDir]` — write generated recipe schemas to disk.
811
+ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
812
+ const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
813
+ (async () => {
814
+ try {
815
+ const { runSchema } = await import("./commands/recipe.js");
816
+ const result = await runSchema(path.resolve(outputDir));
817
+ process.stdout.write(` ✓ Wrote schemas to ${result.outputDir}\n`);
818
+ for (const file of result.filesWritten) {
819
+ process.stdout.write(` ${file}\n`);
820
+ }
821
+ process.exit(0);
822
+ }
823
+ catch (err) {
824
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
825
+ process.exit(1);
826
+ }
827
+ })();
828
+ }
829
+ // Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
830
+ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
831
+ const args = process.argv.slice(4);
832
+ const recipeName = args[0];
833
+ if (!recipeName) {
834
+ process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>]\n");
835
+ process.stderr.write("\nTemplates:\n");
836
+ (async () => {
837
+ const { listTemplates } = await import("./commands/recipe.js");
838
+ for (const t of listTemplates()) {
839
+ process.stderr.write(` ${t}\n`);
840
+ }
841
+ process.exit(1);
842
+ })();
843
+ }
844
+ else {
845
+ (async () => {
846
+ try {
847
+ const { runNew } = await import("./commands/recipe.js");
848
+ const templateIdx = args.indexOf("--template");
849
+ const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
850
+ const descIdx = args.indexOf("--desc");
851
+ const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
852
+ `Recipe: ${recipeName}`;
853
+ const result = runNew({
854
+ name: recipeName,
855
+ description,
856
+ ...(template ? { template } : {}),
857
+ });
858
+ process.stdout.write(` ✓ Created ${result.path}\n`);
859
+ process.exit(0);
860
+ }
861
+ catch (err) {
862
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
863
+ process.exit(1);
864
+ }
865
+ })();
866
+ }
867
+ }
868
+ // Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
869
+ if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
674
870
  const file = process.argv[4];
675
871
  if (!file) {
676
- process.stderr.write("Usage: patchwork recipe install <file.json>\n");
872
+ process.stderr.write("Usage: patchwork recipe lint <file.yaml>\n");
873
+ process.exit(1);
874
+ }
875
+ (async () => {
876
+ try {
877
+ const { runLint } = await import("./commands/recipe.js");
878
+ const result = runLint(path.resolve(file));
879
+ for (const issue of result.issues) {
880
+ const prefix = issue.level === "error" ? "✗" : "⚠";
881
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
882
+ }
883
+ if (result.valid) {
884
+ process.stdout.write(` ✓ Valid recipe (${result.warnings} warnings)\n`);
885
+ process.exit(0);
886
+ }
887
+ else {
888
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
889
+ process.exit(1);
890
+ }
891
+ }
892
+ catch (err) {
893
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
894
+ process.exit(1);
895
+ }
896
+ })();
897
+ }
898
+ // Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
899
+ if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
900
+ const args = process.argv.slice(4);
901
+ const usage = "Usage: patchwork recipe preflight <file.yaml> [--json] [--watch] [--require-fixtures] [--no-require-write-ack] [--allow-write <tool-or-ns>]\n";
902
+ let json = false;
903
+ let watchMode = false;
904
+ let requireFixtures = false;
905
+ let requireWriteAck = true;
906
+ const allowWrites = [];
907
+ let file;
908
+ for (let i = 0; i < args.length; i++) {
909
+ const arg = args[i];
910
+ if (arg === undefined)
911
+ continue;
912
+ if (arg === "--json") {
913
+ json = true;
914
+ continue;
915
+ }
916
+ if (arg === "--watch") {
917
+ watchMode = true;
918
+ continue;
919
+ }
920
+ if (arg === "--require-fixtures") {
921
+ requireFixtures = true;
922
+ continue;
923
+ }
924
+ if (arg === "--no-require-write-ack") {
925
+ requireWriteAck = false;
926
+ continue;
927
+ }
928
+ if (arg === "--allow-write" || arg.startsWith("--allow-write=")) {
929
+ const value = arg === "--allow-write"
930
+ ? args[++i]
931
+ : arg.slice("--allow-write=".length);
932
+ if (!value) {
933
+ process.stderr.write(`Error: --allow-write requires a value\n${usage}`);
934
+ process.exit(1);
935
+ }
936
+ allowWrites.push(value);
937
+ continue;
938
+ }
939
+ if (!arg.startsWith("--")) {
940
+ file = arg;
941
+ }
942
+ }
943
+ if (!file) {
944
+ process.stderr.write(usage);
945
+ process.exit(1);
946
+ }
947
+ const renderResult = (result) => {
948
+ if (json) {
949
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
950
+ return;
951
+ }
952
+ for (const issue of result.issues) {
953
+ const prefix = issue.level === "error" ? "✗" : "⚠";
954
+ const where = issue.stepId ? ` [${issue.stepId}]` : "";
955
+ process.stderr.write(` ${prefix} ${issue.code}${where}: ${issue.message}\n`);
956
+ }
957
+ if (result.ok) {
958
+ process.stdout.write(` ✓ Preflight passed for ${result.recipe} (${result.plan.steps.length} steps)\n`);
959
+ }
960
+ else {
961
+ const errorCount = result.issues.filter((i) => i.level === "error").length;
962
+ process.stdout.write(`\n ${errorCount} error(s) — preflight failed\n`);
963
+ }
964
+ };
965
+ (async () => {
966
+ try {
967
+ const { runPreflight, runPreflightWatch } = await import("./commands/recipe.js");
968
+ const resolvedPath = path.resolve(file);
969
+ if (watchMode) {
970
+ process.stdout.write(` Watching ${resolvedPath} — preflight on save…\n`);
971
+ const stop = runPreflightWatch({
972
+ recipePath: resolvedPath,
973
+ requireWriteAck,
974
+ requireFixtures,
975
+ allowWrites,
976
+ onResult: (result) => renderResult(result),
977
+ onError: (err) => {
978
+ process.stderr.write(`Error: ${err.message}\n`);
979
+ },
980
+ });
981
+ process.on("SIGINT", () => {
982
+ stop();
983
+ process.exit(0);
984
+ });
985
+ return;
986
+ }
987
+ const result = await runPreflight(resolvedPath, {
988
+ requireWriteAck,
989
+ requireFixtures,
990
+ allowWrites,
991
+ });
992
+ renderResult(result);
993
+ process.exit(result.ok ? 0 : 1);
994
+ }
995
+ catch (err) {
996
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
997
+ process.exit(1);
998
+ }
999
+ })();
1000
+ }
1001
+ // Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
1002
+ if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
1003
+ const args = process.argv.slice(4);
1004
+ const check = args.includes("--check");
1005
+ const watchMode = args.includes("--watch");
1006
+ const file = args.find((arg) => !arg.startsWith("--"));
1007
+ if (!file) {
1008
+ process.stderr.write("Usage: patchwork recipe fmt <file.yaml> [--check] [--watch]\n");
1009
+ process.exit(1);
1010
+ }
1011
+ const renderResult = (result, filePath) => {
1012
+ if (check) {
1013
+ process.stdout.write(result.changed
1014
+ ? " ✗ File would be reformatted\n"
1015
+ : " ✓ File is already formatted\n");
1016
+ }
1017
+ else {
1018
+ process.stdout.write(result.changed
1019
+ ? ` ✓ Formatted ${filePath}\n`
1020
+ : ` ✓ Already formatted ${filePath}\n`);
1021
+ }
1022
+ };
1023
+ (async () => {
1024
+ try {
1025
+ const { runFmt, runFmtWatch } = await import("./commands/recipe.js");
1026
+ const resolvedPath = path.resolve(file);
1027
+ if (watchMode) {
1028
+ process.stdout.write(` Watching ${resolvedPath} — fmt on save…\n`);
1029
+ const stop = runFmtWatch({
1030
+ recipePath: resolvedPath,
1031
+ check,
1032
+ onResult: (result) => {
1033
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
1034
+ renderResult(result, resolvedPath);
1035
+ },
1036
+ onError: (err) => {
1037
+ process.stderr.write(`Error: ${err.message}\n`);
1038
+ },
1039
+ });
1040
+ process.on("SIGINT", () => {
1041
+ stop();
1042
+ process.exit(0);
1043
+ });
1044
+ return;
1045
+ }
1046
+ const result = runFmt(resolvedPath, { check });
1047
+ renderResult(result, file);
1048
+ process.exit(check && result.changed ? 1 : 0);
1049
+ }
1050
+ catch (err) {
1051
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1052
+ process.exit(1);
1053
+ }
1054
+ })();
1055
+ }
1056
+ // Patchwork: `patchwork recipe record <file.yaml>` — execute live and record connector fixtures.
1057
+ if (process.argv[2] === "recipe" && process.argv[3] === "record") {
1058
+ const args = process.argv.slice(4);
1059
+ const file = args.find((arg) => !arg.startsWith("--"));
1060
+ const fixturesIdx = args.indexOf("--fixtures");
1061
+ const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
1062
+ if (!file) {
1063
+ process.stderr.write("Usage: patchwork recipe record <file.yaml> [--fixtures <dir>]\n");
677
1064
  process.exit(1);
678
1065
  }
679
1066
  (async () => {
680
1067
  try {
681
- const { installRecipeFromFile } = await import("./recipes/installer.js");
682
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
683
- const result = installRecipeFromFile(path.resolve(file), {
684
- recipesDir,
1068
+ const { runRecord } = await import("./commands/recipe.js");
1069
+ const result = await runRecord(path.resolve(file), {
1070
+ ...(fixturesDir ? { fixturesDir: path.resolve(fixturesDir) } : {}),
685
1071
  });
686
- process.stdout.write(` ✓ ${result.action} ${result.installedPath}\n` +
687
- ` ℹ permissions snippet written to ${result.installedPath}.permissions.json\n` +
688
- ` Review + merge into ~/.claude/settings.json to pre-approve recipe steps.\n`);
689
- process.exit(0);
1072
+ for (const issue of result.issues) {
1073
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1074
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1075
+ }
1076
+ if (result.recordedFixtures.length > 0) {
1077
+ process.stdout.write(` ℹ Recorded fixture libraries: ${result.recordedFixtures.join(", ")}\n`);
1078
+ }
1079
+ if (result.valid) {
1080
+ process.stdout.write(" ✓ Recipe fixtures recorded\n");
1081
+ process.exit(0);
1082
+ }
1083
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1084
+ process.exit(1);
690
1085
  }
691
1086
  catch (err) {
692
1087
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -694,6 +1089,153 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
694
1089
  }
695
1090
  })();
696
1091
  }
1092
+ // Patchwork: `patchwork recipe test <file.yaml>` — validate fixture coverage for mocked execution.
1093
+ if (process.argv[2] === "recipe" && process.argv[3] === "test") {
1094
+ const args = process.argv.slice(4);
1095
+ const file = args.find((arg) => !arg.startsWith("--"));
1096
+ const fixturesIdx = args.indexOf("--fixtures");
1097
+ const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
1098
+ const watchMode = args.includes("--watch");
1099
+ if (!file) {
1100
+ process.stderr.write("Usage: patchwork recipe test <file.yaml> [--fixtures <dir>] [--watch]\n");
1101
+ process.exit(1);
1102
+ }
1103
+ const renderResult = (result) => {
1104
+ for (const issue of result.issues) {
1105
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1106
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1107
+ }
1108
+ if (result.requiredFixtures.length > 0) {
1109
+ process.stdout.write(` ℹ Required fixtures: ${result.requiredFixtures.join(", ")}\n`);
1110
+ }
1111
+ if (result.valid) {
1112
+ process.stdout.write(" ✓ Test passed\n");
1113
+ }
1114
+ else {
1115
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1116
+ }
1117
+ };
1118
+ (async () => {
1119
+ try {
1120
+ const { runTest, runTestWatch } = await import("./commands/recipe.js");
1121
+ const resolvedPath = path.resolve(file);
1122
+ const resolvedFixtures = fixturesDir
1123
+ ? path.resolve(fixturesDir)
1124
+ : undefined;
1125
+ if (watchMode) {
1126
+ process.stdout.write(` Watching ${resolvedPath} — test on save…\n`);
1127
+ const stop = runTestWatch({
1128
+ recipePath: resolvedPath,
1129
+ ...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
1130
+ onResult: (result) => {
1131
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
1132
+ renderResult(result);
1133
+ },
1134
+ onError: (err) => {
1135
+ process.stderr.write(`Error: ${err.message}\n`);
1136
+ },
1137
+ });
1138
+ process.on("SIGINT", () => {
1139
+ stop();
1140
+ process.exit(0);
1141
+ });
1142
+ return;
1143
+ }
1144
+ const result = await runTest(resolvedPath, {
1145
+ ...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
1146
+ });
1147
+ renderResult(result);
1148
+ process.exit(result.valid ? 0 : 1);
1149
+ }
1150
+ catch (err) {
1151
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1152
+ process.exit(1);
1153
+ }
1154
+ })();
1155
+ }
1156
+ // Patchwork: `patchwork recipe watch <file.yaml>` — watch for changes and validate.
1157
+ if (process.argv[2] === "recipe" && process.argv[3] === "watch") {
1158
+ const file = process.argv[4];
1159
+ if (!file) {
1160
+ process.stderr.write("Usage: patchwork recipe watch <file.yaml>\n");
1161
+ process.exit(1);
1162
+ }
1163
+ (async () => {
1164
+ const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
1165
+ const { runWatch, runLint, runWatchedRecipe, formatRunReport, summarizeRecipeExecution, extractRunLogStepResults, } = await import("./commands/recipe.js");
1166
+ const filePath = path.resolve(file);
1167
+ const lock = findBridgeLock();
1168
+ const workdir = lock?.workspace || process.cwd();
1169
+ const initial = runLint(filePath);
1170
+ if (!initial.valid) {
1171
+ process.stderr.write(" ✗ Recipe has errors - fix before watching\n");
1172
+ for (const issue of initial.issues) {
1173
+ process.stderr.write(` ${issue.level}: ${issue.message}\n`);
1174
+ }
1175
+ }
1176
+ else {
1177
+ process.stdout.write(` ✓ Watching ${file} for changes...\n`);
1178
+ }
1179
+ const stop = runWatch({
1180
+ recipePath: filePath,
1181
+ onChange: async () => {
1182
+ process.stdout.write(`\n Change detected, running...\n`);
1183
+ const watched = await runWatchedRecipe(filePath, { workdir });
1184
+ if (!watched.lint.valid) {
1185
+ process.stderr.write(` ✗ Invalid (${watched.lint.errors} errors)\n`);
1186
+ for (const issue of watched.lint.issues) {
1187
+ process.stderr.write(` ${issue.level}: ${issue.message}\n`);
1188
+ }
1189
+ return;
1190
+ }
1191
+ if (watched.run?.stepSelection) {
1192
+ process.stdout.write(` Selected step via ${watched.run.stepSelection.matchedBy}: ${watched.run.stepSelection.matchedValue}\n`);
1193
+ }
1194
+ if (watched.run) {
1195
+ process.stdout.write(`${formatRunReport(watched.run.result, watched.run.recipe.name)}\n`);
1196
+ const summary = summarizeRecipeExecution(watched.run.result);
1197
+ if (summary.errorMessage) {
1198
+ process.stderr.write(` Error: ${summary.errorMessage}\n`);
1199
+ }
1200
+ // Append to run log
1201
+ try {
1202
+ const { RecipeRunLog } = await import("./runLog.js");
1203
+ const runLog = new RecipeRunLog({
1204
+ dir: path.join(os.homedir(), ".patchwork"),
1205
+ });
1206
+ const now = Date.now();
1207
+ const stepResultsForLog = extractRunLogStepResults(watched.run.result);
1208
+ runLog.appendDirect({
1209
+ taskId: `watch-${now}`,
1210
+ recipeName: watched.run.recipe.name,
1211
+ trigger: "recipe",
1212
+ status: summary.ok ? "done" : "error",
1213
+ createdAt: now,
1214
+ startedAt: now,
1215
+ doneAt: now,
1216
+ durationMs: 0,
1217
+ ...(summary.errorMessage
1218
+ ? { errorMessage: summary.errorMessage }
1219
+ : {}),
1220
+ ...(stepResultsForLog ? { stepResults: stepResultsForLog } : {}),
1221
+ });
1222
+ }
1223
+ catch {
1224
+ // non-fatal
1225
+ }
1226
+ }
1227
+ },
1228
+ onError: (err) => {
1229
+ process.stderr.write(` Error: ${err.message}\n`);
1230
+ },
1231
+ });
1232
+ process.on("SIGINT", () => {
1233
+ process.stdout.write("\n Stopping watch...\n");
1234
+ stop();
1235
+ process.exit(0);
1236
+ });
1237
+ })();
1238
+ }
697
1239
  if (process.argv[2] === "init") {
698
1240
  const argv = process.argv.slice(3);
699
1241
  // Handle init --help
@@ -1383,6 +1925,23 @@ Options:
1383
1925
  }
1384
1926
  process.exit(0);
1385
1927
  }
1928
+ // Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
1929
+ if (process.argv[2] === "launchd") {
1930
+ const sub = process.argv[3];
1931
+ if (sub === "install") {
1932
+ const { runLaunchdInstall } = await import("./commands/launchd.js");
1933
+ await runLaunchdInstall(process.argv.slice(4));
1934
+ }
1935
+ else if (sub === "uninstall") {
1936
+ const { runLaunchdUninstall } = await import("./commands/launchd.js");
1937
+ await runLaunchdUninstall(process.argv.slice(4));
1938
+ }
1939
+ else {
1940
+ process.stderr.write("Usage: patchwork-os launchd install|uninstall\n");
1941
+ process.exit(1);
1942
+ }
1943
+ process.exit(0);
1944
+ }
1386
1945
  // F6: "Did you mean?" for unknown CLI subcommands
1387
1946
  // Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
1388
1947
  {
@@ -1413,6 +1972,7 @@ Options:
1413
1972
  "shim",
1414
1973
  "recipe",
1415
1974
  "dashboard",
1975
+ "launchd",
1416
1976
  ];
1417
1977
  const unknownSub = process.argv[2];
1418
1978
  if (unknownSub &&