patchwork-os 0.2.0-alpha.9 → 0.2.0-beta.0
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.bridge.md +6 -0
- package/README.md +315 -35
- package/deploy/bootstrap-new-vps.sh +12 -12
- package/deploy/bootstrap-vps.sh +187 -0
- package/deploy/deploy-dashboard.sh +174 -0
- package/deploy/deploy-landing.sh +136 -0
- package/dist/activationMetrics.d.ts +67 -0
- package/dist/activationMetrics.js +255 -0
- package/dist/activationMetrics.js.map +1 -0
- package/dist/activityLog.d.ts +49 -0
- package/dist/activityLog.js +78 -0
- package/dist/activityLog.js.map +1 -1
- package/dist/approvalHttp.d.ts +49 -2
- package/dist/approvalHttp.js +217 -21
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalInsights.d.ts +49 -0
- package/dist/approvalInsights.js +97 -0
- package/dist/approvalInsights.js.map +1 -0
- package/dist/approvalQueue.d.ts +27 -1
- package/dist/approvalQueue.js +123 -3
- package/dist/approvalQueue.js.map +1 -1
- package/dist/approvalSignals.d.ts +124 -0
- package/dist/approvalSignals.js +512 -0
- package/dist/approvalSignals.js.map +1 -0
- package/dist/automation.d.ts +57 -0
- package/dist/automation.js +156 -59
- package/dist/automation.js.map +1 -1
- package/dist/automationSuggestions.d.ts +79 -0
- package/dist/automationSuggestions.js +150 -0
- package/dist/automationSuggestions.js.map +1 -0
- package/dist/bridge.d.ts +3 -0
- package/dist/bridge.js +174 -143
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeToken.js +57 -19
- package/dist/bridgeToken.js.map +1 -1
- package/dist/ccPermissions.d.ts +15 -0
- package/dist/ccPermissions.js +21 -4
- package/dist/ccPermissions.js.map +1 -1
- package/dist/claudeDriver.js +74 -16
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +14 -8
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/launchd.d.ts +2 -0
- package/dist/commands/launchd.js +94 -0
- package/dist/commands/launchd.js.map +1 -0
- package/dist/commands/patchworkInit.d.ts +8 -0
- package/dist/commands/patchworkInit.js +77 -11
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +289 -0
- package/dist/commands/recipe.js +1359 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipeInstall.d.ts +150 -0
- package/dist/commands/recipeInstall.js +647 -0
- package/dist/commands/recipeInstall.js.map +1 -0
- package/dist/commands/tracesExport.d.ts +83 -0
- package/dist/commands/tracesExport.js +269 -0
- package/dist/commands/tracesExport.js.map +1 -0
- package/dist/commands/tracesImport.d.ts +56 -0
- package/dist/commands/tracesImport.js +161 -0
- package/dist/commands/tracesImport.js.map +1 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +108 -9
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.d.ts +43 -0
- package/dist/connectorRoutes.js +1609 -0
- package/dist/connectorRoutes.js.map +1 -0
- package/dist/connectors/asana.d.ts +198 -0
- package/dist/connectors/asana.js +679 -0
- package/dist/connectors/asana.js.map +1 -0
- package/dist/connectors/baseConnector.d.ts +153 -0
- package/dist/connectors/baseConnector.js +336 -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/datadog.d.ts +116 -0
- package/dist/connectors/datadog.js +385 -0
- package/dist/connectors/datadog.js.map +1 -0
- package/dist/connectors/discord.d.ts +150 -0
- package/dist/connectors/discord.js +543 -0
- package/dist/connectors/discord.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 +17 -18
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gitlab.d.ts +180 -0
- package/dist/connectors/gitlab.js +582 -0
- package/dist/connectors/gitlab.js.map +1 -0
- package/dist/connectors/gmail.d.ts +4 -1
- package/dist/connectors/gmail.js +149 -27
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +4 -1
- package/dist/connectors/googleCalendar.js +88 -25
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/connectors/googleDrive.d.ts +34 -0
- package/dist/connectors/googleDrive.js +321 -0
- package/dist/connectors/googleDrive.js.map +1 -0
- package/dist/connectors/htmlEscape.d.ts +5 -0
- package/dist/connectors/htmlEscape.js +13 -0
- package/dist/connectors/htmlEscape.js.map +1 -0
- package/dist/connectors/hubspot.d.ts +112 -0
- package/dist/connectors/hubspot.js +408 -0
- package/dist/connectors/hubspot.js.map +1 -0
- package/dist/connectors/intercom.d.ts +102 -0
- package/dist/connectors/intercom.js +402 -0
- package/dist/connectors/intercom.js.map +1 -0
- 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 +30 -19
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpOAuth.d.ts +3 -0
- package/dist/connectors/mcpOAuth.js +64 -10
- 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/oauthStateStore.d.ts +31 -0
- package/dist/connectors/oauthStateStore.js +52 -0
- package/dist/connectors/oauthStateStore.js.map +1 -0
- package/dist/connectors/pagerduty.d.ts +160 -0
- package/dist/connectors/pagerduty.js +464 -0
- package/dist/connectors/pagerduty.js.map +1 -0
- package/dist/connectors/sentry.js +5 -13
- package/dist/connectors/sentry.js.map +1 -1
- package/dist/connectors/slack.d.ts +16 -1
- package/dist/connectors/slack.js +155 -32
- package/dist/connectors/slack.js.map +1 -1
- package/dist/connectors/stripe.d.ts +116 -0
- package/dist/connectors/stripe.js +379 -0
- package/dist/connectors/stripe.js.map +1 -0
- package/dist/connectors/tokenStorage.d.ts +35 -0
- package/dist/connectors/tokenStorage.js +484 -0
- package/dist/connectors/tokenStorage.js.map +1 -0
- package/dist/connectors/zendesk.d.ts +104 -0
- package/dist/connectors/zendesk.js +442 -0
- package/dist/connectors/zendesk.js.map +1 -0
- package/dist/cors.d.ts +10 -0
- package/dist/cors.js +29 -0
- package/dist/cors.js.map +1 -0
- package/dist/decisionReplay.d.ts +72 -0
- package/dist/decisionReplay.js +92 -0
- package/dist/decisionReplay.js.map +1 -0
- package/dist/decisionTraceLog.d.ts +6 -0
- package/dist/decisionTraceLog.js +54 -2
- package/dist/decisionTraceLog.js.map +1 -1
- package/dist/drivers/gemini/index.d.ts +5 -1
- package/dist/drivers/gemini/index.js +39 -5
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/index.d.ts +5 -0
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/index.js.map +1 -1
- package/dist/featureFlags.d.ts +79 -0
- package/dist/featureFlags.js +208 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/fp/automationInterpreter.js +26 -21
- 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/automationState.js +4 -1
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/policyParser.js +21 -1
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/inboxRoutes.d.ts +22 -0
- package/dist/inboxRoutes.js +114 -0
- package/dist/inboxRoutes.js.map +1 -0
- package/dist/index.js +1400 -201
- package/dist/index.js.map +1 -1
- package/dist/installGuard.d.ts +25 -0
- package/dist/installGuard.js +48 -0
- package/dist/installGuard.js.map +1 -0
- package/dist/mcpRoutes.d.ts +37 -0
- package/dist/mcpRoutes.js +76 -0
- package/dist/mcpRoutes.js.map +1 -0
- package/dist/oauth.d.ts +7 -1
- package/dist/oauth.js +201 -39
- package/dist/oauth.js.map +1 -1
- package/dist/oauthRoutes.d.ts +32 -0
- package/dist/oauthRoutes.js +124 -0
- package/dist/oauthRoutes.js.map +1 -0
- package/dist/orchestrator/orchestratorBridge.js +2 -2
- package/dist/orchestrator/orchestratorBridge.js.map +1 -1
- package/dist/patchworkConfig.d.ts +16 -0
- package/dist/patchworkConfig.js +1 -1
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/pluginLoader.d.ts +28 -0
- package/dist/pluginLoader.js +77 -11
- package/dist/pluginLoader.js.map +1 -1
- package/dist/pluginWatcher.js +8 -3
- package/dist/pluginWatcher.js.map +1 -1
- package/dist/preToolUseHook.d.ts +12 -0
- package/dist/preToolUseHook.js +23 -0
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +121 -0
- package/dist/recipeOrchestration.js +955 -0
- package/dist/recipeOrchestration.js.map +1 -0
- package/dist/recipeRoutes.d.ts +180 -0
- package/dist/recipeRoutes.js +1345 -0
- package/dist/recipeRoutes.js.map +1 -0
- package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
- package/dist/recipes/RecipeOrchestrator.js +51 -0
- package/dist/recipes/RecipeOrchestrator.js.map +1 -0
- package/dist/recipes/agentExecutor.d.ts +29 -0
- package/dist/recipes/agentExecutor.js +49 -0
- package/dist/recipes/agentExecutor.js.map +1 -0
- package/dist/recipes/chainedRunner.d.ts +191 -0
- package/dist/recipes/chainedRunner.js +759 -0
- package/dist/recipes/chainedRunner.js.map +1 -0
- package/dist/recipes/compiler.js +3 -3
- package/dist/recipes/compiler.js.map +1 -1
- 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/disabledMarkers.d.ts +48 -0
- package/dist/recipes/disabledMarkers.js +52 -0
- package/dist/recipes/disabledMarkers.js.map +1 -0
- package/dist/recipes/installer.js +3 -3
- package/dist/recipes/installer.js.map +1 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
- package/dist/recipes/legacyRecipeCompat.js +131 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -0
- package/dist/recipes/manifest.d.ts +47 -0
- package/dist/recipes/manifest.js +156 -0
- package/dist/recipes/manifest.js.map +1 -0
- package/dist/recipes/migrationWarnings.d.ts +12 -0
- package/dist/recipes/migrationWarnings.js +44 -0
- package/dist/recipes/migrationWarnings.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +24 -0
- package/dist/recipes/migrations/index.js +55 -0
- package/dist/recipes/migrations/index.js.map +1 -0
- package/dist/recipes/migrations/types.d.ts +28 -0
- package/dist/recipes/migrations/types.js +2 -0
- package/dist/recipes/migrations/types.js.map +1 -0
- package/dist/recipes/migrations/v1.d.ts +11 -0
- package/dist/recipes/migrations/v1.js +18 -0
- package/dist/recipes/migrations/v1.js.map +1 -0
- package/dist/recipes/names.d.ts +40 -0
- package/dist/recipes/names.js +66 -0
- package/dist/recipes/names.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/parser.js +4 -1
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/replayRun.d.ts +62 -0
- package/dist/recipes/replayRun.js +97 -0
- package/dist/recipes/replayRun.js.map +1 -0
- package/dist/recipes/resolveRecipePath.d.ts +69 -0
- package/dist/recipes/resolveRecipePath.js +202 -0
- package/dist/recipes/resolveRecipePath.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +23 -7
- package/dist/recipes/scheduler.js +225 -45
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +17 -2
- package/dist/recipes/schemaGenerator.d.ts +28 -0
- package/dist/recipes/schemaGenerator.js +565 -0
- package/dist/recipes/schemaGenerator.js.map +1 -0
- package/dist/recipes/stepObservation.d.ts +44 -0
- package/dist/recipes/stepObservation.js +232 -0
- package/dist/recipes/stepObservation.js.map +1 -0
- package/dist/recipes/templateEngine.d.ts +62 -0
- package/dist/recipes/templateEngine.js +201 -0
- package/dist/recipes/templateEngine.js.map +1 -0
- package/dist/recipes/toolRegistry.d.ts +186 -0
- package/dist/recipes/toolRegistry.js +309 -0
- package/dist/recipes/toolRegistry.js.map +1 -0
- package/dist/recipes/tools/asana.d.ts +16 -0
- package/dist/recipes/tools/asana.js +524 -0
- package/dist/recipes/tools/asana.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/datadog.d.ts +6 -0
- package/dist/recipes/tools/datadog.js +239 -0
- package/dist/recipes/tools/datadog.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/discord.d.ts +18 -0
- package/dist/recipes/tools/discord.js +254 -0
- package/dist/recipes/tools/discord.js.map +1 -0
- package/dist/recipes/tools/file.d.ts +12 -0
- package/dist/recipes/tools/file.js +174 -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 +116 -0
- package/dist/recipes/tools/github.js.map +1 -0
- package/dist/recipes/tools/gitlab.d.ts +11 -0
- package/dist/recipes/tools/gitlab.js +285 -0
- package/dist/recipes/tools/gitlab.js.map +1 -0
- package/dist/recipes/tools/gmail.d.ts +6 -0
- package/dist/recipes/tools/gmail.js +434 -0
- package/dist/recipes/tools/gmail.js.map +1 -0
- package/dist/recipes/tools/googleDrive.d.ts +1 -0
- package/dist/recipes/tools/googleDrive.js +55 -0
- package/dist/recipes/tools/googleDrive.js.map +1 -0
- package/dist/recipes/tools/hubspot.d.ts +6 -0
- package/dist/recipes/tools/hubspot.js +232 -0
- package/dist/recipes/tools/hubspot.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +30 -0
- package/dist/recipes/tools/index.js +33 -0
- package/dist/recipes/tools/index.js.map +1 -0
- package/dist/recipes/tools/intercom.d.ts +6 -0
- package/dist/recipes/tools/intercom.js +226 -0
- package/dist/recipes/tools/intercom.js.map +1 -0
- package/dist/recipes/tools/jira.d.ts +14 -0
- package/dist/recipes/tools/jira.js +369 -0
- package/dist/recipes/tools/jira.js.map +1 -0
- package/dist/recipes/tools/linear.d.ts +7 -0
- package/dist/recipes/tools/linear.js +307 -0
- package/dist/recipes/tools/linear.js.map +1 -0
- package/dist/recipes/tools/meetingNotes.d.ts +21 -0
- package/dist/recipes/tools/meetingNotes.js +701 -0
- package/dist/recipes/tools/meetingNotes.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/pagerduty.d.ts +15 -0
- package/dist/recipes/tools/pagerduty.js +451 -0
- package/dist/recipes/tools/pagerduty.js.map +1 -0
- package/dist/recipes/tools/sentry.d.ts +12 -0
- package/dist/recipes/tools/sentry.js +73 -0
- package/dist/recipes/tools/sentry.js.map +1 -0
- package/dist/recipes/tools/slack.d.ts +6 -0
- package/dist/recipes/tools/slack.js +82 -0
- package/dist/recipes/tools/slack.js.map +1 -0
- package/dist/recipes/tools/stripe.d.ts +6 -0
- package/dist/recipes/tools/stripe.js +265 -0
- package/dist/recipes/tools/stripe.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/validation.d.ts +13 -0
- package/dist/recipes/validation.js +617 -0
- package/dist/recipes/validation.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +116 -1
- package/dist/recipes/yamlRunner.js +1000 -401
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +137 -6
- package/dist/recipesHttp.js +941 -29
- package/dist/recipesHttp.js.map +1 -1
- package/dist/riskTier.js +7 -1
- package/dist/riskTier.js.map +1 -1
- package/dist/runLog.d.ts +100 -1
- package/dist/runLog.js +258 -5
- package/dist/runLog.js.map +1 -1
- package/dist/schemas/dry-run-plan.v1.json +139 -0
- package/dist/schemas/recipe.v1.json +684 -0
- package/dist/server.d.ts +121 -8
- package/dist/server.js +538 -735
- package/dist/server.js.map +1 -1
- package/dist/ssrfGuard.d.ts +54 -0
- package/dist/ssrfGuard.js +122 -0
- package/dist/ssrfGuard.js.map +1 -0
- package/dist/streamableHttp.d.ts +39 -1
- package/dist/streamableHttp.js +128 -17
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tokenUsageTracker.d.ts +33 -0
- package/dist/tokenUsageTracker.js +146 -0
- package/dist/tokenUsageTracker.js.map +1 -0
- package/dist/tools/activityLog.d.ts +2 -0
- package/dist/tools/addLinearComment.d.ts +1 -0
- package/dist/tools/addLinearComment.js +4 -2
- package/dist/tools/addLinearComment.js.map +1 -1
- package/dist/tools/batchLsp.d.ts +3 -0
- package/dist/tools/bridgeDoctor.d.ts +1 -0
- package/dist/tools/bridgeDoctor.js +2 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/bridgeStatus.d.ts +1 -0
- package/dist/tools/cancelClaudeTask.d.ts +2 -0
- package/dist/tools/cancelClaudeTask.js +1 -0
- package/dist/tools/cancelClaudeTask.js.map +1 -1
- package/dist/tools/checkDocumentDirty.d.ts +1 -0
- package/dist/tools/clipboard.d.ts +2 -0
- package/dist/tools/closeTabs.d.ts +2 -0
- package/dist/tools/codeLens.d.ts +1 -0
- package/dist/tools/contextBundle.d.ts +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +1 -0
- package/dist/tools/createLinearIssue.d.ts +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +1 -0
- package/dist/tools/ctxQueryTraces.d.ts +1 -0
- package/dist/tools/ctxSaveTrace.d.ts +1 -0
- package/dist/tools/debug.d.ts +4 -0
- package/dist/tools/decorations.d.ts +2 -0
- package/dist/tools/documentLinks.d.ts +1 -0
- package/dist/tools/editText.d.ts +1 -0
- package/dist/tools/enrichCommit.d.ts +1 -0
- package/dist/tools/enrichStackTrace.d.ts +1 -0
- package/dist/tools/explainDiagnostic.d.ts +1 -0
- package/dist/tools/explainSymbol.d.ts +1 -0
- package/dist/tools/fetchCalendarEvents.d.ts +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +1 -0
- package/dist/tools/fetchGithubPR.d.ts +1 -0
- package/dist/tools/fetchLinearIssue.d.ts +1 -0
- package/dist/tools/fetchSentryIssue.d.ts +1 -0
- package/dist/tools/fetchSlackProfile.d.ts +1 -0
- package/dist/tools/fetchSlackProfile.js +4 -1
- package/dist/tools/fetchSlackProfile.js.map +1 -1
- package/dist/tools/fileOperations.d.ts +3 -0
- package/dist/tools/fileWatcher.d.ts +2 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findRelatedTests.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +1 -0
- package/dist/tools/foldingRanges.d.ts +1 -0
- package/dist/tools/formatDocument.d.ts +1 -0
- package/dist/tools/generateTests.d.ts +1 -0
- package/dist/tools/getAIComments.d.ts +1 -0
- package/dist/tools/getAnalyticsReport.d.ts +1 -0
- package/dist/tools/getArchitectureContext.d.ts +1 -0
- package/dist/tools/getBufferContent.d.ts +1 -0
- package/dist/tools/getChangeImpact.d.ts +1 -0
- package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
- package/dist/tools/getClaudeTaskStatus.js +1 -0
- package/dist/tools/getClaudeTaskStatus.js.map +1 -1
- package/dist/tools/getCodeCoverage.d.ts +1 -0
- package/dist/tools/getCommitsForIssue.d.ts +1 -0
- package/dist/tools/getConnectorStatus.d.ts +1 -0
- package/dist/tools/getCurrentSelection.d.ts +2 -0
- package/dist/tools/getDebugState.d.ts +1 -0
- package/dist/tools/getDependencyTree.d.ts +1 -0
- package/dist/tools/getDiagnostics.d.ts +1 -0
- package/dist/tools/getDiffFromHandoff.d.ts +1 -0
- package/dist/tools/getDocumentSymbols.d.ts +25 -0
- package/dist/tools/getDocumentSymbols.js +74 -8
- package/dist/tools/getDocumentSymbols.js.map +1 -1
- package/dist/tools/getFileTree.d.ts +1 -0
- package/dist/tools/getGitDiff.d.ts +1 -0
- package/dist/tools/getGitHotspots.d.ts +1 -0
- package/dist/tools/getGitLog.d.ts +1 -0
- package/dist/tools/getGitStatus.d.ts +1 -0
- package/dist/tools/getImportTree.d.ts +1 -0
- package/dist/tools/getImportedSignatures.d.ts +1 -0
- package/dist/tools/getOpenEditors.d.ts +1 -0
- package/dist/tools/getPRTemplate.d.ts +1 -0
- package/dist/tools/getProjectContext.d.ts +1 -0
- package/dist/tools/getProjectInfo.d.ts +1 -0
- package/dist/tools/getSecurityAdvisories.d.ts +1 -0
- package/dist/tools/getSecurityAdvisories.js +10 -1
- package/dist/tools/getSecurityAdvisories.js.map +1 -1
- package/dist/tools/getSessionUsage.d.ts +4 -0
- package/dist/tools/getSessionUsage.js +3 -0
- package/dist/tools/getSessionUsage.js.map +1 -1
- package/dist/tools/getSymbolHistory.d.ts +1 -0
- package/dist/tools/getToolCapabilities.d.ts +1 -0
- package/dist/tools/getTypeSignature.d.ts +1 -0
- package/dist/tools/getWorkspaceFolders.d.ts +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +1 -0
- package/dist/tools/gitHistory.d.ts +2 -0
- package/dist/tools/gitWrite.d.ts +11 -0
- package/dist/tools/github/actions.d.ts +2 -0
- package/dist/tools/github/actions.js +4 -2
- package/dist/tools/github/actions.js.map +1 -1
- package/dist/tools/github/composite.d.ts +342 -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.d.ts +4 -0
- package/dist/tools/github/issues.js +8 -4
- package/dist/tools/github/issues.js.map +1 -1
- package/dist/tools/github/pr.d.ts +7 -0
- package/dist/tools/github/pr.js +50 -12
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/handoffNote.d.ts +4 -0
- package/dist/tools/handoffNote.js +2 -0
- package/dist/tools/handoffNote.js.map +1 -1
- package/dist/tools/hoverAtCursor.d.ts +1 -0
- package/dist/tools/httpClient.d.ts +2 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +47 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inlayHints.d.ts +1 -0
- package/dist/tools/launchQuickTask.d.ts +2 -0
- package/dist/tools/launchQuickTask.js +1 -0
- package/dist/tools/launchQuickTask.js.map +1 -1
- package/dist/tools/listClaudeTasks.d.ts +2 -0
- package/dist/tools/listClaudeTasks.js +1 -0
- package/dist/tools/listClaudeTasks.js.map +1 -1
- package/dist/tools/listTerminals.d.ts +1 -0
- package/dist/tools/lsp.d.ts +14 -0
- package/dist/tools/navigateToSymbolByName.d.ts +1 -0
- package/dist/tools/openDiff.d.ts +1 -0
- package/dist/tools/openFile.d.ts +1 -0
- package/dist/tools/openInBrowser.d.ts +1 -0
- package/dist/tools/organizeImports.d.ts +1 -0
- package/dist/tools/performanceReport.d.ts +1 -0
- package/dist/tools/planPersistence.d.ts +5 -0
- package/dist/tools/previewEdit.d.ts +1 -0
- package/dist/tools/refactorAnalyze.d.ts +1 -0
- package/dist/tools/refactorPreview.d.ts +2 -0
- package/dist/tools/refactorPreview.js +1 -0
- package/dist/tools/refactorPreview.js.map +1 -1
- package/dist/tools/replaceBlock.d.ts +1 -0
- package/dist/tools/resumeClaudeTask.d.ts +2 -0
- package/dist/tools/resumeClaudeTask.js +1 -0
- package/dist/tools/resumeClaudeTask.js.map +1 -1
- package/dist/tools/runClaudeTask.d.ts +2 -0
- package/dist/tools/runClaudeTask.js +1 -0
- package/dist/tools/runClaudeTask.js.map +1 -1
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runTests.d.ts +1 -0
- package/dist/tools/saveDocument.d.ts +1 -0
- package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
- package/dist/tools/searchAndReplace.d.ts +1 -0
- package/dist/tools/searchTools.d.ts +1 -0
- package/dist/tools/searchTools.js +1 -1
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/searchWorkspace.d.ts +1 -0
- package/dist/tools/selectionRanges.d.ts +1 -0
- package/dist/tools/semanticTokens.d.ts +1 -0
- package/dist/tools/setActiveWorkspaceFolder.d.ts +1 -0
- package/dist/tools/signatureHelp.d.ts +1 -0
- package/dist/tools/slackListChannels.d.ts +1 -0
- package/dist/tools/slackListChannels.js.map +1 -1
- package/dist/tools/slackPostMessage.d.ts +1 -0
- package/dist/tools/slackPostMessage.js +11 -6
- package/dist/tools/slackPostMessage.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/testTraceToSource.d.ts +1 -0
- package/dist/tools/testTraceToSource.js +2 -2
- package/dist/tools/testTraceToSource.js.map +1 -1
- package/dist/tools/transaction.d.ts +23 -0
- package/dist/tools/transaction.js +29 -0
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/typeHierarchy.d.ts +1 -0
- package/dist/tools/updateLinearIssue.d.ts +1 -0
- package/dist/tools/updateLinearIssue.js +20 -6
- package/dist/tools/updateLinearIssue.js.map +1 -1
- package/dist/tools/utils.d.ts +2 -0
- package/dist/tools/utils.js.map +1 -1
- package/dist/tools/vscodeCommands.d.ts +2 -0
- package/dist/tools/vscodeTasks.d.ts +2 -0
- package/dist/tools/workspaceSettings.d.ts +1 -0
- package/dist/traceEncryption.d.ts +46 -0
- package/dist/traceEncryption.js +124 -0
- package/dist/traceEncryption.js.map +1 -0
- package/dist/transport.d.ts +46 -1
- package/dist/transport.js +173 -19
- package/dist/transport.js.map +1 -1
- package/package.json +30 -8
- package/scripts/mcp-stdio-shim.cjs +19 -3
- package/scripts/start-all.sh +30 -1
- package/templates/automation-policies/recipe-authoring.json +25 -0
- package/templates/automation-policy.example.json +6 -0
- package/templates/co.patchwork-os.bridge.plist +34 -0
- package/templates/policies/README.md +72 -0
- package/templates/policies/conservative.json +14 -0
- package/templates/policies/developer.json +14 -0
- package/templates/policies/headless-ci.json +24 -0
- package/templates/policies/personal-assistant.json +15 -0
- package/templates/policies/regulated-industry.json +18 -0
- package/templates/recipes/lint-on-save.yaml +1 -2
- package/templates/recipes/morning-brief-slack.yaml +57 -0
- package/templates/recipes/morning-brief.yaml +2 -2
- package/templates/recipes/project-health-check.yaml +50 -0
- package/templates/recipes/webhook/README.md +70 -0
- package/templates/recipes/webhook/capture-thought.yaml +26 -0
- package/templates/recipes/webhook/customer-escalation.yaml +49 -0
- package/templates/recipes/webhook/incident-intake.yaml +46 -0
- package/templates/recipes/webhook/meeting-prep.yaml +48 -0
- package/templates/recipes/webhook/morning-brief.yaml +57 -0
package/dist/index.js
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
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));
|
|
7
6
|
try {
|
|
8
7
|
const { readFileSync, existsSync } = await import("node:fs");
|
|
9
|
-
|
|
8
|
+
// Try both "../.env" (compiled dist/) and ".env" (tsx src/ dev run)
|
|
9
|
+
const candidates = [
|
|
10
|
+
_fileURLToPath(new URL("../.env", import.meta.url)),
|
|
11
|
+
_fileURLToPath(new URL(".env", import.meta.url)),
|
|
12
|
+
];
|
|
13
|
+
const envPath = candidates.find(existsSync);
|
|
14
|
+
if (envPath) {
|
|
10
15
|
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
11
16
|
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim());
|
|
12
17
|
if (m?.[1] && !process.env[m[1]]) {
|
|
@@ -35,8 +40,24 @@ import { getAnalyticsPref, setAnalyticsPref } from "./analyticsPrefs.js";
|
|
|
35
40
|
import { Bridge } from "./bridge.js";
|
|
36
41
|
import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
|
|
37
42
|
import { findEditor, parseConfig } from "./config.js";
|
|
43
|
+
import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
|
|
38
44
|
import { PACKAGE_VERSION, semverGt } from "./version.js";
|
|
39
45
|
const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
// Warn when a symlinked global install is detected (`npm install -g .`).
|
|
47
|
+
// launchctl / sandbox environments can fail through that link with EPERM.
|
|
48
|
+
// Warn only — do not crash interactive or dev flows.
|
|
49
|
+
{
|
|
50
|
+
const _symlinkInfo = detectWorkspaceSymlinkInstall();
|
|
51
|
+
if (_symlinkInfo) {
|
|
52
|
+
process.stderr.write(`\n⚠️ Detected a symlinked global ${PATCHWORK_PACKAGE_NAME} install.\n` +
|
|
53
|
+
` Logical root: ${_symlinkInfo.logicalRoot}\n` +
|
|
54
|
+
` Real path: ${_symlinkInfo.realRoot}\n\n` +
|
|
55
|
+
" LaunchAgent startup can fail with EPERM when the macOS sandbox\n" +
|
|
56
|
+
" cannot access workspace files under ~/Documents through that link.\n\n" +
|
|
57
|
+
SYMLINK_INSTALL_FIX +
|
|
58
|
+
"\n");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
40
61
|
const OPEN_VSX_PUBLISHER = "oolab-labs";
|
|
41
62
|
const OPEN_VSX_NAME = "claude-ide-bridge-extension";
|
|
42
63
|
// CLAUDE.md versioned-block patching moved to ./claudeMdPatch.ts so tests
|
|
@@ -71,6 +92,62 @@ async function downloadVsixFromOpenVsx() {
|
|
|
71
92
|
writeFileSync(tmpPath, Buffer.from(buf));
|
|
72
93
|
return tmpPath;
|
|
73
94
|
}
|
|
95
|
+
// Closes the race where bridge.start() began initialising in parallel with
|
|
96
|
+
// a subcommand's async work — observed in the 2026-04-29 dogfood pass
|
|
97
|
+
// where `recipe install` errors interleaved with bridge "Tools: full"
|
|
98
|
+
// startup logs.
|
|
99
|
+
//
|
|
100
|
+
// Every subcommand `if`-block below dispatches via an `(async () => {...})()`
|
|
101
|
+
// IIFE that ends with `process.exit`. The IIFE invocation returns
|
|
102
|
+
// synchronously, so without this gate, control immediately falls through
|
|
103
|
+
// to the bridge.start() block at end-of-file and starts initialising
|
|
104
|
+
// alongside the subcommand's async work. process.exit fires *eventually*
|
|
105
|
+
// after the await chain, but the bridge has already begun in parallel.
|
|
106
|
+
// Two IIFEs (patchwork no-args dashboard, recipe watch) lack process.exit
|
|
107
|
+
// entirely — without this gate they would run alongside the bridge
|
|
108
|
+
// indefinitely.
|
|
109
|
+
//
|
|
110
|
+
// Single source of truth for "is this argv invoking a subcommand?" — the
|
|
111
|
+
// same list is also used by the unknown-command suggester at L2570.
|
|
112
|
+
const KNOWN_SUBCOMMANDS = [
|
|
113
|
+
"init",
|
|
114
|
+
"patchwork-init",
|
|
115
|
+
"start-all",
|
|
116
|
+
"install-extension",
|
|
117
|
+
"gen-claude-md",
|
|
118
|
+
"print-token",
|
|
119
|
+
"gen-plugin-stub",
|
|
120
|
+
"notify",
|
|
121
|
+
"install",
|
|
122
|
+
"marketplace",
|
|
123
|
+
"status",
|
|
124
|
+
"shim",
|
|
125
|
+
"recipe",
|
|
126
|
+
"traces",
|
|
127
|
+
"suggest",
|
|
128
|
+
"dashboard",
|
|
129
|
+
"launchd",
|
|
130
|
+
"start",
|
|
131
|
+
];
|
|
132
|
+
const __invokedSubcommand = (() => {
|
|
133
|
+
const sub = process.argv[2];
|
|
134
|
+
if (!sub || sub.startsWith("-"))
|
|
135
|
+
return null;
|
|
136
|
+
// Treat KNOWN_SUBCOMMANDS as the dispatch source. The bare-binary
|
|
137
|
+
// dashboard launcher (no argv) is handled separately below.
|
|
138
|
+
return KNOWN_SUBCOMMANDS.includes(sub)
|
|
139
|
+
? sub
|
|
140
|
+
: null;
|
|
141
|
+
})();
|
|
142
|
+
const __invokedBareBinaryDashboard = (() => {
|
|
143
|
+
if (process.argv[2] && process.argv[2] !== "dashboard")
|
|
144
|
+
return false;
|
|
145
|
+
const binName = path.basename(process.argv[1] ?? "");
|
|
146
|
+
return (binName === "patchwork-os" ||
|
|
147
|
+
binName === "patchwork" ||
|
|
148
|
+
binName === "patchwork.js");
|
|
149
|
+
})();
|
|
150
|
+
const __subcommandWillRun = __invokedSubcommand !== null || __invokedBareBinaryDashboard;
|
|
74
151
|
// Handle --version flag — print package version and exit.
|
|
75
152
|
if (process.argv[2] === "--version" || process.argv[2] === "-v") {
|
|
76
153
|
console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
|
|
@@ -97,6 +174,49 @@ if (isStartAll) {
|
|
|
97
174
|
});
|
|
98
175
|
process.exit(result.status ?? 1);
|
|
99
176
|
}
|
|
177
|
+
// `patchwork start` — opinionated front door over start-all.
|
|
178
|
+
// Defaults to full mode (all tools registered) and the web dashboard, so the
|
|
179
|
+
// doc-promised "patchwork start → everything works" path actually works.
|
|
180
|
+
// Pass-through args still go to start-all.sh; --help short-circuits.
|
|
181
|
+
if (process.argv[2] === "start") {
|
|
182
|
+
const passthrough = process.argv.slice(3);
|
|
183
|
+
if (passthrough.includes("--help") || passthrough.includes("-h")) {
|
|
184
|
+
process.stdout.write(`patchwork start — Launch the full Patchwork stack
|
|
185
|
+
|
|
186
|
+
Starts bridge + Claude Code + dashboard in a tmux session.
|
|
187
|
+
Defaults to full mode so all bridge tools are registered.
|
|
188
|
+
|
|
189
|
+
Usage: patchwork start [options]
|
|
190
|
+
|
|
191
|
+
Options:
|
|
192
|
+
--workspace <path> Directory to open (default: current directory)
|
|
193
|
+
--no-dashboard Skip the web dashboard
|
|
194
|
+
--dashboard-port <N> Dashboard port (default: 3200)
|
|
195
|
+
--notify <topic> Push notifications via ntfy.sh
|
|
196
|
+
--vps <user@host> SSH reverse tunnel for stable claude.ai URL
|
|
197
|
+
--slim Slim mode (27 IDE-exclusive tools only — overrides default)
|
|
198
|
+
--help, -h Show this help
|
|
199
|
+
|
|
200
|
+
This is a thin wrapper over \`start-all\`. For advanced flags see:
|
|
201
|
+
patchwork start-all --help
|
|
202
|
+
`);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
// Default to --full unless caller opted into slim explicitly.
|
|
206
|
+
const args = [...passthrough];
|
|
207
|
+
const slimIdx = args.indexOf("--slim");
|
|
208
|
+
if (slimIdx >= 0) {
|
|
209
|
+
args.splice(slimIdx, 1); // strip — start-all.sh has no --slim flag, slim is its default
|
|
210
|
+
}
|
|
211
|
+
else if (!args.includes("--full")) {
|
|
212
|
+
args.push("--full");
|
|
213
|
+
}
|
|
214
|
+
const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.sh");
|
|
215
|
+
const result = spawnSync("bash", [scriptPath, ...args], {
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
});
|
|
218
|
+
process.exit(result.status ?? 1);
|
|
219
|
+
}
|
|
100
220
|
function writeRulesFileAtomic(rulesFilePath, content) {
|
|
101
221
|
const tmpPath = `${rulesFilePath}.tmp`;
|
|
102
222
|
writeFileSync(tmpPath, content, { encoding: "utf-8", flag: "wx" });
|
|
@@ -468,14 +588,20 @@ Options:
|
|
|
468
588
|
// Handle gen-plugin-stub subcommand — scaffolds a new plugin directory
|
|
469
589
|
if (process.argv[2] === "gen-plugin-stub") {
|
|
470
590
|
const argv = process.argv.slice(3);
|
|
471
|
-
// Parse args: gen-plugin-stub <dir> [--name <name>] [--prefix <prefix>]
|
|
591
|
+
// Parse args: gen-plugin-stub <dir> [--name <name>] [--prefix <prefix>] [--ts]
|
|
472
592
|
const dirArg = argv.find((a) => !a.startsWith("--"));
|
|
473
593
|
if (!dirArg) {
|
|
474
|
-
process.stderr.write("Usage: claude-ide-bridge gen-plugin-stub <output-dir> [--name <org/plugin-name>] [--prefix <toolPrefix>]\n");
|
|
594
|
+
process.stderr.write("Usage: claude-ide-bridge gen-plugin-stub <output-dir> [--name <org/plugin-name>] [--prefix <toolPrefix>] [--ts]\n");
|
|
475
595
|
process.exit(1);
|
|
476
596
|
}
|
|
477
597
|
const nameIdx = argv.indexOf("--name");
|
|
478
598
|
const prefixIdx = argv.indexOf("--prefix");
|
|
599
|
+
// --ts emits a TypeScript variant (src/index.ts + tsconfig.json + build
|
|
600
|
+
// scripts) alongside a compiled-output manifest pointing at index.mjs.
|
|
601
|
+
// Plugin authors get type-checked tools without changing the hot-reload
|
|
602
|
+
// contract — `npm run dev` watches src/, emits index.mjs, bridge picks
|
|
603
|
+
// up the rebuilt artifact via --plugin-watch.
|
|
604
|
+
const useTypeScript = argv.includes("--ts");
|
|
479
605
|
const pluginName = nameIdx !== -1 && argv[nameIdx + 1]
|
|
480
606
|
? argv[nameIdx + 1]
|
|
481
607
|
: "my-org/my-plugin";
|
|
@@ -509,8 +635,8 @@ if (process.argv[2] === "gen-plugin-stub") {
|
|
|
509
635
|
minBridgeVersion: "2.1.24",
|
|
510
636
|
};
|
|
511
637
|
writeFileSync(path.join(outDir, "claude-ide-bridge-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
512
|
-
//
|
|
513
|
-
const
|
|
638
|
+
// ── shared tool body — same logic, different surface syntax ──
|
|
639
|
+
const jsEntrypoint = `/**
|
|
514
640
|
* ${pluginName} — Claude IDE Bridge plugin
|
|
515
641
|
*
|
|
516
642
|
* Each tool must have a name starting with "${toolPrefix}".
|
|
@@ -518,7 +644,7 @@ if (process.argv[2] === "gen-plugin-stub") {
|
|
|
518
644
|
* ctx.config (commandTimeout, maxResultSize), and ctx.logger.
|
|
519
645
|
*/
|
|
520
646
|
|
|
521
|
-
/** @param {import('
|
|
647
|
+
/** @param {import('patchwork-os/plugin').PluginContext} ctx */
|
|
522
648
|
export function register(ctx) {
|
|
523
649
|
ctx.logger.info(${JSON.stringify(`${pluginName} loaded`)}, { workspace: ctx.workspace });
|
|
524
650
|
|
|
@@ -546,70 +672,381 @@ export function register(ctx) {
|
|
|
546
672
|
};
|
|
547
673
|
}
|
|
548
674
|
`;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
675
|
+
const tsEntrypoint = `/**
|
|
676
|
+
* ${pluginName} — Claude IDE Bridge plugin
|
|
677
|
+
*
|
|
678
|
+
* Each tool must have a name starting with "${toolPrefix}".
|
|
679
|
+
* The \`ctx\` object provides: ctx.workspace, ctx.workspaceFolders,
|
|
680
|
+
* ctx.config (commandTimeout, maxResultSize), and ctx.logger.
|
|
681
|
+
*/
|
|
682
|
+
import type { PluginContext } from "patchwork-os/plugin";
|
|
683
|
+
|
|
684
|
+
interface HelloArgs {
|
|
685
|
+
name: string;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function register(ctx: PluginContext) {
|
|
689
|
+
ctx.logger.info(${JSON.stringify(`${pluginName} loaded`)}, { workspace: ctx.workspace });
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
tools: [
|
|
693
|
+
{
|
|
694
|
+
schema: {
|
|
695
|
+
name: ${JSON.stringify(`${toolPrefix}Hello`)},
|
|
696
|
+
description: "Example tool — returns a greeting",
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: "object" as const,
|
|
699
|
+
required: ["name"] as const,
|
|
700
|
+
additionalProperties: false as const,
|
|
701
|
+
properties: {
|
|
702
|
+
name: { type: "string" as const, description: "Name to greet" },
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
annotations: { readOnlyHint: true },
|
|
706
|
+
},
|
|
707
|
+
handler: async (args: HelloArgs) => ({
|
|
708
|
+
content: [
|
|
709
|
+
{
|
|
710
|
+
type: "text" as const,
|
|
711
|
+
text: \`Hello from ${pluginName}, \${args.name}!\`,
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
}),
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
`;
|
|
720
|
+
// Write entrypoint — TS goes under src/, JS at root.
|
|
721
|
+
if (useTypeScript) {
|
|
722
|
+
mkdirSync(path.join(outDir, "src"), { recursive: true });
|
|
723
|
+
writeFileSync(path.join(outDir, "src", "index.ts"), tsEntrypoint, "utf-8");
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
writeFileSync(path.join(outDir, "index.mjs"), jsEntrypoint, "utf-8");
|
|
727
|
+
}
|
|
728
|
+
// tsconfig.json — TS variant only. Emits a single ESM file at index.mjs
|
|
729
|
+
// so the plugin manifest's entrypoint stays the same shape as the JS
|
|
730
|
+
// scaffold and --plugin-watch reload semantics don't change.
|
|
731
|
+
if (useTypeScript) {
|
|
732
|
+
const tsconfig = {
|
|
733
|
+
compilerOptions: {
|
|
734
|
+
target: "ES2022",
|
|
735
|
+
module: "ES2022",
|
|
736
|
+
moduleResolution: "Bundler",
|
|
737
|
+
outDir: ".",
|
|
738
|
+
rootDir: "src",
|
|
739
|
+
declaration: false,
|
|
740
|
+
strict: true,
|
|
741
|
+
esModuleInterop: true,
|
|
742
|
+
skipLibCheck: true,
|
|
743
|
+
resolveJsonModule: true,
|
|
744
|
+
// Emit .mjs so the plugin loader (which expects ESM) picks it up
|
|
745
|
+
// without relying on package.json "type": "module" alone.
|
|
746
|
+
// tsc doesn't emit .mjs natively, so package.json's "build" script
|
|
747
|
+
// does a rename pass — see below.
|
|
748
|
+
},
|
|
749
|
+
include: ["src/**/*"],
|
|
750
|
+
exclude: ["node_modules"],
|
|
751
|
+
};
|
|
752
|
+
writeFileSync(path.join(outDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`, "utf-8");
|
|
753
|
+
}
|
|
754
|
+
// package.json — TS variant adds build + dev (watch) scripts.
|
|
755
|
+
const pkgBase = {
|
|
552
756
|
name: pluginName.replace(/^@[^/]+\//, "").replace(/\//g, "-"),
|
|
553
757
|
version: "0.1.0",
|
|
554
758
|
description: "A Claude IDE Bridge plugin",
|
|
555
759
|
type: "module",
|
|
556
760
|
main: "index.mjs",
|
|
557
|
-
keywords: ["
|
|
558
|
-
peerDependencies: { "
|
|
761
|
+
keywords: ["patchwork-os", "claude-ide-bridge-plugin"],
|
|
762
|
+
peerDependencies: { "patchwork-os": ">=0.2.0-alpha.0" },
|
|
559
763
|
};
|
|
764
|
+
const pkg = useTypeScript
|
|
765
|
+
? {
|
|
766
|
+
...pkgBase,
|
|
767
|
+
scripts: {
|
|
768
|
+
// tsc emits index.js — rename to index.mjs so the loader treats
|
|
769
|
+
// it as ESM regardless of the consumer's package.json.
|
|
770
|
+
build: "tsc && mv index.js index.mjs",
|
|
771
|
+
dev: "tsc --watch",
|
|
772
|
+
clean: "rm -f index.mjs",
|
|
773
|
+
},
|
|
774
|
+
devDependencies: {
|
|
775
|
+
typescript: "^5.4.0",
|
|
776
|
+
"patchwork-os": ">=0.2.0-alpha.0",
|
|
777
|
+
},
|
|
778
|
+
}
|
|
779
|
+
: pkgBase;
|
|
560
780
|
writeFileSync(path.join(outDir, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
781
|
+
// README.md — included in both variants. Spells out the hot-reload
|
|
782
|
+
// contract so plugin authors don't have to read the platform docs to
|
|
783
|
+
// get started.
|
|
784
|
+
const readmeBody = useTypeScript
|
|
785
|
+
? `# ${pluginName}
|
|
786
|
+
|
|
787
|
+
A [Claude IDE Bridge](https://github.com/Oolab-labs/patchwork-os) plugin (TypeScript).
|
|
788
|
+
|
|
789
|
+
## Quick start
|
|
790
|
+
|
|
791
|
+
\`\`\`sh
|
|
792
|
+
npm install
|
|
793
|
+
npm run dev # in one terminal — watches src/, emits index.mjs
|
|
794
|
+
|
|
795
|
+
# In another terminal:
|
|
796
|
+
claude-ide-bridge --plugin . --plugin-watch
|
|
797
|
+
\`\`\`
|
|
798
|
+
|
|
799
|
+
Edit \`src/index.ts\`. \`tsc --watch\` rebuilds, the bridge hot-reloads, your tool is callable from the live Claude session on the next turn.
|
|
800
|
+
|
|
801
|
+
## Build for distribution
|
|
802
|
+
|
|
803
|
+
\`\`\`sh
|
|
804
|
+
npm run build # emits index.mjs
|
|
805
|
+
npm publish # publish to npm (optional)
|
|
806
|
+
\`\`\`
|
|
807
|
+
|
|
808
|
+
When published with the \`claude-ide-bridge-plugin\` keyword, users can install with:
|
|
809
|
+
|
|
810
|
+
\`\`\`sh
|
|
811
|
+
claude-ide-bridge --plugin ${pluginName.replace(/^@[^/]+\//, "")}
|
|
812
|
+
\`\`\`
|
|
813
|
+
|
|
814
|
+
## Tool naming
|
|
815
|
+
|
|
816
|
+
Every tool exposed by this plugin **must** have a \`name\` starting with \`${toolPrefix}\`. The bridge enforces this at load time (\`/^[a-zA-Z][a-zA-Z0-9_]{1,19}$/\`).
|
|
817
|
+
|
|
818
|
+
## Plugin context
|
|
819
|
+
|
|
820
|
+
The \`ctx\` argument to \`register()\` provides:
|
|
821
|
+
|
|
822
|
+
- \`ctx.workspace\` — workspace root path
|
|
823
|
+
- \`ctx.workspaceFolders\` — array of workspace folders
|
|
824
|
+
- \`ctx.config\` — \`{ commandTimeout, maxResultSize }\`
|
|
825
|
+
- \`ctx.logger\` — \`info\` / \`warn\` / \`error\` logging that respects bridge log level
|
|
826
|
+
|
|
827
|
+
## Live toolsmithing
|
|
828
|
+
|
|
829
|
+
The whole point of plugins is that you can author tools *while Claude is using the bridge*. Edit \`src/index.ts\`, save, the watcher rebuilds, the bridge reloads — Claude's next turn sees the new tool.
|
|
830
|
+
|
|
831
|
+
See [documents/live-toolsmithing.md](https://github.com/Oolab-labs/patchwork-os/blob/main/documents/live-toolsmithing.md) for the full narrative.
|
|
832
|
+
`
|
|
833
|
+
: `# ${pluginName}
|
|
834
|
+
|
|
835
|
+
A [Claude IDE Bridge](https://github.com/Oolab-labs/patchwork-os) plugin.
|
|
836
|
+
|
|
837
|
+
## Quick start
|
|
838
|
+
|
|
839
|
+
\`\`\`sh
|
|
840
|
+
claude-ide-bridge --plugin . --plugin-watch
|
|
841
|
+
\`\`\`
|
|
842
|
+
|
|
843
|
+
Edit \`index.mjs\`. The bridge hot-reloads on save — your tool is callable from the live Claude session on the next turn. No build step needed for the JS variant.
|
|
844
|
+
|
|
845
|
+
## Tool naming
|
|
846
|
+
|
|
847
|
+
Every tool exposed by this plugin **must** have a \`name\` starting with \`${toolPrefix}\`. The bridge enforces this at load time (\`/^[a-zA-Z][a-zA-Z0-9_]{1,19}$/\`).
|
|
848
|
+
|
|
849
|
+
## Plugin context
|
|
850
|
+
|
|
851
|
+
The \`ctx\` argument to \`register()\` provides:
|
|
852
|
+
|
|
853
|
+
- \`ctx.workspace\` — workspace root path
|
|
854
|
+
- \`ctx.workspaceFolders\` — array of workspace folders
|
|
855
|
+
- \`ctx.config\` — \`{ commandTimeout, maxResultSize }\`
|
|
856
|
+
- \`ctx.logger\` — \`info\` / \`warn\` / \`error\` logging that respects bridge log level
|
|
857
|
+
|
|
858
|
+
## Want types?
|
|
859
|
+
|
|
860
|
+
Re-scaffold with \`claude-ide-bridge gen-plugin-stub <dir> --ts\` for a TypeScript variant with \`tsc --watch\` build pipeline.
|
|
861
|
+
|
|
862
|
+
## Live toolsmithing
|
|
863
|
+
|
|
864
|
+
Edit, save, hot-reload — Claude's next turn sees the new tool. See [documents/live-toolsmithing.md](https://github.com/Oolab-labs/patchwork-os/blob/main/documents/live-toolsmithing.md) for the full narrative.
|
|
865
|
+
`;
|
|
866
|
+
writeFileSync(path.join(outDir, "README.md"), readmeBody, "utf-8");
|
|
561
867
|
// .gitignore
|
|
562
|
-
|
|
563
|
-
|
|
868
|
+
const gitignore = useTypeScript
|
|
869
|
+
? "node_modules\nindex.mjs\nindex.js\n*.tsbuildinfo\n"
|
|
870
|
+
: "node_modules\n";
|
|
871
|
+
writeFileSync(path.join(outDir, ".gitignore"), gitignore, "utf-8");
|
|
872
|
+
process.stderr.write(`✓ Plugin stub created at ${outDir} (${useTypeScript ? "TypeScript" : "JavaScript"})\n`);
|
|
564
873
|
process.stderr.write("\nNext steps:\n");
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
874
|
+
if (useTypeScript) {
|
|
875
|
+
process.stderr.write(` 1. cd ${outDir} && npm install\n`);
|
|
876
|
+
process.stderr.write(` 2. Edit ${path.join(outDir, "src", "index.ts")} to implement your tools\n`);
|
|
877
|
+
process.stderr.write(` 3. npm run dev (in one terminal)\n`);
|
|
878
|
+
process.stderr.write(` 4. claude-ide-bridge --plugin ${outDir} --plugin-watch (in another)\n`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
process.stderr.write(` 1. Edit ${path.join(outDir, "index.mjs")} to implement your tools\n`);
|
|
882
|
+
process.stderr.write(` 2. Run the bridge with: claude-ide-bridge --plugin ${outDir} --plugin-watch\n`);
|
|
883
|
+
process.stderr.write(` 3. Or add to your config: { "plugins": ["${outDir}"] }\n`);
|
|
884
|
+
}
|
|
568
885
|
process.exit(0);
|
|
569
886
|
}
|
|
570
887
|
// Patchwork: `patchwork recipe list` — enumerate installed recipes.
|
|
571
888
|
if (process.argv[2] === "recipe" && process.argv[3] === "list") {
|
|
572
889
|
(async () => {
|
|
573
|
-
const {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
890
|
+
const { listInstalledRecipes, printInstalledList } = await import("./commands/recipeInstall.js");
|
|
891
|
+
const entries = listInstalledRecipes();
|
|
892
|
+
printInstalledList(entries);
|
|
893
|
+
process.exit(0);
|
|
894
|
+
})();
|
|
895
|
+
}
|
|
896
|
+
// Patchwork: `patchwork recipe enable <name>` / `recipe disable <name>` —
|
|
897
|
+
// flip the disabled marker so scheduled triggers (cron/file-watch) take
|
|
898
|
+
// effect (or stop). Manual `recipe run` is unaffected.
|
|
899
|
+
if (process.argv[2] === "recipe" &&
|
|
900
|
+
(process.argv[3] === "enable" || process.argv[3] === "disable")) {
|
|
901
|
+
const subcommand = process.argv[3];
|
|
902
|
+
const name = process.argv[4];
|
|
903
|
+
if (!name) {
|
|
904
|
+
process.stderr.write(`Usage: patchwork recipe ${subcommand} <name>\n` +
|
|
905
|
+
` See \`patchwork recipe list\` for installed recipe names.\n`);
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
(async () => {
|
|
909
|
+
try {
|
|
910
|
+
const { runRecipeEnable, runRecipeDisable } = await import("./commands/recipeInstall.js");
|
|
911
|
+
if (subcommand === "enable") {
|
|
912
|
+
const r = runRecipeEnable(name);
|
|
913
|
+
process.stdout.write(r.alreadyEnabled
|
|
914
|
+
? ` ℹ ${r.name} is already enabled\n`
|
|
915
|
+
: ` ✓ enabled ${r.name}\n`);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
const r = runRecipeDisable(name);
|
|
919
|
+
process.stdout.write(r.alreadyDisabled
|
|
920
|
+
? ` ℹ ${r.name} is already disabled\n`
|
|
921
|
+
: ` ✓ disabled ${r.name}\n`);
|
|
922
|
+
}
|
|
923
|
+
process.exit(0);
|
|
578
924
|
}
|
|
579
|
-
|
|
580
|
-
process.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
925
|
+
catch (err) {
|
|
926
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
930
|
+
}
|
|
931
|
+
// Patchwork: `patchwork recipe uninstall <name>` — remove an installed recipe
|
|
932
|
+
// directory and all its files. Sister to `recipe install`. Idempotent on
|
|
933
|
+
// success (subsequent uninstalls error with "no installed recipe").
|
|
934
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
|
|
935
|
+
const name = process.argv[4];
|
|
936
|
+
if (!name) {
|
|
937
|
+
process.stderr.write("Usage: patchwork recipe uninstall <name>\n" +
|
|
938
|
+
" See `patchwork recipe list` for installed recipe names.\n");
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
(async () => {
|
|
942
|
+
try {
|
|
943
|
+
const { runRecipeUninstall } = await import("./commands/recipeInstall.js");
|
|
944
|
+
const r = runRecipeUninstall(name);
|
|
945
|
+
if (!r.ok) {
|
|
946
|
+
process.stderr.write(`Error: ${r.error}\n`);
|
|
947
|
+
process.exit(1);
|
|
584
948
|
}
|
|
585
|
-
process.stdout.write(
|
|
949
|
+
process.stdout.write(` ✓ Uninstalled ${name} (${r.installDir})\n`);
|
|
950
|
+
process.exit(0);
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
954
|
+
process.exit(1);
|
|
586
955
|
}
|
|
587
|
-
process.exit(0);
|
|
588
956
|
})();
|
|
589
957
|
}
|
|
590
958
|
// Patchwork: `patchwork recipe run <name>` — runs a recipe locally or via
|
|
591
959
|
// a running bridge's /recipes/run endpoint if one is available.
|
|
592
960
|
if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
593
961
|
const args = process.argv.slice(4);
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
962
|
+
const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
|
|
963
|
+
let localFlag = false;
|
|
964
|
+
let dryRun = false;
|
|
965
|
+
let recipeRef;
|
|
966
|
+
let step;
|
|
967
|
+
const vars = {};
|
|
968
|
+
for (let i = 0; i < args.length; i++) {
|
|
969
|
+
const arg = args[i];
|
|
970
|
+
if (arg === undefined) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const currentArg = arg;
|
|
974
|
+
if (currentArg === "--local") {
|
|
975
|
+
localFlag = true;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (currentArg === "--dry-run") {
|
|
979
|
+
dryRun = true;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (currentArg === "--step" || currentArg.startsWith("--step=")) {
|
|
983
|
+
const value = currentArg === "--step"
|
|
984
|
+
? args[++i]
|
|
985
|
+
: currentArg.slice("--step=".length);
|
|
986
|
+
if (!value) {
|
|
987
|
+
process.stderr.write(`Error: --step requires a value\n${usage}`);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
step = value;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (currentArg === "--var" || currentArg.startsWith("--var=")) {
|
|
994
|
+
const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
|
|
995
|
+
if (!assignment) {
|
|
996
|
+
process.stderr.write(`Error: --var requires KEY=VALUE\n${usage}`);
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
const eqIndex = assignment.indexOf("=");
|
|
1000
|
+
if (eqIndex <= 0) {
|
|
1001
|
+
process.stderr.write(`Error: invalid --var assignment "${assignment}" (expected KEY=VALUE)\n${usage}`);
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
const key = assignment.slice(0, eqIndex);
|
|
1005
|
+
const value = assignment.slice(eqIndex + 1);
|
|
1006
|
+
vars[key] = value;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (currentArg.startsWith("--")) {
|
|
1010
|
+
process.stderr.write(`Error: unknown option ${currentArg}\n${usage}`);
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
}
|
|
1013
|
+
if (!recipeRef) {
|
|
1014
|
+
recipeRef = currentArg;
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
process.stderr.write(`Error: unexpected argument ${currentArg}\n${usage}`);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
if (!recipeRef) {
|
|
1021
|
+
process.stderr.write(usage);
|
|
598
1022
|
process.exit(1);
|
|
599
1023
|
}
|
|
1024
|
+
const recipeArg = recipeRef;
|
|
600
1025
|
(async () => {
|
|
601
1026
|
try {
|
|
602
|
-
|
|
1027
|
+
const seedVars = Object.keys(vars).length > 0 ? vars : undefined;
|
|
1028
|
+
const explicitFile = (() => {
|
|
1029
|
+
try {
|
|
1030
|
+
const resolved = path.resolve(recipeArg);
|
|
1031
|
+
return existsSync(resolved) && statSync(resolved).isFile();
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
})();
|
|
603
1037
|
const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
|
|
604
1038
|
const lock = localFlag ? null : findBridgeLock();
|
|
605
|
-
if (lock) {
|
|
1039
|
+
if (lock && !dryRun && !step && !explicitFile) {
|
|
606
1040
|
const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
|
|
607
1041
|
method: "POST",
|
|
608
1042
|
headers: {
|
|
609
1043
|
Authorization: `Bearer ${lock.authToken}`,
|
|
610
1044
|
"Content-Type": "application/json",
|
|
611
1045
|
},
|
|
612
|
-
body: JSON.stringify({
|
|
1046
|
+
body: JSON.stringify({
|
|
1047
|
+
name: recipeArg,
|
|
1048
|
+
...(seedVars ? { vars: seedVars } : {}),
|
|
1049
|
+
}),
|
|
613
1050
|
});
|
|
614
1051
|
const body = (await res.json());
|
|
615
1052
|
if (!body.ok) {
|
|
@@ -622,43 +1059,64 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
622
1059
|
// else: fall through to local runner below
|
|
623
1060
|
}
|
|
624
1061
|
else {
|
|
625
|
-
process.stdout.write(` ✓ enqueued recipe "${
|
|
1062
|
+
process.stdout.write(` ✓ enqueued recipe "${recipeArg}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
|
|
626
1063
|
" Watch progress on the dashboard Tasks page or via listClaudeTasks.\n");
|
|
627
1064
|
process.exit(0);
|
|
628
1065
|
return;
|
|
629
1066
|
}
|
|
630
1067
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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);
|
|
1068
|
+
const { runRecipe, runRecipeDryPlan, summarizeRecipeExecution, formatRunReport, extractRunLogStepResults, } = await import("./commands/recipe.js");
|
|
1069
|
+
if (dryRun) {
|
|
1070
|
+
const plan = await runRecipeDryPlan(recipeArg, {
|
|
1071
|
+
...(step ? { step } : {}),
|
|
1072
|
+
...(seedVars ? { vars: seedVars } : {}),
|
|
1073
|
+
});
|
|
1074
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
1075
|
+
process.exit(0);
|
|
653
1076
|
return;
|
|
654
1077
|
}
|
|
655
|
-
process.stdout.write(
|
|
656
|
-
|
|
1078
|
+
process.stdout.write(step
|
|
1079
|
+
? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
|
|
1080
|
+
: ` Running recipe "${recipeArg}" locally…\n`);
|
|
657
1081
|
const workdir = lock?.workspace || process.cwd();
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1082
|
+
const run = await runRecipe(recipeArg, {
|
|
1083
|
+
...(step ? { step } : {}),
|
|
1084
|
+
...(seedVars ? { vars: seedVars } : {}),
|
|
1085
|
+
workdir,
|
|
1086
|
+
});
|
|
1087
|
+
if (run.stepSelection) {
|
|
1088
|
+
process.stdout.write(` Selected step via ${run.stepSelection.matchedBy}: ${run.stepSelection.matchedValue}\n`);
|
|
1089
|
+
}
|
|
1090
|
+
const summary = summarizeRecipeExecution(run.result);
|
|
1091
|
+
process.stdout.write(`${formatRunReport(run.result, run.recipe.name)}\n`);
|
|
1092
|
+
if (summary.errorMessage) {
|
|
1093
|
+
process.stderr.write(` Error: ${summary.errorMessage}\n`);
|
|
1094
|
+
}
|
|
1095
|
+
// Append to run log so CLI runs appear in ctxQueryTraces + dashboard /runs
|
|
1096
|
+
try {
|
|
1097
|
+
const { RecipeRunLog } = await import("./runLog.js");
|
|
1098
|
+
const runLog = new RecipeRunLog({
|
|
1099
|
+
dir: path.join(os.homedir(), ".patchwork"),
|
|
1100
|
+
});
|
|
1101
|
+
const startedAt = Date.now();
|
|
1102
|
+
const stepResultsForLog = extractRunLogStepResults(run.result);
|
|
1103
|
+
runLog.appendDirect({
|
|
1104
|
+
taskId: `cli-${Date.now()}`,
|
|
1105
|
+
recipeName: run.recipe.name,
|
|
1106
|
+
trigger: "recipe",
|
|
1107
|
+
status: summary.ok ? "done" : "error",
|
|
1108
|
+
createdAt: startedAt,
|
|
1109
|
+
startedAt,
|
|
1110
|
+
doneAt: Date.now(),
|
|
1111
|
+
durationMs: 0,
|
|
1112
|
+
...(summary.errorMessage
|
|
1113
|
+
? { errorMessage: summary.errorMessage }
|
|
1114
|
+
: {}),
|
|
1115
|
+
...(stepResultsForLog ? { stepResults: stepResultsForLog } : {}),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
// Non-fatal — run log write failure must not abort the CLI
|
|
662
1120
|
}
|
|
663
1121
|
process.exit(0);
|
|
664
1122
|
}
|
|
@@ -669,24 +1127,596 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
669
1127
|
})();
|
|
670
1128
|
}
|
|
671
1129
|
// Handle init subcommand — one-command setup: install extension + write CLAUDE.md + print next steps
|
|
672
|
-
// Patchwork: `patchwork recipe install <
|
|
1130
|
+
// Patchwork: `patchwork recipe install <source>` subcommand.
|
|
1131
|
+
// Supports: github:owner/repo, github:owner/repo/subdir, https://github.com/owner/repo,
|
|
1132
|
+
// ./local/path, or legacy <file.json> (single-recipe install).
|
|
673
1133
|
if (process.argv[2] === "recipe" && process.argv[3] === "install") {
|
|
1134
|
+
const source = process.argv[4];
|
|
1135
|
+
if (!source) {
|
|
1136
|
+
process.stderr.write("Usage: patchwork recipe install <source>\n" +
|
|
1137
|
+
" <source> can be:\n" +
|
|
1138
|
+
" github:owner/repo\n" +
|
|
1139
|
+
" github:owner/repo/subdir\n" +
|
|
1140
|
+
" https://github.com/owner/repo\n" +
|
|
1141
|
+
" ./local/path\n");
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
(async () => {
|
|
1145
|
+
try {
|
|
1146
|
+
// Legacy path: bare .json file argument → single-file installer
|
|
1147
|
+
if (source.endsWith(".json") &&
|
|
1148
|
+
!source.startsWith("github:") &&
|
|
1149
|
+
!source.startsWith("http")) {
|
|
1150
|
+
const { installRecipeFromFile } = await import("./recipes/installer.js");
|
|
1151
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1152
|
+
const result = installRecipeFromFile(path.resolve(source), {
|
|
1153
|
+
recipesDir,
|
|
1154
|
+
});
|
|
1155
|
+
// alpha.36+ — sidecar `<name>.permissions.json` is no longer written
|
|
1156
|
+
// (was decorative, never read by toolRegistry). Print the suggested
|
|
1157
|
+
// permissions snippet inline so users can hand-merge into settings.
|
|
1158
|
+
process.stdout.write(` ✓ ${result.action} ${result.installedPath}\n` +
|
|
1159
|
+
` ℹ Patchwork does not enforce per-recipe permissions; configure tool gating in ~/.claude/settings.json.\n` +
|
|
1160
|
+
` Suggested permissions snippet:\n${result.permissionsJson
|
|
1161
|
+
.split("\n")
|
|
1162
|
+
.map((l) => ` ${l}`)
|
|
1163
|
+
.join("\n")}\n`);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
// Marketplace install: github:, https://, ./local/
|
|
1167
|
+
const { runRecipeInstall, printInstallResult } = await import("./commands/recipeInstall.js");
|
|
1168
|
+
const result = await runRecipeInstall(source);
|
|
1169
|
+
printInstallResult(result);
|
|
1170
|
+
}
|
|
1171
|
+
process.exit(0);
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
})();
|
|
1178
|
+
}
|
|
1179
|
+
// Patchwork: `patchwork suggest [--since <date>]` — pattern-mine the
|
|
1180
|
+
// activity log + run history for "you've been doing X by hand; want to
|
|
1181
|
+
// make a recipe?" hints. See documents/strategic/2026-05-02/memory-
|
|
1182
|
+
// ecosystem-report.md §6 for the catalog this implements.
|
|
1183
|
+
//
|
|
1184
|
+
// Three suggestion kinds: co-occurring tool pairs (worth a recipe), tools
|
|
1185
|
+
// installed but unused (worth reviewing or pruning), and recipes that
|
|
1186
|
+
// always succeed (worth trust-graduating). Read-only — does not change
|
|
1187
|
+
// any policy or registry state.
|
|
1188
|
+
if (process.argv[2] === "suggest") {
|
|
1189
|
+
(async () => {
|
|
1190
|
+
try {
|
|
1191
|
+
const args = process.argv.slice(3);
|
|
1192
|
+
let sinceDays;
|
|
1193
|
+
for (let i = 0; i < args.length; i++) {
|
|
1194
|
+
const a = args[i];
|
|
1195
|
+
if (a === "--since-days") {
|
|
1196
|
+
const next = args[i + 1];
|
|
1197
|
+
if (next)
|
|
1198
|
+
sinceDays = Number.parseInt(next, 10);
|
|
1199
|
+
i++;
|
|
1200
|
+
}
|
|
1201
|
+
else if (a === "--help" || a === "-h") {
|
|
1202
|
+
process.stdout.write("patchwork suggest [--since-days <N>]\n\n" +
|
|
1203
|
+
"Pattern-mine the activity log + recipe runs for automation hints:\n" +
|
|
1204
|
+
" - Co-occurring tool pairs that don't yet appear in any recipe\n" +
|
|
1205
|
+
" - Installed tools that haven't been called recently\n" +
|
|
1206
|
+
" - Recipes that have succeeded ≥ 10 times in a row (trust-graduation candidates)\n\n" +
|
|
1207
|
+
"Default lookback window is 7 days. --since-days overrides.\n\n" +
|
|
1208
|
+
"Read-only — does not modify policy, registry, or run history.\n");
|
|
1209
|
+
process.exit(0);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const { ActivityLog } = await import("./activityLog.js");
|
|
1213
|
+
const { RecipeRunLog } = await import("./runLog.js");
|
|
1214
|
+
const { computeAutomationSuggestions } = await import("./automationSuggestions.js");
|
|
1215
|
+
// Side-effect import — populates the tool registry that
|
|
1216
|
+
// computeAutomationSuggestions consults for installed-tool inventory.
|
|
1217
|
+
await import("./recipes/tools/index.js");
|
|
1218
|
+
// Wire up the bridge's standard log paths. The CLI reads from
|
|
1219
|
+
// disk; it doesn't need a running bridge.
|
|
1220
|
+
const patchworkDir = path.join(os.homedir(), ".patchwork");
|
|
1221
|
+
const activityLog = new ActivityLog();
|
|
1222
|
+
// Find the most recent activity log file (any port). For the
|
|
1223
|
+
// suggest CLI we union all of them.
|
|
1224
|
+
const claudeIdeDir = path.join(os.homedir(), ".claude", "ide");
|
|
1225
|
+
try {
|
|
1226
|
+
const entries = await import("node:fs").then((m) => m.readdirSync(claudeIdeDir));
|
|
1227
|
+
for (const name of entries) {
|
|
1228
|
+
if (/^activity(-\d+)?\.jsonl$/i.test(name)) {
|
|
1229
|
+
activityLog.setPersistPath(path.join(claudeIdeDir, name));
|
|
1230
|
+
break; // setPersistPath loads on call; first existing wins
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
// No activity dir / files — proceed with an empty log; the
|
|
1236
|
+
// suggestions just return fewer / no items.
|
|
1237
|
+
}
|
|
1238
|
+
const recipeRunLog = new RecipeRunLog({ dir: patchworkDir });
|
|
1239
|
+
const opts = {
|
|
1240
|
+
activityLog,
|
|
1241
|
+
recipeRunLog,
|
|
1242
|
+
};
|
|
1243
|
+
if (sinceDays !== undefined && Number.isFinite(sinceDays)) {
|
|
1244
|
+
opts.activitySinceMs = sinceDays * 24 * 60 * 60 * 1000;
|
|
1245
|
+
}
|
|
1246
|
+
const suggestions = computeAutomationSuggestions(opts);
|
|
1247
|
+
if (suggestions.length === 0) {
|
|
1248
|
+
process.stdout.write("No automation suggestions yet. Patchwork mines patterns from the activity log\n" +
|
|
1249
|
+
"and recipe run history; come back after a few days of use.\n");
|
|
1250
|
+
process.exit(0);
|
|
1251
|
+
}
|
|
1252
|
+
process.stdout.write(`${suggestions.length} suggestion${suggestions.length === 1 ? "" : "s"}:\n\n`);
|
|
1253
|
+
for (const s of suggestions) {
|
|
1254
|
+
const icon = s.kind === "co_occurring_pair"
|
|
1255
|
+
? "→"
|
|
1256
|
+
: s.kind === "installed_but_unused"
|
|
1257
|
+
? "·"
|
|
1258
|
+
: "★";
|
|
1259
|
+
process.stdout.write(` ${icon} ${s.label}\n`);
|
|
1260
|
+
}
|
|
1261
|
+
process.stdout.write("\nRead-only output. Nothing changed.\n");
|
|
1262
|
+
process.exit(0);
|
|
1263
|
+
}
|
|
1264
|
+
catch (err) {
|
|
1265
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
}
|
|
1268
|
+
})();
|
|
1269
|
+
}
|
|
1270
|
+
// Patchwork: `patchwork recipe schema [outputDir]` — write generated recipe schemas to disk.
|
|
1271
|
+
// Patchwork: `patchwork traces export [--output <path>]` — bundle the four
|
|
1272
|
+
// local trace logs into a single .jsonl.gz so a user can move machines,
|
|
1273
|
+
// take a compliance snapshot, or share traces with another tool. See
|
|
1274
|
+
// docs/strategic/2026-05-02/memory-ecosystem-report.md items 1, 3, 12 for
|
|
1275
|
+
// the durability rationale this PR addresses.
|
|
1276
|
+
if (process.argv[2] === "traces" && process.argv[3] === "export") {
|
|
1277
|
+
(async () => {
|
|
1278
|
+
try {
|
|
1279
|
+
const args = process.argv.slice(4);
|
|
1280
|
+
let output;
|
|
1281
|
+
let patchworkDir;
|
|
1282
|
+
let activityDir;
|
|
1283
|
+
for (let i = 0; i < args.length; i++) {
|
|
1284
|
+
const a = args[i];
|
|
1285
|
+
if (a === "--output" || a === "-o") {
|
|
1286
|
+
output = args[i + 1];
|
|
1287
|
+
i++;
|
|
1288
|
+
}
|
|
1289
|
+
else if (a === "--patchwork-dir") {
|
|
1290
|
+
patchworkDir = args[i + 1];
|
|
1291
|
+
i++;
|
|
1292
|
+
}
|
|
1293
|
+
else if (a === "--activity-dir") {
|
|
1294
|
+
activityDir = args[i + 1];
|
|
1295
|
+
i++;
|
|
1296
|
+
}
|
|
1297
|
+
else if (a === "--help" || a === "-h") {
|
|
1298
|
+
process.stdout.write("patchwork traces export [--output <path>] [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
|
|
1299
|
+
"Bundles ~/.patchwork/{runs,decision_traces,commit_issue_links}.jsonl\n" +
|
|
1300
|
+
"and ~/.claude/ide/activity-*.jsonl into a single gzipped JSONL file.\n\n" +
|
|
1301
|
+
"Output is a manifest line followed by one envelope per row:\n" +
|
|
1302
|
+
' {"type":"manifest", ...}\n' +
|
|
1303
|
+
' {"source":"runs", "entry":{...}}\n' +
|
|
1304
|
+
" ...\n\n" +
|
|
1305
|
+
"Filter one source with:\n" +
|
|
1306
|
+
" gunzip -c traces-export-*.jsonl.gz | jq 'select(.source==\"decision_traces\") | .entry'\n");
|
|
1307
|
+
process.exit(0);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const { runTracesExport } = await import("./commands/tracesExport.js");
|
|
1311
|
+
const result = await runTracesExport({
|
|
1312
|
+
...(output !== undefined && { output }),
|
|
1313
|
+
...(patchworkDir !== undefined && { patchworkDir }),
|
|
1314
|
+
...(activityDir !== undefined && { activityDir }),
|
|
1315
|
+
});
|
|
1316
|
+
process.stdout.write(` ✓ Wrote ${result.outputPath}\n`);
|
|
1317
|
+
process.stdout.write(` ${result.totalCount} rows from ${result.files.length} file${result.files.length === 1 ? "" : "s"} (${result.totalBytes} bytes read)\n`);
|
|
1318
|
+
for (const f of result.files) {
|
|
1319
|
+
process.stdout.write(` - ${f.source}: ${f.count} rows (${f.path})\n`);
|
|
1320
|
+
}
|
|
1321
|
+
process.exit(0);
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1325
|
+
process.exit(1);
|
|
1326
|
+
}
|
|
1327
|
+
})();
|
|
1328
|
+
}
|
|
1329
|
+
// Patchwork: `patchwork traces import <bundle>` — restore an export bundle
|
|
1330
|
+
// into the local patchwork dirs. Closes the half-shipped backup loop.
|
|
1331
|
+
if (process.argv[2] === "traces" && process.argv[3] === "import") {
|
|
1332
|
+
(async () => {
|
|
1333
|
+
try {
|
|
1334
|
+
const args = process.argv.slice(4);
|
|
1335
|
+
let input;
|
|
1336
|
+
let patchworkDir;
|
|
1337
|
+
let activityDir;
|
|
1338
|
+
let mode = "append";
|
|
1339
|
+
let dryRun = false;
|
|
1340
|
+
let passphrase;
|
|
1341
|
+
for (let i = 0; i < args.length; i++) {
|
|
1342
|
+
const a = args[i];
|
|
1343
|
+
if (a === "--patchwork-dir") {
|
|
1344
|
+
patchworkDir = args[i + 1];
|
|
1345
|
+
i++;
|
|
1346
|
+
}
|
|
1347
|
+
else if (a === "--activity-dir") {
|
|
1348
|
+
activityDir = args[i + 1];
|
|
1349
|
+
i++;
|
|
1350
|
+
}
|
|
1351
|
+
else if (a === "--mode") {
|
|
1352
|
+
const m = args[i + 1];
|
|
1353
|
+
if (m !== "append" && m !== "overwrite") {
|
|
1354
|
+
process.stderr.write(`Error: --mode must be "append" or "overwrite" (got: ${m})\n`);
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
mode = m;
|
|
1358
|
+
i++;
|
|
1359
|
+
}
|
|
1360
|
+
else if (a === "--passphrase") {
|
|
1361
|
+
passphrase = args[i + 1];
|
|
1362
|
+
i++;
|
|
1363
|
+
}
|
|
1364
|
+
else if (a === "--dry-run") {
|
|
1365
|
+
dryRun = true;
|
|
1366
|
+
}
|
|
1367
|
+
else if (a === "--help" || a === "-h") {
|
|
1368
|
+
process.stdout.write("patchwork traces import <bundle> [--mode append|overwrite] [--dry-run]\n" +
|
|
1369
|
+
" [--passphrase <phrase>]\n" +
|
|
1370
|
+
" [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
|
|
1371
|
+
"Restore a bundle written by `patchwork traces export` into the local\n" +
|
|
1372
|
+
"patchwork dirs (~/.patchwork/ and ~/.claude/ide/ by default).\n\n" +
|
|
1373
|
+
"Formats:\n" +
|
|
1374
|
+
" .jsonl.gz Plain gzip bundle — no passphrase required.\n" +
|
|
1375
|
+
" .enc AES-256-GCM encrypted bundle — pass --passphrase.\n\n" +
|
|
1376
|
+
"Modes:\n" +
|
|
1377
|
+
" append (default) Append rows to existing files.\n" +
|
|
1378
|
+
" overwrite Truncate target files before writing. Use for fresh-machine\n" +
|
|
1379
|
+
" restore; never use when there's local data you want to keep.\n");
|
|
1380
|
+
process.exit(0);
|
|
1381
|
+
}
|
|
1382
|
+
else if (a !== undefined &&
|
|
1383
|
+
!a.startsWith("--") &&
|
|
1384
|
+
input === undefined) {
|
|
1385
|
+
input = a;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (!input) {
|
|
1389
|
+
process.stderr.write("Usage: patchwork traces import <bundle> [--passphrase <phrase>] [--mode append|overwrite] [--dry-run]\n");
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
// Auto-detect encrypted bundle and decrypt before import.
|
|
1393
|
+
if (passphrase !== undefined || input.endsWith(".enc")) {
|
|
1394
|
+
const { readFileSync } = await import("node:fs");
|
|
1395
|
+
const { isEncryptedTraceBundle, decryptTraceBundle } = await import("./traceEncryption.js");
|
|
1396
|
+
const raw = readFileSync(input);
|
|
1397
|
+
if (isEncryptedTraceBundle(raw)) {
|
|
1398
|
+
if (!passphrase) {
|
|
1399
|
+
process.stderr.write("Error: bundle is encrypted — provide --passphrase <phrase>\n");
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
const plain = decryptTraceBundle(raw, passphrase);
|
|
1403
|
+
const { tmpdir } = await import("node:os");
|
|
1404
|
+
const { writeFileSync } = await import("node:fs");
|
|
1405
|
+
const tmp = `${tmpdir()}/patchwork-import-${Date.now()}.jsonl.gz`;
|
|
1406
|
+
writeFileSync(tmp, plain, { mode: 0o600 });
|
|
1407
|
+
input = tmp;
|
|
1408
|
+
process.stderr.write("Decryption succeeded.\n");
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const { runTracesImport } = await import("./commands/tracesImport.js");
|
|
1412
|
+
const result = await runTracesImport({
|
|
1413
|
+
input,
|
|
1414
|
+
...(patchworkDir !== undefined && { patchworkDir }),
|
|
1415
|
+
...(activityDir !== undefined && { activityDir }),
|
|
1416
|
+
mode,
|
|
1417
|
+
dryRun,
|
|
1418
|
+
});
|
|
1419
|
+
const verb = result.dryRun
|
|
1420
|
+
? "Would restore"
|
|
1421
|
+
: result.mode === "overwrite"
|
|
1422
|
+
? "Restored (overwrite)"
|
|
1423
|
+
: "Restored (append)";
|
|
1424
|
+
process.stdout.write(` ${result.dryRun ? "•" : "✓"} ${verb} ${result.totalCount} rows from ${result.inputPath}\n`);
|
|
1425
|
+
process.stdout.write(` Bundle exportedAt: ${result.exportedAt}\n`);
|
|
1426
|
+
for (const f of result.files) {
|
|
1427
|
+
process.stdout.write(` - ${f.source}: ${f.count} rows → ${f.targetPath}\n`);
|
|
1428
|
+
}
|
|
1429
|
+
process.exit(0);
|
|
1430
|
+
}
|
|
1431
|
+
catch (err) {
|
|
1432
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
})();
|
|
1436
|
+
}
|
|
1437
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
|
|
1438
|
+
const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
|
|
1439
|
+
(async () => {
|
|
1440
|
+
try {
|
|
1441
|
+
const { runSchema } = await import("./commands/recipe.js");
|
|
1442
|
+
const result = await runSchema(path.resolve(outputDir));
|
|
1443
|
+
process.stdout.write(` ✓ Wrote schemas to ${result.outputDir}\n`);
|
|
1444
|
+
for (const file of result.filesWritten) {
|
|
1445
|
+
process.stdout.write(` ${file}\n`);
|
|
1446
|
+
}
|
|
1447
|
+
process.exit(0);
|
|
1448
|
+
}
|
|
1449
|
+
catch (err) {
|
|
1450
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1451
|
+
process.exit(1);
|
|
1452
|
+
}
|
|
1453
|
+
})();
|
|
1454
|
+
}
|
|
1455
|
+
// Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
|
|
1456
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "new") {
|
|
1457
|
+
const args = process.argv.slice(4);
|
|
1458
|
+
const recipeName = args[0];
|
|
1459
|
+
if (!recipeName) {
|
|
1460
|
+
process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
|
|
1461
|
+
" --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
|
|
1462
|
+
" Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
|
|
1463
|
+
" write into the current directory instead.\n");
|
|
1464
|
+
process.stderr.write("\nTemplates:\n");
|
|
1465
|
+
(async () => {
|
|
1466
|
+
const { listTemplates } = await import("./commands/recipe.js");
|
|
1467
|
+
for (const t of listTemplates()) {
|
|
1468
|
+
process.stderr.write(` ${t}\n`);
|
|
1469
|
+
}
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
})();
|
|
1472
|
+
}
|
|
1473
|
+
else {
|
|
1474
|
+
(async () => {
|
|
1475
|
+
try {
|
|
1476
|
+
const { runNew } = await import("./commands/recipe.js");
|
|
1477
|
+
const templateIdx = args.indexOf("--template");
|
|
1478
|
+
const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
|
|
1479
|
+
const descIdx = args.indexOf("--desc");
|
|
1480
|
+
const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
|
|
1481
|
+
`Recipe: ${recipeName}`;
|
|
1482
|
+
const outIdx = args.indexOf("--out");
|
|
1483
|
+
const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
|
|
1484
|
+
// `--out .` is the common case for "scaffold in cwd" — resolve so
|
|
1485
|
+
// the success message shows the absolute path the user can open.
|
|
1486
|
+
const outputDir = outRaw ? path.resolve(outRaw) : undefined;
|
|
1487
|
+
const result = runNew({
|
|
1488
|
+
name: recipeName,
|
|
1489
|
+
description,
|
|
1490
|
+
...(template ? { template } : {}),
|
|
1491
|
+
...(outputDir ? { outputDir } : {}),
|
|
1492
|
+
});
|
|
1493
|
+
process.stdout.write(` ✓ Created ${result.path}\n`);
|
|
1494
|
+
process.exit(0);
|
|
1495
|
+
}
|
|
1496
|
+
catch (err) {
|
|
1497
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1498
|
+
process.exit(1);
|
|
1499
|
+
}
|
|
1500
|
+
})();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
// Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
|
|
1504
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
|
|
674
1505
|
const file = process.argv[4];
|
|
675
1506
|
if (!file) {
|
|
676
|
-
process.stderr.write("Usage: patchwork recipe
|
|
1507
|
+
process.stderr.write("Usage: patchwork recipe lint <file.yaml>\n");
|
|
677
1508
|
process.exit(1);
|
|
678
1509
|
}
|
|
679
1510
|
(async () => {
|
|
680
1511
|
try {
|
|
681
|
-
const {
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
|
|
1512
|
+
const { runLint } = await import("./commands/recipe.js");
|
|
1513
|
+
const result = runLint(path.resolve(file));
|
|
1514
|
+
for (const issue of result.issues) {
|
|
1515
|
+
const prefix = issue.level === "error" ? "✗" : "⚠";
|
|
1516
|
+
process.stderr.write(` ${prefix} ${issue.message}\n`);
|
|
1517
|
+
}
|
|
1518
|
+
if (result.valid) {
|
|
1519
|
+
process.stdout.write(` ✓ Valid recipe (${result.warnings} warnings)\n`);
|
|
1520
|
+
process.exit(0);
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1529
|
+
process.exit(1);
|
|
1530
|
+
}
|
|
1531
|
+
})();
|
|
1532
|
+
}
|
|
1533
|
+
// Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
|
|
1534
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
|
|
1535
|
+
const args = process.argv.slice(4);
|
|
1536
|
+
const usage = "Usage: patchwork recipe preflight <file.yaml> [--json] [--watch] [--require-fixtures] [--no-require-write-ack] [--allow-write <tool-or-ns>]\n";
|
|
1537
|
+
let json = false;
|
|
1538
|
+
let watchMode = false;
|
|
1539
|
+
let requireFixtures = false;
|
|
1540
|
+
let requireWriteAck = true;
|
|
1541
|
+
const allowWrites = [];
|
|
1542
|
+
let file;
|
|
1543
|
+
for (let i = 0; i < args.length; i++) {
|
|
1544
|
+
const arg = args[i];
|
|
1545
|
+
if (arg === undefined)
|
|
1546
|
+
continue;
|
|
1547
|
+
if (arg === "--json") {
|
|
1548
|
+
json = true;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (arg === "--watch") {
|
|
1552
|
+
watchMode = true;
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
if (arg === "--require-fixtures") {
|
|
1556
|
+
requireFixtures = true;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (arg === "--no-require-write-ack") {
|
|
1560
|
+
requireWriteAck = false;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (arg === "--allow-write" || arg.startsWith("--allow-write=")) {
|
|
1564
|
+
const value = arg === "--allow-write"
|
|
1565
|
+
? args[++i]
|
|
1566
|
+
: arg.slice("--allow-write=".length);
|
|
1567
|
+
if (!value) {
|
|
1568
|
+
process.stderr.write(`Error: --allow-write requires a value\n${usage}`);
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
}
|
|
1571
|
+
allowWrites.push(value);
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
if (!arg.startsWith("--")) {
|
|
1575
|
+
file = arg;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (!file) {
|
|
1579
|
+
process.stderr.write(usage);
|
|
1580
|
+
process.exit(1);
|
|
1581
|
+
}
|
|
1582
|
+
const renderResult = (result) => {
|
|
1583
|
+
if (json) {
|
|
1584
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
for (const issue of result.issues) {
|
|
1588
|
+
const prefix = issue.level === "error" ? "✗" : "⚠";
|
|
1589
|
+
const where = issue.stepId ? ` [${issue.stepId}]` : "";
|
|
1590
|
+
process.stderr.write(` ${prefix} ${issue.code}${where}: ${issue.message}\n`);
|
|
1591
|
+
}
|
|
1592
|
+
if (result.ok) {
|
|
1593
|
+
process.stdout.write(` ✓ Preflight passed for ${result.recipe} (${result.plan.steps.length} steps)\n`);
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
const errorCount = result.issues.filter((i) => i.level === "error").length;
|
|
1597
|
+
process.stdout.write(`\n ${errorCount} error(s) — preflight failed\n`);
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
(async () => {
|
|
1601
|
+
try {
|
|
1602
|
+
const { runPreflight, runPreflightWatch } = await import("./commands/recipe.js");
|
|
1603
|
+
const resolvedPath = path.resolve(file);
|
|
1604
|
+
if (watchMode) {
|
|
1605
|
+
process.stdout.write(` Watching ${resolvedPath} — preflight on save…\n`);
|
|
1606
|
+
const stop = runPreflightWatch({
|
|
1607
|
+
recipePath: resolvedPath,
|
|
1608
|
+
requireWriteAck,
|
|
1609
|
+
requireFixtures,
|
|
1610
|
+
allowWrites,
|
|
1611
|
+
onResult: (result) => renderResult(result),
|
|
1612
|
+
onError: (err) => {
|
|
1613
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
1614
|
+
},
|
|
1615
|
+
});
|
|
1616
|
+
process.on("SIGINT", () => {
|
|
1617
|
+
stop();
|
|
1618
|
+
process.exit(0);
|
|
1619
|
+
});
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const result = await runPreflight(resolvedPath, {
|
|
1623
|
+
requireWriteAck,
|
|
1624
|
+
requireFixtures,
|
|
1625
|
+
allowWrites,
|
|
685
1626
|
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1627
|
+
renderResult(result);
|
|
1628
|
+
process.exit(result.ok ? 0 : 1);
|
|
1629
|
+
}
|
|
1630
|
+
catch (err) {
|
|
1631
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
})();
|
|
1635
|
+
}
|
|
1636
|
+
// Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
|
|
1637
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
|
|
1638
|
+
const args = process.argv.slice(4);
|
|
1639
|
+
const check = args.includes("--check");
|
|
1640
|
+
const watchMode = args.includes("--watch");
|
|
1641
|
+
const file = args.find((arg) => !arg.startsWith("--"));
|
|
1642
|
+
if (!file) {
|
|
1643
|
+
process.stderr.write("Usage: patchwork recipe fmt <file.yaml> [--check] [--watch]\n");
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
const renderResult = (result, filePath) => {
|
|
1647
|
+
if (check) {
|
|
1648
|
+
process.stdout.write(result.changed
|
|
1649
|
+
? " ✗ File would be reformatted\n"
|
|
1650
|
+
: " ✓ File is already formatted\n");
|
|
1651
|
+
}
|
|
1652
|
+
else {
|
|
1653
|
+
process.stdout.write(result.changed
|
|
1654
|
+
? ` ✓ Formatted ${filePath}\n`
|
|
1655
|
+
: ` ✓ Already formatted ${filePath}\n`);
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
(async () => {
|
|
1659
|
+
try {
|
|
1660
|
+
const { runFmt, runFmtWatch } = await import("./commands/recipe.js");
|
|
1661
|
+
const resolvedPath = path.resolve(file);
|
|
1662
|
+
if (watchMode) {
|
|
1663
|
+
process.stdout.write(` Watching ${resolvedPath} — fmt on save…\n`);
|
|
1664
|
+
const stop = runFmtWatch({
|
|
1665
|
+
recipePath: resolvedPath,
|
|
1666
|
+
check,
|
|
1667
|
+
onResult: (result) => {
|
|
1668
|
+
process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
|
|
1669
|
+
renderResult(result, resolvedPath);
|
|
1670
|
+
},
|
|
1671
|
+
onError: (err) => {
|
|
1672
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
1673
|
+
},
|
|
1674
|
+
});
|
|
1675
|
+
process.on("SIGINT", () => {
|
|
1676
|
+
stop();
|
|
1677
|
+
process.exit(0);
|
|
1678
|
+
});
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const result = runFmt(resolvedPath, { check });
|
|
1682
|
+
renderResult(result, file);
|
|
1683
|
+
process.exit(check && result.changed ? 1 : 0);
|
|
1684
|
+
}
|
|
1685
|
+
catch (err) {
|
|
1686
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
}
|
|
1689
|
+
})();
|
|
1690
|
+
}
|
|
1691
|
+
// Patchwork: `patchwork recipe record <file.yaml>` — execute live and record connector fixtures.
|
|
1692
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "record") {
|
|
1693
|
+
const args = process.argv.slice(4);
|
|
1694
|
+
const file = args.find((arg) => !arg.startsWith("--"));
|
|
1695
|
+
const fixturesIdx = args.indexOf("--fixtures");
|
|
1696
|
+
const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
|
|
1697
|
+
if (!file) {
|
|
1698
|
+
process.stderr.write("Usage: patchwork recipe record <file.yaml> [--fixtures <dir>]\n");
|
|
1699
|
+
process.exit(1);
|
|
1700
|
+
}
|
|
1701
|
+
(async () => {
|
|
1702
|
+
try {
|
|
1703
|
+
const { runRecord } = await import("./commands/recipe.js");
|
|
1704
|
+
const result = await runRecord(path.resolve(file), {
|
|
1705
|
+
...(fixturesDir ? { fixturesDir: path.resolve(fixturesDir) } : {}),
|
|
1706
|
+
});
|
|
1707
|
+
for (const issue of result.issues) {
|
|
1708
|
+
const prefix = issue.level === "error" ? "✗" : "⚠";
|
|
1709
|
+
process.stderr.write(` ${prefix} ${issue.message}\n`);
|
|
1710
|
+
}
|
|
1711
|
+
if (result.recordedFixtures.length > 0) {
|
|
1712
|
+
process.stdout.write(` ℹ Recorded fixture libraries: ${result.recordedFixtures.join(", ")}\n`);
|
|
1713
|
+
}
|
|
1714
|
+
if (result.valid) {
|
|
1715
|
+
process.stdout.write(" ✓ Recipe fixtures recorded\n");
|
|
1716
|
+
process.exit(0);
|
|
1717
|
+
}
|
|
1718
|
+
process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
|
|
1719
|
+
process.exit(1);
|
|
690
1720
|
}
|
|
691
1721
|
catch (err) {
|
|
692
1722
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -694,6 +1724,153 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
|
|
|
694
1724
|
}
|
|
695
1725
|
})();
|
|
696
1726
|
}
|
|
1727
|
+
// Patchwork: `patchwork recipe test <file.yaml>` — validate fixture coverage for mocked execution.
|
|
1728
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "test") {
|
|
1729
|
+
const args = process.argv.slice(4);
|
|
1730
|
+
const file = args.find((arg) => !arg.startsWith("--"));
|
|
1731
|
+
const fixturesIdx = args.indexOf("--fixtures");
|
|
1732
|
+
const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
|
|
1733
|
+
const watchMode = args.includes("--watch");
|
|
1734
|
+
if (!file) {
|
|
1735
|
+
process.stderr.write("Usage: patchwork recipe test <file.yaml> [--fixtures <dir>] [--watch]\n");
|
|
1736
|
+
process.exit(1);
|
|
1737
|
+
}
|
|
1738
|
+
const renderResult = (result) => {
|
|
1739
|
+
for (const issue of result.issues) {
|
|
1740
|
+
const prefix = issue.level === "error" ? "✗" : "⚠";
|
|
1741
|
+
process.stderr.write(` ${prefix} ${issue.message}\n`);
|
|
1742
|
+
}
|
|
1743
|
+
if (result.requiredFixtures.length > 0) {
|
|
1744
|
+
process.stdout.write(` ℹ Required fixtures: ${result.requiredFixtures.join(", ")}\n`);
|
|
1745
|
+
}
|
|
1746
|
+
if (result.valid) {
|
|
1747
|
+
process.stdout.write(" ✓ Test passed\n");
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
(async () => {
|
|
1754
|
+
try {
|
|
1755
|
+
const { runTest, runTestWatch } = await import("./commands/recipe.js");
|
|
1756
|
+
const resolvedPath = path.resolve(file);
|
|
1757
|
+
const resolvedFixtures = fixturesDir
|
|
1758
|
+
? path.resolve(fixturesDir)
|
|
1759
|
+
: undefined;
|
|
1760
|
+
if (watchMode) {
|
|
1761
|
+
process.stdout.write(` Watching ${resolvedPath} — test on save…\n`);
|
|
1762
|
+
const stop = runTestWatch({
|
|
1763
|
+
recipePath: resolvedPath,
|
|
1764
|
+
...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
|
|
1765
|
+
onResult: (result) => {
|
|
1766
|
+
process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
|
|
1767
|
+
renderResult(result);
|
|
1768
|
+
},
|
|
1769
|
+
onError: (err) => {
|
|
1770
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
process.on("SIGINT", () => {
|
|
1774
|
+
stop();
|
|
1775
|
+
process.exit(0);
|
|
1776
|
+
});
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const result = await runTest(resolvedPath, {
|
|
1780
|
+
...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
|
|
1781
|
+
});
|
|
1782
|
+
renderResult(result);
|
|
1783
|
+
process.exit(result.valid ? 0 : 1);
|
|
1784
|
+
}
|
|
1785
|
+
catch (err) {
|
|
1786
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
}
|
|
1789
|
+
})();
|
|
1790
|
+
}
|
|
1791
|
+
// Patchwork: `patchwork recipe watch <file.yaml>` — watch for changes and validate.
|
|
1792
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "watch") {
|
|
1793
|
+
const file = process.argv[4];
|
|
1794
|
+
if (!file) {
|
|
1795
|
+
process.stderr.write("Usage: patchwork recipe watch <file.yaml>\n");
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
(async () => {
|
|
1799
|
+
const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
|
|
1800
|
+
const { runWatch, runLint, runWatchedRecipe, formatRunReport, summarizeRecipeExecution, extractRunLogStepResults, } = await import("./commands/recipe.js");
|
|
1801
|
+
const filePath = path.resolve(file);
|
|
1802
|
+
const lock = findBridgeLock();
|
|
1803
|
+
const workdir = lock?.workspace || process.cwd();
|
|
1804
|
+
const initial = runLint(filePath);
|
|
1805
|
+
if (!initial.valid) {
|
|
1806
|
+
process.stderr.write(" ✗ Recipe has errors - fix before watching\n");
|
|
1807
|
+
for (const issue of initial.issues) {
|
|
1808
|
+
process.stderr.write(` ${issue.level}: ${issue.message}\n`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
process.stdout.write(` ✓ Watching ${file} for changes...\n`);
|
|
1813
|
+
}
|
|
1814
|
+
const stop = runWatch({
|
|
1815
|
+
recipePath: filePath,
|
|
1816
|
+
onChange: async () => {
|
|
1817
|
+
process.stdout.write(`\n Change detected, running...\n`);
|
|
1818
|
+
const watched = await runWatchedRecipe(filePath, { workdir });
|
|
1819
|
+
if (!watched.lint.valid) {
|
|
1820
|
+
process.stderr.write(` ✗ Invalid (${watched.lint.errors} errors)\n`);
|
|
1821
|
+
for (const issue of watched.lint.issues) {
|
|
1822
|
+
process.stderr.write(` ${issue.level}: ${issue.message}\n`);
|
|
1823
|
+
}
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
if (watched.run?.stepSelection) {
|
|
1827
|
+
process.stdout.write(` Selected step via ${watched.run.stepSelection.matchedBy}: ${watched.run.stepSelection.matchedValue}\n`);
|
|
1828
|
+
}
|
|
1829
|
+
if (watched.run) {
|
|
1830
|
+
process.stdout.write(`${formatRunReport(watched.run.result, watched.run.recipe.name)}\n`);
|
|
1831
|
+
const summary = summarizeRecipeExecution(watched.run.result);
|
|
1832
|
+
if (summary.errorMessage) {
|
|
1833
|
+
process.stderr.write(` Error: ${summary.errorMessage}\n`);
|
|
1834
|
+
}
|
|
1835
|
+
// Append to run log
|
|
1836
|
+
try {
|
|
1837
|
+
const { RecipeRunLog } = await import("./runLog.js");
|
|
1838
|
+
const runLog = new RecipeRunLog({
|
|
1839
|
+
dir: path.join(os.homedir(), ".patchwork"),
|
|
1840
|
+
});
|
|
1841
|
+
const now = Date.now();
|
|
1842
|
+
const stepResultsForLog = extractRunLogStepResults(watched.run.result);
|
|
1843
|
+
runLog.appendDirect({
|
|
1844
|
+
taskId: `watch-${now}`,
|
|
1845
|
+
recipeName: watched.run.recipe.name,
|
|
1846
|
+
trigger: "recipe",
|
|
1847
|
+
status: summary.ok ? "done" : "error",
|
|
1848
|
+
createdAt: now,
|
|
1849
|
+
startedAt: now,
|
|
1850
|
+
doneAt: now,
|
|
1851
|
+
durationMs: 0,
|
|
1852
|
+
...(summary.errorMessage
|
|
1853
|
+
? { errorMessage: summary.errorMessage }
|
|
1854
|
+
: {}),
|
|
1855
|
+
...(stepResultsForLog ? { stepResults: stepResultsForLog } : {}),
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
catch {
|
|
1859
|
+
// non-fatal
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
},
|
|
1863
|
+
onError: (err) => {
|
|
1864
|
+
process.stderr.write(` Error: ${err.message}\n`);
|
|
1865
|
+
},
|
|
1866
|
+
});
|
|
1867
|
+
process.on("SIGINT", () => {
|
|
1868
|
+
process.stdout.write("\n Stopping watch...\n");
|
|
1869
|
+
stop();
|
|
1870
|
+
process.exit(0);
|
|
1871
|
+
});
|
|
1872
|
+
})();
|
|
1873
|
+
}
|
|
697
1874
|
if (process.argv[2] === "init") {
|
|
698
1875
|
const argv = process.argv.slice(3);
|
|
699
1876
|
// Handle init --help
|
|
@@ -1383,6 +2560,23 @@ Options:
|
|
|
1383
2560
|
}
|
|
1384
2561
|
process.exit(0);
|
|
1385
2562
|
}
|
|
2563
|
+
// Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
|
|
2564
|
+
if (process.argv[2] === "launchd") {
|
|
2565
|
+
const sub = process.argv[3];
|
|
2566
|
+
if (sub === "install") {
|
|
2567
|
+
const { runLaunchdInstall } = await import("./commands/launchd.js");
|
|
2568
|
+
await runLaunchdInstall(process.argv.slice(4));
|
|
2569
|
+
}
|
|
2570
|
+
else if (sub === "uninstall") {
|
|
2571
|
+
const { runLaunchdUninstall } = await import("./commands/launchd.js");
|
|
2572
|
+
await runLaunchdUninstall(process.argv.slice(4));
|
|
2573
|
+
}
|
|
2574
|
+
else {
|
|
2575
|
+
process.stderr.write("Usage: patchwork-os launchd install|uninstall\n");
|
|
2576
|
+
process.exit(1);
|
|
2577
|
+
}
|
|
2578
|
+
process.exit(0);
|
|
2579
|
+
}
|
|
1386
2580
|
// F6: "Did you mean?" for unknown CLI subcommands
|
|
1387
2581
|
// Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
|
|
1388
2582
|
{
|
|
@@ -1390,7 +2584,7 @@ Options:
|
|
|
1390
2584
|
const isPatchworkBin = binName === "patchwork-os" ||
|
|
1391
2585
|
binName === "patchwork" ||
|
|
1392
2586
|
binName === "patchwork.js";
|
|
1393
|
-
if (isPatchworkBin && !process.argv[2]) {
|
|
2587
|
+
if (isPatchworkBin && (!process.argv[2] || process.argv[2] === "dashboard")) {
|
|
1394
2588
|
(async () => {
|
|
1395
2589
|
const { runDashboard } = await import("./commands/dashboard.js");
|
|
1396
2590
|
await runDashboard();
|
|
@@ -1398,26 +2592,12 @@ Options:
|
|
|
1398
2592
|
}
|
|
1399
2593
|
}
|
|
1400
2594
|
{
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
"patchwork-init",
|
|
1404
|
-
"start-all",
|
|
1405
|
-
"install-extension",
|
|
1406
|
-
"gen-claude-md",
|
|
1407
|
-
"print-token",
|
|
1408
|
-
"gen-plugin-stub",
|
|
1409
|
-
"notify",
|
|
1410
|
-
"install",
|
|
1411
|
-
"marketplace",
|
|
1412
|
-
"status",
|
|
1413
|
-
"shim",
|
|
1414
|
-
"recipe",
|
|
1415
|
-
"dashboard",
|
|
1416
|
-
];
|
|
2595
|
+
// Reuses the KNOWN_SUBCOMMANDS list from the top of this file as a single
|
|
2596
|
+
// source of truth for "what subcommand argv tokens are recognized".
|
|
1417
2597
|
const unknownSub = process.argv[2];
|
|
1418
2598
|
if (unknownSub &&
|
|
1419
2599
|
!unknownSub.startsWith("-") &&
|
|
1420
|
-
!
|
|
2600
|
+
!KNOWN_SUBCOMMANDS.includes(unknownSub)) {
|
|
1421
2601
|
const lev = (a, b) => {
|
|
1422
2602
|
const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
1423
2603
|
for (let i = 1; i <= a.length; i++)
|
|
@@ -1433,127 +2613,146 @@ Options:
|
|
|
1433
2613
|
// biome-ignore lint/style/noNonNullAssertion: dp is fully pre-allocated
|
|
1434
2614
|
return dp[a.length][b.length];
|
|
1435
2615
|
};
|
|
1436
|
-
const closest = [...
|
|
2616
|
+
const closest = [...KNOWN_SUBCOMMANDS].sort((a, b) => lev(unknownSub, a) - lev(unknownSub, b))[0];
|
|
1437
2617
|
console.error(`Unknown command: '${unknownSub}'. Did you mean: ${closest}?`);
|
|
1438
2618
|
process.exit(1);
|
|
1439
2619
|
}
|
|
1440
2620
|
}
|
|
1441
|
-
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
resolved.adapter;
|
|
1451
|
-
process.stderr.write(`[patchwork] model adapter initialized: ${resolved.adapter.name}\n`);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
catch (err) {
|
|
1455
|
-
process.stderr.write(`[patchwork] adapter init failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2621
|
+
// Skip the bridge-mode tail entirely when a subcommand IIFE will own the
|
|
2622
|
+
// process. `parseConfig` validates argv against the bridge's known-flag list
|
|
2623
|
+
// and raises "Unknown option" for subcommand-specific flags (e.g. `recipe
|
|
2624
|
+
// new --out .`); without this guard that throw kills the process before
|
|
2625
|
+
// the IIFE's microtask runs. The subcommand handles its own arg parsing.
|
|
2626
|
+
if (__subcommandWillRun) {
|
|
2627
|
+
// Subcommand IIFE is in flight or about to fire; sit tight until it
|
|
2628
|
+
// process.exits. Empty body — control naturally falls past end-of-file
|
|
2629
|
+
// and Node keeps the process alive on the IIFE's pending microtask.
|
|
1456
2630
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
//
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
.createHash("sha256")
|
|
1470
|
-
.update(config.workspace)
|
|
1471
|
-
.digest("hex")
|
|
1472
|
-
.slice(0, 6);
|
|
1473
|
-
const sessionName = `claude-bridge-${ws}${hash}`;
|
|
1474
|
-
// Check if tmux is available
|
|
1475
|
-
const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
|
|
1476
|
-
if (tmuxCheck.status !== 0) {
|
|
1477
|
-
process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
|
|
1478
|
-
}
|
|
1479
|
-
else {
|
|
1480
|
-
// Strip --auto-tmux from argv to avoid infinite re-exec loop
|
|
1481
|
-
const newArgv = process.argv.filter((a) => a !== "--auto-tmux");
|
|
1482
|
-
// Pass each argv token as a separate tmux argument so paths with spaces work correctly
|
|
1483
|
-
const result = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, ...newArgv], { stdio: "inherit", timeout: 5000 });
|
|
1484
|
-
if (result.status === 0) {
|
|
1485
|
-
process.stderr.write(`Bridge launched in tmux session '${sessionName}'.\n`);
|
|
1486
|
-
process.stderr.write(` Attach with: tmux attach -t ${sessionName}\n`);
|
|
1487
|
-
process.exit(0);
|
|
1488
|
-
}
|
|
1489
|
-
else {
|
|
1490
|
-
// tmux session likely already exists — attach to it or fall through
|
|
1491
|
-
process.stderr.write(`WARNING: Could not create tmux session '${sessionName}' (already exists?). Running without auto-tmux.\n`);
|
|
2631
|
+
else {
|
|
2632
|
+
const config = parseConfig(process.argv);
|
|
2633
|
+
// Patchwork: resolve --model flag (optional, non-invasive) — stashes the
|
|
2634
|
+
// configured adapter on globalThis for consumers that opt into the adapter
|
|
2635
|
+
// layer. Bridge subprocess driver still works when --model is absent.
|
|
2636
|
+
try {
|
|
2637
|
+
const { resolveModel } = await import("./patchworkCli.js");
|
|
2638
|
+
const resolved = resolveModel(process.argv);
|
|
2639
|
+
if (resolved) {
|
|
2640
|
+
globalThis.__patchworkAdapter =
|
|
2641
|
+
resolved.adapter;
|
|
2642
|
+
process.stderr.write(`[patchwork] model adapter initialized: ${resolved.adapter.name}\n`);
|
|
1492
2643
|
}
|
|
1493
2644
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
});
|
|
2645
|
+
catch (err) {
|
|
2646
|
+
process.stderr.write(`[patchwork] adapter init failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2647
|
+
}
|
|
2648
|
+
// If --analytics flag was passed, persist the preference immediately
|
|
2649
|
+
if (config.analyticsEnabled !== null) {
|
|
2650
|
+
setAnalyticsPref(config.analyticsEnabled);
|
|
2651
|
+
}
|
|
2652
|
+
// Auto-tmux: if requested and not already inside tmux or screen, re-exec inside a tmux session
|
|
2653
|
+
if (config.autoTmux &&
|
|
2654
|
+
!process.env.TMUX &&
|
|
2655
|
+
!process.env.STY &&
|
|
2656
|
+
!process.env.ZELLIJ &&
|
|
2657
|
+
!process.env.ZELLIJ_SESSION_NAME) {
|
|
2658
|
+
const ws = config.workspace.replace(/[^a-zA-Z0-9]/g, "").slice(-8);
|
|
2659
|
+
const hash = crypto
|
|
2660
|
+
.createHash("sha256")
|
|
2661
|
+
.update(config.workspace)
|
|
2662
|
+
.digest("hex")
|
|
2663
|
+
.slice(0, 6);
|
|
2664
|
+
const sessionName = `claude-bridge-${ws}${hash}`;
|
|
2665
|
+
// Check if tmux is available
|
|
2666
|
+
const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
|
|
2667
|
+
if (tmuxCheck.status !== 0) {
|
|
2668
|
+
process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
|
|
1519
2669
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2670
|
+
else {
|
|
2671
|
+
// Strip --auto-tmux from argv to avoid infinite re-exec loop
|
|
2672
|
+
const newArgv = process.argv.filter((a) => a !== "--auto-tmux");
|
|
2673
|
+
// Pass each argv token as a separate tmux argument so paths with spaces work correctly
|
|
2674
|
+
const result = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, ...newArgv], { stdio: "inherit", timeout: 5000 });
|
|
2675
|
+
if (result.status === 0) {
|
|
2676
|
+
process.stderr.write(`Bridge launched in tmux session '${sessionName}'.\n`);
|
|
2677
|
+
process.stderr.write(` Attach with: tmux attach -t ${sessionName}\n`);
|
|
1523
2678
|
process.exit(0);
|
|
1524
2679
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
2680
|
+
else {
|
|
2681
|
+
// tmux session likely already exists — attach to it or fall through
|
|
2682
|
+
process.stderr.write(`WARNING: Could not create tmux session '${sessionName}' (already exists?). Running without auto-tmux.\n`);
|
|
1528
2683
|
}
|
|
1529
|
-
|
|
1530
|
-
setTimeout(() => {
|
|
1531
|
-
delay = Math.min(delay * 2, MAX_DELAY_MS);
|
|
1532
|
-
runChild();
|
|
1533
|
-
}, delay);
|
|
1534
|
-
});
|
|
2684
|
+
}
|
|
1535
2685
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
2686
|
+
// Skip bridge boot when a subcommand IIFE is doing the work — avoids the
|
|
2687
|
+
// race where bridge.start() began initialising in parallel with the
|
|
2688
|
+
// subcommand's async path. See the KNOWN_SUBCOMMANDS / __subcommandWillRun
|
|
2689
|
+
// gate at the top of this file.
|
|
2690
|
+
if (__subcommandWillRun) {
|
|
2691
|
+
// intentionally empty — subcommand IIFE owns the process from here.
|
|
2692
|
+
}
|
|
2693
|
+
// --watch: supervisor mode — spawn this binary as a child (without --watch) and restart on crash
|
|
2694
|
+
else if (config.watch) {
|
|
2695
|
+
const childArgv = process.argv.filter((a) => a !== "--watch");
|
|
2696
|
+
const STABLE_THRESHOLD_MS = 60_000;
|
|
2697
|
+
const BASE_DELAY_MS = 2_000;
|
|
2698
|
+
const MAX_DELAY_MS = 30_000;
|
|
2699
|
+
let delay = BASE_DELAY_MS;
|
|
2700
|
+
let stopping = false;
|
|
2701
|
+
function runChild() {
|
|
2702
|
+
if (stopping)
|
|
2703
|
+
return;
|
|
2704
|
+
const startAt = Date.now();
|
|
2705
|
+
process.stderr.write("[supervisor] starting bridge\n");
|
|
2706
|
+
const [cmd, ...args] = childArgv;
|
|
2707
|
+
if (!cmd)
|
|
1550
2708
|
return;
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
2709
|
+
const child = spawn(cmd, args, {
|
|
2710
|
+
stdio: "inherit",
|
|
2711
|
+
});
|
|
2712
|
+
for (const sig of ["SIGTERM", "SIGINT"]) {
|
|
2713
|
+
process.once(sig, () => {
|
|
2714
|
+
stopping = true;
|
|
2715
|
+
child.kill(sig);
|
|
2716
|
+
});
|
|
1554
2717
|
}
|
|
2718
|
+
child.on("exit", (code, signal) => {
|
|
2719
|
+
if (stopping) {
|
|
2720
|
+
process.stderr.write("[supervisor] bridge stopped\n");
|
|
2721
|
+
process.exit(0);
|
|
2722
|
+
}
|
|
2723
|
+
const uptime = Date.now() - startAt;
|
|
2724
|
+
if (uptime >= STABLE_THRESHOLD_MS) {
|
|
2725
|
+
delay = BASE_DELAY_MS; // reset backoff after a stable run
|
|
2726
|
+
}
|
|
2727
|
+
process.stderr.write(`[supervisor] bridge exited (code=${code ?? signal}), restarting in ${delay / 1000}s\n`);
|
|
2728
|
+
setTimeout(() => {
|
|
2729
|
+
delay = Math.min(delay * 2, MAX_DELAY_MS);
|
|
2730
|
+
runChild();
|
|
2731
|
+
}, delay);
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
runChild();
|
|
2735
|
+
}
|
|
2736
|
+
else {
|
|
2737
|
+
const bridge = new Bridge(config);
|
|
2738
|
+
bridge.start().catch((err) => {
|
|
2739
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2740
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
2741
|
+
process.exit(1);
|
|
1555
2742
|
});
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
}
|
|
2743
|
+
// F5: Silent self-update nudge (fire-and-forget)
|
|
2744
|
+
import("node:child_process")
|
|
2745
|
+
.then(({ exec }) => {
|
|
2746
|
+
exec("npm view claude-ide-bridge version", { timeout: 5000 }, (err, stdout) => {
|
|
2747
|
+
if (err || !stdout)
|
|
2748
|
+
return;
|
|
2749
|
+
const latest = stdout.trim();
|
|
2750
|
+
if (latest && semverGt(latest, PACKAGE_VERSION)) {
|
|
2751
|
+
console.log(`\n Bridge v${latest} available — run: npm update -g claude-ide-bridge\n`);
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
})
|
|
2755
|
+
.catch(() => { });
|
|
2756
|
+
}
|
|
2757
|
+
} // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
|
|
1559
2758
|
//# sourceMappingURL=index.js.map
|