patchwork-os 0.2.0-beta.1 → 0.2.0-beta.10.canary.100
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 +201 -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.js +1 -1
- 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 +38 -2
- package/dist/analyticsPrefs.js +148 -23
- 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 +219 -35
- 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.js +23 -8
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +87 -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/connect.d.ts +47 -0
- package/dist/commands/connect.js +419 -0
- package/dist/commands/connect.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 +151 -0
- package/dist/commands/recipe.js +649 -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.d.ts +25 -0
- package/dist/commands/task.js +62 -42
- 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/tracesExport.d.ts +15 -1
- package/dist/commands/tracesExport.js +39 -5
- package/dist/commands/tracesExport.js.map +1 -1
- package/dist/commands/tracesImport.js +21 -4
- package/dist/commands/tracesImport.js.map +1 -1
- package/dist/commitIssueLinkLog.d.ts +16 -0
- package/dist/commitIssueLinkLog.js +110 -24
- 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 +47 -3
- package/dist/config.js +111 -21
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.js +1422 -57
- 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 +60 -0
- package/dist/connectors/connectorRegistry.js +354 -0
- package/dist/connectors/connectorRegistry.js.map +1 -0
- package/dist/connectors/datadog.js +41 -5
- 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 +234 -3
- 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/mcpClient.js +37 -23
- package/dist/connectors/mcpClient.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/notion.d.ts +1 -1
- package/dist/connectors/notion.js +13 -4
- package/dist/connectors/notion.js.map +1 -1
- 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 +579 -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 +633 -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.d.ts +8 -6
- package/dist/decisionReplay.js +50 -17
- 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 +33 -9
- 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.js +69 -16
- package/dist/drivers/claude/subprocess.js.map +1 -1
- 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/local/index.d.ts +2 -0
- package/dist/drivers/local/index.js +12 -0
- package/dist/drivers/local/index.js.map +1 -1
- 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 +267 -87
- 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 +34 -2
- 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/inboxRoutes.d.ts +22 -0
- package/dist/inboxRoutes.js +61 -1
- package/dist/inboxRoutes.js.map +1 -1
- package/dist/index.js +1206 -158
- package/dist/index.js.map +1 -1
- package/dist/installGuard.js +6 -2
- package/dist/installGuard.js.map +1 -1
- package/dist/localEndpointGuard.d.ts +25 -0
- package/dist/localEndpointGuard.js +101 -0
- package/dist/localEndpointGuard.js.map +1 -0
- package/dist/lockfile.js +31 -4
- package/dist/lockfile.js.map +1 -1
- package/dist/oauth.d.ts +9 -0
- package/dist/oauth.js +33 -0
- package/dist/oauth.js.map +1 -1
- package/dist/oauthRoutes.d.ts +1 -1
- package/dist/oauthRoutes.js +5 -3
- package/dist/oauthRoutes.js.map +1 -1
- package/dist/orchestrator/childBridgeClient.d.ts +5 -0
- package/dist/orchestrator/childBridgeClient.js +28 -1
- package/dist/orchestrator/childBridgeClient.js.map +1 -1
- package/dist/orchestrator/childBridgeRegistry.js +29 -11
- package/dist/orchestrator/childBridgeRegistry.js.map +1 -1
- package/dist/orchestrator/orchestratorBridge.d.ts +33 -0
- package/dist/orchestrator/orchestratorBridge.js +70 -9
- package/dist/orchestrator/orchestratorBridge.js.map +1 -1
- package/dist/patchworkConfig.d.ts +44 -1
- package/dist/patchworkConfig.js +28 -3
- 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 +3 -2
- 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 +3 -3
- package/dist/prompts.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +9 -0
- package/dist/recipeOrchestration.js +341 -24
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +58 -2
- package/dist/recipeRoutes.js +744 -115
- 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 +25 -5
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +2 -0
- package/dist/recipes/chainedRunner.js +156 -9
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/compiler.js +9 -5
- package/dist/recipes/compiler.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.d.ts +9 -0
- package/dist/recipes/dependencyGraph.js +32 -7
- package/dist/recipes/dependencyGraph.js.map +1 -1
- package/dist/recipes/githubInstallSource.d.ts +135 -0
- package/dist/recipes/githubInstallSource.js +236 -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 +154 -0
- package/dist/recipes/idempotencyKey.js +375 -0
- package/dist/recipes/idempotencyKey.js.map +1 -0
- package/dist/recipes/installer.js +58 -3
- 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 +116 -6
- 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 +65 -2
- package/dist/recipes/schemaGenerator.js +119 -7
- 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 +33 -7
- package/dist/recipes/toolRegistry.js.map +1 -1
- package/dist/recipes/tools/airtable.d.ts +15 -0
- package/dist/recipes/tools/airtable.js +239 -0
- package/dist/recipes/tools/airtable.js.map +1 -0
- 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 +27 -5
- 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 +7 -0
- package/dist/recipes/tools/index.js +7 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/tools/resend.d.ts +8 -0
- package/dist/recipes/tools/resend.js +152 -0
- package/dist/recipes/tools/resend.js.map +1 -0
- package/dist/recipes/tools/sendgrid.d.ts +9 -0
- package/dist/recipes/tools/sendgrid.js +174 -0
- package/dist/recipes/tools/sendgrid.js.map +1 -0
- package/dist/recipes/tools/slack.js +1 -1
- package/dist/recipes/tools/twilio.d.ts +11 -0
- package/dist/recipes/tools/twilio.js +179 -0
- package/dist/recipes/tools/twilio.js.map +1 -0
- package/dist/recipes/tools/vercel.d.ts +9 -0
- package/dist/recipes/tools/vercel.js +145 -0
- package/dist/recipes/tools/vercel.js.map +1 -0
- package/dist/recipes/validation.d.ts +17 -0
- package/dist/recipes/validation.js +141 -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 +185 -8
- package/dist/recipes/yamlRunner.js +967 -224
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +19 -3
- package/dist/recipesHttp.js +67 -11
- 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 +887 -198
- package/dist/server.d.ts +160 -3
- package/dist/server.js +1099 -122
- 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 +37 -19
- 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/getGitStatus.js +6 -1
- package/dist/tools/getGitStatus.js.map +1 -1
- 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 +116 -204
- 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 +4 -1
- package/dist/tools/github/composite.js +7 -7
- package/dist/tools/github/composite.js.map +1 -1
- package/dist/tools/github/issues.d.ts +4 -0
- package/dist/tools/github/pr.d.ts +13 -6
- package/dist/tools/github/pr.js +18 -11
- package/dist/tools/github/pr.js.map +1 -1
- 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/index.js +14 -10
- package/dist/tools/index.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 +31 -5
- 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/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 +21 -12
- package/dist/tools/searchAndReplace.js.map +1 -1
- package/dist/tools/searchTools.d.ts +26 -0
- package/dist/tools/searchTools.js +62 -21
- 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/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 +13 -1
- package/dist/tools/transaction.js +63 -5
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/typeHierarchy.d.ts +1 -0
- package/dist/tools/utils.d.ts +33 -0
- package/dist/tools/utils.js +122 -18
- 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 +126 -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 +73 -17
- 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 +1 -1
- 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 -16
- package/dist/commands/marketplace.js +0 -32
- 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
|
@@ -38,10 +38,14 @@ import readline from "node:readline";
|
|
|
38
38
|
import { fileURLToPath } from "node:url";
|
|
39
39
|
import { getAnalyticsPref, setAnalyticsPref } from "./analyticsPrefs.js";
|
|
40
40
|
import { Bridge } from "./bridge.js";
|
|
41
|
+
import { findAllLiveBridges, findBridgeLock, } from "./bridgeLockDiscovery.js";
|
|
41
42
|
import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
|
|
42
43
|
import { findEditor, parseConfig } from "./config.js";
|
|
43
44
|
import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
|
|
45
|
+
import { treeKill } from "./processTree.js";
|
|
44
46
|
import { PACKAGE_VERSION, semverGt } from "./version.js";
|
|
47
|
+
import { ensureCmdShim } from "./winShim.js";
|
|
48
|
+
import { writeFileAtomicSync } from "./writeFileAtomic.js";
|
|
45
49
|
const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
|
|
46
50
|
// Warn when a symlinked global install is detected (`npm install -g .`).
|
|
47
51
|
// launchctl / sandbox environments can fail through that link with EPERM.
|
|
@@ -119,15 +123,26 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
119
123
|
"gen-plugin-stub",
|
|
120
124
|
"notify",
|
|
121
125
|
"install",
|
|
122
|
-
"marketplace",
|
|
123
126
|
"status",
|
|
124
127
|
"shim",
|
|
128
|
+
"quick-task",
|
|
129
|
+
"start-task",
|
|
130
|
+
"continue-handoff",
|
|
131
|
+
"token-efficiency",
|
|
125
132
|
"recipe",
|
|
133
|
+
"connect",
|
|
126
134
|
"traces",
|
|
127
135
|
"suggest",
|
|
128
136
|
"dashboard",
|
|
129
137
|
"launchd",
|
|
130
138
|
"start",
|
|
139
|
+
"kill-switch",
|
|
140
|
+
"panic",
|
|
141
|
+
"halts",
|
|
142
|
+
"judgments",
|
|
143
|
+
"analytics",
|
|
144
|
+
"doctor",
|
|
145
|
+
"shadow-scan",
|
|
131
146
|
];
|
|
132
147
|
const __invokedSubcommand = (() => {
|
|
133
148
|
const sub = process.argv[2];
|
|
@@ -139,10 +154,20 @@ const __invokedSubcommand = (() => {
|
|
|
139
154
|
? sub
|
|
140
155
|
: null;
|
|
141
156
|
})();
|
|
157
|
+
// bash/zsh set process.env._ to the actual invoked binary path (e.g. /usr/local/bin/patchwork-os).
|
|
158
|
+
// More reliable than argv[1] which resolves to the .js entrypoint via npm global shim.
|
|
159
|
+
function invokedBinaryName() {
|
|
160
|
+
const fromEnv = process.env._
|
|
161
|
+
? path.basename(process.env._).replace(/\.(cmd|js)$/i, "")
|
|
162
|
+
: "";
|
|
163
|
+
if (fromEnv && fromEnv !== "node" && fromEnv !== "npm")
|
|
164
|
+
return fromEnv;
|
|
165
|
+
return path.basename(process.argv[1] ?? "").replace(/\.js$/, "");
|
|
166
|
+
}
|
|
142
167
|
const __invokedBareBinaryDashboard = (() => {
|
|
143
168
|
if (process.argv[2] && process.argv[2] !== "dashboard")
|
|
144
169
|
return false;
|
|
145
|
-
const binName =
|
|
170
|
+
const binName = invokedBinaryName();
|
|
146
171
|
return (binName === "patchwork-os" ||
|
|
147
172
|
binName === "patchwork" ||
|
|
148
173
|
binName === "patchwork.js");
|
|
@@ -153,6 +178,61 @@ if (process.argv[2] === "--version" || process.argv[2] === "-v") {
|
|
|
153
178
|
console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
|
|
154
179
|
process.exit(0);
|
|
155
180
|
}
|
|
181
|
+
// Handle top-level --help / -h / help — print a grouped command index so a
|
|
182
|
+
// first-time user has a discoverable entry point. Without this, bare
|
|
183
|
+
// `patchwork --help` falls through to bridge-daemon arg parsing and errors.
|
|
184
|
+
if (process.argv[2] === "--help" ||
|
|
185
|
+
process.argv[2] === "-h" ||
|
|
186
|
+
process.argv[2] === "help") {
|
|
187
|
+
const binName = path.basename(process.argv[1] ?? "patchwork");
|
|
188
|
+
process.stdout.write(`${binName} ${PACKAGE_VERSION}\n\n` +
|
|
189
|
+
`First time? Run:\n` +
|
|
190
|
+
` ${binName} init # set up ~/.patchwork + Claude Code hooks\n` +
|
|
191
|
+
` ${binName} start-all # bridge + Claude + dashboard\n\n` +
|
|
192
|
+
`Get started\n` +
|
|
193
|
+
` init [--workspace <dir>] Scaffold ~/.patchwork; register CC hooks\n` +
|
|
194
|
+
` install-extension Install the VS Code / Cursor / Windsurf extension\n` +
|
|
195
|
+
` start-all [--no-dashboard] Launch bridge + Claude --ide + dashboard\n` +
|
|
196
|
+
` start-orchestrator Multi-IDE-window meta-bridge\n\n` +
|
|
197
|
+
`Recipes\n` +
|
|
198
|
+
` recipe new <name> [-i] Scaffold a recipe\n` +
|
|
199
|
+
` recipe list List installed recipes\n` +
|
|
200
|
+
` recipe run <name> [--vars k=v] Run a recipe by name\n` +
|
|
201
|
+
` recipe install <source> Install from a path or GitHub source\n` +
|
|
202
|
+
` recipe --help Full recipe subcommand index\n\n` +
|
|
203
|
+
`Connectors\n` +
|
|
204
|
+
` connect [list] List connectors + connection status\n` +
|
|
205
|
+
` connect <vendor> OAuth: print authorize URL / PAT: connect\n` +
|
|
206
|
+
` connect test <vendor> Health-probe a connector\n` +
|
|
207
|
+
` connect disconnect <vendor> Revoke a connector\n\n` +
|
|
208
|
+
`Operate\n` +
|
|
209
|
+
` start [--port N] [--workspace <dir>] Start a single bridge (no tmux)\n` +
|
|
210
|
+
` status One-line bridge status (port, uptime)\n` +
|
|
211
|
+
` tools [list|search <q>] [--slim] [--json] List tools the bridge would register\n` +
|
|
212
|
+
` analytics <show|configure|clear|test> Manage opt-in telemetry config\n` +
|
|
213
|
+
` launchd <install|uninstall|status> Manage the macOS auto-start LaunchAgent\n\n` +
|
|
214
|
+
`Diagnose\n` +
|
|
215
|
+
` halts [--window 1h|24h|overnight|7d] Morning summary of recent recipe halts\n` +
|
|
216
|
+
` judgments [--window ...] [--recipe N] Recent judge-step verdicts across runs\n` +
|
|
217
|
+
` suggest [--since-days N] Recipe + unused-tool suggestions\n` +
|
|
218
|
+
` token-efficiency benchmark Measure token cost across tool sets\n` +
|
|
219
|
+
` traces export Bundle approval / recipe / decision traces\n` +
|
|
220
|
+
` print-token [--port N] Print the active bridge auth token\n\n` +
|
|
221
|
+
`Safety\n` +
|
|
222
|
+
` kill-switch <engage|release|status> Block / resume write-tier tools across bridges\n` +
|
|
223
|
+
` panic [--reason "..."] Shorthand for kill-switch engage\n\n` +
|
|
224
|
+
`Daemon (no subcommand)\n` +
|
|
225
|
+
` --workspace <dir> Start the bridge in foreground\n` +
|
|
226
|
+
` --watch Auto-restart supervisor\n` +
|
|
227
|
+
` --slim 27 IDE-only tools (default: full)\n\n` +
|
|
228
|
+
`Other\n` +
|
|
229
|
+
` --version, -v Print package version\n` +
|
|
230
|
+
` shim stdio↔WebSocket shim (used by MCP clients)\n` +
|
|
231
|
+
` notify <event> Notify a running bridge of a CC hook event\n\n` +
|
|
232
|
+
`Bridge-daemon flags: run \`${binName} --workspace . --help-flags\` for the full list,\n` +
|
|
233
|
+
`or see https://github.com/Oolab-labs/patchwork-os#readme.\n`);
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
156
236
|
// Handle patchwork-init subcommand — T2 from docs/install-ux-plan.md.
|
|
157
237
|
// Separate from the bridge-only `init` to preserve back-compat. See ADR-0008.
|
|
158
238
|
if (process.argv[2] === "patchwork-init") {
|
|
@@ -160,6 +240,13 @@ if (process.argv[2] === "patchwork-init") {
|
|
|
160
240
|
await runPatchworkInit(process.argv.slice(3));
|
|
161
241
|
process.exit(0);
|
|
162
242
|
}
|
|
243
|
+
// `patchwork-os init` → dashboard setup, not IDE bridge installer.
|
|
244
|
+
// patchwork init / claude-ide-bridge init still go to the bridge path below.
|
|
245
|
+
if (process.argv[2] === "init" && invokedBinaryName() === "patchwork-os") {
|
|
246
|
+
const { runPatchworkInit } = await import("./commands/patchworkInit.js");
|
|
247
|
+
await runPatchworkInit(process.argv.slice(3));
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
163
250
|
// Handle start-all subcommand — launches the full 3-pane tmux orchestrator.
|
|
164
251
|
// Also triggered when invoked as `claude-ide-bridge-start` directly.
|
|
165
252
|
const isStartAll = process.argv[2] === "start-all" ||
|
|
@@ -168,8 +255,11 @@ if (isStartAll) {
|
|
|
168
255
|
const startAllArgs = process.argv[2] === "start-all"
|
|
169
256
|
? process.argv.slice(3)
|
|
170
257
|
: process.argv.slice(2);
|
|
171
|
-
|
|
172
|
-
|
|
258
|
+
// Dispatch the cross-platform Node orchestrator (start-all.mjs). The
|
|
259
|
+
// bash entry-point is kept as a developer shortcut but Windows has no
|
|
260
|
+
// `bash` on PATH by default, and the .mjs is functionally equivalent.
|
|
261
|
+
const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
|
|
262
|
+
const result = spawnSync(process.execPath, [scriptPath, ...startAllArgs], {
|
|
173
263
|
stdio: "inherit",
|
|
174
264
|
});
|
|
175
265
|
process.exit(result.status ?? 1);
|
|
@@ -177,14 +267,16 @@ if (isStartAll) {
|
|
|
177
267
|
// `patchwork start` — opinionated front door over start-all.
|
|
178
268
|
// Defaults to full mode (all tools registered) and the web dashboard, so the
|
|
179
269
|
// doc-promised "patchwork start → everything works" path actually works.
|
|
180
|
-
// Pass-through args still go to start-all.
|
|
270
|
+
// Pass-through args still go to start-all.mjs; --help short-circuits.
|
|
181
271
|
if (process.argv[2] === "start") {
|
|
182
272
|
const passthrough = process.argv.slice(3);
|
|
183
273
|
if (passthrough.includes("--help") || passthrough.includes("-h")) {
|
|
184
274
|
process.stdout.write(`patchwork start — Launch the full Patchwork stack
|
|
185
275
|
|
|
186
|
-
Starts bridge + Claude
|
|
276
|
+
Starts bridge + Claude + dashboard via the cross-platform Node orchestrator.
|
|
187
277
|
Defaults to full mode so all bridge tools are registered.
|
|
278
|
+
On macOS/Linux: uses tmux when available, falls back to background mode.
|
|
279
|
+
On Windows: runs natively via the Node orchestrator (no WSL required).
|
|
188
280
|
|
|
189
281
|
Usage: patchwork start [options]
|
|
190
282
|
|
|
@@ -206,13 +298,20 @@ This is a thin wrapper over \`start-all\`. For advanced flags see:
|
|
|
206
298
|
const args = [...passthrough];
|
|
207
299
|
const slimIdx = args.indexOf("--slim");
|
|
208
300
|
if (slimIdx >= 0) {
|
|
209
|
-
args.splice(slimIdx, 1); //
|
|
301
|
+
args.splice(slimIdx, 1); // slim is the .mjs default; strip so --full isn't re-added below
|
|
210
302
|
}
|
|
211
303
|
else if (!args.includes("--full")) {
|
|
212
304
|
args.push("--full");
|
|
213
305
|
}
|
|
214
|
-
|
|
215
|
-
|
|
306
|
+
// On non-Windows: auto-detect tmux; fall back to --no-tmux background mode if absent.
|
|
307
|
+
if (process.platform !== "win32" && !args.includes("--no-tmux")) {
|
|
308
|
+
const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
|
|
309
|
+
if (tmuxCheck.status !== 0)
|
|
310
|
+
args.push("--no-tmux");
|
|
311
|
+
}
|
|
312
|
+
// Dispatch to the cross-platform Node orchestrator (see above).
|
|
313
|
+
const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.mjs");
|
|
314
|
+
const result = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
216
315
|
stdio: "inherit",
|
|
217
316
|
});
|
|
218
317
|
process.exit(result.status ?? 1);
|
|
@@ -368,13 +467,6 @@ if (process.argv[2] === "install") {
|
|
|
368
467
|
await runInstall(process.argv.slice(3));
|
|
369
468
|
process.exit(0);
|
|
370
469
|
}
|
|
371
|
-
// Handle marketplace subcommand — DEPRECATED, prints migration message.
|
|
372
|
-
// See issue #279 and src/commands/marketplace.ts for the rationale.
|
|
373
|
-
if (process.argv[2] === "marketplace") {
|
|
374
|
-
const { runMarketplace } = await import("./commands/marketplace.js");
|
|
375
|
-
await runMarketplace(process.argv.slice(3));
|
|
376
|
-
process.exit(0);
|
|
377
|
-
}
|
|
378
470
|
// Handle tools subcommand — search/list tools without a bridge connection
|
|
379
471
|
if (process.argv[2] === "tools") {
|
|
380
472
|
const { runToolsCommand } = await import("./commands/tools.js");
|
|
@@ -412,50 +504,31 @@ Options:
|
|
|
412
504
|
const portIdx = argv.indexOf("--port");
|
|
413
505
|
const portArg = portIdx !== -1 ? argv[portIdx + 1] : undefined;
|
|
414
506
|
const lockDir = path.join(process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude"), "ide");
|
|
415
|
-
|
|
507
|
+
// Select a *running bridge* lock (isBridge:true + live PID) rather than the
|
|
508
|
+
// most-recently-touched lock in ~/.claude/ide, which can be an IDE-owned
|
|
509
|
+
// lock or a dead bridge.
|
|
510
|
+
let bridgeLock;
|
|
416
511
|
if (portArg) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
512
|
+
const port = Number(portArg);
|
|
513
|
+
bridgeLock = findAllLiveBridges().find((b) => b.port === port) ?? null;
|
|
514
|
+
if (!bridgeLock) {
|
|
515
|
+
process.stderr.write(`Error: No running bridge found for port ${portArg} (no lock file for that port, the lock is IDE-owned, or its process is not alive).\n`);
|
|
420
516
|
process.exit(1);
|
|
421
517
|
}
|
|
422
518
|
}
|
|
423
519
|
else {
|
|
424
|
-
|
|
425
|
-
let bestMtime = 0;
|
|
426
|
-
try {
|
|
427
|
-
for (const f of readdirSync(lockDir)) {
|
|
428
|
-
if (!f.endsWith(".lock"))
|
|
429
|
-
continue;
|
|
430
|
-
const full = path.join(lockDir, f);
|
|
431
|
-
const mtime = statSync(full).mtimeMs;
|
|
432
|
-
if (mtime > bestMtime) {
|
|
433
|
-
bestMtime = mtime;
|
|
434
|
-
lockFile = full;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
439
|
-
// lock dir doesn't exist — handled below
|
|
440
|
-
}
|
|
520
|
+
bridgeLock = findBridgeLock();
|
|
441
521
|
}
|
|
442
|
-
if (!
|
|
443
|
-
process.stderr.write(`Error: No bridge lock file found in ${lockDir}\n`);
|
|
522
|
+
if (!bridgeLock) {
|
|
523
|
+
process.stderr.write(`Error: No running bridge lock file found in ${lockDir}\n`);
|
|
444
524
|
process.stderr.write("Make sure the bridge is running first, or pass --port <port>.\n");
|
|
445
525
|
process.exit(1);
|
|
446
526
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (!data.authToken) {
|
|
450
|
-
process.stderr.write(`Error: Lock file ${lockFile} has no authToken field\n`);
|
|
451
|
-
process.exit(1);
|
|
452
|
-
}
|
|
453
|
-
process.stdout.write(`${data.authToken}\n`);
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
process.stderr.write(`Error: Could not read lock file ${lockFile}\n`);
|
|
527
|
+
if (!bridgeLock.authToken) {
|
|
528
|
+
process.stderr.write(`Error: Bridge lock for port ${bridgeLock.port} has no authToken field\n`);
|
|
457
529
|
process.exit(1);
|
|
458
530
|
}
|
|
531
|
+
process.stdout.write(`${bridgeLock.authToken}\n`);
|
|
459
532
|
process.exit(0);
|
|
460
533
|
}
|
|
461
534
|
// Handle notify subcommand — called from CC hooks to fire bridge automation
|
|
@@ -487,53 +560,32 @@ if (process.argv[2] === "notify") {
|
|
|
487
560
|
}
|
|
488
561
|
}
|
|
489
562
|
const notifyLockDir = path.join(process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude"), "ide");
|
|
490
|
-
|
|
563
|
+
// Select a *running bridge* lock (isBridge:true + live PID) rather than the
|
|
564
|
+
// most-recently-touched lock, which can be an IDE-owned or dead-bridge lock.
|
|
491
565
|
let notifyPort;
|
|
566
|
+
let notifyToken;
|
|
492
567
|
if (namedArgs.port) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (!
|
|
496
|
-
process.stderr.write(`Error: No
|
|
568
|
+
const port = Number(namedArgs.port);
|
|
569
|
+
const lock = findAllLiveBridges().find((b) => b.port === port);
|
|
570
|
+
if (!lock) {
|
|
571
|
+
process.stderr.write(`Error: No running bridge found for port ${namedArgs.port} (no lock for that port, the lock is IDE-owned, or its process is not alive).\n`);
|
|
497
572
|
process.exit(1);
|
|
498
573
|
}
|
|
574
|
+
notifyPort = lock.port;
|
|
575
|
+
notifyToken = lock.authToken;
|
|
499
576
|
}
|
|
500
577
|
else {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
continue;
|
|
506
|
-
const full = path.join(notifyLockDir, f);
|
|
507
|
-
const mtime = statSync(full).mtimeMs;
|
|
508
|
-
if (mtime > bestMtime) {
|
|
509
|
-
bestMtime = mtime;
|
|
510
|
-
notifyLockFile = full;
|
|
511
|
-
notifyPort = Number(path.basename(f, ".lock"));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
catch {
|
|
516
|
-
// lock dir doesn't exist
|
|
578
|
+
const lock = findBridgeLock();
|
|
579
|
+
if (lock) {
|
|
580
|
+
notifyPort = lock.port;
|
|
581
|
+
notifyToken = lock.authToken;
|
|
517
582
|
}
|
|
518
583
|
}
|
|
519
|
-
if (!
|
|
520
|
-
process.stderr.write(`Error: No bridge lock file found in ${notifyLockDir}\n`);
|
|
584
|
+
if (!notifyPort || !notifyToken) {
|
|
585
|
+
process.stderr.write(`Error: No running bridge lock file found in ${notifyLockDir}\n`);
|
|
521
586
|
process.stderr.write("Make sure the bridge is running first (claude-ide-bridge --watch ...).\n");
|
|
522
587
|
process.exit(1);
|
|
523
588
|
}
|
|
524
|
-
let notifyToken;
|
|
525
|
-
try {
|
|
526
|
-
const data = JSON.parse(readFileSync(notifyLockFile, "utf-8"));
|
|
527
|
-
if (!data.authToken) {
|
|
528
|
-
process.stderr.write(`Error: Lock file has no authToken field\n`);
|
|
529
|
-
process.exit(1);
|
|
530
|
-
}
|
|
531
|
-
notifyToken = data.authToken;
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
process.stderr.write(`Error: Could not read lock file ${notifyLockFile}\n`);
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
589
|
try {
|
|
538
590
|
const resp = await fetch(`http://127.0.0.1:${notifyPort}/notify`, {
|
|
539
591
|
method: "POST",
|
|
@@ -885,6 +937,33 @@ Edit, save, hot-reload — Claude's next turn sees the new tool. See [documents/
|
|
|
885
937
|
}
|
|
886
938
|
process.exit(0);
|
|
887
939
|
}
|
|
940
|
+
// Patchwork: `patchwork recipe` (no subcommand) / `recipe --help` — print
|
|
941
|
+
// the subcommand index. Without this branch, `patchwork recipe` falls through
|
|
942
|
+
// to the bridge daemon, leaving subcommands completely undiscoverable from
|
|
943
|
+
// the CLI (the only way to find them today is to read CLAUDE.md or source).
|
|
944
|
+
if (process.argv[2] === "recipe" &&
|
|
945
|
+
(process.argv[3] === undefined ||
|
|
946
|
+
process.argv[3] === "--help" ||
|
|
947
|
+
process.argv[3] === "-h" ||
|
|
948
|
+
process.argv[3] === "help")) {
|
|
949
|
+
process.stdout.write(`Usage: patchwork recipe <subcommand> [args...]\n\n` +
|
|
950
|
+
`Subcommands:\n` +
|
|
951
|
+
` new <name> Scaffold a recipe (interactive with -i)\n` +
|
|
952
|
+
` list List installed recipes (workspace + user)\n` +
|
|
953
|
+
` run <name> Run a recipe by name\n` +
|
|
954
|
+
` install <src> Install a recipe from a path or GitHub source\n` +
|
|
955
|
+
` uninstall <name> Remove an installed recipe\n` +
|
|
956
|
+
` enable <name> Re-enable a disabled recipe\n` +
|
|
957
|
+
` disable <name> Pause a recipe (scheduled triggers stop firing)\n` +
|
|
958
|
+
` preflight <file> Static-validate a recipe YAML before running\n` +
|
|
959
|
+
` doctor <name|file> Diagnose a recipe: lint + policy + recent halts\n` +
|
|
960
|
+
` lint <file> Run all lint checks on a recipe YAML\n` +
|
|
961
|
+
` fmt <file> Format a recipe YAML in place\n` +
|
|
962
|
+
` schema Print the recipe JSON Schema\n` +
|
|
963
|
+
` audit-env <recipe> Check {{env.FOO}} vars are present in environment\n\n` +
|
|
964
|
+
`Run \`patchwork recipe <subcommand> --help\` for subcommand-specific options.\n`);
|
|
965
|
+
process.exit(0);
|
|
966
|
+
}
|
|
888
967
|
// Patchwork: `patchwork recipe list` — enumerate installed recipes.
|
|
889
968
|
if (process.argv[2] === "recipe" && process.argv[3] === "list") {
|
|
890
969
|
(async () => {
|
|
@@ -960,11 +1039,13 @@ if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
|
|
|
960
1039
|
// a running bridge's /recipes/run endpoint if one is available.
|
|
961
1040
|
if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
962
1041
|
const args = process.argv.slice(4);
|
|
963
|
-
const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
|
|
1042
|
+
const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE] [--attempt <id>] [--ledger-dir <path>]\n";
|
|
964
1043
|
let localFlag = false;
|
|
965
1044
|
let dryRun = false;
|
|
966
1045
|
let recipeRef;
|
|
967
1046
|
let step;
|
|
1047
|
+
let attemptId;
|
|
1048
|
+
let ledgerDir;
|
|
968
1049
|
const vars = {};
|
|
969
1050
|
for (let i = 0; i < args.length; i++) {
|
|
970
1051
|
const arg = args[i];
|
|
@@ -991,6 +1072,29 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
991
1072
|
step = value;
|
|
992
1073
|
continue;
|
|
993
1074
|
}
|
|
1075
|
+
if (currentArg === "--attempt" || currentArg.startsWith("--attempt=")) {
|
|
1076
|
+
const value = currentArg === "--attempt"
|
|
1077
|
+
? args[++i]
|
|
1078
|
+
: currentArg.slice("--attempt=".length);
|
|
1079
|
+
if (!value) {
|
|
1080
|
+
process.stderr.write(`Error: --attempt requires a value\n${usage}`);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
attemptId = value;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (currentArg === "--ledger-dir" ||
|
|
1087
|
+
currentArg.startsWith("--ledger-dir=")) {
|
|
1088
|
+
const value = currentArg === "--ledger-dir"
|
|
1089
|
+
? args[++i]
|
|
1090
|
+
: currentArg.slice("--ledger-dir=".length);
|
|
1091
|
+
if (!value) {
|
|
1092
|
+
process.stderr.write(`Error: --ledger-dir requires a value\n${usage}`);
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
ledgerDir = value;
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
994
1098
|
if (currentArg === "--var" || currentArg.startsWith("--var=")) {
|
|
995
1099
|
const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
|
|
996
1100
|
if (!assignment) {
|
|
@@ -1037,19 +1141,42 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
1037
1141
|
})();
|
|
1038
1142
|
const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
|
|
1039
1143
|
const lock = localFlag ? null : findBridgeLock();
|
|
1040
|
-
if (lock && !dryRun && !step && !explicitFile) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1144
|
+
if (lock && !dryRun && !step && !explicitFile && !attemptId) {
|
|
1145
|
+
// 30s per-request deadline — the bridge can pass the findBridgeLock
|
|
1146
|
+
// PID check yet be wedged on HTTP. Without this abort the fetch
|
|
1147
|
+
// blocks forever. Mirrors the AbortController pattern used by every
|
|
1148
|
+
// sibling bridge call (halts, judgments, recipe doctor, kill-switch).
|
|
1149
|
+
const controller = new AbortController();
|
|
1150
|
+
const timer = setTimeout(() => controller.abort(), 30_000);
|
|
1151
|
+
let res;
|
|
1152
|
+
let body;
|
|
1153
|
+
try {
|
|
1154
|
+
res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
headers: {
|
|
1157
|
+
Authorization: `Bearer ${lock.authToken}`,
|
|
1158
|
+
"Content-Type": "application/json",
|
|
1159
|
+
},
|
|
1160
|
+
body: JSON.stringify({
|
|
1161
|
+
name: recipeArg,
|
|
1162
|
+
...(seedVars ? { vars: seedVars } : {}),
|
|
1163
|
+
}),
|
|
1164
|
+
signal: controller.signal,
|
|
1165
|
+
});
|
|
1166
|
+
body = (await res.json());
|
|
1167
|
+
}
|
|
1168
|
+
catch (err) {
|
|
1169
|
+
const aborted = err instanceof Error &&
|
|
1170
|
+
(err.name === "AbortError" || controller.signal.aborted);
|
|
1171
|
+
process.stderr.write(aborted
|
|
1172
|
+
? `Error: bridge on port ${lock.port} did not respond within 30s — it may be wedged. Restart the bridge or re-run with --local.\n`
|
|
1173
|
+
: `Error: failed to reach bridge on port ${lock.port}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
finally {
|
|
1178
|
+
clearTimeout(timer);
|
|
1179
|
+
}
|
|
1053
1180
|
if (!body.ok) {
|
|
1054
1181
|
// Fall through to local YAML runner if bridge doesn't know the recipe.
|
|
1055
1182
|
if (!(body.error ?? "").includes("not found")) {
|
|
@@ -1080,9 +1207,37 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
|
1080
1207
|
? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
|
|
1081
1208
|
: ` Running recipe "${recipeArg}" locally…\n`);
|
|
1082
1209
|
const workdir = lock?.workspace || process.cwd();
|
|
1210
|
+
// PR5c — resume support: when --attempt is given, mint or reuse a
|
|
1211
|
+
// stable id and point the runner at a disk-backed effect ledger.
|
|
1212
|
+
// `--attempt new` always mints a fresh id; any other value is
|
|
1213
|
+
// taken verbatim (so the user can re-run the same attempt and
|
|
1214
|
+
// skip already-completed write tools).
|
|
1215
|
+
let resolvedAttempt;
|
|
1216
|
+
let resolvedLedgerDir;
|
|
1217
|
+
if (attemptId !== undefined) {
|
|
1218
|
+
resolvedAttempt =
|
|
1219
|
+
attemptId === "new"
|
|
1220
|
+
? `mr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
1221
|
+
: attemptId;
|
|
1222
|
+
// Validate at the CLI boundary so an invalid id fails loudly
|
|
1223
|
+
// before any side effects run (and before it lands in the run
|
|
1224
|
+
// log or hashed into a ledger scope key).
|
|
1225
|
+
try {
|
|
1226
|
+
const { assertValidManualRunId } = await import("./recipes/idempotencyKey.js");
|
|
1227
|
+
assertValidManualRunId(resolvedAttempt);
|
|
1228
|
+
}
|
|
1229
|
+
catch (err) {
|
|
1230
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
resolvedLedgerDir = ledgerDir ?? path.join(os.homedir(), ".patchwork");
|
|
1234
|
+
process.stdout.write(` Attempt id: ${resolvedAttempt} (ledger: ${resolvedLedgerDir})\n`);
|
|
1235
|
+
}
|
|
1083
1236
|
const run = await runRecipe(recipeArg, {
|
|
1084
1237
|
...(step ? { step } : {}),
|
|
1085
1238
|
...(seedVars ? { vars: seedVars } : {}),
|
|
1239
|
+
...(resolvedAttempt && { manualRunId: resolvedAttempt }),
|
|
1240
|
+
...(resolvedLedgerDir && { ledgerDir: resolvedLedgerDir }),
|
|
1086
1241
|
workdir,
|
|
1087
1242
|
});
|
|
1088
1243
|
if (run.stepSelection) {
|
|
@@ -1281,6 +1436,8 @@ if (process.argv[2] === "traces" && process.argv[3] === "export") {
|
|
|
1281
1436
|
let output;
|
|
1282
1437
|
let patchworkDir;
|
|
1283
1438
|
let activityDir;
|
|
1439
|
+
let mode;
|
|
1440
|
+
let passphrase;
|
|
1284
1441
|
for (let i = 0; i < args.length; i++) {
|
|
1285
1442
|
const a = args[i];
|
|
1286
1443
|
if (a === "--output" || a === "-o") {
|
|
@@ -1295,10 +1452,30 @@ if (process.argv[2] === "traces" && process.argv[3] === "export") {
|
|
|
1295
1452
|
activityDir = args[i + 1];
|
|
1296
1453
|
i++;
|
|
1297
1454
|
}
|
|
1455
|
+
else if (a === "--mode") {
|
|
1456
|
+
const m = args[i + 1];
|
|
1457
|
+
if (m !== "public" && m !== "keyed") {
|
|
1458
|
+
process.stderr.write(`Error: --mode must be "public" or "keyed" (got: ${m})\n`);
|
|
1459
|
+
process.exit(1);
|
|
1460
|
+
}
|
|
1461
|
+
mode = m;
|
|
1462
|
+
i++;
|
|
1463
|
+
}
|
|
1464
|
+
else if (a === "--passphrase") {
|
|
1465
|
+
passphrase = args[i + 1];
|
|
1466
|
+
i++;
|
|
1467
|
+
}
|
|
1298
1468
|
else if (a === "--help" || a === "-h") {
|
|
1299
|
-
process.stdout.write("patchwork traces export [--output <path>] [--
|
|
1469
|
+
process.stdout.write("patchwork traces export [--output <path>] [--mode public|keyed]\n" +
|
|
1470
|
+
" [--passphrase <phrase>]\n" +
|
|
1471
|
+
" [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
|
|
1300
1472
|
"Bundles ~/.patchwork/{runs,decision_traces,commit_issue_links}.jsonl\n" +
|
|
1301
1473
|
"and ~/.claude/ide/activity-*.jsonl into a single gzipped JSONL file.\n\n" +
|
|
1474
|
+
"Modes:\n" +
|
|
1475
|
+
" public (default) Plain gzip bundle (.jsonl.gz). Anyone with the file\n" +
|
|
1476
|
+
" can read it.\n" +
|
|
1477
|
+
" keyed AES-256-GCM encrypted bundle (.enc). Requires --passphrase;\n" +
|
|
1478
|
+
" auto-detected and decrypted by `traces import --passphrase`.\n\n" +
|
|
1302
1479
|
"Output is a manifest line followed by one envelope per row:\n" +
|
|
1303
1480
|
' {"type":"manifest", ...}\n' +
|
|
1304
1481
|
' {"source":"runs", "entry":{...}}\n' +
|
|
@@ -1308,11 +1485,17 @@ if (process.argv[2] === "traces" && process.argv[3] === "export") {
|
|
|
1308
1485
|
process.exit(0);
|
|
1309
1486
|
}
|
|
1310
1487
|
}
|
|
1488
|
+
if (mode === "keyed" && !passphrase) {
|
|
1489
|
+
process.stderr.write("Error: --mode keyed requires --passphrase <phrase>\n");
|
|
1490
|
+
process.exit(1);
|
|
1491
|
+
}
|
|
1311
1492
|
const { runTracesExport } = await import("./commands/tracesExport.js");
|
|
1312
1493
|
const result = await runTracesExport({
|
|
1313
1494
|
...(output !== undefined && { output }),
|
|
1314
1495
|
...(patchworkDir !== undefined && { patchworkDir }),
|
|
1315
1496
|
...(activityDir !== undefined && { activityDir }),
|
|
1497
|
+
...(mode !== undefined && { mode }),
|
|
1498
|
+
...(passphrase !== undefined && { passphrase }),
|
|
1316
1499
|
});
|
|
1317
1500
|
process.stdout.write(` ✓ Wrote ${result.outputPath}\n`);
|
|
1318
1501
|
process.stdout.write(` ${result.totalCount} rows from ${result.files.length} file${result.files.length === 1 ? "" : "s"} (${result.totalBytes} bytes read)\n`);
|
|
@@ -1402,8 +1585,9 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
|
|
|
1402
1585
|
}
|
|
1403
1586
|
const plain = decryptTraceBundle(raw, passphrase);
|
|
1404
1587
|
const { tmpdir } = await import("node:os");
|
|
1588
|
+
const { join: pathJoin } = await import("node:path");
|
|
1405
1589
|
const { writeFileSync } = await import("node:fs");
|
|
1406
|
-
const tmp =
|
|
1590
|
+
const tmp = pathJoin(tmpdir(), `patchwork-import-${Date.now()}.jsonl.gz`);
|
|
1407
1591
|
writeFileSync(tmp, plain, { mode: 0o600 });
|
|
1408
1592
|
input = tmp;
|
|
1409
1593
|
process.stderr.write("Decryption succeeded.\n");
|
|
@@ -1435,6 +1619,527 @@ if (process.argv[2] === "traces" && process.argv[3] === "import") {
|
|
|
1435
1619
|
}
|
|
1436
1620
|
})();
|
|
1437
1621
|
}
|
|
1622
|
+
// `patchwork kill-switch engage|release|status` — issue #422 step 3.
|
|
1623
|
+
//
|
|
1624
|
+
// Discovers the running bridge via lock file, POSTs /kill-switch with
|
|
1625
|
+
// Bearer auth, and surfaces structured errors (env-locked, no-bridge,
|
|
1626
|
+
// wedged-bridge). Multi-bridge fan-out: iterates ALL live `isBridge:true`
|
|
1627
|
+
// locks and engages/releases each (v2-B2 from #422).
|
|
1628
|
+
//
|
|
1629
|
+
// v2-I4: mandatory 10s deadline per request. No silent fallback on
|
|
1630
|
+
// timeout/ECONNREFUSED/non-2xx — error message + exit non-zero.
|
|
1631
|
+
if (process.argv[2] === "kill-switch") {
|
|
1632
|
+
const sub = process.argv[3];
|
|
1633
|
+
if (!sub || (sub !== "engage" && sub !== "release" && sub !== "status")) {
|
|
1634
|
+
process.stderr.write('Usage: patchwork kill-switch <engage|release|status> [--reason "..."]\n' +
|
|
1635
|
+
"\n" +
|
|
1636
|
+
" engage Block all write-tier tool calls across every running bridge.\n" +
|
|
1637
|
+
" release Resume writes.\n" +
|
|
1638
|
+
" status Print engaged/locked state per running bridge.\n" +
|
|
1639
|
+
"\n" +
|
|
1640
|
+
"Exits non-zero if any bridge is unreachable or env-locked.\n");
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
(async () => {
|
|
1644
|
+
try {
|
|
1645
|
+
// Parse optional flags early so --force-local can be used without a bridge.
|
|
1646
|
+
const args = process.argv.slice(4);
|
|
1647
|
+
const reasonIdx = args.findIndex((a) => a === "--reason" || a === "-m");
|
|
1648
|
+
const reason = reasonIdx >= 0 && reasonIdx + 1 < args.length
|
|
1649
|
+
? args[reasonIdx + 1]
|
|
1650
|
+
: undefined;
|
|
1651
|
+
// v2-I4: --force-local writes flags.json directly when no live bridge
|
|
1652
|
+
// is reachable. The running bridge's fs.watch (v2-S1) picks up the
|
|
1653
|
+
// change within ~100ms; without a running bridge this is "effective
|
|
1654
|
+
// next boot" — which is still better than a silent noop.
|
|
1655
|
+
const forceLocal = args.includes("--force-local");
|
|
1656
|
+
// v2-B2: enumerate ALL live bridge locks (not just the first).
|
|
1657
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
1658
|
+
const liveLocks = findAllLiveBridges();
|
|
1659
|
+
if (liveLocks.length === 0) {
|
|
1660
|
+
if (forceLocal && (sub === "engage" || sub === "release")) {
|
|
1661
|
+
// --force-local: write flags.json directly. The running bridge's
|
|
1662
|
+
// fs.watch picks this up within ~100ms; if the bridge is wedged
|
|
1663
|
+
// or not started, this is effective on next start.
|
|
1664
|
+
const { setFlag, KILL_SWITCH_WRITES } = await import("./featureFlags.js");
|
|
1665
|
+
const engage = sub === "engage";
|
|
1666
|
+
setFlag(KILL_SWITCH_WRITES, engage, true);
|
|
1667
|
+
// Audit in a sibling CLI-only JSONL (v2-I10: bridge-only writes
|
|
1668
|
+
// go to decision_traces.jsonl; CLI fallback is distinct).
|
|
1669
|
+
const os = await import("node:os");
|
|
1670
|
+
const path = await import("node:path");
|
|
1671
|
+
const fs = await import("node:fs");
|
|
1672
|
+
const cliTraceFile = path.join(process.env.PATCHWORK_HOME ??
|
|
1673
|
+
path.join(os.default.homedir(), ".patchwork"), "decision_traces.cli.jsonl");
|
|
1674
|
+
const dir = path.dirname(cliTraceFile);
|
|
1675
|
+
if (!fs.existsSync(dir))
|
|
1676
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1677
|
+
const entry = JSON.stringify({
|
|
1678
|
+
ts: new Date().toISOString(),
|
|
1679
|
+
event: engage ? "engage" : "release",
|
|
1680
|
+
actor: "cli-force-local",
|
|
1681
|
+
...(reason ? { reason } : {}),
|
|
1682
|
+
});
|
|
1683
|
+
fs.appendFileSync(cliTraceFile, `${entry}\n`);
|
|
1684
|
+
process.stdout.write(` ✓ kill-switch ${engage ? "ENGAGED" : "released"} via --force-local (flags.json written directly).\n` +
|
|
1685
|
+
" Running bridges will pick this up via fs.watch within ~100ms.\n");
|
|
1686
|
+
process.exit(0);
|
|
1687
|
+
}
|
|
1688
|
+
process.stderr.write("No running bridge found.\n" +
|
|
1689
|
+
" - For `engage`/`release`, kill-switch has no live target to update.\n" +
|
|
1690
|
+
" - Use --force-local to write flags.json directly (bridge fs.watch picks it up).\n" +
|
|
1691
|
+
" - Or restart the bridge and re-run this command.\n");
|
|
1692
|
+
process.exit(2);
|
|
1693
|
+
}
|
|
1694
|
+
// v2-I4: 10s per-request deadline. AbortController per call.
|
|
1695
|
+
async function callBridge(lock, method, body) {
|
|
1696
|
+
const controller = new AbortController();
|
|
1697
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1698
|
+
try {
|
|
1699
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/kill-switch`, {
|
|
1700
|
+
method,
|
|
1701
|
+
headers: {
|
|
1702
|
+
Authorization: `Bearer ${lock.authToken}`,
|
|
1703
|
+
"Content-Type": "application/json",
|
|
1704
|
+
},
|
|
1705
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
1706
|
+
signal: controller.signal,
|
|
1707
|
+
});
|
|
1708
|
+
let json;
|
|
1709
|
+
try {
|
|
1710
|
+
json = (await res.json());
|
|
1711
|
+
}
|
|
1712
|
+
catch {
|
|
1713
|
+
json = undefined;
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
ok: res.status >= 200 && res.status < 300,
|
|
1717
|
+
status: res.status,
|
|
1718
|
+
...(json ? { json } : {}),
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
catch (err) {
|
|
1722
|
+
return {
|
|
1723
|
+
ok: false,
|
|
1724
|
+
status: 0,
|
|
1725
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
finally {
|
|
1729
|
+
clearTimeout(timer);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (sub === "status") {
|
|
1733
|
+
let anyFailed = false;
|
|
1734
|
+
for (const lock of liveLocks) {
|
|
1735
|
+
const result = await callBridge(lock, "GET");
|
|
1736
|
+
if (!result.ok) {
|
|
1737
|
+
anyFailed = true;
|
|
1738
|
+
process.stderr.write(` ✗ bridge pid=${lock.pid} port=${lock.port} unreachable (${result.error ?? `status ${result.status}`})\n`);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
const j = result.json ?? {};
|
|
1742
|
+
const engaged = j.engaged === true ? "ENGAGED" : "released";
|
|
1743
|
+
const lockedSuffix = j.locked
|
|
1744
|
+
? ` [env-locked: ${j.lockedReason ?? "yes"}]`
|
|
1745
|
+
: "";
|
|
1746
|
+
const wsLabel = lock.workspace
|
|
1747
|
+
? lock.workspace.split("/").slice(-2).join("/")
|
|
1748
|
+
: `pid=${lock.pid}`;
|
|
1749
|
+
process.stdout.write(` ${engaged} port=${lock.port} ${wsLabel}${lockedSuffix}\n`);
|
|
1750
|
+
}
|
|
1751
|
+
process.exit(anyFailed ? 2 : 0);
|
|
1752
|
+
}
|
|
1753
|
+
// engage / release: POST to every live bridge, surface aggregate result.
|
|
1754
|
+
const engage = sub === "engage";
|
|
1755
|
+
let anyFailed = false;
|
|
1756
|
+
let anyChanged = false;
|
|
1757
|
+
for (const lock of liveLocks) {
|
|
1758
|
+
const result = await callBridge(lock, "POST", {
|
|
1759
|
+
engage,
|
|
1760
|
+
...(reason ? { reason } : {}),
|
|
1761
|
+
});
|
|
1762
|
+
const wsLabel = lock.workspace
|
|
1763
|
+
? lock.workspace.split("/").slice(-2).join("/")
|
|
1764
|
+
: `pid=${lock.pid}`;
|
|
1765
|
+
if (result.status === 409) {
|
|
1766
|
+
anyFailed = true;
|
|
1767
|
+
const lr = result.json?.lockedReason ??
|
|
1768
|
+
"env-locked at boot";
|
|
1769
|
+
process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: cannot ${sub} — ${lr}\n`);
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (!result.ok) {
|
|
1773
|
+
anyFailed = true;
|
|
1774
|
+
process.stderr.write(` ✗ port=${lock.port} ${wsLabel}: ${result.error ?? `status ${result.status}`}\n`);
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
const j = result.json ?? {};
|
|
1778
|
+
const changedTag = j.changed === true ? "" : " (no-op, already in state)";
|
|
1779
|
+
if (j.changed === true)
|
|
1780
|
+
anyChanged = true;
|
|
1781
|
+
process.stdout.write(` ✓ port=${lock.port} ${wsLabel}: ${engage ? "ENGAGED" : "released"}${changedTag}\n`);
|
|
1782
|
+
}
|
|
1783
|
+
if (anyFailed) {
|
|
1784
|
+
process.exit(2);
|
|
1785
|
+
}
|
|
1786
|
+
if (!anyChanged) {
|
|
1787
|
+
process.stdout.write(`\n All ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"} already in target state — no audit emit.\n`);
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
process.stdout.write(`\n Kill-switch ${engage ? "engaged" : "released"} on ${liveLocks.length} bridge${liveLocks.length === 1 ? "" : "s"}.\n`);
|
|
1791
|
+
}
|
|
1792
|
+
process.exit(0);
|
|
1793
|
+
}
|
|
1794
|
+
catch (err) {
|
|
1795
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
})();
|
|
1799
|
+
}
|
|
1800
|
+
// `patchwork panic` — alias for `patchwork kill-switch engage` (v2-Strong-2).
|
|
1801
|
+
//
|
|
1802
|
+
// Discoverable under stress (short command, obvious intent). Canonical noun
|
|
1803
|
+
// form is `kill-switch engage`; this alias matches it so shell history six
|
|
1804
|
+
// months later still makes sense. Does not accept sub-verbs — just runs engage.
|
|
1805
|
+
if (process.argv[2] === "panic") {
|
|
1806
|
+
const extra = process.argv.slice(3); // e.g. --reason "..." --force-local
|
|
1807
|
+
// Guard against `panic --help` engaging the kill switch — a real
|
|
1808
|
+
// footgun if you tab-completed the verb to confirm syntax before
|
|
1809
|
+
// committing to the action. `panic` is an alias, so we honor --help
|
|
1810
|
+
// here ourselves rather than forwarding to kill-switch engage.
|
|
1811
|
+
if (extra.includes("--help") || extra.includes("-h")) {
|
|
1812
|
+
console.log('Usage: patchwork panic [--reason "..."] [--force-local]\n\n' +
|
|
1813
|
+
" Alias for `patchwork kill-switch engage` — blocks all write-tier\n" +
|
|
1814
|
+
" tool calls across every running bridge. Use --reason to leave a\n" +
|
|
1815
|
+
" note in the audit trail. Release with `patchwork kill-switch release`.\n");
|
|
1816
|
+
process.exit(0);
|
|
1817
|
+
}
|
|
1818
|
+
// Spawn self with kill-switch engage to reuse the full handler without
|
|
1819
|
+
// duplicating 200+ LOC. Passes through any flags (--reason, --force-local).
|
|
1820
|
+
import("node:child_process").then(({ spawnSync }) => {
|
|
1821
|
+
const self = process.argv[1] ?? process.execPath;
|
|
1822
|
+
const result = spawnSync(process.execPath, [self, "kill-switch", "engage", ...extra], { stdio: "inherit" });
|
|
1823
|
+
process.exit(result.status ?? 1);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
// `patchwork halts` — one-screen morning summary of recent recipe halts.
|
|
1827
|
+
//
|
|
1828
|
+
// Composes the haltReason field (#441), category aggregator + endpoint
|
|
1829
|
+
// (#444), and dashboard pill conventions: queries the live bridge's
|
|
1830
|
+
// /runs/halt-summary endpoint over the chosen window and prints a
|
|
1831
|
+
// per-category breakdown plus the 5 most-recent halt reasons. Default
|
|
1832
|
+
// window is "overnight" (since 6pm yesterday local) so it lines up with
|
|
1833
|
+
// "what halted while I was asleep?".
|
|
1834
|
+
if (process.argv[2] === "halts") {
|
|
1835
|
+
const args = process.argv.slice(3);
|
|
1836
|
+
const wantHelp = args.includes("--help") || args.includes("-h");
|
|
1837
|
+
if (wantHelp) {
|
|
1838
|
+
process.stdout.write("Usage: patchwork halts [--window <name>] [--recipe <name>] [--json]\n" +
|
|
1839
|
+
"\n" +
|
|
1840
|
+
" --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
|
|
1841
|
+
" --recipe <name> filter to one recipe by name\n" +
|
|
1842
|
+
" --json emit raw JSON (for scripting)\n" +
|
|
1843
|
+
"\n" +
|
|
1844
|
+
'"overnight" = since 6pm yesterday local time.\n');
|
|
1845
|
+
process.exit(0);
|
|
1846
|
+
}
|
|
1847
|
+
function parseWindow() {
|
|
1848
|
+
const idx = args.findIndex((a) => a === "--window" || a === "-w");
|
|
1849
|
+
const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
|
|
1850
|
+
if (raw === "1h" ||
|
|
1851
|
+
raw === "24h" ||
|
|
1852
|
+
raw === "overnight" ||
|
|
1853
|
+
raw === "7d" ||
|
|
1854
|
+
raw === "any")
|
|
1855
|
+
return raw;
|
|
1856
|
+
process.stderr.write(`Unknown --window value: "${raw}"\n`);
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
}
|
|
1859
|
+
function windowSinceMs(w) {
|
|
1860
|
+
if (w === "any")
|
|
1861
|
+
return null;
|
|
1862
|
+
if (w === "1h")
|
|
1863
|
+
return 60 * 60 * 1000;
|
|
1864
|
+
if (w === "24h")
|
|
1865
|
+
return 24 * 60 * 60 * 1000;
|
|
1866
|
+
if (w === "7d")
|
|
1867
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
1868
|
+
const d = new Date();
|
|
1869
|
+
d.setHours(18, 0, 0, 0);
|
|
1870
|
+
if (d.getTime() > Date.now())
|
|
1871
|
+
d.setDate(d.getDate() - 1);
|
|
1872
|
+
return Date.now() - d.getTime();
|
|
1873
|
+
}
|
|
1874
|
+
const window = parseWindow();
|
|
1875
|
+
const wantJson = args.includes("--json");
|
|
1876
|
+
const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
|
|
1877
|
+
const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
|
|
1878
|
+
? args[recipeIdx + 1]
|
|
1879
|
+
: undefined;
|
|
1880
|
+
(async () => {
|
|
1881
|
+
try {
|
|
1882
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
1883
|
+
const liveLocks = findAllLiveBridges();
|
|
1884
|
+
if (liveLocks.length === 0) {
|
|
1885
|
+
process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
|
|
1886
|
+
process.exit(2);
|
|
1887
|
+
}
|
|
1888
|
+
const sinceMs = windowSinceMs(window);
|
|
1889
|
+
const params = [];
|
|
1890
|
+
if (sinceMs != null)
|
|
1891
|
+
params.push(`sinceMs=${sinceMs}`);
|
|
1892
|
+
if (recipeFilter)
|
|
1893
|
+
params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
|
|
1894
|
+
const qs = params.length > 0 ? `?${params.join("&")}` : "";
|
|
1895
|
+
// Walk live bridges in order; first responsive one wins. See the
|
|
1896
|
+
// matching block in the `judgments` handler — findAllLiveBridges
|
|
1897
|
+
// can include stale entries when a recycled PID still answers
|
|
1898
|
+
// `kill(pid, 0)` but the lock points at a dead bridge.
|
|
1899
|
+
let res = null;
|
|
1900
|
+
let lastStatus = 0;
|
|
1901
|
+
for (const lock of liveLocks) {
|
|
1902
|
+
const controller = new AbortController();
|
|
1903
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1904
|
+
try {
|
|
1905
|
+
const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
|
|
1906
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
1907
|
+
signal: controller.signal,
|
|
1908
|
+
});
|
|
1909
|
+
if (candidate.ok) {
|
|
1910
|
+
res = candidate;
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
lastStatus = candidate.status;
|
|
1914
|
+
}
|
|
1915
|
+
catch {
|
|
1916
|
+
/* unreachable lock — try next */
|
|
1917
|
+
}
|
|
1918
|
+
finally {
|
|
1919
|
+
clearTimeout(timer);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (!res) {
|
|
1923
|
+
process.stderr.write(`No live bridge served /runs/halt-summary (last status: ${lastStatus || "unreachable"}).\n`);
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
const summary = (await res.json());
|
|
1927
|
+
if (wantJson) {
|
|
1928
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
1929
|
+
process.exit(0);
|
|
1930
|
+
}
|
|
1931
|
+
// Shared label + hint maps (also used by `recipe doctor` and
|
|
1932
|
+
// mirrored in the dashboard) so the wording stays consistent.
|
|
1933
|
+
const { HALT_CATEGORY_LABELS, HALT_CATEGORY_HINTS } = await import("./recipes/haltCategory.js");
|
|
1934
|
+
const windowLabel = {
|
|
1935
|
+
"1h": "last hour",
|
|
1936
|
+
"24h": "last 24h",
|
|
1937
|
+
overnight: "since 6pm yesterday",
|
|
1938
|
+
"7d": "last 7 days",
|
|
1939
|
+
any: "all time",
|
|
1940
|
+
};
|
|
1941
|
+
const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
|
|
1942
|
+
process.stdout.write(`Halts — ${windowLabel[window]}${recipeSuffix}\n`);
|
|
1943
|
+
process.stdout.write(`Total: ${summary.total}\n`);
|
|
1944
|
+
if (summary.total === 0) {
|
|
1945
|
+
process.stdout.write("\n (nothing halted in this window)\n");
|
|
1946
|
+
process.exit(0);
|
|
1947
|
+
}
|
|
1948
|
+
const entries = Object.entries(summary.byCategory).sort(([, a], [, b]) => b - a);
|
|
1949
|
+
process.stdout.write("\nBy category:\n");
|
|
1950
|
+
for (const [cat, count] of entries) {
|
|
1951
|
+
const label = HALT_CATEGORY_LABELS[cat] ?? cat;
|
|
1952
|
+
const hint = HALT_CATEGORY_HINTS[cat];
|
|
1953
|
+
const hintSuffix = hint ? ` — ${hint}` : "";
|
|
1954
|
+
process.stdout.write(` ${String(count).padStart(3)} ${label}${hintSuffix}\n`);
|
|
1955
|
+
}
|
|
1956
|
+
if (summary.recent.length > 0) {
|
|
1957
|
+
process.stdout.write("\nMost recent:\n");
|
|
1958
|
+
for (const r of summary.recent) {
|
|
1959
|
+
// Truncate the reason to ~120 chars so a wide stack trace
|
|
1960
|
+
// can't blow up the terminal width on phones / narrow panes.
|
|
1961
|
+
const reason = r.reason.length > 120 ? `${r.reason.slice(0, 117)}…` : r.reason;
|
|
1962
|
+
process.stdout.write(` #${r.runSeq} [${r.category}] ${reason}\n`);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
process.exit(0);
|
|
1966
|
+
}
|
|
1967
|
+
catch (err) {
|
|
1968
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1969
|
+
process.exit(1);
|
|
1970
|
+
}
|
|
1971
|
+
})();
|
|
1972
|
+
}
|
|
1973
|
+
// `patchwork connect` — connector front-door. Thin CLI→HTTP shim over the
|
|
1974
|
+
// bridge's `/connections/*` routes (list / OAuth auth / PAT connect / test /
|
|
1975
|
+
// disconnect). This is the command the 23 connector "Run: patchwork connect
|
|
1976
|
+
// <vendor>" error messages point at. Lock discovery reuses the same
|
|
1977
|
+
// isBridge:true + live-PID selector as `recipe run`, `quick-task`, etc.
|
|
1978
|
+
if (process.argv[2] === "connect") {
|
|
1979
|
+
(async () => {
|
|
1980
|
+
const { runConnect } = await import("./commands/connect.js");
|
|
1981
|
+
const { findBridgeLockForTask } = await import("./commands/task.js");
|
|
1982
|
+
await runConnect(process.argv.slice(3), {
|
|
1983
|
+
findBridgeLock: (port) => findBridgeLockForTask(port),
|
|
1984
|
+
});
|
|
1985
|
+
})();
|
|
1986
|
+
}
|
|
1987
|
+
// `patchwork analytics` — manage the self-hosted telemetry collector config.
|
|
1988
|
+
// Replaces the brittle "endpoint+secret in launchd plist" pattern with a
|
|
1989
|
+
// proper config file the bridge reads at startup.
|
|
1990
|
+
if (process.argv[2] === "analytics") {
|
|
1991
|
+
(async () => {
|
|
1992
|
+
const { runAnalyticsCommand } = await import("./commands/analytics.js");
|
|
1993
|
+
const code = await runAnalyticsCommand(process.argv.slice(3));
|
|
1994
|
+
process.exit(code);
|
|
1995
|
+
})();
|
|
1996
|
+
}
|
|
1997
|
+
// `patchwork judgments` — PR3b sibling of `patchwork halts`. Same window
|
|
1998
|
+
// + recipe filter shape; queries /runs/judge-summary and prints a
|
|
1999
|
+
// per-verdict breakdown plus the 5 most-recent verdicts.
|
|
2000
|
+
if (process.argv[2] === "judgments") {
|
|
2001
|
+
const args = process.argv.slice(3);
|
|
2002
|
+
const wantHelp = args.includes("--help") || args.includes("-h");
|
|
2003
|
+
if (wantHelp) {
|
|
2004
|
+
process.stdout.write("Usage: patchwork judgments [--window <name>] [--recipe <name>] [--json]\n" +
|
|
2005
|
+
"\n" +
|
|
2006
|
+
" --window 1h | 24h | overnight | 7d | any (default: overnight)\n" +
|
|
2007
|
+
" --recipe <name> filter to one recipe by name\n" +
|
|
2008
|
+
" --json emit raw JSON (for scripting)\n" +
|
|
2009
|
+
"\n" +
|
|
2010
|
+
'"overnight" = since 6pm yesterday local time.\n');
|
|
2011
|
+
process.exit(0);
|
|
2012
|
+
}
|
|
2013
|
+
function parseWindow() {
|
|
2014
|
+
const idx = args.findIndex((a) => a === "--window" || a === "-w");
|
|
2015
|
+
const raw = idx >= 0 && idx + 1 < args.length ? args[idx + 1] : "overnight";
|
|
2016
|
+
if (raw === "1h" ||
|
|
2017
|
+
raw === "24h" ||
|
|
2018
|
+
raw === "overnight" ||
|
|
2019
|
+
raw === "7d" ||
|
|
2020
|
+
raw === "any")
|
|
2021
|
+
return raw;
|
|
2022
|
+
process.stderr.write(`Unknown --window value: "${raw}"\n`);
|
|
2023
|
+
process.exit(1);
|
|
2024
|
+
}
|
|
2025
|
+
function windowSinceMs(w) {
|
|
2026
|
+
if (w === "any")
|
|
2027
|
+
return null;
|
|
2028
|
+
if (w === "1h")
|
|
2029
|
+
return 60 * 60 * 1000;
|
|
2030
|
+
if (w === "24h")
|
|
2031
|
+
return 24 * 60 * 60 * 1000;
|
|
2032
|
+
if (w === "7d")
|
|
2033
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
2034
|
+
const d = new Date();
|
|
2035
|
+
d.setHours(18, 0, 0, 0);
|
|
2036
|
+
if (d.getTime() > Date.now())
|
|
2037
|
+
d.setDate(d.getDate() - 1);
|
|
2038
|
+
return Date.now() - d.getTime();
|
|
2039
|
+
}
|
|
2040
|
+
const window = parseWindow();
|
|
2041
|
+
const wantJson = args.includes("--json");
|
|
2042
|
+
const recipeIdx = args.findIndex((a) => a === "--recipe" || a === "-r");
|
|
2043
|
+
const recipeFilter = recipeIdx >= 0 && recipeIdx + 1 < args.length
|
|
2044
|
+
? args[recipeIdx + 1]
|
|
2045
|
+
: undefined;
|
|
2046
|
+
(async () => {
|
|
2047
|
+
try {
|
|
2048
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
2049
|
+
const liveLocks = findAllLiveBridges();
|
|
2050
|
+
if (liveLocks.length === 0) {
|
|
2051
|
+
process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
|
|
2052
|
+
process.exit(2);
|
|
2053
|
+
}
|
|
2054
|
+
const sinceMs = windowSinceMs(window);
|
|
2055
|
+
const params = [];
|
|
2056
|
+
if (sinceMs != null)
|
|
2057
|
+
params.push(`sinceMs=${sinceMs}`);
|
|
2058
|
+
if (recipeFilter)
|
|
2059
|
+
params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
|
|
2060
|
+
const qs = params.length > 0 ? `?${params.join("&")}` : "";
|
|
2061
|
+
// Walk live bridges in order; the first responsive one wins.
|
|
2062
|
+
// findAllLiveBridges uses `kill(pid, 0)` for liveness, which
|
|
2063
|
+
// returns true for any recycled PID — so liveLocks can contain
|
|
2064
|
+
// stale entries from dead bridges. Previously we picked [0]
|
|
2065
|
+
// unconditionally and surfaced a confusing 404; now we try each
|
|
2066
|
+
// and only fall through to the error path when *all* fail.
|
|
2067
|
+
let res = null;
|
|
2068
|
+
let lastStatus = 0;
|
|
2069
|
+
for (const lock of liveLocks) {
|
|
2070
|
+
const controller = new AbortController();
|
|
2071
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
2072
|
+
try {
|
|
2073
|
+
const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/judge-summary${qs}`, {
|
|
2074
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
2075
|
+
signal: controller.signal,
|
|
2076
|
+
});
|
|
2077
|
+
if (candidate.ok) {
|
|
2078
|
+
res = candidate;
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
lastStatus = candidate.status;
|
|
2082
|
+
}
|
|
2083
|
+
catch {
|
|
2084
|
+
/* unreachable lock — try next */
|
|
2085
|
+
}
|
|
2086
|
+
finally {
|
|
2087
|
+
clearTimeout(timer);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
if (!res) {
|
|
2091
|
+
process.stderr.write(`No live bridge served /runs/judge-summary (last status: ${lastStatus || "unreachable"}).\n`);
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
const summary = (await res.json());
|
|
2095
|
+
if (wantJson) {
|
|
2096
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
2097
|
+
process.exit(0);
|
|
2098
|
+
}
|
|
2099
|
+
const labels = {
|
|
2100
|
+
approve: "approve",
|
|
2101
|
+
request_changes: "request changes",
|
|
2102
|
+
unparseable: "unparseable",
|
|
2103
|
+
};
|
|
2104
|
+
const windowLabel = {
|
|
2105
|
+
"1h": "last hour",
|
|
2106
|
+
"24h": "last 24h",
|
|
2107
|
+
overnight: "since 6pm yesterday",
|
|
2108
|
+
"7d": "last 7 days",
|
|
2109
|
+
any: "all time",
|
|
2110
|
+
};
|
|
2111
|
+
const recipeSuffix = recipeFilter ? ` · recipe="${recipeFilter}"` : "";
|
|
2112
|
+
process.stdout.write(`Judgments — ${windowLabel[window]}${recipeSuffix}\n`);
|
|
2113
|
+
process.stdout.write(`Total: ${summary.total}\n`);
|
|
2114
|
+
if (summary.total === 0) {
|
|
2115
|
+
process.stdout.write("\n (no judge steps fired in this window)\n");
|
|
2116
|
+
process.exit(0);
|
|
2117
|
+
}
|
|
2118
|
+
const entries = Object.entries(summary.byVerdict).sort(([, a], [, b]) => b - a);
|
|
2119
|
+
process.stdout.write("\nBy verdict:\n");
|
|
2120
|
+
for (const [verdict, count] of entries) {
|
|
2121
|
+
const label = labels[verdict] ?? verdict;
|
|
2122
|
+
process.stdout.write(` ${String(count).padStart(3)} ${label}\n`);
|
|
2123
|
+
}
|
|
2124
|
+
if (summary.recent.length > 0) {
|
|
2125
|
+
process.stdout.write("\nMost recent:\n");
|
|
2126
|
+
for (const r of summary.recent) {
|
|
2127
|
+
const reason = r.firstReason
|
|
2128
|
+
? r.firstReason.length > 120
|
|
2129
|
+
? `${r.firstReason.slice(0, 117)}…`
|
|
2130
|
+
: r.firstReason
|
|
2131
|
+
: "(no reason)";
|
|
2132
|
+
process.stdout.write(` #${r.runSeq} [${r.verdict}] ${r.stepId}: ${reason}\n`);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
process.exit(0);
|
|
2136
|
+
}
|
|
2137
|
+
catch (err) {
|
|
2138
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2139
|
+
process.exit(1);
|
|
2140
|
+
}
|
|
2141
|
+
})();
|
|
2142
|
+
}
|
|
1438
2143
|
if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
|
|
1439
2144
|
const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
|
|
1440
2145
|
(async () => {
|
|
@@ -1454,44 +2159,62 @@ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
|
|
|
1454
2159
|
})();
|
|
1455
2160
|
}
|
|
1456
2161
|
// Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
|
|
2162
|
+
// With `--interactive`, drops into a connector-aware prompt tree instead.
|
|
1457
2163
|
if (process.argv[2] === "recipe" && process.argv[3] === "new") {
|
|
1458
2164
|
const args = process.argv.slice(4);
|
|
1459
|
-
const
|
|
1460
|
-
if (
|
|
1461
|
-
process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
|
|
1462
|
-
" --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
|
|
1463
|
-
" Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
|
|
1464
|
-
" write into the current directory instead.\n");
|
|
1465
|
-
process.stderr.write("\nTemplates:\n");
|
|
1466
|
-
(async () => {
|
|
1467
|
-
const { listTemplates } = await import("./commands/recipe.js");
|
|
1468
|
-
for (const t of listTemplates()) {
|
|
1469
|
-
process.stderr.write(` ${t}\n`);
|
|
1470
|
-
}
|
|
1471
|
-
process.exit(1);
|
|
1472
|
-
})();
|
|
1473
|
-
}
|
|
1474
|
-
else {
|
|
2165
|
+
const isInteractive = args.includes("--interactive") || args.includes("-i");
|
|
2166
|
+
if (isInteractive) {
|
|
1475
2167
|
(async () => {
|
|
1476
2168
|
try {
|
|
1477
|
-
const {
|
|
1478
|
-
const
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2169
|
+
const { runNewInteractive } = await import("./commands/recipe.js");
|
|
2170
|
+
const { createInterface } = await import("node:readline/promises");
|
|
2171
|
+
const rl = createInterface({
|
|
2172
|
+
input: process.stdin,
|
|
2173
|
+
output: process.stdout,
|
|
2174
|
+
});
|
|
1483
2175
|
const outIdx = args.indexOf("--out");
|
|
1484
2176
|
const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
|
|
1485
|
-
// `--out .` is the common case for "scaffold in cwd" — resolve so
|
|
1486
|
-
// the success message shows the absolute path the user can open.
|
|
1487
2177
|
const outputDir = outRaw ? path.resolve(outRaw) : undefined;
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2178
|
+
const deps = {
|
|
2179
|
+
ask: async (q) => (await rl.question(`${q}: `)).trim(),
|
|
2180
|
+
pickFromList: async (q, options) => {
|
|
2181
|
+
process.stdout.write(`\n${q}\n`);
|
|
2182
|
+
options.forEach((opt, i) => {
|
|
2183
|
+
process.stdout.write(` ${i + 1}. ${opt}\n`);
|
|
2184
|
+
});
|
|
2185
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2186
|
+
const raw = (await rl.question(`Choose 1-${options.length}: `)).trim();
|
|
2187
|
+
const idx = Number.parseInt(raw, 10);
|
|
2188
|
+
if (Number.isFinite(idx) && idx >= 1 && idx <= options.length) {
|
|
2189
|
+
return idx;
|
|
2190
|
+
}
|
|
2191
|
+
process.stdout.write(`Invalid choice. Enter a number 1-${options.length}.\n`);
|
|
2192
|
+
}
|
|
2193
|
+
throw new Error("Too many invalid choices");
|
|
2194
|
+
},
|
|
2195
|
+
confirm: async (q) => {
|
|
2196
|
+
const a = (await rl.question(`${q} [y/N]: `)).trim().toLowerCase();
|
|
2197
|
+
return a === "y" || a === "yes";
|
|
2198
|
+
},
|
|
2199
|
+
preview: (yaml) => {
|
|
2200
|
+
process.stdout.write("\n--- Preview ---\n");
|
|
2201
|
+
process.stdout.write(yaml);
|
|
2202
|
+
process.stdout.write("---\n\n");
|
|
2203
|
+
},
|
|
2204
|
+
};
|
|
2205
|
+
const result = await runNewInteractive({
|
|
2206
|
+
deps,
|
|
1492
2207
|
...(outputDir ? { outputDir } : {}),
|
|
1493
2208
|
});
|
|
2209
|
+
rl.close();
|
|
1494
2210
|
process.stdout.write(` ✓ Created ${result.path}\n`);
|
|
2211
|
+
if (result.warnings.length > 0) {
|
|
2212
|
+
process.stdout.write(`\n ⚠ Lint warnings (recipe still written):\n`);
|
|
2213
|
+
for (const w of result.warnings) {
|
|
2214
|
+
process.stdout.write(` [${w.level}] ${w.message}\n`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
process.stdout.write(`\n Run with: patchwork recipe run ${result.path}\n`);
|
|
1495
2218
|
process.exit(0);
|
|
1496
2219
|
}
|
|
1497
2220
|
catch (err) {
|
|
@@ -1500,6 +2223,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
|
|
|
1500
2223
|
}
|
|
1501
2224
|
})();
|
|
1502
2225
|
}
|
|
2226
|
+
else {
|
|
2227
|
+
const recipeName = args[0];
|
|
2228
|
+
if (!recipeName) {
|
|
2229
|
+
process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
|
|
2230
|
+
" --interactive (-i) Run the connector-aware prompt tree instead of using a template.\n" +
|
|
2231
|
+
" --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
|
|
2232
|
+
" Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
|
|
2233
|
+
" write into the current directory instead.\n");
|
|
2234
|
+
process.stderr.write("\nTemplates:\n");
|
|
2235
|
+
(async () => {
|
|
2236
|
+
const { listTemplates } = await import("./commands/recipe.js");
|
|
2237
|
+
for (const t of listTemplates()) {
|
|
2238
|
+
process.stderr.write(` ${t}\n`);
|
|
2239
|
+
}
|
|
2240
|
+
process.exit(1);
|
|
2241
|
+
})();
|
|
2242
|
+
}
|
|
2243
|
+
else {
|
|
2244
|
+
(async () => {
|
|
2245
|
+
try {
|
|
2246
|
+
const { runNew } = await import("./commands/recipe.js");
|
|
2247
|
+
const templateIdx = args.indexOf("--template");
|
|
2248
|
+
const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
|
|
2249
|
+
const descIdx = args.indexOf("--desc");
|
|
2250
|
+
const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
|
|
2251
|
+
`Recipe: ${recipeName}`;
|
|
2252
|
+
const outIdx = args.indexOf("--out");
|
|
2253
|
+
const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
|
|
2254
|
+
// `--out .` is the common case for "scaffold in cwd" — resolve so
|
|
2255
|
+
// the success message shows the absolute path the user can open.
|
|
2256
|
+
const outputDir = outRaw ? path.resolve(outRaw) : undefined;
|
|
2257
|
+
const result = runNew({
|
|
2258
|
+
name: recipeName,
|
|
2259
|
+
description,
|
|
2260
|
+
...(template ? { template } : {}),
|
|
2261
|
+
...(outputDir ? { outputDir } : {}),
|
|
2262
|
+
});
|
|
2263
|
+
process.stdout.write(` ✓ Created ${result.path}\n`);
|
|
2264
|
+
process.exit(0);
|
|
2265
|
+
}
|
|
2266
|
+
catch (err) {
|
|
2267
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2268
|
+
process.exit(1);
|
|
2269
|
+
}
|
|
2270
|
+
})();
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
1503
2273
|
}
|
|
1504
2274
|
// Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
|
|
1505
2275
|
if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
|
|
@@ -1531,6 +2301,53 @@ if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
|
|
|
1531
2301
|
}
|
|
1532
2302
|
})();
|
|
1533
2303
|
}
|
|
2304
|
+
// Patchwork: `patchwork recipe audit-env <recipe>` — check {{env.FOO}} references are satisfied.
|
|
2305
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "audit-env") {
|
|
2306
|
+
const recipeArg = process.argv[4];
|
|
2307
|
+
if (!recipeArg) {
|
|
2308
|
+
process.stderr.write("Usage: patchwork recipe audit-env <recipe> [--env-file <path>] [--workspace <path>]\n");
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
2311
|
+
(async () => {
|
|
2312
|
+
try {
|
|
2313
|
+
const args = process.argv.slice(5);
|
|
2314
|
+
const envFileIdx = args.indexOf("--env-file");
|
|
2315
|
+
const envFile = envFileIdx !== -1 ? args[envFileIdx + 1] : undefined;
|
|
2316
|
+
const workspaceIdx = args.indexOf("--workspace");
|
|
2317
|
+
const workspace = workspaceIdx !== -1 && args[workspaceIdx + 1]
|
|
2318
|
+
? args[workspaceIdx + 1]
|
|
2319
|
+
: process.cwd();
|
|
2320
|
+
const { runAuditEnv } = await import("./commands/auditEnv.js");
|
|
2321
|
+
const result = await runAuditEnv(recipeArg, {
|
|
2322
|
+
...(envFile ? { envFile } : {}),
|
|
2323
|
+
workspace,
|
|
2324
|
+
});
|
|
2325
|
+
if (result.warnings.length > 0) {
|
|
2326
|
+
for (const w of result.warnings) {
|
|
2327
|
+
process.stderr.write(` ⚠ ${w}\n`);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (result.present.length > 0) {
|
|
2331
|
+
for (const v of result.present) {
|
|
2332
|
+
process.stdout.write(` ✓ ${v}\n`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
if (result.missing.length > 0) {
|
|
2336
|
+
for (const v of result.missing) {
|
|
2337
|
+
process.stderr.write(` ✗ missing: ${v}\n`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (!result.ok) {
|
|
2341
|
+
process.exit(1);
|
|
2342
|
+
}
|
|
2343
|
+
process.exit(0);
|
|
2344
|
+
}
|
|
2345
|
+
catch (err) {
|
|
2346
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2347
|
+
process.exit(1);
|
|
2348
|
+
}
|
|
2349
|
+
})();
|
|
2350
|
+
}
|
|
1534
2351
|
// Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
|
|
1535
2352
|
if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
|
|
1536
2353
|
const args = process.argv.slice(4);
|
|
@@ -1634,6 +2451,114 @@ if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
|
|
|
1634
2451
|
}
|
|
1635
2452
|
})();
|
|
1636
2453
|
}
|
|
2454
|
+
// `recipe doctor <name|file>` — one-screen "why is this recipe unhealthy
|
|
2455
|
+
// + how do I fix it" diagnosis. Composes the static preflight check with
|
|
2456
|
+
// the recipe-scoped runtime halt summary from a live bridge (fail-soft:
|
|
2457
|
+
// no bridge → static-only). See runRecipeDoctor in commands/recipe.ts.
|
|
2458
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "doctor") {
|
|
2459
|
+
const args = process.argv.slice(4);
|
|
2460
|
+
const usage = "Usage: patchwork recipe doctor <name|file.yaml> [--json] [--local]\n\n" +
|
|
2461
|
+
"Diagnoses a recipe: lint + write-policy + plan (static) plus recent\n" +
|
|
2462
|
+
"runtime halts from a live bridge, each mapped to a fix hint.\n" +
|
|
2463
|
+
"--local skips the bridge runtime check. Exits 1 when unhealthy.\n";
|
|
2464
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2465
|
+
process.stdout.write(usage);
|
|
2466
|
+
process.exit(0);
|
|
2467
|
+
}
|
|
2468
|
+
const wantJson = args.includes("--json");
|
|
2469
|
+
const localOnly = args.includes("--local");
|
|
2470
|
+
const ref = args.find((a) => !a.startsWith("--"));
|
|
2471
|
+
if (!ref) {
|
|
2472
|
+
process.stderr.write(usage);
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
}
|
|
2475
|
+
(async () => {
|
|
2476
|
+
try {
|
|
2477
|
+
const { runRecipeDoctor, formatRecipeDoctorReport } = await import("./commands/recipe.js");
|
|
2478
|
+
// Runtime halt fetcher: walk live bridges, query the recipe-scoped
|
|
2479
|
+
// halt summary over the last 7 days. Returns null when no bridge is
|
|
2480
|
+
// reachable so doctor degrades to static-only.
|
|
2481
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
2482
|
+
const fetchHalts = localOnly
|
|
2483
|
+
? undefined
|
|
2484
|
+
: async (recipeName) => {
|
|
2485
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
2486
|
+
const liveLocks = findAllLiveBridges();
|
|
2487
|
+
if (liveLocks.length === 0)
|
|
2488
|
+
return null;
|
|
2489
|
+
const sinceMs = Date.now() - SEVEN_DAYS_MS;
|
|
2490
|
+
const qs = `?sinceMs=${sinceMs}&recipe=${encodeURIComponent(recipeName)}`;
|
|
2491
|
+
for (const lock of liveLocks) {
|
|
2492
|
+
const controller = new AbortController();
|
|
2493
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
2494
|
+
try {
|
|
2495
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
|
|
2496
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
2497
|
+
signal: controller.signal,
|
|
2498
|
+
});
|
|
2499
|
+
if (res.ok) {
|
|
2500
|
+
return (await res.json());
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
catch {
|
|
2504
|
+
/* unreachable lock — try next */
|
|
2505
|
+
}
|
|
2506
|
+
finally {
|
|
2507
|
+
clearTimeout(timer);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return null;
|
|
2511
|
+
};
|
|
2512
|
+
// Connector auth fetcher: walk live bridges, GET /connections to read
|
|
2513
|
+
// each connector's status. Returns null when no bridge is reachable so
|
|
2514
|
+
// doctor degrades to static-only (lists required connectors, no auth).
|
|
2515
|
+
const fetchConnections = localOnly
|
|
2516
|
+
? undefined
|
|
2517
|
+
: async () => {
|
|
2518
|
+
const { findAllLiveBridges } = await import("./bridgeLockDiscovery.js");
|
|
2519
|
+
const liveLocks = findAllLiveBridges();
|
|
2520
|
+
if (liveLocks.length === 0)
|
|
2521
|
+
return null;
|
|
2522
|
+
for (const lock of liveLocks) {
|
|
2523
|
+
const controller = new AbortController();
|
|
2524
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
2525
|
+
try {
|
|
2526
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/connections`, {
|
|
2527
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
2528
|
+
signal: controller.signal,
|
|
2529
|
+
});
|
|
2530
|
+
if (res.ok) {
|
|
2531
|
+
const body = (await res.json());
|
|
2532
|
+
return body.connectors ?? [];
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
catch {
|
|
2536
|
+
/* unreachable lock — try next */
|
|
2537
|
+
}
|
|
2538
|
+
finally {
|
|
2539
|
+
clearTimeout(timer);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
return null;
|
|
2543
|
+
};
|
|
2544
|
+
const result = await runRecipeDoctor(ref, {
|
|
2545
|
+
fetchHalts,
|
|
2546
|
+
fetchConnections,
|
|
2547
|
+
});
|
|
2548
|
+
if (wantJson) {
|
|
2549
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2550
|
+
}
|
|
2551
|
+
else {
|
|
2552
|
+
process.stdout.write(formatRecipeDoctorReport(result));
|
|
2553
|
+
}
|
|
2554
|
+
process.exit(result.ok ? 0 : 1);
|
|
2555
|
+
}
|
|
2556
|
+
catch (err) {
|
|
2557
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2558
|
+
process.exit(1);
|
|
2559
|
+
}
|
|
2560
|
+
})();
|
|
2561
|
+
}
|
|
1637
2562
|
// Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
|
|
1638
2563
|
if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
|
|
1639
2564
|
const args = process.argv.slice(4);
|
|
@@ -1952,9 +2877,12 @@ Steps performed:
|
|
|
1952
2877
|
}
|
|
1953
2878
|
if (extensionArg2) {
|
|
1954
2879
|
try {
|
|
2880
|
+
// Windows: editor binaries (code/cursor/windsurf) are `.cmd` shims that
|
|
2881
|
+
// Node's execFileSync can't launch without a shell. See bridgeProcess.ts.
|
|
1955
2882
|
execFileSync(editor, ["--install-extension", extensionArg2], {
|
|
1956
2883
|
stdio: "pipe",
|
|
1957
2884
|
timeout: 30000,
|
|
2885
|
+
shell: process.platform === "win32",
|
|
1958
2886
|
});
|
|
1959
2887
|
process.stderr.write(` ✓ Extension installed via ${editor}\n\n`);
|
|
1960
2888
|
}
|
|
@@ -2083,13 +3011,18 @@ Steps performed:
|
|
|
2083
3011
|
process.stderr.write(` ✓ MCP shim — already registered in ${claudeJsonAbs}\n\n`);
|
|
2084
3012
|
}
|
|
2085
3013
|
else {
|
|
3014
|
+
// claude -p spawns the stdio command via Node's child_process, which
|
|
3015
|
+
// can't resolve a bare `.cmd` shim on Windows. Record the `.cmd` form
|
|
3016
|
+
// on win32 so the bridge binary is findable by the spawned process.
|
|
2086
3017
|
mcpServers["claude-ide-bridge"] = {
|
|
2087
|
-
command: "claude-ide-bridge",
|
|
3018
|
+
command: ensureCmdShim("claude-ide-bridge"),
|
|
2088
3019
|
args: ["shim"],
|
|
2089
3020
|
type: "stdio",
|
|
2090
3021
|
};
|
|
2091
3022
|
claudeJson.mcpServers = mcpServers;
|
|
2092
|
-
|
|
3023
|
+
// Atomic — `~/.claude.json` holds every MCP server registration on
|
|
3024
|
+
// the machine. A crash mid-write would brick Claude Code globally.
|
|
3025
|
+
writeFileAtomicSync(claudeJsonAbs, `${JSON.stringify(claudeJson, null, 2)}\n`);
|
|
2093
3026
|
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`);
|
|
2094
3027
|
}
|
|
2095
3028
|
}
|
|
@@ -2140,7 +3073,9 @@ Steps performed:
|
|
|
2140
3073
|
}
|
|
2141
3074
|
if (added.length > 0 || migrated.length > 0) {
|
|
2142
3075
|
ccSettings.hooks = ccHooks;
|
|
2143
|
-
|
|
3076
|
+
// Atomic — `~/.claude/settings.json` holds every CC hook entry; a
|
|
3077
|
+
// crash mid-write loses the user's full hook configuration.
|
|
3078
|
+
writeFileAtomicSync(ccSettingsPath, `${JSON.stringify(ccSettings, null, 2)}\n`);
|
|
2144
3079
|
const addMsg = added.length > 0
|
|
2145
3080
|
? ` ✓ CC hooks — wired ${added.length} automation hook(s) in ${ccSettingsPath}\n Added: ${added.join(", ")}\n`
|
|
2146
3081
|
: "";
|
|
@@ -2181,6 +3116,9 @@ Steps performed:
|
|
|
2181
3116
|
execFileSync("claude-ide-bridge", ["--version"], {
|
|
2182
3117
|
stdio: "pipe",
|
|
2183
3118
|
timeout: 5000,
|
|
3119
|
+
// Windows: global npm bin is a `.cmd` shim that Node's execFileSync
|
|
3120
|
+
// can't launch without a shell. See bridgeProcess.ts for context.
|
|
3121
|
+
shell: process.platform === "win32",
|
|
2184
3122
|
});
|
|
2185
3123
|
shimOnPath = true;
|
|
2186
3124
|
}
|
|
@@ -2240,13 +3178,8 @@ Steps performed:
|
|
|
2240
3178
|
let hooksWired = false;
|
|
2241
3179
|
try {
|
|
2242
3180
|
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
if (hooksObj && typeof hooksObj === "object") {
|
|
2246
|
-
hooksWired = Object.values(hooksObj).flat().some((e) => typeof e?.command ===
|
|
2247
|
-
"string" &&
|
|
2248
|
-
(e.command ?? "").includes("claude-ide-bridge"));
|
|
2249
|
-
}
|
|
3181
|
+
const { isPreToolUseHookRegistered } = await import("./preToolUseHook.js");
|
|
3182
|
+
hooksWired = isPreToolUseHookRegistered(settingsPath);
|
|
2250
3183
|
}
|
|
2251
3184
|
catch {
|
|
2252
3185
|
/* file may not exist yet — non-fatal */
|
|
@@ -2264,8 +3197,11 @@ Steps performed:
|
|
|
2264
3197
|
? fallbackDocs
|
|
2265
3198
|
: null;
|
|
2266
3199
|
if (target) {
|
|
2267
|
-
|
|
2268
|
-
|
|
3200
|
+
// Use execFile with argv (no shell) — exec(`code "${target}"`) was
|
|
3201
|
+
// shell-evaluated and could be injected via `--workspace '"; ...'`
|
|
3202
|
+
// since path.resolve preserves shell metachars. Audit 2026-05-17.
|
|
3203
|
+
const { execFile } = await import("node:child_process");
|
|
3204
|
+
execFile("code", [target], { timeout: 3000 }, () => { });
|
|
2269
3205
|
}
|
|
2270
3206
|
}
|
|
2271
3207
|
// Analytics opt-in prompt — only ask once; skip if preference already set
|
|
@@ -2382,9 +3318,11 @@ else if (process.argv[2] === "install-extension") {
|
|
|
2382
3318
|
}
|
|
2383
3319
|
try {
|
|
2384
3320
|
process.stderr.write(`Installing extension via ${editor}...\n`);
|
|
3321
|
+
// Windows: editor binaries are `.cmd` shims; need shell for resolution.
|
|
2385
3322
|
execFileSync(editor, ["--install-extension", extensionArg], {
|
|
2386
3323
|
stdio: "inherit",
|
|
2387
3324
|
timeout: 30000,
|
|
3325
|
+
shell: process.platform === "win32",
|
|
2388
3326
|
});
|
|
2389
3327
|
process.stderr.write("Extension installed successfully.\n");
|
|
2390
3328
|
}
|
|
@@ -2561,6 +3499,88 @@ Options:
|
|
|
2561
3499
|
}
|
|
2562
3500
|
process.exit(0);
|
|
2563
3501
|
}
|
|
3502
|
+
// `patchwork doctor` — run CLI-safe bridge health checks.
|
|
3503
|
+
if (process.argv[2] === "doctor") {
|
|
3504
|
+
const args = process.argv.slice(3);
|
|
3505
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3506
|
+
process.stdout.write("Usage: patchwork doctor [--workspace <path>] [--port <n>] [--json]\n\n" +
|
|
3507
|
+
"Runs bridge health checks (workspace, git, lock file, automation policy).\n" +
|
|
3508
|
+
"Exits 1 if any check fails.\n");
|
|
3509
|
+
process.exit(0);
|
|
3510
|
+
}
|
|
3511
|
+
(async () => {
|
|
3512
|
+
try {
|
|
3513
|
+
const workspaceIdx = args.indexOf("--workspace");
|
|
3514
|
+
const workspace = workspaceIdx !== -1 && args[workspaceIdx + 1]
|
|
3515
|
+
? args[workspaceIdx + 1]
|
|
3516
|
+
: process.cwd();
|
|
3517
|
+
const portIdx = args.indexOf("--port");
|
|
3518
|
+
const portArg = portIdx !== -1 ? args[portIdx + 1] : undefined;
|
|
3519
|
+
const port = portArg !== undefined ? Number(portArg) : undefined;
|
|
3520
|
+
const jsonFlag = args.includes("--json");
|
|
3521
|
+
const { runDoctor } = await import("./commands/doctor.js");
|
|
3522
|
+
const result = await runDoctor({ workspace, port, json: jsonFlag });
|
|
3523
|
+
if (jsonFlag) {
|
|
3524
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
3525
|
+
}
|
|
3526
|
+
else {
|
|
3527
|
+
for (const check of result.checks) {
|
|
3528
|
+
const icon = check.status === "ok" ? "✓" : check.status === "warn" ? "⚠" : "✗";
|
|
3529
|
+
const detail = check.detail ? ` ${check.detail}` : "";
|
|
3530
|
+
process.stdout.write(` ${icon} ${check.name}${detail}\n`);
|
|
3531
|
+
if (check.suggestion) {
|
|
3532
|
+
process.stdout.write(` → ${check.suggestion}\n`);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
process.exit(result.ok ? 0 : 1);
|
|
3537
|
+
}
|
|
3538
|
+
catch (err) {
|
|
3539
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
})();
|
|
3543
|
+
}
|
|
3544
|
+
// `patchwork shadow-scan` — replay run history through the destructive-tool classifier.
|
|
3545
|
+
if (process.argv[2] === "shadow-scan") {
|
|
3546
|
+
const args = process.argv.slice(3);
|
|
3547
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3548
|
+
process.stdout.write("Usage: patchwork shadow-scan [--since <duration|ISO>] [--limit <n>] [--runs-file <path>] [--json]\n\n" +
|
|
3549
|
+
"Replays historical run data through the destructive-tool classifier.\n" +
|
|
3550
|
+
"Exits 1 if any runs would be reclassified.\n\n" +
|
|
3551
|
+
" --since <duration|ISO> Lookback window, e.g. '24h', '7d', or ISO date (default: 7d)\n" +
|
|
3552
|
+
" --limit <n> Cap the number of runs to scan\n" +
|
|
3553
|
+
" --runs-file <path> Override default ~/.claude/ide/runs.jsonl path\n" +
|
|
3554
|
+
" --json Emit JSON output\n");
|
|
3555
|
+
process.exit(0);
|
|
3556
|
+
}
|
|
3557
|
+
(async () => {
|
|
3558
|
+
try {
|
|
3559
|
+
const sinceIdx = args.indexOf("--since");
|
|
3560
|
+
const since = sinceIdx !== -1 ? args[sinceIdx + 1] : undefined;
|
|
3561
|
+
const limitIdx = args.indexOf("--limit");
|
|
3562
|
+
const limitArg = limitIdx !== -1 ? args[limitIdx + 1] : undefined;
|
|
3563
|
+
const limit = limitArg !== undefined ? Number(limitArg) : undefined;
|
|
3564
|
+
const runsFileIdx = args.indexOf("--runs-file");
|
|
3565
|
+
const runsFile = runsFileIdx !== -1 ? args[runsFileIdx + 1] : undefined;
|
|
3566
|
+
const jsonFlag = args.includes("--json");
|
|
3567
|
+
const { runShadowScanCli } = await import("./commands/shadowScan.js");
|
|
3568
|
+
await runShadowScanCli({
|
|
3569
|
+
...(since !== undefined ? { since } : {}),
|
|
3570
|
+
...(limit !== undefined ? { limit } : {}),
|
|
3571
|
+
...(runsFile !== undefined
|
|
3572
|
+
? { runsFile, workspace: process.cwd() }
|
|
3573
|
+
: {}),
|
|
3574
|
+
json: jsonFlag,
|
|
3575
|
+
});
|
|
3576
|
+
// runShadowScanCli sets process.exitCode = 1 on reclassified > 0; no explicit exit needed.
|
|
3577
|
+
}
|
|
3578
|
+
catch (err) {
|
|
3579
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
3580
|
+
process.exit(1);
|
|
3581
|
+
}
|
|
3582
|
+
})();
|
|
3583
|
+
}
|
|
2564
3584
|
// Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
|
|
2565
3585
|
if (process.argv[2] === "launchd") {
|
|
2566
3586
|
const sub = process.argv[3];
|
|
@@ -2581,11 +3601,24 @@ if (process.argv[2] === "launchd") {
|
|
|
2581
3601
|
// F6: "Did you mean?" for unknown CLI subcommands
|
|
2582
3602
|
// Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
|
|
2583
3603
|
{
|
|
2584
|
-
const binName =
|
|
3604
|
+
const binName = invokedBinaryName();
|
|
2585
3605
|
const isPatchworkBin = binName === "patchwork-os" ||
|
|
2586
3606
|
binName === "patchwork" ||
|
|
2587
3607
|
binName === "patchwork.js";
|
|
2588
3608
|
if (isPatchworkBin && (!process.argv[2] || process.argv[2] === "dashboard")) {
|
|
3609
|
+
// First-run guard: if the user hasn't run `patchwork init` yet, launching
|
|
3610
|
+
// the dashboard renders an empty panel with no signpost. Print an
|
|
3611
|
+
// actionable pointer instead and exit cleanly.
|
|
3612
|
+
const cfgPath = path.join(os.homedir(), ".patchwork", "config.json");
|
|
3613
|
+
if (!existsSync(cfgPath) && !process.argv[2]) {
|
|
3614
|
+
process.stdout.write(`No Patchwork config found at ${cfgPath}.\n\n` +
|
|
3615
|
+
`Run \`${binName} init\` to scaffold ~/.patchwork and wire up\n` +
|
|
3616
|
+
`Claude Code hooks, then \`${binName}\` again to open the dashboard.\n\n` +
|
|
3617
|
+
`For just the IDE bridge (no recipes / approval queue), run:\n` +
|
|
3618
|
+
` ${binName} install-extension\n` +
|
|
3619
|
+
` ${binName} --workspace .\n`);
|
|
3620
|
+
process.exit(0);
|
|
3621
|
+
}
|
|
2589
3622
|
(async () => {
|
|
2590
3623
|
const { runDashboard } = await import("./commands/dashboard.js");
|
|
2591
3624
|
await runDashboard();
|
|
@@ -2663,8 +3696,10 @@ else {
|
|
|
2663
3696
|
.digest("hex")
|
|
2664
3697
|
.slice(0, 6);
|
|
2665
3698
|
const sessionName = `claude-bridge-${ws}${hash}`;
|
|
2666
|
-
// Check if tmux is available
|
|
2667
|
-
const tmuxCheck =
|
|
3699
|
+
// Check if tmux is available (skip on Windows — tmux doesn't exist there)
|
|
3700
|
+
const tmuxCheck = process.platform !== "win32"
|
|
3701
|
+
? spawnSync("which", ["tmux"], { stdio: "ignore" })
|
|
3702
|
+
: { status: 1 };
|
|
2668
3703
|
if (tmuxCheck.status !== 0) {
|
|
2669
3704
|
process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
|
|
2670
3705
|
}
|
|
@@ -2713,7 +3748,12 @@ else {
|
|
|
2713
3748
|
for (const sig of ["SIGTERM", "SIGINT"]) {
|
|
2714
3749
|
process.once(sig, () => {
|
|
2715
3750
|
stopping = true;
|
|
2716
|
-
|
|
3751
|
+
// Use treeKill so grandchildren (recipe runners, claude
|
|
3752
|
+
// subprocesses, extension watchers) are reaped on Windows.
|
|
3753
|
+
// Bare `child.kill(sig)` maps to TerminateProcess on win32
|
|
3754
|
+
// and skips descendants → orphaned processes survive a
|
|
3755
|
+
// supervisor SIGTERM. Audit 2026-05-17.
|
|
3756
|
+
treeKill(child, sig);
|
|
2717
3757
|
});
|
|
2718
3758
|
}
|
|
2719
3759
|
child.on("exit", (code, signal) => {
|
|
@@ -2741,19 +3781,27 @@ else {
|
|
|
2741
3781
|
process.stderr.write(`Error: ${message}\n`);
|
|
2742
3782
|
process.exit(1);
|
|
2743
3783
|
});
|
|
2744
|
-
// F5: Silent self-update nudge (fire-and-forget)
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
3784
|
+
// F5: Silent self-update nudge (fire-and-forget).
|
|
3785
|
+
// Skip when running from a source tree (any of: a `.git` sibling of the
|
|
3786
|
+
// package, or __dirnameTop not under a node_modules/). Otherwise a dev
|
|
3787
|
+
// who built locally sees "Bridge v<X> available" pointing at an npm
|
|
3788
|
+
// install path they're not using.
|
|
3789
|
+
const isSourceBuild = existsSync(path.join(__dirnameTop, "..", ".git")) ||
|
|
3790
|
+
!__dirnameTop.includes(`${path.sep}node_modules${path.sep}`);
|
|
3791
|
+
if (!isSourceBuild) {
|
|
3792
|
+
import("node:child_process")
|
|
3793
|
+
.then(({ exec }) => {
|
|
3794
|
+
exec("npm view patchwork-os version", { timeout: 5000 }, (err, stdout) => {
|
|
3795
|
+
if (err || !stdout)
|
|
3796
|
+
return;
|
|
3797
|
+
const latest = stdout.trim();
|
|
3798
|
+
if (latest && semverGt(latest, PACKAGE_VERSION)) {
|
|
3799
|
+
console.log(`\n Patchwork OS v${latest} available — run: npm update -g patchwork-os\n`);
|
|
3800
|
+
}
|
|
3801
|
+
});
|
|
3802
|
+
})
|
|
3803
|
+
.catch(() => { });
|
|
3804
|
+
}
|
|
2757
3805
|
}
|
|
2758
3806
|
} // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
|
|
2759
3807
|
//# sourceMappingURL=index.js.map
|