patchwork-os 0.2.0-beta.0 → 0.2.0-beta.10
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 +14 -14
- package/README.md +204 -34
- package/deploy/README.md +1 -1
- package/deploy/bootstrap-vps.sh +14 -9
- package/deploy/deploy-dashboard.sh +25 -1
- package/deploy/macos/README.md +153 -0
- package/deploy/macos/com.patchwork.bridge.plist.template +54 -0
- package/deploy/macos/com.patchwork.tunnel.plist.template +76 -0
- package/deploy/macos/install-mac-bridge.sh +244 -0
- package/deploy/macos/uninstall-mac-bridge.sh +22 -0
- package/dist/activityLog.d.ts +6 -0
- package/dist/activityLog.js +63 -26
- package/dist/activityLog.js.map +1 -1
- package/dist/adapters/claude.js +22 -16
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.js +15 -11
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.js +9 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/ajv2020.d.ts +25 -0
- package/dist/ajv2020.js +33 -0
- package/dist/ajv2020.js.map +1 -0
- package/dist/analyticsAggregator.d.ts +5 -1
- package/dist/analyticsAggregator.js +16 -5
- package/dist/analyticsAggregator.js.map +1 -1
- package/dist/analyticsConfig.d.ts +29 -0
- package/dist/analyticsConfig.js +102 -0
- package/dist/analyticsConfig.js.map +1 -0
- package/dist/analyticsPrefs.d.ts +49 -2
- package/dist/analyticsPrefs.js +177 -19
- package/dist/analyticsPrefs.js.map +1 -1
- package/dist/analyticsSend.d.ts +17 -1
- package/dist/analyticsSend.js +67 -5
- package/dist/analyticsSend.js.map +1 -1
- package/dist/approvalHttp.d.ts +14 -0
- package/dist/approvalHttp.js +212 -9
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalInsights.js +13 -5
- package/dist/approvalInsights.js.map +1 -1
- package/dist/approvalQueue.d.ts +97 -3
- package/dist/approvalQueue.js +193 -19
- package/dist/approvalQueue.js.map +1 -1
- package/dist/approvalSignals.js +19 -11
- package/dist/approvalSignals.js.map +1 -1
- package/dist/automation.d.ts +35 -10
- package/dist/automation.js +133 -37
- package/dist/automation.js.map +1 -1
- package/dist/automationSuggestions.js +10 -8
- package/dist/automationSuggestions.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +238 -51
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeLockDiscovery.d.ts +27 -1
- package/dist/bridgeLockDiscovery.js +38 -11
- package/dist/bridgeLockDiscovery.js.map +1 -1
- package/dist/bridgeToken.js +3 -2
- package/dist/bridgeToken.js.map +1 -1
- package/dist/claudeDriver.d.ts +0 -16
- package/dist/claudeDriver.js +42 -28
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeMdPatch.d.ts +9 -3
- package/dist/claudeMdPatch.js +79 -13
- package/dist/claudeMdPatch.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +13 -1
- package/dist/claudeOrchestrator.js +89 -44
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/analytics.d.ts +8 -0
- package/dist/commands/analytics.js +134 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/auditEnv.d.ts +36 -0
- package/dist/commands/auditEnv.js +202 -0
- package/dist/commands/auditEnv.js.map +1 -0
- package/dist/commands/dashboard.js +8 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +35 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/install.js +3 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/launchd.js +15 -16
- package/dist/commands/launchd.js.map +1 -1
- package/dist/commands/patchworkInit.d.ts +5 -0
- package/dist/commands/patchworkInit.js +89 -7
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +110 -0
- package/dist/commands/recipe.js +512 -9
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipeInstall.js +11 -4
- package/dist/commands/recipeInstall.js.map +1 -1
- package/dist/commands/shadowScan.d.ts +34 -0
- package/dist/commands/shadowScan.js +142 -0
- package/dist/commands/shadowScan.js.map +1 -0
- package/dist/commands/task.js +2 -2
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/tools.d.ts +20 -1
- package/dist/commands/tools.js +112 -3
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/tracesImport.js +21 -4
- package/dist/commands/tracesImport.js.map +1 -1
- package/dist/commitIssueLinkLog.d.ts +24 -0
- package/dist/commitIssueLinkLog.js +160 -22
- package/dist/commitIssueLinkLog.js.map +1 -1
- package/dist/companions/registry.js +15 -7
- package/dist/companions/registry.js.map +1 -1
- package/dist/config.d.ts +49 -5
- package/dist/config.js +123 -22
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.js +913 -413
- package/dist/connectorRoutes.js.map +1 -1
- package/dist/connectors/airtable.d.ts +230 -0
- package/dist/connectors/airtable.js +700 -0
- package/dist/connectors/airtable.js.map +1 -0
- package/dist/connectors/asana.js +13 -9
- package/dist/connectors/asana.js.map +1 -1
- package/dist/connectors/baseConnector.js +25 -3
- package/dist/connectors/baseConnector.js.map +1 -1
- package/dist/connectors/caldiy.d.ts +137 -0
- package/dist/connectors/caldiy.js +424 -0
- package/dist/connectors/caldiy.js.map +1 -0
- package/dist/connectors/circleci.d.ts +162 -0
- package/dist/connectors/circleci.js +576 -0
- package/dist/connectors/circleci.js.map +1 -0
- package/dist/connectors/cloudflare.d.ts +132 -0
- package/dist/connectors/cloudflare.js +622 -0
- package/dist/connectors/cloudflare.js.map +1 -0
- package/dist/connectors/confluence.js +35 -0
- package/dist/connectors/confluence.js.map +1 -1
- package/dist/connectors/connectorRedirectUri.d.ts +30 -0
- package/dist/connectors/connectorRedirectUri.js +38 -0
- package/dist/connectors/connectorRedirectUri.js.map +1 -0
- package/dist/connectors/connectorRegistry.d.ts +63 -0
- package/dist/connectors/connectorRegistry.js +354 -0
- package/dist/connectors/connectorRegistry.js.map +1 -0
- package/dist/connectors/datadog.js +33 -4
- package/dist/connectors/datadog.js.map +1 -1
- package/dist/connectors/discord.js +14 -10
- package/dist/connectors/discord.js.map +1 -1
- package/dist/connectors/elasticsearch.d.ts +141 -0
- package/dist/connectors/elasticsearch.js +601 -0
- package/dist/connectors/elasticsearch.js.map +1 -0
- package/dist/connectors/figma.d.ts +130 -0
- package/dist/connectors/figma.js +387 -0
- package/dist/connectors/figma.js.map +1 -0
- package/dist/connectors/github.d.ts +17 -0
- package/dist/connectors/github.js +53 -2
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gitlab.js +12 -6
- package/dist/connectors/gitlab.js.map +1 -1
- package/dist/connectors/gmail.js +72 -21
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.js +8 -8
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/connectors/googleDocs.d.ts +103 -0
- package/dist/connectors/googleDocs.js +409 -0
- package/dist/connectors/googleDocs.js.map +1 -0
- package/dist/connectors/googleDrive.d.ts +12 -0
- package/dist/connectors/googleDrive.js +35 -8
- package/dist/connectors/googleDrive.js.map +1 -1
- package/dist/connectors/grafana.d.ts +133 -0
- package/dist/connectors/grafana.js +478 -0
- package/dist/connectors/grafana.js.map +1 -0
- package/dist/connectors/jira.d.ts +25 -0
- package/dist/connectors/jira.js +245 -2
- package/dist/connectors/jira.js.map +1 -1
- package/dist/connectors/linear.js +1 -1
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpOAuth.js +78 -16
- package/dist/connectors/mcpOAuth.js.map +1 -1
- package/dist/connectors/monday.d.ts +217 -0
- package/dist/connectors/monday.js +655 -0
- package/dist/connectors/monday.js.map +1 -0
- package/dist/connectors/mongodb.d.ts +139 -0
- package/dist/connectors/mongodb.js +455 -0
- package/dist/connectors/mongodb.js.map +1 -0
- package/dist/connectors/oauthStateStore.d.ts +20 -0
- package/dist/connectors/oauthStateStore.js +94 -4
- package/dist/connectors/oauthStateStore.js.map +1 -1
- package/dist/connectors/obsidian.d.ts +83 -0
- package/dist/connectors/obsidian.js +441 -0
- package/dist/connectors/obsidian.js.map +1 -0
- package/dist/connectors/paystack.d.ts +159 -0
- package/dist/connectors/paystack.js +607 -0
- package/dist/connectors/paystack.js.map +1 -0
- package/dist/connectors/pipedrive.d.ts +189 -0
- package/dist/connectors/pipedrive.js +559 -0
- package/dist/connectors/pipedrive.js.map +1 -0
- package/dist/connectors/postgres.d.ts +127 -0
- package/dist/connectors/postgres.js +512 -0
- package/dist/connectors/postgres.js.map +1 -0
- package/dist/connectors/posthog.d.ts +119 -0
- package/dist/connectors/posthog.js +479 -0
- package/dist/connectors/posthog.js.map +1 -0
- package/dist/connectors/redis.d.ts +140 -0
- package/dist/connectors/redis.js +571 -0
- package/dist/connectors/redis.js.map +1 -0
- package/dist/connectors/resend.d.ts +131 -0
- package/dist/connectors/resend.js +529 -0
- package/dist/connectors/resend.js.map +1 -0
- package/dist/connectors/salesforce.d.ts +136 -0
- package/dist/connectors/salesforce.js +603 -0
- package/dist/connectors/salesforce.js.map +1 -0
- package/dist/connectors/secrets.d.ts +51 -0
- package/dist/connectors/secrets.js +102 -0
- package/dist/connectors/secrets.js.map +1 -0
- package/dist/connectors/sendgrid.d.ts +102 -0
- package/dist/connectors/sendgrid.js +423 -0
- package/dist/connectors/sendgrid.js.map +1 -0
- package/dist/connectors/shopify.d.ts +145 -0
- package/dist/connectors/shopify.js +502 -0
- package/dist/connectors/shopify.js.map +1 -0
- package/dist/connectors/slack.d.ts +1 -1
- package/dist/connectors/slack.js +61 -9
- package/dist/connectors/slack.js.map +1 -1
- package/dist/connectors/snowflake.d.ts +119 -0
- package/dist/connectors/snowflake.js +615 -0
- package/dist/connectors/snowflake.js.map +1 -0
- package/dist/connectors/supabase.d.ts +92 -0
- package/dist/connectors/supabase.js +630 -0
- package/dist/connectors/supabase.js.map +1 -0
- package/dist/connectors/todoist.d.ts +117 -0
- package/dist/connectors/todoist.js +485 -0
- package/dist/connectors/todoist.js.map +1 -0
- package/dist/connectors/tokenStorage.js +56 -14
- package/dist/connectors/tokenStorage.js.map +1 -1
- package/dist/connectors/twilio.d.ts +118 -0
- package/dist/connectors/twilio.js +475 -0
- package/dist/connectors/twilio.js.map +1 -0
- package/dist/connectors/vercel.d.ts +110 -0
- package/dist/connectors/vercel.js +479 -0
- package/dist/connectors/vercel.js.map +1 -0
- package/dist/connectors/webflow.d.ts +118 -0
- package/dist/connectors/webflow.js +393 -0
- package/dist/connectors/webflow.js.map +1 -0
- package/dist/connectors/woocommerce.d.ts +220 -0
- package/dist/connectors/woocommerce.js +464 -0
- package/dist/connectors/woocommerce.js.map +1 -0
- package/dist/crypto.js +7 -2
- package/dist/crypto.js.map +1 -1
- package/dist/decisionReplay.js +15 -6
- package/dist/decisionReplay.js.map +1 -1
- package/dist/decisionTraceLog.d.ts +28 -0
- package/dist/decisionTraceLog.js +156 -29
- package/dist/decisionTraceLog.js.map +1 -1
- package/dist/drivers/claude/api.js +5 -4
- package/dist/drivers/claude/api.js.map +1 -1
- package/dist/drivers/claude/envSanitizer.d.ts +0 -6
- package/dist/drivers/claude/envSanitizer.js +17 -2
- package/dist/drivers/claude/envSanitizer.js.map +1 -1
- package/dist/drivers/claude/streamParser.js +5 -4
- package/dist/drivers/claude/streamParser.js.map +1 -1
- package/dist/drivers/claude/subprocess.d.ts +12 -2
- package/dist/drivers/claude/subprocess.js +100 -8
- package/dist/drivers/claude/subprocess.js.map +1 -1
- package/dist/drivers/gemini/api.d.ts +18 -0
- package/dist/drivers/gemini/api.js +29 -0
- package/dist/drivers/gemini/api.js.map +1 -0
- package/dist/drivers/gemini/index.d.ts +22 -0
- package/dist/drivers/gemini/index.js +256 -129
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/index.d.ts +3 -1
- package/dist/drivers/index.js +9 -1
- package/dist/drivers/index.js.map +1 -1
- package/dist/drivers/local/index.d.ts +43 -0
- package/dist/drivers/local/index.js +140 -0
- package/dist/drivers/local/index.js.map +1 -0
- package/dist/drivers/openai/index.js +30 -2
- package/dist/drivers/openai/index.js.map +1 -1
- package/dist/extensionClient.d.ts +37 -4
- package/dist/extensionClient.js +58 -16
- package/dist/extensionClient.js.map +1 -1
- package/dist/featureFlags.d.ts +91 -0
- package/dist/featureFlags.js +174 -3
- package/dist/featureFlags.js.map +1 -1
- package/dist/fileLock.js +21 -12
- package/dist/fileLock.js.map +1 -1
- package/dist/fileLockSync.d.ts +67 -0
- package/dist/fileLockSync.js +126 -0
- package/dist/fileLockSync.js.map +1 -0
- package/dist/fp/activityAnalytics.js +26 -12
- package/dist/fp/activityAnalytics.js.map +1 -1
- package/dist/fp/automationInterpreter.d.ts +15 -1
- package/dist/fp/automationInterpreter.js +217 -82
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +30 -0
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/automationState.d.ts +24 -5
- package/dist/fp/automationState.js +56 -9
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/automationUtils.js +1 -1
- package/dist/fp/automationUtils.js.map +1 -1
- package/dist/fp/commandDescription.js +7 -1
- package/dist/fp/commandDescription.js.map +1 -1
- package/dist/fp/extensionSnapshot.js +8 -2
- package/dist/fp/extensionSnapshot.js.map +1 -1
- package/dist/fp/interpreterContext.d.ts +66 -1
- package/dist/fp/interpreterContext.js +91 -1
- package/dist/fp/interpreterContext.js.map +1 -1
- package/dist/fp/policyParser.js +29 -1
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/fsWatchWithFallback.d.ts +36 -0
- package/dist/fsWatchWithFallback.js +128 -0
- package/dist/fsWatchWithFallback.js.map +1 -0
- package/dist/haltPushDispatch.d.ts +33 -0
- package/dist/haltPushDispatch.js +103 -0
- package/dist/haltPushDispatch.js.map +1 -0
- package/dist/httpBodyValidation.d.ts +41 -0
- package/dist/httpBodyValidation.js +45 -0
- package/dist/httpBodyValidation.js.map +1 -0
- package/dist/httpErrorResponse.d.ts +36 -0
- package/dist/httpErrorResponse.js +46 -0
- package/dist/httpErrorResponse.js.map +1 -0
- package/dist/inboxRoutes.d.ts +22 -0
- package/dist/inboxRoutes.js +151 -12
- package/dist/inboxRoutes.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1054 -76
- package/dist/index.js.map +1 -1
- package/dist/installGuard.js +6 -2
- package/dist/installGuard.js.map +1 -1
- package/dist/lockfile.js +31 -4
- package/dist/lockfile.js.map +1 -1
- package/dist/oauth.d.ts +22 -0
- package/dist/oauth.js +46 -0
- package/dist/oauth.js.map +1 -1
- package/dist/oauthRoutes.d.ts +1 -1
- package/dist/oauthRoutes.js +8 -11
- package/dist/oauthRoutes.js.map +1 -1
- package/dist/orchestrator/childBridgeRegistry.js +29 -11
- package/dist/orchestrator/childBridgeRegistry.js.map +1 -1
- package/dist/patchworkConfig.d.ts +57 -1
- package/dist/patchworkConfig.js +125 -5
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/pluginLoader.js +10 -1
- package/dist/pluginLoader.js.map +1 -1
- package/dist/pluginWatcher.js +12 -14
- package/dist/pluginWatcher.js.map +1 -1
- package/dist/preToolUseHook.js +10 -3
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/processTree.d.ts +34 -0
- package/dist/processTree.js +105 -0
- package/dist/processTree.js.map +1 -0
- package/dist/prompts.js +7 -3
- package/dist/prompts.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +9 -0
- package/dist/recipeOrchestration.js +354 -27
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +63 -2
- package/dist/recipeRoutes.js +790 -137
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/RecipeOrchestrator.d.ts +2 -0
- package/dist/recipes/RecipeOrchestrator.js +6 -1
- package/dist/recipes/RecipeOrchestrator.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +34 -5
- package/dist/recipes/agentExecutor.js +5 -4
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +2 -0
- package/dist/recipes/chainedRunner.js +142 -5
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/connectorPreflight.d.ts +66 -0
- package/dist/recipes/connectorPreflight.js +169 -0
- package/dist/recipes/connectorPreflight.js.map +1 -0
- package/dist/recipes/dependencyGraph.js +13 -5
- package/dist/recipes/dependencyGraph.js.map +1 -1
- package/dist/recipes/githubInstallSource.d.ts +128 -0
- package/dist/recipes/githubInstallSource.js +206 -0
- package/dist/recipes/githubInstallSource.js.map +1 -0
- package/dist/recipes/haltCategory.d.ts +119 -0
- package/dist/recipes/haltCategory.js +188 -0
- package/dist/recipes/haltCategory.js.map +1 -0
- package/dist/recipes/idempotencyKey.d.ts +134 -0
- package/dist/recipes/idempotencyKey.js +322 -0
- package/dist/recipes/idempotencyKey.js.map +1 -0
- package/dist/recipes/installer.js +48 -2
- package/dist/recipes/installer.js.map +1 -1
- package/dist/recipes/judgeSummary.d.ts +50 -0
- package/dist/recipes/judgeSummary.js +47 -0
- package/dist/recipes/judgeSummary.js.map +1 -0
- package/dist/recipes/judgeVerdict.d.ts +48 -0
- package/dist/recipes/judgeVerdict.js +174 -0
- package/dist/recipes/judgeVerdict.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +9 -0
- package/dist/recipes/migrations/index.js +133 -0
- package/dist/recipes/migrations/index.js.map +1 -1
- package/dist/recipes/names.d.ts +20 -0
- package/dist/recipes/names.js +25 -0
- package/dist/recipes/names.js.map +1 -1
- package/dist/recipes/parser.js +88 -5
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/replayRun.js +1 -1
- package/dist/recipes/replayRun.js.map +1 -1
- package/dist/recipes/runBudget.d.ts +70 -0
- package/dist/recipes/runBudget.js +109 -0
- package/dist/recipes/runBudget.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +30 -0
- package/dist/recipes/scheduler.js +69 -19
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +36 -0
- package/dist/recipes/schemaGenerator.js +103 -5
- package/dist/recipes/schemaGenerator.js.map +1 -1
- package/dist/recipes/stepObservation.js +9 -0
- package/dist/recipes/stepObservation.js.map +1 -1
- package/dist/recipes/toolRegistry.js +20 -3
- package/dist/recipes/toolRegistry.js.map +1 -1
- package/dist/recipes/tools/fanOut.d.ts +20 -0
- package/dist/recipes/tools/fanOut.js +199 -0
- package/dist/recipes/tools/fanOut.js.map +1 -0
- package/dist/recipes/tools/file.js +5 -2
- package/dist/recipes/tools/file.js.map +1 -1
- package/dist/recipes/tools/github.d.ts +1 -1
- package/dist/recipes/tools/github.js +75 -1
- package/dist/recipes/tools/github.js.map +1 -1
- package/dist/recipes/tools/gmail.js +45 -6
- package/dist/recipes/tools/gmail.js.map +1 -1
- package/dist/recipes/tools/googleDrive.js +64 -0
- package/dist/recipes/tools/googleDrive.js.map +1 -1
- package/dist/recipes/tools/http.d.ts +10 -0
- package/dist/recipes/tools/http.js +176 -0
- package/dist/recipes/tools/http.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +2 -0
- package/dist/recipes/tools/index.js +2 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/tools/slack.js +1 -1
- package/dist/recipes/validation.d.ts +17 -0
- package/dist/recipes/validation.js +29 -11
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/workspaceRoot.d.ts +37 -0
- package/dist/recipes/workspaceRoot.js +73 -0
- package/dist/recipes/workspaceRoot.js.map +1 -0
- package/dist/recipes/yamlPositions.d.ts +56 -0
- package/dist/recipes/yamlPositions.js +183 -0
- package/dist/recipes/yamlPositions.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +195 -8
- package/dist/recipes/yamlRunner.js +877 -209
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +33 -3
- package/dist/recipesHttp.js +126 -12
- package/dist/recipesHttp.js.map +1 -1
- package/dist/resources.js +21 -13
- package/dist/resources.js.map +1 -1
- package/dist/runLog.d.ts +58 -5
- package/dist/runLog.js +98 -21
- package/dist/runLog.js.map +1 -1
- package/dist/sanitizeParsedJson.d.ts +39 -0
- package/dist/sanitizeParsedJson.js +55 -0
- package/dist/sanitizeParsedJson.js.map +1 -0
- package/dist/schemas/recipe.v1.json +885 -196
- package/dist/server.d.ts +166 -3
- package/dist/server.js +1313 -332
- package/dist/server.js.map +1 -1
- package/dist/sessionCheckpoint.d.ts +8 -0
- package/dist/sessionCheckpoint.js +33 -3
- package/dist/sessionCheckpoint.js.map +1 -1
- package/dist/ssrfGuard.d.ts +16 -0
- package/dist/ssrfGuard.js +87 -4
- package/dist/ssrfGuard.js.map +1 -1
- package/dist/streamableHttp.d.ts +9 -4
- package/dist/streamableHttp.js +76 -20
- package/dist/streamableHttp.js.map +1 -1
- package/dist/telemetry.js +19 -12
- package/dist/telemetry.js.map +1 -1
- package/dist/testing/shadowRun.d.ts +52 -0
- package/dist/testing/shadowRun.js +71 -0
- package/dist/testing/shadowRun.js.map +1 -0
- package/dist/tools/batchLsp.d.ts +3 -0
- package/dist/tools/bridgeDoctor.d.ts +11 -0
- package/dist/tools/bridgeDoctor.js +48 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/bridgeStatus.d.ts +0 -12
- package/dist/tools/bridgeStatus.js +2 -28
- package/dist/tools/bridgeStatus.js.map +1 -1
- package/dist/tools/cancelClaudeTask.d.ts +1 -0
- package/dist/tools/clipboard.d.ts +2 -0
- package/dist/tools/closeTabs.d.ts +1 -0
- package/dist/tools/codeLens.d.ts +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +9 -0
- package/dist/tools/ctxGetTaskContext.js +50 -2
- package/dist/tools/ctxGetTaskContext.js.map +1 -1
- package/dist/tools/ctxQueryTraces.js +32 -24
- package/dist/tools/ctxQueryTraces.js.map +1 -1
- 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/detectUnusedCode.js +9 -7
- package/dist/tools/detectUnusedCode.js.map +1 -1
- package/dist/tools/documentLinks.d.ts +1 -0
- package/dist/tools/editText.d.ts +1 -0
- package/dist/tools/editText.js +2 -1
- package/dist/tools/editText.js.map +1 -1
- package/dist/tools/enrichCommit.d.ts +1 -0
- package/dist/tools/enrichStackTrace.js +11 -5
- package/dist/tools/enrichStackTrace.js.map +1 -1
- package/dist/tools/explainDiagnostic.d.ts +1 -0
- package/dist/tools/explainSymbol.d.ts +1 -0
- package/dist/tools/fileOperations.d.ts +3 -0
- package/dist/tools/fileOperations.js +2 -1
- package/dist/tools/fileOperations.js.map +1 -1
- package/dist/tools/fileWatcher.d.ts +2 -0
- package/dist/tools/fileWatcher.js +8 -2
- package/dist/tools/fileWatcher.js.map +1 -1
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.js +10 -5
- package/dist/tools/fixAllLintErrors.js.map +1 -1
- package/dist/tools/foldingRanges.d.ts +1 -0
- package/dist/tools/formatDocument.d.ts +1 -0
- package/dist/tools/formatDocument.js +10 -5
- package/dist/tools/formatDocument.js.map +1 -1
- package/dist/tools/generateTests.d.ts +1 -0
- package/dist/tools/getAIComments.d.ts +1 -0
- package/dist/tools/getAnalyticsReport.js +5 -1
- package/dist/tools/getAnalyticsReport.js.map +1 -1
- package/dist/tools/getBufferContent.d.ts +1 -0
- package/dist/tools/getChangeImpact.d.ts +1 -0
- package/dist/tools/getChangeImpact.js +23 -14
- package/dist/tools/getChangeImpact.js.map +1 -1
- package/dist/tools/getClaudeTaskStatus.d.ts +1 -0
- package/dist/tools/getCodeCoverage.d.ts +1 -0
- package/dist/tools/getCodeCoverage.js +32 -14
- package/dist/tools/getCodeCoverage.js.map +1 -1
- package/dist/tools/getCommitsForIssue.d.ts +1 -0
- package/dist/tools/getDebugState.d.ts +1 -0
- package/dist/tools/getDiagnostics.js +32 -29
- package/dist/tools/getDiagnostics.js.map +1 -1
- package/dist/tools/getDiffFromHandoff.js +8 -2
- package/dist/tools/getDiffFromHandoff.js.map +1 -1
- package/dist/tools/getDocumentSymbols.d.ts +1 -0
- package/dist/tools/getGitHotspots.d.ts +1 -0
- package/dist/tools/getImportedSignatures.d.ts +1 -0
- package/dist/tools/getImportedSignatures.js +18 -14
- package/dist/tools/getImportedSignatures.js.map +1 -1
- package/dist/tools/getPRTemplate.d.ts +1 -0
- package/dist/tools/getProjectContext.js +23 -10
- package/dist/tools/getProjectContext.js.map +1 -1
- package/dist/tools/getSymbolHistory.d.ts +1 -0
- package/dist/tools/getToolCapabilities.d.ts +10 -5
- package/dist/tools/getToolCapabilities.js +79 -202
- package/dist/tools/getToolCapabilities.js.map +1 -1
- package/dist/tools/getTypeSignature.d.ts +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +1 -0
- package/dist/tools/gitWrite.d.ts +11 -0
- package/dist/tools/github/actions.d.ts +2 -0
- package/dist/tools/github/composite.d.ts +3 -0
- package/dist/tools/github/issues.d.ts +4 -0
- package/dist/tools/github/pr.d.ts +7 -0
- package/dist/tools/handoffNote.d.ts +1 -0
- package/dist/tools/handoffNote.js +2 -1
- package/dist/tools/handoffNote.js.map +1 -1
- package/dist/tools/headless/lspClient.js +3 -0
- package/dist/tools/headless/lspClient.js.map +1 -1
- package/dist/tools/hoverAtCursor.d.ts +1 -0
- package/dist/tools/httpClient.d.ts +2 -0
- package/dist/tools/httpClient.js +38 -71
- package/dist/tools/httpClient.js.map +1 -1
- package/dist/tools/inlayHints.d.ts +1 -0
- package/dist/tools/launchQuickTask.d.ts +1 -0
- package/dist/tools/listClaudeTasks.d.ts +1 -0
- package/dist/tools/listClaudeTasks.js +10 -8
- package/dist/tools/listClaudeTasks.js.map +1 -1
- package/dist/tools/listTerminals.d.ts +1 -0
- package/dist/tools/lsp.d.ts +15 -0
- package/dist/tools/lsp.js +17 -0
- package/dist/tools/lsp.js.map +1 -1
- package/dist/tools/navigateToSymbolByName.d.ts +1 -0
- package/dist/tools/openDiff.d.ts +1 -0
- package/dist/tools/openDiff.js +4 -1
- package/dist/tools/openDiff.js.map +1 -1
- package/dist/tools/openFile.d.ts +1 -0
- package/dist/tools/openFile.js +4 -1
- package/dist/tools/openFile.js.map +1 -1
- package/dist/tools/openInBrowser.js +6 -1
- package/dist/tools/openInBrowser.js.map +1 -1
- package/dist/tools/organizeImports.d.ts +1 -0
- package/dist/tools/organizeImports.js +5 -3
- package/dist/tools/organizeImports.js.map +1 -1
- package/dist/tools/performanceReport.js +5 -3
- package/dist/tools/performanceReport.js.map +1 -1
- package/dist/tools/planPersistence.d.ts +3 -0
- package/dist/tools/planPersistence.js +4 -1
- package/dist/tools/planPersistence.js.map +1 -1
- package/dist/tools/previewEdit.d.ts +1 -0
- package/dist/tools/previewEdit.js +15 -4
- package/dist/tools/previewEdit.js.map +1 -1
- package/dist/tools/recentTracesDigest.js +54 -11
- package/dist/tools/recentTracesDigest.js.map +1 -1
- package/dist/tools/refactorAnalyze.d.ts +1 -0
- package/dist/tools/refactorExtractFunction.js +4 -1
- package/dist/tools/refactorExtractFunction.js.map +1 -1
- package/dist/tools/refactorPreview.d.ts +1 -0
- package/dist/tools/refactorPreview.js +10 -2
- package/dist/tools/refactorPreview.js.map +1 -1
- package/dist/tools/replaceBlock.d.ts +1 -0
- package/dist/tools/replaceBlock.js +2 -1
- package/dist/tools/replaceBlock.js.map +1 -1
- package/dist/tools/resumeClaudeTask.d.ts +1 -0
- package/dist/tools/runClaudeTask.d.ts +1 -0
- package/dist/tools/runCommand.js +5 -0
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/runTests.js +15 -4
- package/dist/tools/runTests.js.map +1 -1
- package/dist/tools/screenshot.d.ts +1 -0
- package/dist/tools/screenshotAndAnnotate.js +6 -2
- package/dist/tools/screenshotAndAnnotate.js.map +1 -1
- package/dist/tools/searchAndReplace.d.ts +1 -0
- package/dist/tools/searchAndReplace.js +17 -7
- package/dist/tools/searchAndReplace.js.map +1 -1
- package/dist/tools/searchTools.js +20 -19
- 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/signatureHelp.d.ts +1 -0
- package/dist/tools/spawnWorkspace.js +15 -7
- package/dist/tools/spawnWorkspace.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/terminal.js +4 -0
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/testRunners/pytest.js +6 -2
- package/dist/tools/testRunners/pytest.js.map +1 -1
- package/dist/tools/testRunners/vitestJest.js +3 -1
- package/dist/tools/testRunners/vitestJest.js.map +1 -1
- package/dist/tools/testTraceToSource.d.ts +1 -0
- package/dist/tools/transaction.d.ts +4 -0
- package/dist/tools/transaction.js +4 -1
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/typeHierarchy.d.ts +1 -0
- package/dist/tools/utils.d.ts +22 -0
- package/dist/tools/utils.js +158 -14
- 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/transport.d.ts +3 -1
- package/dist/transport.js +103 -52
- package/dist/transport.js.map +1 -1
- package/dist/winShim.d.ts +34 -0
- package/dist/winShim.js +94 -0
- package/dist/winShim.js.map +1 -0
- package/dist/wireHaltPushDispatch.d.ts +38 -0
- package/dist/wireHaltPushDispatch.js +71 -0
- package/dist/wireHaltPushDispatch.js.map +1 -0
- package/dist/writeFileAtomic.d.ts +23 -0
- package/dist/writeFileAtomic.js +121 -0
- package/dist/writeFileAtomic.js.map +1 -0
- package/package.json +23 -7
- package/scripts/postinstall.mjs +42 -2
- package/scripts/smoke/run-all.mjs +213 -0
- package/scripts/start-all.mjs +572 -0
- package/scripts/start-all.ps1 +209 -0
- package/scripts/start-all.sh +77 -19
- package/scripts/start-orchestrator.ps1 +158 -0
- package/scripts/start-remote.mjs +122 -0
- package/templates/automation-policies/recipe-authoring.json +1 -1
- package/templates/automation-policies/security-first.json +1 -1
- package/templates/automation-policies/strict-lint.json +1 -1
- package/templates/automation-policies/test-driven.json +1 -1
- package/templates/automation-policy.example.json +1 -1
- package/templates/co.patchwork-os.bridge.plist +2 -2
- package/templates/recipes/approval-queue-ui-test.yaml +205 -0
- package/templates/recipes/ctx-loop-test.yaml +1 -1
- package/templates/recipes/fix-errors-on-save.yaml +71 -0
- package/templates/recipes/morning-brief.yaml +5 -2
- package/templates/recipes/project-health-check.yaml +4 -1
- package/templates/recipes/sentry-to-linear.yaml +72 -38
- package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
- package/templates/recipes/webhook/customer-escalation.yaml +8 -9
- package/templates/recipes/webhook/meeting-prep.yaml +11 -5
- package/dist/commands/marketplace.d.ts +0 -11
- package/dist/commands/marketplace.js +0 -120
- package/dist/commands/marketplace.js.map +0 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +0 -10
- package/dist/recipes/legacyRecipeCompat.js +0 -131
- package/dist/recipes/legacyRecipeCompat.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -41,7 +41,10 @@ import { Bridge } from "./bridge.js";
|
|
|
41
41
|
import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
|
|
42
42
|
import { findEditor, parseConfig } from "./config.js";
|
|
43
43
|
import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
|
|
44
|
+
import { treeKill } from "./processTree.js";
|
|
44
45
|
import { PACKAGE_VERSION, semverGt } from "./version.js";
|
|
46
|
+
import { ensureCmdShim } from "./winShim.js";
|
|
47
|
+
import { writeFileAtomicSync } from "./writeFileAtomic.js";
|
|
45
48
|
const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
|
|
46
49
|
// Warn when a symlinked global install is detected (`npm install -g .`).
|
|
47
50
|
// launchctl / sandbox environments can fail through that link with EPERM.
|
|
@@ -63,7 +66,7 @@ const OPEN_VSX_NAME = "claude-ide-bridge-extension";
|
|
|
63
66
|
// CLAUDE.md versioned-block patching moved to ./claudeMdPatch.ts so tests
|
|
64
67
|
// can import the helpers without triggering the top-level CLI side effects
|
|
65
68
|
// at the bottom of this file. Re-exported here for back-compat.
|
|
66
|
-
export { BRIDGE_BLOCK_END,
|
|
69
|
+
export { BRIDGE_BLOCK_END, bridgeBlockStartMarker, extractClaudeMdBlockVersion, patchClaudeMdImport, replaceAllBridgeBlocks, } from "./claudeMdPatch.js";
|
|
67
70
|
import { extractClaudeMdBlockVersion, patchClaudeMdImport, } from "./claudeMdPatch.js";
|
|
68
71
|
/**
|
|
69
72
|
* Downloads the latest VSIX from Open VSX Registry to a temp file.
|
|
@@ -119,7 +122,6 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
119
122
|
"gen-plugin-stub",
|
|
120
123
|
"notify",
|
|
121
124
|
"install",
|
|
122
|
-
"marketplace",
|
|
123
125
|
"status",
|
|
124
126
|
"shim",
|
|
125
127
|
"recipe",
|
|
@@ -128,6 +130,13 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
128
130
|
"dashboard",
|
|
129
131
|
"launchd",
|
|
130
132
|
"start",
|
|
133
|
+
"kill-switch",
|
|
134
|
+
"panic",
|
|
135
|
+
"halts",
|
|
136
|
+
"judgments",
|
|
137
|
+
"analytics",
|
|
138
|
+
"doctor",
|
|
139
|
+
"shadow-scan",
|
|
131
140
|
];
|
|
132
141
|
const __invokedSubcommand = (() => {
|
|
133
142
|
const sub = process.argv[2];
|
|
@@ -139,10 +148,20 @@ const __invokedSubcommand = (() => {
|
|
|
139
148
|
? sub
|
|
140
149
|
: null;
|
|
141
150
|
})();
|
|
151
|
+
// bash/zsh set process.env._ to the actual invoked binary path (e.g. /usr/local/bin/patchwork-os).
|
|
152
|
+
// More reliable than argv[1] which resolves to the .js entrypoint via npm global shim.
|
|
153
|
+
function invokedBinaryName() {
|
|
154
|
+
const fromEnv = process.env._
|
|
155
|
+
? path.basename(process.env._).replace(/\.(cmd|js)$/i, "")
|
|
156
|
+
: "";
|
|
157
|
+
if (fromEnv && fromEnv !== "node" && fromEnv !== "npm")
|
|
158
|
+
return fromEnv;
|
|
159
|
+
return path.basename(process.argv[1] ?? "").replace(/\.js$/, "");
|
|
160
|
+
}
|
|
142
161
|
const __invokedBareBinaryDashboard = (() => {
|
|
143
162
|
if (process.argv[2] && process.argv[2] !== "dashboard")
|
|
144
163
|
return false;
|
|
145
|
-
const binName =
|
|
164
|
+
const binName = invokedBinaryName();
|
|
146
165
|
return (binName === "patchwork-os" ||
|
|
147
166
|
binName === "patchwork" ||
|
|
148
167
|
binName === "patchwork.js");
|
|
@@ -153,6 +172,56 @@ if (process.argv[2] === "--version" || process.argv[2] === "-v") {
|
|
|
153
172
|
console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
|
|
154
173
|
process.exit(0);
|
|
155
174
|
}
|
|
175
|
+
// Handle top-level --help / -h / help — print a grouped command index so a
|
|
176
|
+
// first-time user has a discoverable entry point. Without this, bare
|
|
177
|
+
// `patchwork --help` falls through to bridge-daemon arg parsing and errors.
|
|
178
|
+
if (process.argv[2] === "--help" ||
|
|
179
|
+
process.argv[2] === "-h" ||
|
|
180
|
+
process.argv[2] === "help") {
|
|
181
|
+
const binName = path.basename(process.argv[1] ?? "patchwork");
|
|
182
|
+
process.stdout.write(`${binName} ${PACKAGE_VERSION}\n\n` +
|
|
183
|
+
`First time? Run:\n` +
|
|
184
|
+
` ${binName} init # set up ~/.patchwork + Claude Code hooks\n` +
|
|
185
|
+
` ${binName} start-all # bridge + Claude + dashboard\n\n` +
|
|
186
|
+
`Get started\n` +
|
|
187
|
+
` init [--workspace <dir>] Scaffold ~/.patchwork; register CC hooks\n` +
|
|
188
|
+
` install-extension Install the VS Code / Cursor / Windsurf extension\n` +
|
|
189
|
+
` start-all [--no-dashboard] Launch bridge + Claude --ide + dashboard\n` +
|
|
190
|
+
` start-orchestrator Multi-IDE-window meta-bridge\n\n` +
|
|
191
|
+
`Recipes\n` +
|
|
192
|
+
` recipe new <name> [-i] Scaffold a recipe\n` +
|
|
193
|
+
` recipe list List installed recipes\n` +
|
|
194
|
+
` recipe run <name> [--vars k=v] Run a recipe by name\n` +
|
|
195
|
+
` recipe install <source> Install from a path or GitHub source\n` +
|
|
196
|
+
` recipe --help Full recipe subcommand index\n\n` +
|
|
197
|
+
`Operate\n` +
|
|
198
|
+
` start [--port N] [--workspace <dir>] Start a single bridge (no tmux)\n` +
|
|
199
|
+
` status One-line bridge status (port, uptime)\n` +
|
|
200
|
+
` tools [list|search <q>] [--slim] [--json] List tools the bridge would register\n` +
|
|
201
|
+
` analytics <show|configure|clear|test> Manage opt-in telemetry config\n` +
|
|
202
|
+
` launchd <install|uninstall|status> Manage the macOS auto-start LaunchAgent\n\n` +
|
|
203
|
+
`Diagnose\n` +
|
|
204
|
+
` halts [--window 1h|24h|overnight|7d] Morning summary of recent recipe halts\n` +
|
|
205
|
+
` judgments [--window ...] [--recipe N] Recent judge-step verdicts across runs\n` +
|
|
206
|
+
` suggest [--since-days N] Recipe + unused-tool suggestions\n` +
|
|
207
|
+
` token-efficiency benchmark Measure token cost across tool sets\n` +
|
|
208
|
+
` traces export Bundle approval / recipe / decision traces\n` +
|
|
209
|
+
` print-token [--port N] Print the active bridge auth token\n\n` +
|
|
210
|
+
`Safety\n` +
|
|
211
|
+
` kill-switch <engage|release|status> Block / resume write-tier tools across bridges\n` +
|
|
212
|
+
` panic [--reason "..."] Shorthand for kill-switch engage\n\n` +
|
|
213
|
+
`Daemon (no subcommand)\n` +
|
|
214
|
+
` --workspace <dir> Start the bridge in foreground\n` +
|
|
215
|
+
` --watch Auto-restart supervisor\n` +
|
|
216
|
+
` --slim 27 IDE-only tools (default: full)\n\n` +
|
|
217
|
+
`Other\n` +
|
|
218
|
+
` --version, -v Print package version\n` +
|
|
219
|
+
` shim stdio↔WebSocket shim (used by MCP clients)\n` +
|
|
220
|
+
` notify <event> Notify a running bridge of a CC hook event\n\n` +
|
|
221
|
+
`Bridge-daemon flags: run \`${binName} --workspace . --help-flags\` for the full list,\n` +
|
|
222
|
+
`or see https://github.com/Oolab-labs/patchwork-os#readme.\n`);
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
156
225
|
// Handle patchwork-init subcommand — T2 from docs/install-ux-plan.md.
|
|
157
226
|
// Separate from the bridge-only `init` to preserve back-compat. See ADR-0008.
|
|
158
227
|
if (process.argv[2] === "patchwork-init") {
|
|
@@ -160,6 +229,13 @@ if (process.argv[2] === "patchwork-init") {
|
|
|
160
229
|
await runPatchworkInit(process.argv.slice(3));
|
|
161
230
|
process.exit(0);
|
|
162
231
|
}
|
|
232
|
+
// `patchwork-os init` → dashboard setup, not IDE bridge installer.
|
|
233
|
+
// patchwork init / claude-ide-bridge init still go to the bridge path below.
|
|
234
|
+
if (process.argv[2] === "init" && invokedBinaryName() === "patchwork-os") {
|
|
235
|
+
const { runPatchworkInit } = await import("./commands/patchworkInit.js");
|
|
236
|
+
await runPatchworkInit(process.argv.slice(3));
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
163
239
|
// Handle start-all subcommand — launches the full 3-pane tmux orchestrator.
|
|
164
240
|
// Also triggered when invoked as `claude-ide-bridge-start` directly.
|
|
165
241
|
const isStartAll = process.argv[2] === "start-all" ||
|
|
@@ -168,8 +244,11 @@ if (isStartAll) {
|
|
|
168
244
|
const startAllArgs = process.argv[2] === "start-all"
|
|
169
245
|
? process.argv.slice(3)
|
|
170
246
|
: process.argv.slice(2);
|
|
171
|
-
|
|
172
|
-
|
|
247
|
+
// Dispatch the cross-platform Node orchestrator (start-all.mjs). The
|
|
248
|
+
// bash entry-point is kept as a developer shortcut but Windows has no
|
|
249
|
+
// `bash` on PATH by default, and the .mjs is functionally equivalent.
|
|
250
|
+
const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
|
|
251
|
+
const result = spawnSync(process.execPath, [scriptPath, ...startAllArgs], {
|
|
173
252
|
stdio: "inherit",
|
|
174
253
|
});
|
|
175
254
|
process.exit(result.status ?? 1);
|
|
@@ -177,14 +256,16 @@ if (isStartAll) {
|
|
|
177
256
|
// `patchwork start` — opinionated front door over start-all.
|
|
178
257
|
// Defaults to full mode (all tools registered) and the web dashboard, so the
|
|
179
258
|
// doc-promised "patchwork start → everything works" path actually works.
|
|
180
|
-
// Pass-through args still go to start-all.
|
|
259
|
+
// Pass-through args still go to start-all.mjs; --help short-circuits.
|
|
181
260
|
if (process.argv[2] === "start") {
|
|
182
261
|
const passthrough = process.argv.slice(3);
|
|
183
262
|
if (passthrough.includes("--help") || passthrough.includes("-h")) {
|
|
184
263
|
process.stdout.write(`patchwork start — Launch the full Patchwork stack
|
|
185
264
|
|
|
186
|
-
Starts bridge + Claude
|
|
265
|
+
Starts bridge + Claude + dashboard via the cross-platform Node orchestrator.
|
|
187
266
|
Defaults to full mode so all bridge tools are registered.
|
|
267
|
+
On macOS/Linux: uses tmux when available, falls back to background mode.
|
|
268
|
+
On Windows: runs natively via the Node orchestrator (no WSL required).
|
|
188
269
|
|
|
189
270
|
Usage: patchwork start [options]
|
|
190
271
|
|
|
@@ -206,13 +287,20 @@ This is a thin wrapper over \`start-all\`. For advanced flags see:
|
|
|
206
287
|
const args = [...passthrough];
|
|
207
288
|
const slimIdx = args.indexOf("--slim");
|
|
208
289
|
if (slimIdx >= 0) {
|
|
209
|
-
args.splice(slimIdx, 1); //
|
|
290
|
+
args.splice(slimIdx, 1); // slim is the .mjs default; strip so --full isn't re-added below
|
|
210
291
|
}
|
|
211
292
|
else if (!args.includes("--full")) {
|
|
212
293
|
args.push("--full");
|
|
213
294
|
}
|
|
214
|
-
|
|
215
|
-
|
|
295
|
+
// On non-Windows: auto-detect tmux; fall back to --no-tmux background mode if absent.
|
|
296
|
+
if (process.platform !== "win32" && !args.includes("--no-tmux")) {
|
|
297
|
+
const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
|
|
298
|
+
if (tmuxCheck.status !== 0)
|
|
299
|
+
args.push("--no-tmux");
|
|
300
|
+
}
|
|
301
|
+
// Dispatch to the cross-platform Node orchestrator (see above).
|
|
302
|
+
const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
|
|
303
|
+
const result = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
216
304
|
stdio: "inherit",
|
|
217
305
|
});
|
|
218
306
|
process.exit(result.status ?? 1);
|
|
@@ -368,12 +456,6 @@ if (process.argv[2] === "install") {
|
|
|
368
456
|
await runInstall(process.argv.slice(3));
|
|
369
457
|
process.exit(0);
|
|
370
458
|
}
|
|
371
|
-
// Handle marketplace subcommand — browse and install community skills
|
|
372
|
-
if (process.argv[2] === "marketplace") {
|
|
373
|
-
const { runMarketplace } = await import("./commands/marketplace.js");
|
|
374
|
-
await runMarketplace(process.argv.slice(3));
|
|
375
|
-
process.exit(0);
|
|
376
|
-
}
|
|
377
459
|
// Handle tools subcommand — search/list tools without a bridge connection
|
|
378
460
|
if (process.argv[2] === "tools") {
|
|
379
461
|
const { runToolsCommand } = await import("./commands/tools.js");
|
|
@@ -884,6 +966,33 @@ Edit, save, hot-reload — Claude's next turn sees the new tool. See [documents/
|
|
|
884
966
|
}
|
|
885
967
|
process.exit(0);
|
|
886
968
|
}
|
|
969
|
+
// Patchwork: `patchwork recipe` (no subcommand) / `recipe --help` — print
|
|
970
|
+
// the subcommand index. Without this branch, `patchwork recipe` falls through
|
|
971
|
+
// to the bridge daemon, leaving subcommands completely undiscoverable from
|
|
972
|
+
// the CLI (the only way to find them today is to read CLAUDE.md or source).
|
|
973
|
+
if (process.argv[2] === "recipe" &&
|
|
974
|
+
(process.argv[3] === undefined ||
|
|
975
|
+
process.argv[3] === "--help" ||
|
|
976
|
+
process.argv[3] === "-h" ||
|
|
977
|
+
process.argv[3] === "help")) {
|
|
978
|
+
process.stdout.write(`Usage: patchwork recipe <subcommand> [args...]\n\n` +
|
|
979
|
+
`Subcommands:\n` +
|
|
980
|
+
` new <name> Scaffold a recipe (interactive with -i)\n` +
|
|
981
|
+
` list List installed recipes (workspace + user)\n` +
|
|
982
|
+
` run <name> Run a recipe by name\n` +
|
|
983
|
+
` install <src> Install a recipe from a path or GitHub source\n` +
|
|
984
|
+
` uninstall <name> Remove an installed recipe\n` +
|
|
985
|
+
` enable <name> Re-enable a disabled recipe\n` +
|
|
986
|
+
` disable <name> Pause a recipe (scheduled triggers stop firing)\n` +
|
|
987
|
+
` preflight <file> Static-validate a recipe YAML before running\n` +
|
|
988
|
+
` doctor <name|file> Diagnose a recipe: lint + policy + recent halts\n` +
|
|
989
|
+
` lint <file> Run all lint checks on a recipe YAML\n` +
|
|
990
|
+
` fmt <file> Format a recipe YAML in place\n` +
|
|
991
|
+
` schema Print the recipe JSON Schema\n` +
|
|
992
|
+
` audit-env <recipe> Check {{env.FOO}} vars are present in environment\n\n` +
|
|
993
|
+
`Run \`patchwork recipe <subcommand> --help\` for subcommand-specific options.\n`);
|
|
994
|
+
process.exit(0);
|
|
995
|
+
}
|
|
887
996
|
// Patchwork: `patchwork recipe list` — enumerate installed recipes.
|
|
888
997
|
if (process.argv[2] === "recipe" && process.argv[3] === "list") {
|
|
889
998
|
(async () => {
|
|
@@ -959,11 +1068,13 @@ if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
|
|
|
959
1068
|
// a running bridge's /recipes/run endpoint if one is available.
|
|
960
1069
|
if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
961
1070
|
const args = process.argv.slice(4);
|
|
962
|
-
const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
|
|
1071
|
+
const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE] [--attempt <id>] [--ledger-dir <path>]\n";
|
|
963
1072
|
let localFlag = false;
|
|
964
1073
|
let dryRun = false;
|
|
965
1074
|
let recipeRef;
|
|
966
1075
|
let step;
|
|
1076
|
+
let attemptId;
|
|
1077
|
+
let ledgerDir;
|
|
967
1078
|
const vars = {};
|
|
968
1079
|
for (let i = 0; i < args.length; i++) {
|
|
969
1080
|
const arg = args[i];
|
|
@@ -990,6 +1101,29 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
990
1101
|
step = value;
|
|
991
1102
|
continue;
|
|
992
1103
|
}
|
|
1104
|
+
if (currentArg === "--attempt" || currentArg.startsWith("--attempt=")) {
|
|
1105
|
+
const value = currentArg === "--attempt"
|
|
1106
|
+
? args[++i]
|
|
1107
|
+
: currentArg.slice("--attempt=".length);
|
|
1108
|
+
if (!value) {
|
|
1109
|
+
process.stderr.write(`Error: --attempt requires a value\n${usage}`);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
attemptId = value;
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
if (currentArg === "--ledger-dir" ||
|
|
1116
|
+
currentArg.startsWith("--ledger-dir=")) {
|
|
1117
|
+
const value = currentArg === "--ledger-dir"
|
|
1118
|
+
? args[++i]
|
|
1119
|
+
: currentArg.slice("--ledger-dir=".length);
|
|
1120
|
+
if (!value) {
|
|
1121
|
+
process.stderr.write(`Error: --ledger-dir requires a value\n${usage}`);
|
|
1122
|
+
process.exit(1);
|
|
1123
|
+
}
|
|
1124
|
+
ledgerDir = value;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
993
1127
|
if (currentArg === "--var" || currentArg.startsWith("--var=")) {
|
|
994
1128
|
const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
|
|
995
1129
|
if (!assignment) {
|
|
@@ -1036,7 +1170,7 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
1036
1170
|
})();
|
|
1037
1171
|
const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
|
|
1038
1172
|
const lock = localFlag ? null : findBridgeLock();
|
|
1039
|
-
if (lock && !dryRun && !step && !explicitFile) {
|
|
1173
|
+
if (lock && !dryRun && !step && !explicitFile && !attemptId) {
|
|
1040
1174
|
const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
|
|
1041
1175
|
method: "POST",
|
|
1042
1176
|
headers: {
|
|
@@ -1079,9 +1213,37 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
1079
1213
|
? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
|
|
1080
1214
|
: ` Running recipe "${recipeArg}" locally…\n`);
|
|
1081
1215
|
const workdir = lock?.workspace || process.cwd();
|
|
1216
|
+
// PR5c — resume support: when --attempt is given, mint or reuse a
|
|
1217
|
+
// stable id and point the runner at a disk-backed effect ledger.
|
|
1218
|
+
// `--attempt new` always mints a fresh id; any other value is
|
|
1219
|
+
// taken verbatim (so the user can re-run the same attempt and
|
|
1220
|
+
// skip already-completed write tools).
|
|
1221
|
+
let resolvedAttempt;
|
|
1222
|
+
let resolvedLedgerDir;
|
|
1223
|
+
if (attemptId !== undefined) {
|
|
1224
|
+
resolvedAttempt =
|
|
1225
|
+
attemptId === "new"
|
|
1226
|
+
? `mr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
1227
|
+
: attemptId;
|
|
1228
|
+
// Validate at the CLI boundary so an invalid id fails loudly
|
|
1229
|
+
// before any side effects run (and before it lands in the run
|
|
1230
|
+
// log or hashed into a ledger scope key).
|
|
1231
|
+
try {
|
|
1232
|
+
const { assertValidManualRunId } = await import("./recipes/idempotencyKey.js");
|
|
1233
|
+
assertValidManualRunId(resolvedAttempt);
|
|
1234
|
+
}
|
|
1235
|
+
catch (err) {
|
|
1236
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
resolvedLedgerDir = ledgerDir ?? path.join(os.homedir(), ".patchwork");
|
|
1240
|
+
process.stdout.write(` Attempt id: ${resolvedAttempt} (ledger: ${resolvedLedgerDir})\n`);
|
|
1241
|
+
}
|
|
1082
1242
|
const run = await runRecipe(recipeArg, {
|
|
1083
1243
|
...(step ? { step } : {}),
|
|
1084
1244
|
...(seedVars ? { vars: seedVars } : {}),
|
|
1245
|
+
...(resolvedAttempt && { manualRunId: resolvedAttempt }),
|
|
1246
|
+
...(resolvedLedgerDir && { ledgerDir: resolvedLedgerDir }),
|
|
1085
1247
|
workdir,
|
|
1086
1248
|
});
|
|
1087
1249
|
if (run.stepSelection) {
|
|
@@ -1401,8 +1563,9 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
|
|
|
1401
1563
|
}
|
|
1402
1564
|
const plain = decryptTraceBundle(raw, passphrase);
|
|
1403
1565
|
const { tmpdir } = await import("node:os");
|
|
1566
|
+
const { join: pathJoin } = await import("node:path");
|
|
1404
1567
|
const { writeFileSync } = await import("node:fs");
|
|
1405
|
-
const tmp =
|
|
1568
|
+
const tmp = pathJoin(tmpdir(), `patchwork-import-${Date.now()}.jsonl.gz`);
|
|
1406
1569
|
writeFileSync(tmp, plain, { mode: 0o600 });
|
|
1407
1570
|
input = tmp;
|
|
1408
1571
|
process.stderr.write("Decryption succeeded.\n");
|
|
@@ -1434,6 +1597,513 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
|
|
|
1434
1597
|
}
|
|
1435
1598
|
})();
|
|
1436
1599
|
}
|
|
1600
|
+
// `patchwork kill-switch engage|release|status` — issue #422 step 3.
|
|
1601
|
+
//
|
|
1602
|
+
// Discovers the running bridge via lock file, POSTs /kill-switch with
|
|
1603
|
+
// Bearer auth, and surfaces structured errors (env-locked, no-bridge,
|
|
1604
|
+
// wedged-bridge). Multi-bridge fan-out: iterates ALL live `isBridge:true`
|
|
1605
|
+
// locks and engages/releases each (v2-B2 from #422).
|
|
1606
|
+
//
|
|
1607
|
+
// v2-I4: mandatory 10s deadline per request. No silent fallback on
|
|
1608
|
+
// timeout/ECONNREFUSED/non-2xx — error message + exit non-zero.
|
|
1609
|
+
if (process.argv[2] === "kill-switch") {
|
|
1610
|
+
const sub = process.argv[3];
|
|
1611
|
+
if (!sub || (sub !== "engage" && sub !== "release" && sub !== "status")) {
|
|
1612
|
+
process.stderr.write('Usage: patchwork kill-switch <engage|release|status> [--reason "..."]\n' +
|
|
1613
|
+
"\n" +
|
|
1614
|
+
" engage Block all write-tier tool calls across every running bridge.\n" +
|
|
1615
|
+
" release Resume writes.\n" +
|
|
1616
|
+
" status Print engaged/locked state per running bridge.\n" +
|
|
1617
|
+
"\n" +
|
|
1618
|
+
"Exits non-zero if any bridge is unreachable or env-locked.\n");
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
(async () => {
|
|
1622
|
+
try {
|
|
1623
|
+
// Parse optional flags early so --force-local can be used without a bridge.
|
|
1624
|
+
const args = process.argv.slice(4);
|
|
1625
|
+
const reasonIdx = args.findIndex((a) => a === "--reason" || a === "-m");
|
|
1626
|
+
const reason = reasonIdx >= 0 && reasonIdx + 1 < args.length
|
|
1627
|
+
? args[reasonIdx + 1]
|
|
1628
|
+
: undefined;
|
|
1629
|
+
// v2-I4: --force-local writes flags.json directly when no live bridge
|
|
1630
|
+
// is reachable. The running bridge's fs.watch (v2-S1) picks up the
|
|
1631
|
+
// change within ~100ms; without a running bridge this is "effective
|
|
1632
|
+
// next boot" — which is still better than a silent noop.
|
|
1633
|
+
const forceLocal = args.includes("--force-local");
|
|
1634
|
+
// v2-B2: enumerate ALL live bridge locks (not just the first).
|
|
1635
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
1636
|
+
const liveLocks = findAllLiveBridges();
|
|
1637
|
+
if (liveLocks.length === 0) {
|
|
1638
|
+
if (forceLocal && (sub === "engage" || sub === "release")) {
|
|
1639
|
+
// --force-local: write flags.json directly. The running bridge's
|
|
1640
|
+
// fs.watch picks this up within ~100ms; if the bridge is wedged
|
|
1641
|
+
// or not started, this is effective on next start.
|
|
1642
|
+
const { setFlag, KILL_SWITCH_WRITES } = await import("./featureFlags.js");
|
|
1643
|
+
const engage = sub === "engage";
|
|
1644
|
+
setFlag(KILL_SWITCH_WRITES, engage, true);
|
|
1645
|
+
// Audit in a sibling CLI-only JSONL (v2-I10: bridge-only writes
|
|
1646
|
+
// go to decision_traces.jsonl; CLI fallback is distinct).
|
|
1647
|
+
const os = await import("node:os");
|
|
1648
|
+
const path = await import("node:path");
|
|
1649
|
+
const fs = await import("node:fs");
|
|
1650
|
+
const cliTraceFile = path.join(process.env.PATCHWORK_HOME ??
|
|
1651
|
+
path.join(os.default.homedir(), ".patchwork"), "decision_traces.cli.jsonl");
|
|
1652
|
+
const dir = path.dirname(cliTraceFile);
|
|
1653
|
+
if (!fs.existsSync(dir))
|
|
1654
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1655
|
+
const entry = JSON.stringify({
|
|
1656
|
+
ts: new Date().toISOString(),
|
|
1657
|
+
event: engage ? "engage" : "release",
|
|
1658
|
+
actor: "cli-force-local",
|
|
1659
|
+
...(reason ? { reason } : {}),
|
|
1660
|
+
});
|
|
1661
|
+
fs.appendFileSync(cliTraceFile, `${entry}\n`);
|
|
1662
|
+
process.stdout.write(` ✓ kill-switch ${engage ? "ENGAGED" : "released"} via --force-local (flags.json written directly).\n` +
|
|
1663
|
+
" Running bridges will pick this up via fs.watch within ~100ms.\n");
|
|
1664
|
+
process.exit(0);
|
|
1665
|
+
}
|
|
1666
|
+
process.stderr.write("No running bridge found.\n" +
|
|
1667
|
+
" - For `engage`/`release`, kill-switch has no live target to update.\n" +
|
|
1668
|
+
" - Use --force-local to write flags.json directly (bridge fs.watch picks it up).\n" +
|
|
1669
|
+
" - Or restart the bridge and re-run this command.\n");
|
|
1670
|
+
process.exit(2);
|
|
1671
|
+
}
|
|
1672
|
+
// v2-I4: 10s per-request deadline. AbortController per call.
|
|
1673
|
+
async function callBridge(lock, method, body) {
|
|
1674
|
+
const controller = new AbortController();
|
|
1675
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1676
|
+
try {
|
|
1677
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/kill-switch`, {
|
|
1678
|
+
method,
|
|
1679
|
+
headers: {
|
|
1680
|
+
Authorization: `Bearer ${lock.authToken}`,
|
|
1681
|
+
"Content-Type": "application/json",
|
|
1682
|
+
},
|
|
1683
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
1684
|
+
signal: controller.signal,
|
|
1685
|
+
});
|
|
1686
|
+
let json;
|
|
1687
|
+
try {
|
|
1688
|
+
json = (await res.json());
|
|
1689
|
+
}
|
|
1690
|
+
catch {
|
|
1691
|
+
json = undefined;
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
ok: res.status >= 200 && res.status < 300,
|
|
1695
|
+
status: res.status,
|
|
1696
|
+
...(json ? { json } : {}),
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
catch (err) {
|
|
1700
|
+
return {
|
|
1701
|
+
ok: false,
|
|
1702
|
+
status: 0,
|
|
1703
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
finally {
|
|
1707
|
+
clearTimeout(timer);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (sub === "status") {
|
|
1711
|
+
let anyFailed = false;
|
|
1712
|
+
for (const lock of liveLocks) {
|
|
1713
|
+
const result = await callBridge(lock, "GET");
|
|
1714
|
+
if (!result.ok) {
|
|
1715
|
+
anyFailed = true;
|
|
1716
|
+
process.stderr.write(` ✗ bridge pid=${lock.pid} port=${lock.port} unreachable (${result.error ?? `status ${result.status}`})\n`);
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const j = result.json ?? {};
|
|
1720
|
+
const engaged = j.engaged === true ? "ENGAGED" : "released";
|
|
1721
|
+
const lockedSuffix = j.locked
|
|
1722
|
+
? ` [env-locked: ${j.lockedReason ?? "yes"}]`
|
|
1723
|
+
: "";
|
|
1724
|
+
const wsLabel = lock.workspace
|
|
1725
|
+
? lock.workspace.split("/").slice(-2).join("/")
|
|
1726
|
+
: `pid=${lock.pid}`;
|
|
1727
|
+
process.stdout.write(` ${engaged} port=${lock.port} ${wsLabel}${lockedSuffix}\n`);
|
|
1728
|
+
}
|
|
1729
|
+
process.exit(anyFailed ? 2 : 0);
|
|
1730
|
+
}
|
|
1731
|
+
// engage / release: POST to every live bridge, surface aggregate result.
|
|
1732
|
+
const engage = sub === "engage";
|
|
1733
|
+
let anyFailed = false;
|
|
1734
|
+
let anyChanged = false;
|
|
1735
|
+
for (const lock of liveLocks) {
|
|
1736
|
+
const result = await callBridge(lock, "POST", {
|
|
1737
|
+
engage,
|
|
1738
|
+
...(reason ? { reason } : {}),
|
|
1739
|
+
});
|
|
1740
|
+
const wsLabel = lock.workspace
|
|
1741
|
+
? lock.workspace.split("/").slice(-2).join("/")
|
|
1742
|
+
: `pid=${lock.pid}`;
|
|
1743
|
+
if (result.status === 409) {
|
|
1744
|
+
anyFailed = true;
|
|
1745
|
+
const lr = result.json?.lockedReason ??
|
|
1746
|
+
"env-locked at boot";
|
|
1747
|
+
process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: cannot ${sub} — ${lr}\n`);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
if (!result.ok) {
|
|
1751
|
+
anyFailed = true;
|
|
1752
|
+
process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: ${result.error ?? `status ${result.status}`}\n`);
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
const j = result.json ?? {};
|
|
1756
|
+
const changedTag = j.changed === true ? "" : " (no-op, already in state)";
|
|
1757
|
+
if (j.changed === true)
|
|
1758
|
+
anyChanged = true;
|
|
1759
|
+
process.stdout.write(` ✓ port=${lock.port} ${wsLabel}: ${engage ? "ENGAGED" : "released"}${changedTag}\n`);
|
|
1760
|
+
}
|
|
1761
|
+
if (anyFailed) {
|
|
1762
|
+
process.exit(2);
|
|
1763
|
+
}
|
|
1764
|
+
if (!anyChanged) {
|
|
1765
|
+
process.stdout.write(`\n All ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"} already in target state — no audit emit.\n`);
|
|
1766
|
+
}
|
|
1767
|
+
else {
|
|
1768
|
+
process.stdout.write(`\n Kill-switch ${engage ? "engaged" : "released"} on ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"}.\n`);
|
|
1769
|
+
}
|
|
1770
|
+
process.exit(0);
|
|
1771
|
+
}
|
|
1772
|
+
catch (err) {
|
|
1773
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
}
|
|
1776
|
+
})();
|
|
1777
|
+
}
|
|
1778
|
+
// `patchwork panic` — alias for `patchwork kill-switch engage` (v2-Strong-2).
|
|
1779
|
+
//
|
|
1780
|
+
// Discoverable under stress (short command, obvious intent). Canonical noun
|
|
1781
|
+
// form is `kill-switch engage`; this alias matches it so shell history six
|
|
1782
|
+
// months later still makes sense. Does not accept sub-verbs — just runs engage.
|
|
1783
|
+
if (process.argv[2] === "panic") {
|
|
1784
|
+
const extra = process.argv.slice(3); // e.g. --reason "..." --force-local
|
|
1785
|
+
// Guard against `panic --help` engaging the kill switch — a real
|
|
1786
|
+
// footgun if you tab-completed the verb to confirm syntax before
|
|
1787
|
+
// committing to the action. `panic` is an alias, so we honor --help
|
|
1788
|
+
// here ourselves rather than forwarding to kill-switch engage.
|
|
1789
|
+
if (extra.includes("--help") || extra.includes("-h")) {
|
|
1790
|
+
console.log('Usage: patchwork panic [--reason "..."] [--force-local]\n\n' +
|
|
1791
|
+
" Alias for `patchwork kill-switch engage` — blocks all write-tier\n" +
|
|
1792
|
+
" tool calls across every running bridge. Use --reason to leave a\n" +
|
|
1793
|
+
" note in the audit trail. Release with `patchwork kill-switch release`.\n");
|
|
1794
|
+
process.exit(0);
|
|
1795
|
+
}
|
|
1796
|
+
// Spawn self with kill-switch engage to reuse the full handler without
|
|
1797
|
+
// duplicating 200+ LOC. Passes through any flags (--reason, --force-local).
|
|
1798
|
+
import("node:child_process").then(({ spawnSync }) => {
|
|
1799
|
+
const self = process.argv[1] ?? process.execPath;
|
|
1800
|
+
const result = spawnSync(process.execPath, [self, "kill-switch", "engage", ...extra], { stdio: "inherit" });
|
|
1801
|
+
process.exit(result.status ?? 1);
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
// `patchwork halts` — one-screen morning summary of recent recipe halts.
|
|
1805
|
+
//
|
|
1806
|
+
// Composes the haltReason field (#441), category aggregator + endpoint
|
|
1807
|
+
// (#444), and dashboard pill conventions: queries the live bridge's
|
|
1808
|
+
// /runs/halt-summary endpoint over the chosen window and prints a
|
|
1809
|
+
// per-category breakdown plus the 5 most-recent halt reasons. Default
|
|
1810
|
+
// window is "overnight" (since 6pm yesterday local) so it lines up with
|
|
1811
|
+
// "what halted while I was asleep?".
|
|
1812
|
+
if (process.argv[2] === "halts") {
|
|
1813
|
+
const args = process.argv.slice(3);
|
|
1814
|
+
const wantHelp = args.includes("--help") || args.includes("-h");
|
|
1815
|
+
if (wantHelp) {
|
|
1816
|
+
process.stdout.write("Usage: patchwork halts [--window <name>] [--recipe <name>] [--json]\n" +
|
|
1817
|
+
"\n" +
|
|
1818
|
+
" --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
|
|
1819
|
+
" --recipe <name> filter to one recipe by name\n" +
|
|
1820
|
+
" --json emit raw JSON (for scripting)\n" +
|
|
1821
|
+
"\n" +
|
|
1822
|
+
'"overnight" = since 6pm yesterday local time.\n');
|
|
1823
|
+
process.exit(0);
|
|
1824
|
+
}
|
|
1825
|
+
function parseWindow() {
|
|
1826
|
+
const idx = args.findIndex((a) => a === "--window" || a === "-w");
|
|
1827
|
+
const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
|
|
1828
|
+
if (raw === "1h" ||
|
|
1829
|
+
raw === "24h" ||
|
|
1830
|
+
raw === "overnight" ||
|
|
1831
|
+
raw === "7d" ||
|
|
1832
|
+
raw === "any")
|
|
1833
|
+
return raw;
|
|
1834
|
+
process.stderr.write(`Unknown --window value: "${raw}"\n`);
|
|
1835
|
+
process.exit(1);
|
|
1836
|
+
}
|
|
1837
|
+
function windowSinceMs(w) {
|
|
1838
|
+
if (w === "any")
|
|
1839
|
+
return null;
|
|
1840
|
+
if (w === "1h")
|
|
1841
|
+
return 60 * 60 * 1000;
|
|
1842
|
+
if (w === "24h")
|
|
1843
|
+
return 24 * 60 * 60 * 1000;
|
|
1844
|
+
if (w === "7d")
|
|
1845
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
1846
|
+
const d = new Date();
|
|
1847
|
+
d.setHours(18, 0, 0, 0);
|
|
1848
|
+
if (d.getTime() > Date.now())
|
|
1849
|
+
d.setDate(d.getDate() - 1);
|
|
1850
|
+
return Date.now() - d.getTime();
|
|
1851
|
+
}
|
|
1852
|
+
const window = parseWindow();
|
|
1853
|
+
const wantJson = args.includes("--json");
|
|
1854
|
+
const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
|
|
1855
|
+
const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
|
|
1856
|
+
? args[recipeIdx + 1]
|
|
1857
|
+
: undefined;
|
|
1858
|
+
(async () => {
|
|
1859
|
+
try {
|
|
1860
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
1861
|
+
const liveLocks = findAllLiveBridges();
|
|
1862
|
+
if (liveLocks.length === 0) {
|
|
1863
|
+
process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
|
|
1864
|
+
process.exit(2);
|
|
1865
|
+
}
|
|
1866
|
+
const sinceMs = windowSinceMs(window);
|
|
1867
|
+
const params = [];
|
|
1868
|
+
if (sinceMs != null)
|
|
1869
|
+
params.push(`sinceMs=${sinceMs}`);
|
|
1870
|
+
if (recipeFilter)
|
|
1871
|
+
params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
|
|
1872
|
+
const qs = params.length > 0 ? `?${params.join("&")}` : "";
|
|
1873
|
+
// Walk live bridges in order; first responsive one wins. See the
|
|
1874
|
+
// matching block in the `judgments` handler — findAllLiveBridges
|
|
1875
|
+
// can include stale entries when a recycled PID still answers
|
|
1876
|
+
// `kill(pid, 0)` but the lock points at a dead bridge.
|
|
1877
|
+
let res = null;
|
|
1878
|
+
let lastStatus = 0;
|
|
1879
|
+
for (const lock of liveLocks) {
|
|
1880
|
+
const controller = new AbortController();
|
|
1881
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1882
|
+
try {
|
|
1883
|
+
const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
|
|
1884
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
1885
|
+
signal: controller.signal,
|
|
1886
|
+
});
|
|
1887
|
+
if (candidate.ok) {
|
|
1888
|
+
res = candidate;
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
lastStatus = candidate.status;
|
|
1892
|
+
}
|
|
1893
|
+
catch {
|
|
1894
|
+
/* unreachable lock — try next */
|
|
1895
|
+
}
|
|
1896
|
+
finally {
|
|
1897
|
+
clearTimeout(timer);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (!res) {
|
|
1901
|
+
process.stderr.write(`No live bridge served /runs/halt-summary (last status: ${lastStatus || "unreachable"}).\n`);
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
const summary = (await res.json());
|
|
1905
|
+
if (wantJson) {
|
|
1906
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
1907
|
+
process.exit(0);
|
|
1908
|
+
}
|
|
1909
|
+
// Shared label + hint maps (also used by `recipe doctor` and
|
|
1910
|
+
// mirrored in the dashboard) so the wording stays consistent.
|
|
1911
|
+
const { HALT_CATEGORY_LABELS, HALT_CATEGORY_HINTS } = await import("./recipes/haltCategory.js");
|
|
1912
|
+
const windowLabel = {
|
|
1913
|
+
"1h": "last hour",
|
|
1914
|
+
"24h": "last 24h",
|
|
1915
|
+
overnight: "since 6pm yesterday",
|
|
1916
|
+
"7d": "last 7 days",
|
|
1917
|
+
any: "all time",
|
|
1918
|
+
};
|
|
1919
|
+
const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
|
|
1920
|
+
process.stdout.write(`Halts — ${windowLabel[window]}${recipeSuffix}\n`);
|
|
1921
|
+
process.stdout.write(`Total: ${summary.total}\n`);
|
|
1922
|
+
if (summary.total === 0) {
|
|
1923
|
+
process.stdout.write("\n (nothing halted in this window)\n");
|
|
1924
|
+
process.exit(0);
|
|
1925
|
+
}
|
|
1926
|
+
const entries = Object.entries(summary.byCategory).sort(([, a], [, b]) => b - a);
|
|
1927
|
+
process.stdout.write("\nBy category:\n");
|
|
1928
|
+
for (const [cat, count] of entries) {
|
|
1929
|
+
const label = HALT_CATEGORY_LABELS[cat] ?? cat;
|
|
1930
|
+
const hint = HALT_CATEGORY_HINTS[cat];
|
|
1931
|
+
const hintSuffix = hint ? ` — ${hint}` : "";
|
|
1932
|
+
process.stdout.write(` ${String(count).padStart(3)} ${label}${hintSuffix}\n`);
|
|
1933
|
+
}
|
|
1934
|
+
if (summary.recent.length > 0) {
|
|
1935
|
+
process.stdout.write("\nMost recent:\n");
|
|
1936
|
+
for (const r of summary.recent) {
|
|
1937
|
+
// Truncate the reason to ~120 chars so a wide stack trace
|
|
1938
|
+
// can't blow up the terminal width on phones / narrow panes.
|
|
1939
|
+
const reason = r.reason.length > 120 ? `${r.reason.slice(0, 117)}…` : r.reason;
|
|
1940
|
+
process.stdout.write(` #${r.runSeq} [${r.category}] ${reason}\n`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
process.exit(0);
|
|
1944
|
+
}
|
|
1945
|
+
catch (err) {
|
|
1946
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
})();
|
|
1950
|
+
}
|
|
1951
|
+
// `patchwork analytics` — manage the self-hosted telemetry collector config.
|
|
1952
|
+
// Replaces the brittle "endpoint+secret in launchd plist" pattern with a
|
|
1953
|
+
// proper config file the bridge reads at startup.
|
|
1954
|
+
if (process.argv[2] === "analytics") {
|
|
1955
|
+
(async () => {
|
|
1956
|
+
const { runAnalyticsCommand } = await import("./commands/analytics.js");
|
|
1957
|
+
const code = await runAnalyticsCommand(process.argv.slice(3));
|
|
1958
|
+
process.exit(code);
|
|
1959
|
+
})();
|
|
1960
|
+
}
|
|
1961
|
+
// `patchwork judgments` — PR3b sibling of `patchwork halts`. Same window
|
|
1962
|
+
// + recipe filter shape; queries /runs/judge-summary and prints a
|
|
1963
|
+
// per-verdict breakdown plus the 5 most-recent verdicts.
|
|
1964
|
+
if (process.argv[2] === "judgments") {
|
|
1965
|
+
const args = process.argv.slice(3);
|
|
1966
|
+
const wantHelp = args.includes("--help") || args.includes("-h");
|
|
1967
|
+
if (wantHelp) {
|
|
1968
|
+
process.stdout.write("Usage: patchwork judgments [--window <name>] [--recipe <name>] [--json]\n" +
|
|
1969
|
+
"\n" +
|
|
1970
|
+
" --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
|
|
1971
|
+
" --recipe <name> filter to one recipe by name\n" +
|
|
1972
|
+
" --json emit raw JSON (for scripting)\n" +
|
|
1973
|
+
"\n" +
|
|
1974
|
+
'"overnight" = since 6pm yesterday local time.\n');
|
|
1975
|
+
process.exit(0);
|
|
1976
|
+
}
|
|
1977
|
+
function parseWindow() {
|
|
1978
|
+
const idx = args.findIndex((a) => a === "--window" || a === "-w");
|
|
1979
|
+
const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
|
|
1980
|
+
if (raw === "1h" ||
|
|
1981
|
+
raw === "24h" ||
|
|
1982
|
+
raw === "overnight" ||
|
|
1983
|
+
raw === "7d" ||
|
|
1984
|
+
raw === "any")
|
|
1985
|
+
return raw;
|
|
1986
|
+
process.stderr.write(`Unknown --window value: "${raw}"\n`);
|
|
1987
|
+
process.exit(1);
|
|
1988
|
+
}
|
|
1989
|
+
function windowSinceMs(w) {
|
|
1990
|
+
if (w === "any")
|
|
1991
|
+
return null;
|
|
1992
|
+
if (w === "1h")
|
|
1993
|
+
return 60 * 60 * 1000;
|
|
1994
|
+
if (w === "24h")
|
|
1995
|
+
return 24 * 60 * 60 * 1000;
|
|
1996
|
+
if (w === "7d")
|
|
1997
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
1998
|
+
const d = new Date();
|
|
1999
|
+
d.setHours(18, 0, 0, 0);
|
|
2000
|
+
if (d.getTime() > Date.now())
|
|
2001
|
+
d.setDate(d.getDate() - 1);
|
|
2002
|
+
return Date.now() - d.getTime();
|
|
2003
|
+
}
|
|
2004
|
+
const window = parseWindow();
|
|
2005
|
+
const wantJson = args.includes("--json");
|
|
2006
|
+
const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
|
|
2007
|
+
const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
|
|
2008
|
+
? args[recipeIdx + 1]
|
|
2009
|
+
: undefined;
|
|
2010
|
+
(async () => {
|
|
2011
|
+
try {
|
|
2012
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
2013
|
+
const liveLocks = findAllLiveBridges();
|
|
2014
|
+
if (liveLocks.length === 0) {
|
|
2015
|
+
process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
|
|
2016
|
+
process.exit(2);
|
|
2017
|
+
}
|
|
2018
|
+
const sinceMs = windowSinceMs(window);
|
|
2019
|
+
const params = [];
|
|
2020
|
+
if (sinceMs != null)
|
|
2021
|
+
params.push(`sinceMs=${sinceMs}`);
|
|
2022
|
+
if (recipeFilter)
|
|
2023
|
+
params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
|
|
2024
|
+
const qs = params.length > 0 ? `?${params.join("&")}` : "";
|
|
2025
|
+
// Walk live bridges in order; the first responsive one wins.
|
|
2026
|
+
// findAllLiveBridges uses `kill(pid, 0)` for liveness, which
|
|
2027
|
+
// returns true for any recycled PID — so liveLocks can contain
|
|
2028
|
+
// stale entries from dead bridges. Previously we picked [0]
|
|
2029
|
+
// unconditionally and surfaced a confusing 404; now we try each
|
|
2030
|
+
// and only fall through to the error path when *all* fail.
|
|
2031
|
+
let res = null;
|
|
2032
|
+
let lastStatus = 0;
|
|
2033
|
+
for (const lock of liveLocks) {
|
|
2034
|
+
const controller = new AbortController();
|
|
2035
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
2036
|
+
try {
|
|
2037
|
+
const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/judge-summary${qs}`, {
|
|
2038
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
2039
|
+
signal: controller.signal,
|
|
2040
|
+
});
|
|
2041
|
+
if (candidate.ok) {
|
|
2042
|
+
res = candidate;
|
|
2043
|
+
break;
|
|
2044
|
+
}
|
|
2045
|
+
lastStatus = candidate.status;
|
|
2046
|
+
}
|
|
2047
|
+
catch {
|
|
2048
|
+
/* unreachable lock — try next */
|
|
2049
|
+
}
|
|
2050
|
+
finally {
|
|
2051
|
+
clearTimeout(timer);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (!res) {
|
|
2055
|
+
process.stderr.write(`No live bridge served /runs/judge-summary (last status: ${lastStatus || "unreachable"}).\n`);
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
const summary = (await res.json());
|
|
2059
|
+
if (wantJson) {
|
|
2060
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
2061
|
+
process.exit(0);
|
|
2062
|
+
}
|
|
2063
|
+
const labels = {
|
|
2064
|
+
approve: "approve",
|
|
2065
|
+
request_changes: "request changes",
|
|
2066
|
+
unparseable: "unparseable",
|
|
2067
|
+
};
|
|
2068
|
+
const windowLabel = {
|
|
2069
|
+
"1h": "last hour",
|
|
2070
|
+
"24h": "last 24h",
|
|
2071
|
+
overnight: "since 6pm yesterday",
|
|
2072
|
+
"7d": "last 7 days",
|
|
2073
|
+
any: "all time",
|
|
2074
|
+
};
|
|
2075
|
+
const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
|
|
2076
|
+
process.stdout.write(`Judgments — ${windowLabel[window]}${recipeSuffix}\n`);
|
|
2077
|
+
process.stdout.write(`Total: ${summary.total}\n`);
|
|
2078
|
+
if (summary.total === 0) {
|
|
2079
|
+
process.stdout.write("\n (no judge steps fired in this window)\n");
|
|
2080
|
+
process.exit(0);
|
|
2081
|
+
}
|
|
2082
|
+
const entries = Object.entries(summary.byVerdict).sort(([, a], [, b]) => b - a);
|
|
2083
|
+
process.stdout.write("\nBy verdict:\n");
|
|
2084
|
+
for (const [verdict, count] of entries) {
|
|
2085
|
+
const label = labels[verdict] ?? verdict;
|
|
2086
|
+
process.stdout.write(` ${String(count).padStart(3)} ${label}\n`);
|
|
2087
|
+
}
|
|
2088
|
+
if (summary.recent.length > 0) {
|
|
2089
|
+
process.stdout.write("\nMost recent:\n");
|
|
2090
|
+
for (const r of summary.recent) {
|
|
2091
|
+
const reason = r.firstReason
|
|
2092
|
+
? r.firstReason.length > 120
|
|
2093
|
+
? `${r.firstReason.slice(0, 117)}…`
|
|
2094
|
+
: r.firstReason
|
|
2095
|
+
: "(no reason)";
|
|
2096
|
+
process.stdout.write(` #${r.runSeq} [${r.verdict}] ${r.stepId}: ${reason}\n`);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
process.exit(0);
|
|
2100
|
+
}
|
|
2101
|
+
catch (err) {
|
|
2102
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2103
|
+
process.exit(1);
|
|
2104
|
+
}
|
|
2105
|
+
})();
|
|
2106
|
+
}
|
|
1437
2107
|
if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
|
|
1438
2108
|
const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
|
|
1439
2109
|
(async () => {
|
|
@@ -1453,44 +2123,62 @@ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
|
|
|
1453
2123
|
})();
|
|
1454
2124
|
}
|
|
1455
2125
|
// Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
|
|
2126
|
+
// With `--interactive`, drops into a connector-aware prompt tree instead.
|
|
1456
2127
|
if (process.argv[2] === "recipe" && process.argv[3] === "new") {
|
|
1457
2128
|
const args = process.argv.slice(4);
|
|
1458
|
-
const
|
|
1459
|
-
if (
|
|
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 {
|
|
2129
|
+
const isInteractive = args.includes("--interactive") || args.includes("-i");
|
|
2130
|
+
if (isInteractive) {
|
|
1474
2131
|
(async () => {
|
|
1475
2132
|
try {
|
|
1476
|
-
const {
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
2133
|
+
const { runNewInteractive } = await import("./commands/recipe.js");
|
|
2134
|
+
const { createInterface } = await import("node:readline/promises");
|
|
2135
|
+
const rl = createInterface({
|
|
2136
|
+
input: process.stdin,
|
|
2137
|
+
output: process.stdout,
|
|
2138
|
+
});
|
|
1482
2139
|
const outIdx = args.indexOf("--out");
|
|
1483
2140
|
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
2141
|
const outputDir = outRaw ? path.resolve(outRaw) : undefined;
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
2142
|
+
const deps = {
|
|
2143
|
+
ask: async (q) => (await rl.question(`${q}: `)).trim(),
|
|
2144
|
+
pickFromList: async (q, options) => {
|
|
2145
|
+
process.stdout.write(`\n${q}\n`);
|
|
2146
|
+
options.forEach((opt, i) => {
|
|
2147
|
+
process.stdout.write(` ${i + 1}. ${opt}\n`);
|
|
2148
|
+
});
|
|
2149
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2150
|
+
const raw = (await rl.question(`Choose 1-${options.length}: `)).trim();
|
|
2151
|
+
const idx = Number.parseInt(raw, 10);
|
|
2152
|
+
if (Number.isFinite(idx) && idx >= 1 && idx <= options.length) {
|
|
2153
|
+
return idx;
|
|
2154
|
+
}
|
|
2155
|
+
process.stdout.write(`Invalid choice. Enter a number 1-${options.length}.\n`);
|
|
2156
|
+
}
|
|
2157
|
+
throw new Error("Too many invalid choices");
|
|
2158
|
+
},
|
|
2159
|
+
confirm: async (q) => {
|
|
2160
|
+
const a = (await rl.question(`${q} [y/N]: `)).trim().toLowerCase();
|
|
2161
|
+
return a === "y" || a === "yes";
|
|
2162
|
+
},
|
|
2163
|
+
preview: (yaml) => {
|
|
2164
|
+
process.stdout.write("\n--- Preview ---\n");
|
|
2165
|
+
process.stdout.write(yaml);
|
|
2166
|
+
process.stdout.write("---\n\n");
|
|
2167
|
+
},
|
|
2168
|
+
};
|
|
2169
|
+
const result = await runNewInteractive({
|
|
2170
|
+
deps,
|
|
1491
2171
|
...(outputDir ? { outputDir } : {}),
|
|
1492
2172
|
});
|
|
2173
|
+
rl.close();
|
|
1493
2174
|
process.stdout.write(` ✓ Created ${result.path}\n`);
|
|
2175
|
+
if (result.warnings.length > 0) {
|
|
2176
|
+
process.stdout.write(`\n ⚠ Lint warnings (recipe still written):\n`);
|
|
2177
|
+
for (const w of result.warnings) {
|
|
2178
|
+
process.stdout.write(` [${w.level}] ${w.message}\n`);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
process.stdout.write(`\n Run with: patchwork recipe run ${result.path}\n`);
|
|
1494
2182
|
process.exit(0);
|
|
1495
2183
|
}
|
|
1496
2184
|
catch (err) {
|
|
@@ -1499,6 +2187,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
|
|
|
1499
2187
|
}
|
|
1500
2188
|
})();
|
|
1501
2189
|
}
|
|
2190
|
+
else {
|
|
2191
|
+
const recipeName = args[0];
|
|
2192
|
+
if (!recipeName) {
|
|
2193
|
+
process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
|
|
2194
|
+
" --interactive (-i) Run the connector-aware prompt tree instead of using a template.\n" +
|
|
2195
|
+
" --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
|
|
2196
|
+
" Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
|
|
2197
|
+
" write into the current directory instead.\n");
|
|
2198
|
+
process.stderr.write("\nTemplates:\n");
|
|
2199
|
+
(async () => {
|
|
2200
|
+
const { listTemplates } = await import("./commands/recipe.js");
|
|
2201
|
+
for (const t of listTemplates()) {
|
|
2202
|
+
process.stderr.write(` ${t}\n`);
|
|
2203
|
+
}
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
})();
|
|
2206
|
+
}
|
|
2207
|
+
else {
|
|
2208
|
+
(async () => {
|
|
2209
|
+
try {
|
|
2210
|
+
const { runNew } = await import("./commands/recipe.js");
|
|
2211
|
+
const templateIdx = args.indexOf("--template");
|
|
2212
|
+
const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
|
|
2213
|
+
const descIdx = args.indexOf("--desc");
|
|
2214
|
+
const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
|
|
2215
|
+
`Recipe: ${recipeName}`;
|
|
2216
|
+
const outIdx = args.indexOf("--out");
|
|
2217
|
+
const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
|
|
2218
|
+
// `--out .` is the common case for "scaffold in cwd" — resolve so
|
|
2219
|
+
// the success message shows the absolute path the user can open.
|
|
2220
|
+
const outputDir = outRaw ? path.resolve(outRaw) : undefined;
|
|
2221
|
+
const result = runNew({
|
|
2222
|
+
name: recipeName,
|
|
2223
|
+
description,
|
|
2224
|
+
...(template ? { template } : {}),
|
|
2225
|
+
...(outputDir ? { outputDir } : {}),
|
|
2226
|
+
});
|
|
2227
|
+
process.stdout.write(` ✓ Created ${result.path}\n`);
|
|
2228
|
+
process.exit(0);
|
|
2229
|
+
}
|
|
2230
|
+
catch (err) {
|
|
2231
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2232
|
+
process.exit(1);
|
|
2233
|
+
}
|
|
2234
|
+
})();
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
1502
2237
|
}
|
|
1503
2238
|
// Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
|
|
1504
2239
|
if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
|
|
@@ -1530,6 +2265,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
|
|
|
1530
2265
|
}
|
|
1531
2266
|
})();
|
|
1532
2267
|
}
|
|
2268
|
+
// Patchwork: `patchwork recipe audit-env <recipe>` — check {{env.FOO}} references are satisfied.
|
|
2269
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "audit-env") {
|
|
2270
|
+
const recipeArg = process.argv[4];
|
|
2271
|
+
if (!recipeArg) {
|
|
2272
|
+
process.stderr.write("Usage: patchwork recipe audit-env <recipe> [--env-file <path>] [--workspace <path>]\n");
|
|
2273
|
+
process.exit(1);
|
|
2274
|
+
}
|
|
2275
|
+
(async () => {
|
|
2276
|
+
try {
|
|
2277
|
+
const args = process.argv.slice(5);
|
|
2278
|
+
const envFileIdx = args.indexOf("--env-file");
|
|
2279
|
+
const envFile = envFileIdx !== -1 ? args[envFileIdx + 1] : undefined;
|
|
2280
|
+
const workspaceIdx = args.indexOf("--workspace");
|
|
2281
|
+
const workspace = workspaceIdx !== -1 && args[workspaceIdx + 1]
|
|
2282
|
+
? args[workspaceIdx + 1]
|
|
2283
|
+
: process.cwd();
|
|
2284
|
+
const { runAuditEnv } = await import("./commands/auditEnv.js");
|
|
2285
|
+
const result = await runAuditEnv(recipeArg, {
|
|
2286
|
+
...(envFile ? { envFile } : {}),
|
|
2287
|
+
workspace,
|
|
2288
|
+
});
|
|
2289
|
+
if (result.warnings.length > 0) {
|
|
2290
|
+
for (const w of result.warnings) {
|
|
2291
|
+
process.stderr.write(` ⚠ ${w}\n`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
if (result.present.length > 0) {
|
|
2295
|
+
for (const v of result.present) {
|
|
2296
|
+
process.stdout.write(` ✓ ${v}\n`);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (result.missing.length > 0) {
|
|
2300
|
+
for (const v of result.missing) {
|
|
2301
|
+
process.stderr.write(` ✗ missing: ${v}\n`);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
if (!result.ok) {
|
|
2305
|
+
process.exit(1);
|
|
2306
|
+
}
|
|
2307
|
+
process.exit(0);
|
|
2308
|
+
}
|
|
2309
|
+
catch (err) {
|
|
2310
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2311
|
+
process.exit(1);
|
|
2312
|
+
}
|
|
2313
|
+
})();
|
|
2314
|
+
}
|
|
1533
2315
|
// Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
|
|
1534
2316
|
if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
|
|
1535
2317
|
const args = process.argv.slice(4);
|
|
@@ -1633,6 +2415,79 @@ if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
|
|
|
1633
2415
|
}
|
|
1634
2416
|
})();
|
|
1635
2417
|
}
|
|
2418
|
+
// `recipe doctor <name|file>` — one-screen "why is this recipe unhealthy
|
|
2419
|
+
// + how do I fix it" diagnosis. Composes the static preflight check with
|
|
2420
|
+
// the recipe-scoped runtime halt summary from a live bridge (fail-soft:
|
|
2421
|
+
// no bridge → static-only). See runRecipeDoctor in commands/recipe.ts.
|
|
2422
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "doctor") {
|
|
2423
|
+
const args = process.argv.slice(4);
|
|
2424
|
+
const usage = "Usage: patchwork recipe doctor <name|file.yaml> [--json] [--local]\n\n" +
|
|
2425
|
+
"Diagnoses a recipe: lint + write-policy + plan (static) plus recent\n" +
|
|
2426
|
+
"runtime halts from a live bridge, each mapped to a fix hint.\n" +
|
|
2427
|
+
"--local skips the bridge runtime check. Exits 1 when unhealthy.\n";
|
|
2428
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2429
|
+
process.stdout.write(usage);
|
|
2430
|
+
process.exit(0);
|
|
2431
|
+
}
|
|
2432
|
+
const wantJson = args.includes("--json");
|
|
2433
|
+
const localOnly = args.includes("--local");
|
|
2434
|
+
const ref = args.find((a) => !a.startsWith("--"));
|
|
2435
|
+
if (!ref) {
|
|
2436
|
+
process.stderr.write(usage);
|
|
2437
|
+
process.exit(1);
|
|
2438
|
+
}
|
|
2439
|
+
(async () => {
|
|
2440
|
+
try {
|
|
2441
|
+
const { runRecipeDoctor, formatRecipeDoctorReport } = await import("./commands/recipe.js");
|
|
2442
|
+
// Runtime halt fetcher: walk live bridges, query the recipe-scoped
|
|
2443
|
+
// halt summary over the last 7 days. Returns null when no bridge is
|
|
2444
|
+
// reachable so doctor degrades to static-only.
|
|
2445
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
2446
|
+
const fetchHalts = localOnly
|
|
2447
|
+
? undefined
|
|
2448
|
+
: async (recipeName) => {
|
|
2449
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
2450
|
+
const liveLocks = findAllLiveBridges();
|
|
2451
|
+
if (liveLocks.length === 0)
|
|
2452
|
+
return null;
|
|
2453
|
+
const sinceMs = Date.now() - SEVEN_DAYS_MS;
|
|
2454
|
+
const qs = `?sinceMs=${sinceMs}&recipe=${encodeURIComponent(recipeName)}`;
|
|
2455
|
+
for (const lock of liveLocks) {
|
|
2456
|
+
const controller = new AbortController();
|
|
2457
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
2458
|
+
try {
|
|
2459
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
|
|
2460
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
2461
|
+
signal: controller.signal,
|
|
2462
|
+
});
|
|
2463
|
+
if (res.ok) {
|
|
2464
|
+
return (await res.json());
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
catch {
|
|
2468
|
+
/* unreachable lock — try next */
|
|
2469
|
+
}
|
|
2470
|
+
finally {
|
|
2471
|
+
clearTimeout(timer);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return null;
|
|
2475
|
+
};
|
|
2476
|
+
const result = await runRecipeDoctor(ref, { fetchHalts });
|
|
2477
|
+
if (wantJson) {
|
|
2478
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2479
|
+
}
|
|
2480
|
+
else {
|
|
2481
|
+
process.stdout.write(formatRecipeDoctorReport(result));
|
|
2482
|
+
}
|
|
2483
|
+
process.exit(result.ok ? 0 : 1);
|
|
2484
|
+
}
|
|
2485
|
+
catch (err) {
|
|
2486
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2487
|
+
process.exit(1);
|
|
2488
|
+
}
|
|
2489
|
+
})();
|
|
2490
|
+
}
|
|
1636
2491
|
// Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
|
|
1637
2492
|
if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
|
|
1638
2493
|
const args = process.argv.slice(4);
|
|
@@ -1951,9 +2806,12 @@ Steps performed:
|
|
|
1951
2806
|
}
|
|
1952
2807
|
if (extensionArg2) {
|
|
1953
2808
|
try {
|
|
2809
|
+
// Windows: editor binaries (code/cursor/windsurf) are `.cmd` shims that
|
|
2810
|
+
// Node's execFileSync can't launch without a shell. See bridgeProcess.ts.
|
|
1954
2811
|
execFileSync(editor, ["--install-extension", extensionArg2], {
|
|
1955
2812
|
stdio: "pipe",
|
|
1956
2813
|
timeout: 30000,
|
|
2814
|
+
shell: process.platform === "win32",
|
|
1957
2815
|
});
|
|
1958
2816
|
process.stderr.write(` ✓ Extension installed via ${editor}\n\n`);
|
|
1959
2817
|
}
|
|
@@ -2082,13 +2940,18 @@ Steps performed:
|
|
|
2082
2940
|
process.stderr.write(` ✓ MCP shim — already registered in ${claudeJsonAbs}\n\n`);
|
|
2083
2941
|
}
|
|
2084
2942
|
else {
|
|
2943
|
+
// claude -p spawns the stdio command via Node's child_process, which
|
|
2944
|
+
// can't resolve a bare `.cmd` shim on Windows. Record the `.cmd` form
|
|
2945
|
+
// on win32 so the bridge binary is findable by the spawned process.
|
|
2085
2946
|
mcpServers["claude-ide-bridge"] = {
|
|
2086
|
-
command: "claude-ide-bridge",
|
|
2947
|
+
command: ensureCmdShim("claude-ide-bridge"),
|
|
2087
2948
|
args: ["shim"],
|
|
2088
2949
|
type: "stdio",
|
|
2089
2950
|
};
|
|
2090
2951
|
claudeJson.mcpServers = mcpServers;
|
|
2091
|
-
|
|
2952
|
+
// Atomic — `~/.claude.json` holds every MCP server registration on
|
|
2953
|
+
// the machine. A crash mid-write would brick Claude Code globally.
|
|
2954
|
+
writeFileAtomicSync(claudeJsonAbs, `${JSON.stringify(claudeJson, null, 2)}\n`);
|
|
2092
2955
|
process.stderr.write(` ✓ MCP shim — registered in ${claudeJsonAbs}\n Note: bridge tools are wired via ~/.claude.json (global), not .mcp.json.\n This is intentional — when VS Code/Windsurf/Cursor launches Claude Code it\n injects --mcp-config which overrides any project .mcp.json. Only ~/.claude.json\n is always loaded. You do not need to add anything to .mcp.json.\n\n`);
|
|
2093
2956
|
}
|
|
2094
2957
|
}
|
|
@@ -2139,7 +3002,9 @@ Steps performed:
|
|
|
2139
3002
|
}
|
|
2140
3003
|
if (added.length > 0 || migrated.length > 0) {
|
|
2141
3004
|
ccSettings.hooks = ccHooks;
|
|
2142
|
-
|
|
3005
|
+
// Atomic — `~/.claude/settings.json` holds every CC hook entry; a
|
|
3006
|
+
// crash mid-write loses the user's full hook configuration.
|
|
3007
|
+
writeFileAtomicSync(ccSettingsPath, `${JSON.stringify(ccSettings, null, 2)}\n`);
|
|
2143
3008
|
const addMsg = added.length > 0
|
|
2144
3009
|
? ` ✓ CC hooks — wired ${added.length} automation hook(s) in ${ccSettingsPath}\n Added: ${added.join(", ")}\n`
|
|
2145
3010
|
: "";
|
|
@@ -2180,6 +3045,9 @@ Steps performed:
|
|
|
2180
3045
|
execFileSync("claude-ide-bridge", ["--version"], {
|
|
2181
3046
|
stdio: "pipe",
|
|
2182
3047
|
timeout: 5000,
|
|
3048
|
+
// Windows: global npm bin is a `.cmd` shim that Node's execFileSync
|
|
3049
|
+
// can't launch without a shell. See bridgeProcess.ts for context.
|
|
3050
|
+
shell: process.platform === "win32",
|
|
2183
3051
|
});
|
|
2184
3052
|
shimOnPath = true;
|
|
2185
3053
|
}
|
|
@@ -2239,13 +3107,8 @@ Steps performed:
|
|
|
2239
3107
|
let hooksWired = false;
|
|
2240
3108
|
try {
|
|
2241
3109
|
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2244
|
-
if (hooksObj && typeof hooksObj === "object") {
|
|
2245
|
-
hooksWired = Object.values(hooksObj).flat().some((e) => typeof e?.command ===
|
|
2246
|
-
"string" &&
|
|
2247
|
-
(e.command ?? "").includes("claude-ide-bridge"));
|
|
2248
|
-
}
|
|
3110
|
+
const { isPreToolUseHookRegistered } = await import("./preToolUseHook.js");
|
|
3111
|
+
hooksWired = isPreToolUseHookRegistered(settingsPath);
|
|
2249
3112
|
}
|
|
2250
3113
|
catch {
|
|
2251
3114
|
/* file may not exist yet — non-fatal */
|
|
@@ -2263,8 +3126,11 @@ Steps performed:
|
|
|
2263
3126
|
? fallbackDocs
|
|
2264
3127
|
: null;
|
|
2265
3128
|
if (target) {
|
|
2266
|
-
|
|
2267
|
-
|
|
3129
|
+
// Use execFile with argv (no shell) — exec(`code "${target}"`) was
|
|
3130
|
+
// shell-evaluated and could be injected via `--workspace '"; ...'`
|
|
3131
|
+
// since path.resolve preserves shell metachars. Audit 2026-05-17.
|
|
3132
|
+
const { execFile } = await import("node:child_process");
|
|
3133
|
+
execFile("code", [target], { timeout: 3000 }, () => { });
|
|
2268
3134
|
}
|
|
2269
3135
|
}
|
|
2270
3136
|
// Analytics opt-in prompt — only ask once; skip if preference already set
|
|
@@ -2381,9 +3247,11 @@ else if (process.argv[2] === "install-extension") {
|
|
|
2381
3247
|
}
|
|
2382
3248
|
try {
|
|
2383
3249
|
process.stderr.write(`Installing extension via ${editor}...\n`);
|
|
3250
|
+
// Windows: editor binaries are `.cmd` shims; need shell for resolution.
|
|
2384
3251
|
execFileSync(editor, ["--install-extension", extensionArg], {
|
|
2385
3252
|
stdio: "inherit",
|
|
2386
3253
|
timeout: 30000,
|
|
3254
|
+
shell: process.platform === "win32",
|
|
2387
3255
|
});
|
|
2388
3256
|
process.stderr.write("Extension installed successfully.\n");
|
|
2389
3257
|
}
|
|
@@ -2560,6 +3428,88 @@ Options:
|
|
|
2560
3428
|
}
|
|
2561
3429
|
process.exit(0);
|
|
2562
3430
|
}
|
|
3431
|
+
// `patchwork doctor` — run CLI-safe bridge health checks.
|
|
3432
|
+
if (process.argv[2] === "doctor") {
|
|
3433
|
+
const args = process.argv.slice(3);
|
|
3434
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3435
|
+
process.stdout.write("Usage: patchwork doctor [--workspace <path>] [--port <n>] [--json]\n\n" +
|
|
3436
|
+
"Runs bridge health checks (workspace, git, lock file, automation policy).\n" +
|
|
3437
|
+
"Exits 1 if any check fails.\n");
|
|
3438
|
+
process.exit(0);
|
|
3439
|
+
}
|
|
3440
|
+
(async () => {
|
|
3441
|
+
try {
|
|
3442
|
+
const workspaceIdx = args.indexOf("--workspace");
|
|
3443
|
+
const workspace = workspaceIdx !== -1 && args[workspaceIdx + 1]
|
|
3444
|
+
? args[workspaceIdx + 1]
|
|
3445
|
+
: process.cwd();
|
|
3446
|
+
const portIdx = args.indexOf("--port");
|
|
3447
|
+
const portArg = portIdx !== -1 ? args[portIdx + 1] : undefined;
|
|
3448
|
+
const port = portArg !== undefined ? Number(portArg) : undefined;
|
|
3449
|
+
const jsonFlag = args.includes("--json");
|
|
3450
|
+
const { runDoctor } = await import("./commands/doctor.js");
|
|
3451
|
+
const result = await runDoctor({ workspace, port, json: jsonFlag });
|
|
3452
|
+
if (jsonFlag) {
|
|
3453
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
3454
|
+
}
|
|
3455
|
+
else {
|
|
3456
|
+
for (const check of result.checks) {
|
|
3457
|
+
const icon = check.status === "ok" ? "✓" : check.status === "warn" ? "⚠" : "✗";
|
|
3458
|
+
const detail = check.detail ? ` ${check.detail}` : "";
|
|
3459
|
+
process.stdout.write(` ${icon} ${check.name}${detail}\n`);
|
|
3460
|
+
if (check.suggestion) {
|
|
3461
|
+
process.stdout.write(` → ${check.suggestion}\n`);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
process.exit(result.ok ? 0 : 1);
|
|
3466
|
+
}
|
|
3467
|
+
catch (err) {
|
|
3468
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
3469
|
+
process.exit(1);
|
|
3470
|
+
}
|
|
3471
|
+
})();
|
|
3472
|
+
}
|
|
3473
|
+
// `patchwork shadow-scan` — replay run history through the destructive-tool classifier.
|
|
3474
|
+
if (process.argv[2] === "shadow-scan") {
|
|
3475
|
+
const args = process.argv.slice(3);
|
|
3476
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3477
|
+
process.stdout.write("Usage: patchwork shadow-scan [--since <duration|ISO>] [--limit <n>] [--runs-file <path>] [--json]\n\n" +
|
|
3478
|
+
"Replays historical run data through the destructive-tool classifier.\n" +
|
|
3479
|
+
"Exits 1 if any runs would be reclassified.\n\n" +
|
|
3480
|
+
" --since <duration|ISO> Lookback window, e.g. '24h', '7d', or ISO date (default: 7d)\n" +
|
|
3481
|
+
" --limit <n> Cap the number of runs to scan\n" +
|
|
3482
|
+
" --runs-file <path> Override default ~/.claude/ide/runs.jsonl path\n" +
|
|
3483
|
+
" --json Emit JSON output\n");
|
|
3484
|
+
process.exit(0);
|
|
3485
|
+
}
|
|
3486
|
+
(async () => {
|
|
3487
|
+
try {
|
|
3488
|
+
const sinceIdx = args.indexOf("--since");
|
|
3489
|
+
const since = sinceIdx !== -1 ? args[sinceIdx + 1] : undefined;
|
|
3490
|
+
const limitIdx = args.indexOf("--limit");
|
|
3491
|
+
const limitArg = limitIdx !== -1 ? args[limitIdx + 1] : undefined;
|
|
3492
|
+
const limit = limitArg !== undefined ? Number(limitArg) : undefined;
|
|
3493
|
+
const runsFileIdx = args.indexOf("--runs-file");
|
|
3494
|
+
const runsFile = runsFileIdx !== -1 ? args[runsFileIdx + 1] : undefined;
|
|
3495
|
+
const jsonFlag = args.includes("--json");
|
|
3496
|
+
const { runShadowScanCli } = await import("./commands/shadowScan.js");
|
|
3497
|
+
await runShadowScanCli({
|
|
3498
|
+
...(since !== undefined ? { since } : {}),
|
|
3499
|
+
...(limit !== undefined ? { limit } : {}),
|
|
3500
|
+
...(runsFile !== undefined
|
|
3501
|
+
? { runsFile, workspace: process.cwd() }
|
|
3502
|
+
: {}),
|
|
3503
|
+
json: jsonFlag,
|
|
3504
|
+
});
|
|
3505
|
+
// runShadowScanCli sets process.exitCode = 1 on reclassified > 0; no explicit exit needed.
|
|
3506
|
+
}
|
|
3507
|
+
catch (err) {
|
|
3508
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
3509
|
+
process.exit(1);
|
|
3510
|
+
}
|
|
3511
|
+
})();
|
|
3512
|
+
}
|
|
2563
3513
|
// Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
|
|
2564
3514
|
if (process.argv[2] === "launchd") {
|
|
2565
3515
|
const sub = process.argv[3];
|
|
@@ -2580,11 +3530,24 @@ if (process.argv[2] === "launchd") {
|
|
|
2580
3530
|
// F6: "Did you mean?" for unknown CLI subcommands
|
|
2581
3531
|
// Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
|
|
2582
3532
|
{
|
|
2583
|
-
const binName =
|
|
3533
|
+
const binName = invokedBinaryName();
|
|
2584
3534
|
const isPatchworkBin = binName === "patchwork-os" ||
|
|
2585
3535
|
binName === "patchwork" ||
|
|
2586
3536
|
binName === "patchwork.js";
|
|
2587
3537
|
if (isPatchworkBin && (!process.argv[2] || process.argv[2] === "dashboard")) {
|
|
3538
|
+
// First-run guard: if the user hasn't run `patchwork init` yet, launching
|
|
3539
|
+
// the dashboard renders an empty panel with no signpost. Print an
|
|
3540
|
+
// actionable pointer instead and exit cleanly.
|
|
3541
|
+
const cfgPath = path.join(os.homedir(), ".patchwork", "config.json");
|
|
3542
|
+
if (!existsSync(cfgPath) && !process.argv[2]) {
|
|
3543
|
+
process.stdout.write(`No Patchwork config found at ${cfgPath}.\n\n` +
|
|
3544
|
+
`Run \`${binName} init\` to scaffold ~/.patchwork and wire up\n` +
|
|
3545
|
+
`Claude Code hooks, then \`${binName}\` again to open the dashboard.\n\n` +
|
|
3546
|
+
`For just the IDE bridge (no recipes / approval queue), run:\n` +
|
|
3547
|
+
` ${binName} install-extension\n` +
|
|
3548
|
+
` ${binName} --workspace .\n`);
|
|
3549
|
+
process.exit(0);
|
|
3550
|
+
}
|
|
2588
3551
|
(async () => {
|
|
2589
3552
|
const { runDashboard } = await import("./commands/dashboard.js");
|
|
2590
3553
|
await runDashboard();
|
|
@@ -2662,8 +3625,10 @@ else {
|
|
|
2662
3625
|
.digest("hex")
|
|
2663
3626
|
.slice(0, 6);
|
|
2664
3627
|
const sessionName = `claude-bridge-${ws}${hash}`;
|
|
2665
|
-
// Check if tmux is available
|
|
2666
|
-
const tmuxCheck =
|
|
3628
|
+
// Check if tmux is available (skip on Windows — tmux doesn't exist there)
|
|
3629
|
+
const tmuxCheck = process.platform !== "win32"
|
|
3630
|
+
? spawnSync("which", ["tmux"], { stdio: "ignore" })
|
|
3631
|
+
: { status: 1 };
|
|
2667
3632
|
if (tmuxCheck.status !== 0) {
|
|
2668
3633
|
process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
|
|
2669
3634
|
}
|
|
@@ -2712,7 +3677,12 @@ else {
|
|
|
2712
3677
|
for (const sig of ["SIGTERM", "SIGINT"]) {
|
|
2713
3678
|
process.once(sig, () => {
|
|
2714
3679
|
stopping = true;
|
|
2715
|
-
|
|
3680
|
+
// Use treeKill so grandchildren (recipe runners, claude
|
|
3681
|
+
// subprocesses, extension watchers) are reaped on Windows.
|
|
3682
|
+
// Bare `child.kill(sig)` maps to TerminateProcess on win32
|
|
3683
|
+
// and skips descendants → orphaned processes survive a
|
|
3684
|
+
// supervisor SIGTERM. Audit 2026-05-17.
|
|
3685
|
+
treeKill(child, sig);
|
|
2716
3686
|
});
|
|
2717
3687
|
}
|
|
2718
3688
|
child.on("exit", (code, signal) => {
|
|
@@ -2740,19 +3710,27 @@ else {
|
|
|
2740
3710
|
process.stderr.write(`Error: ${message}\n`);
|
|
2741
3711
|
process.exit(1);
|
|
2742
3712
|
});
|
|
2743
|
-
// F5: Silent self-update nudge (fire-and-forget)
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
3713
|
+
// F5: Silent self-update nudge (fire-and-forget).
|
|
3714
|
+
// Skip when running from a source tree (any of: a `.git` sibling of the
|
|
3715
|
+
// package, or __dirnameTop not under a node_modules/). Otherwise a dev
|
|
3716
|
+
// who built locally sees "Bridge v<X> available" pointing at an npm
|
|
3717
|
+
// install path they're not using.
|
|
3718
|
+
const isSourceBuild = existsSync(path.join(__dirnameTop, "..", ".git")) ||
|
|
3719
|
+
!__dirnameTop.includes(`${path.sep}node_modules${path.sep}`);
|
|
3720
|
+
if (!isSourceBuild) {
|
|
3721
|
+
import("node:child_process")
|
|
3722
|
+
.then(({ exec }) => {
|
|
3723
|
+
exec("npm view patchwork-os version", { timeout: 5000 }, (err, stdout) => {
|
|
3724
|
+
if (err || !stdout)
|
|
3725
|
+
return;
|
|
3726
|
+
const latest = stdout.trim();
|
|
3727
|
+
if (latest && semverGt(latest, PACKAGE_VERSION)) {
|
|
3728
|
+
console.log(`\n Patchwork OS v${latest} available — run: npm update -g patchwork-os\n`);
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3731
|
+
})
|
|
3732
|
+
.catch(() => { });
|
|
3733
|
+
}
|
|
2756
3734
|
}
|
|
2757
3735
|
} // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
|
|
2758
3736
|
//# sourceMappingURL=index.js.map
|