stageflow 0.33.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/agent/adapterSpill.d.ts +14 -0
- package/dist/agent/adapterSpill.js +83 -0
- package/dist/agent/agentBackend.d.ts +2 -2
- package/dist/agent/agentBackend.js +2 -2
- package/dist/agent/claudeAdapter.d.ts +13 -2
- package/dist/agent/claudeAdapter.js +56 -6
- package/dist/agent/cursorProvider.d.ts +3 -0
- package/dist/agent/cursorProvider.js +23 -6
- package/dist/agent/fakeAgent.d.ts +3 -3
- package/dist/agent/piAdapter.d.ts +2 -2
- package/dist/agent/piAdapter.js +125 -15
- package/dist/agent/piIsolatedMcp.d.ts +1 -0
- package/dist/agent/piIsolatedMcp.js +19 -1
- package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
- package/dist/agent/piIsolatedMcpProbe.js +20 -0
- package/dist/agent/port.d.ts +4 -2
- package/dist/agent/providerAuth.js +36 -3
- package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -8
- package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +11 -11
- package/dist/agent/stageTurnDriver.d.ts +33 -0
- package/dist/agent/stageTurnDriver.js +64 -0
- package/dist/agent/toolOutputBudget.d.ts +46 -0
- package/dist/agent/toolOutputBudget.js +97 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
- package/dist/agent/toolOutputBudgetExtension.js +141 -0
- package/dist/agent/toolOutputSpill.d.ts +34 -0
- package/dist/agent/toolOutputSpill.js +85 -0
- package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
- package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
- package/dist/browser/anchor.d.ts +2 -0
- package/dist/browser/anchor.js +2 -1
- package/dist/browser/auditSink.d.ts +8 -0
- package/dist/browser/browserHost.d.ts +29 -0
- package/dist/browser/browserSweep.d.ts +4 -1
- package/dist/browser/browserSweep.js +7 -0
- package/dist/browser/browserTeardown.d.ts +1 -0
- package/dist/browser/browserTeardown.js +32 -2
- package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
- package/dist/browser/containerBrowserEndpoint.js +57 -0
- package/dist/browser/containerBrowserHost.d.ts +24 -0
- package/dist/browser/containerBrowserHost.js +147 -0
- package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
- package/dist/browser/dockerSandboxOrchestrator.js +142 -0
- package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
- package/dist/browser/fakeLiveViewRelay.js +94 -0
- package/dist/browser/fakeLiveViewSource.d.ts +6 -0
- package/dist/browser/fakeLiveViewSource.js +16 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
- package/dist/browser/fakeRemoteBrowserHost.js +2 -1
- package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
- package/dist/browser/fakeSandboxOrchestrator.js +44 -0
- package/dist/browser/gateHandoff.d.ts +0 -2
- package/dist/browser/gateHandoff.js +5 -11
- package/dist/browser/hostCapabilities.d.ts +23 -0
- package/dist/browser/hostCapabilities.js +23 -0
- package/dist/browser/humanLogin.d.ts +2 -2
- package/dist/browser/humanLogin.js +11 -2
- package/dist/browser/liveViewPersisted.d.ts +11 -0
- package/dist/browser/liveViewPersisted.js +22 -0
- package/dist/browser/liveViewRelay.d.ts +76 -0
- package/dist/browser/liveViewRelay.js +1 -0
- package/dist/browser/liveViewSessions.d.ts +25 -0
- package/dist/browser/liveViewSessions.js +139 -0
- package/dist/browser/liveViewSource.d.ts +25 -0
- package/dist/browser/liveViewSource.js +7 -0
- package/dist/browser/liveViewTickets.d.ts +46 -0
- package/dist/browser/liveViewTickets.js +122 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +53 -7
- package/dist/browser/ownerScope.d.ts +10 -0
- package/dist/browser/ownerScope.js +6 -0
- package/dist/browser/profileStore.d.ts +5 -2
- package/dist/browser/profileStore.js +3 -5
- package/dist/browser/sandboxOrchestrator.d.ts +60 -0
- package/dist/browser/sandboxOrchestrator.js +8 -0
- package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
- package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
- package/dist/browser/stageBrowserEnv.d.ts +7 -0
- package/dist/browser/stageBrowserEnv.js +53 -15
- package/dist/browser/stageHandoff.d.ts +47 -0
- package/dist/browser/stageHandoff.js +101 -0
- package/dist/browser/stageProfileLock.d.ts +1 -0
- package/dist/browser/stageProfileLock.js +1 -3
- package/dist/browser/volumeProfileStore.d.ts +7 -0
- package/dist/browser/volumeProfileStore.js +35 -0
- package/dist/channels/config.d.ts +11 -0
- package/dist/channels/config.js +95 -0
- package/dist/channels/configWriter.d.ts +40 -0
- package/dist/channels/configWriter.js +148 -0
- package/dist/channels/connectionEdits.d.ts +34 -0
- package/dist/channels/connectionEdits.js +53 -0
- package/dist/channels/errorInfo.d.ts +18 -0
- package/dist/channels/errorInfo.js +71 -0
- package/dist/channels/errors.d.ts +12 -0
- package/dist/channels/errors.js +13 -0
- package/dist/channels/health.d.ts +41 -0
- package/dist/channels/health.js +99 -0
- package/dist/channels/host.d.ts +55 -0
- package/dist/channels/host.js +185 -0
- package/dist/channels/http.d.ts +31 -0
- package/dist/channels/http.js +79 -0
- package/dist/channels/index.d.ts +10 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/ledger.d.ts +37 -0
- package/dist/channels/ledger.js +95 -0
- package/dist/channels/providers/agentmail.d.ts +26 -0
- package/dist/channels/providers/agentmail.js +81 -0
- package/dist/channels/providers/kit.d.ts +40 -0
- package/dist/channels/providers/kit.js +78 -0
- package/dist/channels/providers/photonImessage.d.ts +45 -0
- package/dist/channels/providers/photonImessage.js +152 -0
- package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
- package/dist/channels/providers/photonWhatsapp.js +146 -0
- package/dist/channels/providers/slack.d.ts +25 -0
- package/dist/channels/providers/slack.js +101 -0
- package/dist/channels/providers/telegram.d.ts +21 -0
- package/dist/channels/providers/telegram.js +100 -0
- package/dist/channels/providers.d.ts +5 -0
- package/dist/channels/providers.js +22 -0
- package/dist/channels/registry.d.ts +54 -0
- package/dist/channels/registry.js +186 -0
- package/dist/channels/secretStore.d.ts +21 -0
- package/dist/channels/secretStore.js +75 -0
- package/dist/channels/secrets.d.ts +15 -0
- package/dist/channels/secrets.js +50 -0
- package/dist/channels/service.d.ts +54 -0
- package/dist/channels/service.js +307 -0
- package/dist/channels/stageChannels.d.ts +45 -0
- package/dist/channels/stageChannels.js +110 -0
- package/dist/channels/types.d.ts +142 -0
- package/dist/channels/types.js +1 -0
- package/dist/cli/browserCommand.d.ts +3 -1
- package/dist/cli/browserCommand.js +25 -20
- package/dist/cli/doctorCommand.js +3 -1
- package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
- package/dist/cli/resolveCliProjectRoot.js +27 -0
- package/dist/cli/runCommand.d.ts +1 -1
- package/dist/cli/runCommand.js +59 -17
- package/dist/cli/skillsCommand.d.ts +20 -0
- package/dist/cli/skillsCommand.js +74 -31
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +22 -4
- package/dist/cli.js +4 -4
- package/dist/config/browseCatalog.d.ts +2 -0
- package/dist/config/browseCatalog.js +6 -0
- package/dist/config/catalogRelativePath.d.ts +4 -0
- package/dist/config/catalogRelativePath.js +37 -9
- package/dist/config/createStage.d.ts +1 -0
- package/dist/config/createStage.js +11 -2
- package/dist/config/createTask.d.ts +30 -0
- package/dist/config/createTask.js +161 -0
- package/dist/config/createTrigger.d.ts +15 -0
- package/dist/config/createTrigger.js +32 -25
- package/dist/config/draftPackage.d.ts +42 -0
- package/dist/config/draftPackage.js +220 -23
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
- package/dist/config/ensureCatalogScanRoot.js +6 -2
- package/dist/config/findTaskInCatalog.d.ts +33 -0
- package/dist/config/findTaskInCatalog.js +69 -0
- package/dist/config/hostConfig.d.ts +21 -0
- package/dist/config/hostConfig.js +114 -1
- package/dist/config/importTask.d.ts +19 -0
- package/dist/config/importTask.js +133 -0
- package/dist/config/installSkillRequest.d.ts +40 -0
- package/dist/config/installSkillRequest.js +286 -0
- package/dist/config/loadOutcome.d.ts +11 -6
- package/dist/config/loadPipeline.js +27 -4
- package/dist/config/loadStage.js +17 -0
- package/dist/config/loadStageflowManifest.js +4 -1
- package/dist/config/loadTask.js +2 -0
- package/dist/config/loadTrigger.js +2 -0
- package/dist/config/mergePipelineIncludes.d.ts +3 -0
- package/dist/config/mergePipelineIncludes.js +30 -0
- package/dist/config/normalizePipelineStageEntry.js +10 -1
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/printTargetYaml.js +0 -1
- package/dist/config/publishDraftPackage.d.ts +5 -1
- package/dist/config/publishDraftPackage.js +4 -1
- package/dist/config/resolveCatalogRoots.d.ts +4 -1
- package/dist/config/resolveCatalogRoots.js +18 -1
- package/dist/config/skillUsageIndex.d.ts +6 -0
- package/dist/config/skillUsageIndex.js +39 -0
- package/dist/config/stageBrowser.js +13 -0
- package/dist/config/triggerAdapterStatus.d.ts +11 -0
- package/dist/config/triggerAdapterStatus.js +90 -0
- package/dist/config/triggerCatalog.d.ts +41 -2
- package/dist/config/triggerCatalog.js +107 -15
- package/dist/config/updateTask.d.ts +30 -0
- package/dist/config/updateTask.js +119 -0
- package/dist/config/updateTrigger.d.ts +38 -0
- package/dist/config/updateTrigger.js +220 -0
- package/dist/config/validateCatalog.d.ts +9 -2
- package/dist/config/validateCatalog.js +71 -7
- package/dist/config/workshopCatalogFilenames.d.ts +37 -0
- package/dist/config/workshopCatalogFilenames.js +125 -0
- package/dist/config/workshopCatalogRepair.d.ts +7 -0
- package/dist/config/workshopCatalogRepair.js +144 -0
- package/dist/config/yamlParsePosition.d.ts +4 -0
- package/dist/config/yamlParsePosition.js +9 -0
- package/dist/errors/codes.d.ts +1 -1
- package/dist/errors/codes.js +1 -0
- package/dist/hosted/runtime.d.ts +2 -2
- package/dist/hosted/types.d.ts +1 -1
- package/dist/hosted/types.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/mcp/catalogTools.js +1 -0
- package/dist/mcp/getStartedTool.js +15 -6
- package/dist/mcp/triggerTools.js +2 -8
- package/dist/operatorAgent/agentPort.d.ts +11 -0
- package/dist/operatorAgent/agentPort.js +12 -0
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/piModel.js +26 -6
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/preflight/channelChecks.d.ts +17 -0
- package/dist/preflight/channelChecks.js +106 -0
- package/dist/preflight/pipelinePreflight.d.ts +9 -2
- package/dist/preflight/pipelinePreflight.js +9 -1
- package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
- package/dist/project/ensureProjectRootsFromEnv.js +22 -0
- package/dist/project/globalHome.d.ts +1 -0
- package/dist/project/globalHome.js +18 -0
- package/dist/project/workspaceCatalog.d.ts +10 -0
- package/dist/project/workspaceCatalog.js +91 -0
- package/dist/runstore/backup.js +14 -4
- package/dist/runstore/port.d.ts +11 -1
- package/dist/runstore/restore.js +5 -2
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
- package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/runstore/stageSnapshot.d.ts +3 -1
- package/dist/runstore/stageSnapshot.js +19 -0
- package/dist/runstore/workspaceLayout.js +27 -0
- package/dist/runstore/writeContainedArtifact.d.ts +1 -0
- package/dist/runstore/writeContainedArtifact.js +52 -0
- package/dist/runtime/credentialBinding.js +14 -6
- package/dist/runtime/emailSource.js +20 -28
- package/dist/runtime/githubPollSource.js +20 -28
- package/dist/runtime/pipelineRunner.d.ts +4 -4
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/pipelineScheduler.d.ts +2 -2
- package/dist/runtime/pipelineScheduler.js +3 -0
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/resumeReconstruct.d.ts +2 -2
- package/dist/runtime/resumeTimedOut.d.ts +2 -2
- package/dist/runtime/runManager.d.ts +18 -2
- package/dist/runtime/runManager.js +99 -31
- package/dist/runtime/runRetryCoordinator.d.ts +2 -2
- package/dist/runtime/scheduleSource.js +15 -24
- package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
- package/dist/runtime/stageAttemptBootstrap.js +26 -1
- package/dist/runtime/stageProcessLauncher.js +33 -3
- package/dist/runtime/stageRoots.js +8 -1
- package/dist/runtime/stageRunner.d.ts +2 -2
- package/dist/runtime/stageRunner.js +8 -2
- package/dist/runtime/stageWorker.js +2 -2
- package/dist/runtime/triggerPort.d.ts +5 -3
- package/dist/runtime/triggerRunner.d.ts +1 -0
- package/dist/runtime/triggerRunner.js +27 -20
- package/dist/runtime/webhookAccept.d.ts +25 -0
- package/dist/runtime/webhookAccept.js +56 -0
- package/dist/server/bootstrap.d.ts +10 -2
- package/dist/server/bootstrap.js +19 -1
- package/dist/server/channelRoutes.d.ts +9 -0
- package/dist/server/channelRoutes.js +88 -0
- package/dist/server/controlToken.d.ts +1 -1
- package/dist/server/controlToken.js +18 -2
- package/dist/server/healthSurfaces.d.ts +12 -0
- package/dist/server/healthSurfaces.js +27 -6
- package/dist/server/http.d.ts +19 -10
- package/dist/server/http.js +616 -85
- package/dist/server/liveViewPath.d.ts +9 -0
- package/dist/server/liveViewPath.js +8 -0
- package/dist/server/liveViewRoutes.d.ts +58 -0
- package/dist/server/liveViewRoutes.js +565 -0
- package/dist/server/mcpHost.d.ts +4 -2
- package/dist/server/mcpHost.js +2 -0
- package/dist/server/readCatalogFile.d.ts +29 -0
- package/dist/server/readCatalogFile.js +215 -0
- package/dist/server/triggerRoutes.d.ts +1 -0
- package/dist/server/triggerRoutes.js +200 -73
- package/dist/tools/sendMessage.d.ts +21 -0
- package/dist/tools/sendMessage.js +25 -0
- package/dist/tools/writeStageArtifact.d.ts +9 -0
- package/dist/tools/writeStageArtifact.js +2 -47
- package/dist/types/pipeline.d.ts +5 -2
- package/dist/types/stage.d.ts +14 -1
- package/dist/types/stageflowManifest.d.ts +1 -1
- package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/assets/index-BOhqnPNg.js +260 -0
- package/dist/ui/assets/{index-Bv4p1aPK.css → index-Clv4sJpU.css} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/workshop/autosave.d.ts +2 -2
- package/dist/workshop/autosave.js +4 -6
- package/dist/workshop/buildStore.d.ts +2 -0
- package/dist/workshop/chatAttachments.d.ts +27 -0
- package/dist/workshop/chatAttachments.js +98 -0
- package/dist/workshop/chatTurn.d.ts +5 -2
- package/dist/workshop/chatTurn.js +22 -18
- package/dist/workshop/sessionMutations.d.ts +17 -0
- package/dist/workshop/sessionMutations.js +107 -0
- package/dist/workshop/sessionStore.d.ts +10 -0
- package/dist/workshop/sessionStore.js +52 -0
- package/dist/workshop/workshopPin.d.ts +4 -3
- package/dist/workshop/workshopPin.js +26 -6
- package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
- package/dist/workshop/workshopTranscriptTurn.js +46 -0
- package/examples/README.md +1 -1
- package/examples/archify-on-pr/README.md +1 -1
- package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
- package/examples/archify-on-pr/author-diagrams.yaml +1 -0
- package/examples/channels-smoke/README.md +12 -0
- package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
- package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
- package/examples/channels-smoke/send-email.yaml +32 -0
- package/examples/channels-smoke/send-text.yaml +31 -0
- package/examples/conditional-fork/branch-a.yaml +1 -1
- package/examples/conditional-fork/decide.yaml +2 -1
- package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
- package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
- package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
- package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
- package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
- package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
- package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
- package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
- package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
- package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
- package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
- package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
- package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
- package/examples/hello-world/hello.pipeline.yaml +2 -2
- package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
- package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
- package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
- package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
- package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
- package/examples/stageflow.yaml +6 -0
- package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
- package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
- package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
- package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
- package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
- package/examples/triggers-readiness/README.md +48 -0
- package/examples/triggers-readiness/readiness-emit.yaml +18 -0
- package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
- package/examples/triggers-readiness/readiness.task.yaml +6 -0
- package/examples/triggers-readiness/stageflow.yaml +13 -0
- package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
- package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
- package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
- package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
- package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
- package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
- package/package.json +8 -3
- package/skills/stageflow/references/control-surface.md +1 -1
- package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
- package/skills/stageflow-run/SKILL.md +1 -1
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-setup/SKILL.md +1 -1
- package/dist/ui/assets/index-59cjF0OA.js +0 -133
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { globalStageflowHome } from "../project/globalHome.js";
|
|
5
|
+
import { validateProfileScope } from "../browser/profileStore.js";
|
|
5
6
|
import { readSecretFromEnvOrFile } from "./secretFromEnvOrFile.js";
|
|
6
7
|
export class HostConfigError extends Error {
|
|
7
8
|
code;
|
|
@@ -16,7 +17,10 @@ export class HostConfigError extends Error {
|
|
|
16
17
|
/** Env keys that are known STAGEFLOW_* (or dynamic families) so unknown-key check accepts them. */
|
|
17
18
|
export const KNOWN_STAGEFLOW_ENV_KEYS = new Set([
|
|
18
19
|
"STAGEFLOW_HOME",
|
|
20
|
+
"STAGEFLOW_CREDENTIAL_HOME",
|
|
21
|
+
"STAGEFLOW_CONNECTIONS_FILE",
|
|
19
22
|
"STAGEFLOW_BIND",
|
|
23
|
+
"STAGEFLOW_ALLOW_UNAUTHENTICATED_BIND",
|
|
20
24
|
"STAGEFLOW_ALLOWED_HOSTS",
|
|
21
25
|
"STAGEFLOW_CONTROL_TOKEN",
|
|
22
26
|
"STAGEFLOW_CONTROL_TOKEN_FILE",
|
|
@@ -40,6 +44,7 @@ export const KNOWN_STAGEFLOW_ENV_KEYS = new Set([
|
|
|
40
44
|
"STAGEFLOW_LOG_MAX_LINE_BYTES",
|
|
41
45
|
"STAGEFLOW_MCP_STATELESS",
|
|
42
46
|
"STAGEFLOW_DISK_WARN_BYTES",
|
|
47
|
+
"STAGEFLOW_ENSURE_PROJECT_ROOTS",
|
|
43
48
|
"STAGEFLOW_MIN_FREE_DISK_BYTES",
|
|
44
49
|
"STAGEFLOW_BARE_CACHE_TTL_MS",
|
|
45
50
|
"STAGEFLOW_SLIM_ARTIFACT_MAX_BYTES",
|
|
@@ -82,6 +87,11 @@ const FILE_KEY_TO_FIELD = {
|
|
|
82
87
|
callers: "callers",
|
|
83
88
|
browser: "browser",
|
|
84
89
|
};
|
|
90
|
+
export const DEFAULT_BROWSER_DIALOG_TIMEOUT_SECONDS = 60;
|
|
91
|
+
export const DEFAULT_BROWSER_CONTAINER_IMAGE = "stageflow-browser-sandbox:local";
|
|
92
|
+
export function browserHostForOwner(config, scope) {
|
|
93
|
+
return config.browserOwnerHosts[scope] ?? config.browserHost;
|
|
94
|
+
}
|
|
85
95
|
const DEFAULT_MAX_CONCURRENT = 3;
|
|
86
96
|
const DEFAULT_MAX_QUEUED = 32;
|
|
87
97
|
function isKnownStageflowEnvKey(key) {
|
|
@@ -303,16 +313,113 @@ export function loadHostConfig(options) {
|
|
|
303
313
|
callers = coerceCallers(file.values.callers, "callers");
|
|
304
314
|
}
|
|
305
315
|
let browserBlockedSites = [];
|
|
316
|
+
let browserLaunchArgs = [];
|
|
317
|
+
let browserExecutablePath;
|
|
318
|
+
let browserDialogTimeoutSeconds = DEFAULT_BROWSER_DIALOG_TIMEOUT_SECONDS;
|
|
319
|
+
let browserHost = "local";
|
|
320
|
+
const browserOwnerHosts = {};
|
|
321
|
+
const browserContainer = {
|
|
322
|
+
image: DEFAULT_BROWSER_CONTAINER_IMAGE,
|
|
323
|
+
shmSize: "1g",
|
|
324
|
+
memory: "1g",
|
|
325
|
+
pidsLimit: 512,
|
|
326
|
+
};
|
|
306
327
|
if (file.values.browser !== undefined) {
|
|
307
328
|
const raw = file.values.browser;
|
|
308
329
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
309
330
|
throw new HostConfigError(`Invalid value for browser`, "config_invalid", "browser");
|
|
310
331
|
}
|
|
311
332
|
for (const sub of Object.keys(raw)) {
|
|
312
|
-
if (sub !== "blocked_sites") {
|
|
333
|
+
if (sub !== "blocked_sites" && sub !== "launch_args" && sub !== "executable_path" && sub !== "dialog_timeout_seconds" && sub !== "host" && sub !== "container" && sub !== "owner_hosts") {
|
|
313
334
|
throw new HostConfigError(`Unknown key "${sub}" under browser`, "config_unknown_key", `browser.${sub}`);
|
|
314
335
|
}
|
|
315
336
|
}
|
|
337
|
+
const args = raw.launch_args;
|
|
338
|
+
if (args !== undefined && args !== null) {
|
|
339
|
+
if (!Array.isArray(args) || !args.every((a) => typeof a === "string")) {
|
|
340
|
+
throw new HostConfigError("Invalid value for browser.launch_args; expected a list of strings", "config_invalid", "browser.launch_args");
|
|
341
|
+
}
|
|
342
|
+
for (const arg of args) {
|
|
343
|
+
if (arg.trim() === "" || /[,\r\n]/.test(arg)) {
|
|
344
|
+
throw new HostConfigError(`Invalid item ${JSON.stringify(arg)} in browser.launch_args; each item must be non-empty with no comma or newline`, "config_invalid", "browser.launch_args");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
browserLaunchArgs = args.map((a) => a.trim());
|
|
348
|
+
}
|
|
349
|
+
const exe = raw.executable_path;
|
|
350
|
+
if (exe !== undefined && exe !== null) {
|
|
351
|
+
if (typeof exe !== "string" ||
|
|
352
|
+
exe.trim() === "" ||
|
|
353
|
+
!(path.posix.isAbsolute(exe.trim()) || path.win32.isAbsolute(exe.trim()))) {
|
|
354
|
+
throw new HostConfigError("Invalid value for browser.executable_path; expected a non-empty absolute path", "config_invalid", "browser.executable_path");
|
|
355
|
+
}
|
|
356
|
+
browserExecutablePath = exe.trim();
|
|
357
|
+
}
|
|
358
|
+
const dialogTimeout = raw.dialog_timeout_seconds;
|
|
359
|
+
if (dialogTimeout !== undefined && dialogTimeout !== null) {
|
|
360
|
+
if (typeof dialogTimeout !== "number" ||
|
|
361
|
+
!Number.isInteger(dialogTimeout) ||
|
|
362
|
+
dialogTimeout < 1) {
|
|
363
|
+
throw new HostConfigError("Invalid value for browser.dialog_timeout_seconds; expected a positive integer", "config_invalid", "browser.dialog_timeout_seconds");
|
|
364
|
+
}
|
|
365
|
+
browserDialogTimeoutSeconds = dialogTimeout;
|
|
366
|
+
}
|
|
367
|
+
const host = raw.host;
|
|
368
|
+
if (host !== undefined && host !== null) {
|
|
369
|
+
if (host !== "local" && host !== "container") {
|
|
370
|
+
throw new HostConfigError('Invalid value for browser.host; expected "local" or "container"', "config_invalid", "browser.host");
|
|
371
|
+
}
|
|
372
|
+
browserHost = host;
|
|
373
|
+
}
|
|
374
|
+
const ownerHosts = raw.owner_hosts;
|
|
375
|
+
if (ownerHosts !== undefined && ownerHosts !== null) {
|
|
376
|
+
if (typeof ownerHosts !== "object" || Array.isArray(ownerHosts)) {
|
|
377
|
+
throw new HostConfigError("Invalid value for browser.owner_hosts", "config_invalid", "browser.owner_hosts");
|
|
378
|
+
}
|
|
379
|
+
for (const [scope, value] of Object.entries(ownerHosts)) {
|
|
380
|
+
try {
|
|
381
|
+
validateProfileScope(scope);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
throw new HostConfigError(`Invalid owner scope "${scope}" in browser.owner_hosts`, "config_invalid", "browser.owner_hosts");
|
|
385
|
+
}
|
|
386
|
+
if (value !== "local" && value !== "container") {
|
|
387
|
+
throw new HostConfigError(`Invalid value for browser.owner_hosts.${scope}; expected "local" or "container"`, "config_invalid", `browser.owner_hosts.${scope}`);
|
|
388
|
+
}
|
|
389
|
+
browserOwnerHosts[scope] = value;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const container = raw.container;
|
|
393
|
+
if (container !== undefined && container !== null) {
|
|
394
|
+
if (typeof container !== "object" || Array.isArray(container)) {
|
|
395
|
+
throw new HostConfigError("Invalid value for browser.container", "config_invalid", "browser.container");
|
|
396
|
+
}
|
|
397
|
+
const fields = container;
|
|
398
|
+
for (const sub of Object.keys(fields)) {
|
|
399
|
+
if (!["image", "shm_size", "memory", "pids_limit"].includes(sub)) {
|
|
400
|
+
throw new HostConfigError(`Unknown key "${sub}" under browser.container`, "config_unknown_key", `browser.container.${sub}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const text = (key, pattern, target) => {
|
|
404
|
+
const value = fields[key];
|
|
405
|
+
if (value === undefined || value === null)
|
|
406
|
+
return;
|
|
407
|
+
if (typeof value !== "string" || !pattern.test(value.trim())) {
|
|
408
|
+
throw new HostConfigError(`Invalid value for browser.container.${key}`, "config_invalid", `browser.container.${key}`);
|
|
409
|
+
}
|
|
410
|
+
browserContainer[target] = value.trim();
|
|
411
|
+
};
|
|
412
|
+
text("image", /^[A-Za-z0-9][A-Za-z0-9._\-/:@]*$/, "image");
|
|
413
|
+
text("shm_size", /^[1-9][0-9]*[bkmg]?$/i, "shmSize");
|
|
414
|
+
text("memory", /^[1-9][0-9]*[bkmg]?$/i, "memory");
|
|
415
|
+
const pids = fields.pids_limit;
|
|
416
|
+
if (pids !== undefined && pids !== null) {
|
|
417
|
+
if (typeof pids !== "number" || !Number.isInteger(pids) || pids < 1) {
|
|
418
|
+
throw new HostConfigError("Invalid value for browser.container.pids_limit; expected a positive integer", "config_invalid", "browser.container.pids_limit");
|
|
419
|
+
}
|
|
420
|
+
browserContainer.pidsLimit = pids;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
316
423
|
const sites = raw.blocked_sites;
|
|
317
424
|
if (sites !== undefined) {
|
|
318
425
|
browserBlockedSites = coerceFileStringList(sites, "browser.blocked_sites")
|
|
@@ -336,6 +443,12 @@ export function loadHostConfig(options) {
|
|
|
336
443
|
readToken,
|
|
337
444
|
callers,
|
|
338
445
|
browserBlockedSites,
|
|
446
|
+
browserLaunchArgs,
|
|
447
|
+
browserExecutablePath,
|
|
448
|
+
browserDialogTimeoutSeconds,
|
|
449
|
+
browserHost,
|
|
450
|
+
browserOwnerHosts,
|
|
451
|
+
browserContainer,
|
|
339
452
|
configFilePath: configFilePath !== undefined && existsSync(configFilePath)
|
|
340
453
|
? configFilePath
|
|
341
454
|
: undefined,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CreateTaskResult } from "./createTask.js";
|
|
2
|
+
export declare const GITHUB_REPO_PATTERN: RegExp;
|
|
3
|
+
export type ImportTaskInput = {
|
|
4
|
+
repo: string;
|
|
5
|
+
number: number;
|
|
6
|
+
directory: string;
|
|
7
|
+
};
|
|
8
|
+
export type ImportTaskParseError = {
|
|
9
|
+
ok: false;
|
|
10
|
+
status: 400;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export type IssueFetch = (url: string, init: {
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
}) => Promise<Response>;
|
|
16
|
+
export declare function parseImportTaskBody(body: unknown): ImportTaskInput | ImportTaskParseError;
|
|
17
|
+
export declare function slugIssueTitle(title: string, issueNumber: number): string;
|
|
18
|
+
export declare function taskIdWithSuffix(baseId: string, suffixNumber: number): string;
|
|
19
|
+
export declare function importTaskFromIssue(rawProjectRoot: string, input: ImportTaskInput, fetchImpl?: IssueFetch): Promise<CreateTaskResult>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { STAGE_ID_PATTERN } from "./createStage.js";
|
|
4
|
+
import { createTask, findTaskIdCollision, resolveTaskDirectory, validateTaskId, } from "./createTask.js";
|
|
5
|
+
export const GITHUB_REPO_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
export function parseImportTaskBody(body) {
|
|
10
|
+
if (!isPlainObject(body)) {
|
|
11
|
+
return { ok: false, status: 400, error: "Request body must be an object" };
|
|
12
|
+
}
|
|
13
|
+
if (typeof body.repo !== "string" || !GITHUB_REPO_PATTERN.test(body.repo)) {
|
|
14
|
+
return { ok: false, status: 400, error: "repo must be a GitHub owner/repo" };
|
|
15
|
+
}
|
|
16
|
+
if (typeof body.number !== "number" || !Number.isInteger(body.number) || body.number <= 0) {
|
|
17
|
+
return { ok: false, status: 400, error: "number must be a positive integer" };
|
|
18
|
+
}
|
|
19
|
+
if (typeof body.directory !== "string" || body.directory.trim().length === 0) {
|
|
20
|
+
return { ok: false, status: 400, error: "directory is required" };
|
|
21
|
+
}
|
|
22
|
+
const directory = body.directory.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
23
|
+
if (directory.length === 0) {
|
|
24
|
+
return { ok: false, status: 400, error: "directory is required" };
|
|
25
|
+
}
|
|
26
|
+
return { repo: body.repo, number: body.number, directory };
|
|
27
|
+
}
|
|
28
|
+
export function slugIssueTitle(title, issueNumber) {
|
|
29
|
+
const fallback = `issue-${issueNumber}`;
|
|
30
|
+
let slug = title
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
33
|
+
.replace(/^-+|-+$/g, "")
|
|
34
|
+
.replace(/^[^a-z]+/, "")
|
|
35
|
+
.replace(/^-+|-+$/g, "");
|
|
36
|
+
if (slug.length > 64) {
|
|
37
|
+
slug = slug.slice(0, 64).replace(/-+$/g, "");
|
|
38
|
+
}
|
|
39
|
+
if (!slug || !STAGE_ID_PATTERN.test(slug))
|
|
40
|
+
return fallback;
|
|
41
|
+
return slug;
|
|
42
|
+
}
|
|
43
|
+
export function taskIdWithSuffix(baseId, suffixNumber) {
|
|
44
|
+
const suffix = `-${suffixNumber}`;
|
|
45
|
+
let stem = baseId;
|
|
46
|
+
if (stem.length + suffix.length > 64) {
|
|
47
|
+
stem = stem.slice(0, 64 - suffix.length).replace(/-+$/g, "");
|
|
48
|
+
}
|
|
49
|
+
if (!stem || !STAGE_ID_PATTERN.test(stem))
|
|
50
|
+
stem = "issue";
|
|
51
|
+
const candidate = `${stem}${suffix}`;
|
|
52
|
+
if (candidate.length <= 64 && STAGE_ID_PATTERN.test(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
return `issue${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
async function allocateTaskId(projectRoot, directory, baseId) {
|
|
57
|
+
for (let n = 1; n <= 100; n += 1) {
|
|
58
|
+
const id = n === 1 ? baseId : taskIdWithSuffix(baseId, n);
|
|
59
|
+
const idError = validateTaskId(id);
|
|
60
|
+
if (idError)
|
|
61
|
+
continue;
|
|
62
|
+
const targetPath = path.join(directory, `${id}.task.yaml`);
|
|
63
|
+
const collision = await findTaskIdCollision(projectRoot, id, targetPath);
|
|
64
|
+
if (!collision)
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function githubHeaders() {
|
|
70
|
+
const headers = {
|
|
71
|
+
Accept: "application/vnd.github+json",
|
|
72
|
+
"User-Agent": "stageflow",
|
|
73
|
+
};
|
|
74
|
+
const token = process.env.GITHUB_TOKEN;
|
|
75
|
+
if (typeof token === "string" && token.length > 0) {
|
|
76
|
+
headers.Authorization = `Bearer ${token}`;
|
|
77
|
+
}
|
|
78
|
+
return headers;
|
|
79
|
+
}
|
|
80
|
+
export async function importTaskFromIssue(rawProjectRoot, input, fetchImpl = (url, init) => fetch(url, init)) {
|
|
81
|
+
if (!GITHUB_REPO_PATTERN.test(input.repo)) {
|
|
82
|
+
return { ok: false, status: 400, error: "repo must be a GitHub owner/repo" };
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(input.number) || input.number <= 0) {
|
|
85
|
+
return { ok: false, status: 400, error: "number must be a positive integer" };
|
|
86
|
+
}
|
|
87
|
+
const projectRoot = await realpath(rawProjectRoot).catch(() => rawProjectRoot);
|
|
88
|
+
const directory = resolveTaskDirectory(projectRoot, input.directory);
|
|
89
|
+
if (!directory) {
|
|
90
|
+
return { ok: false, status: 400, error: "directory must be inside the project root" };
|
|
91
|
+
}
|
|
92
|
+
const url = `https://api.github.com/repos/${input.repo}/issues/${input.number}`;
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await fetchImpl(url, { headers: githubHeaders() });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
return { ok: false, status: 502, error: message };
|
|
100
|
+
}
|
|
101
|
+
if (response.status === 404) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
status: 404,
|
|
105
|
+
error: `GitHub issue not found: ${input.repo}#${input.number}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
return { ok: false, status: 502, error: `GitHub request failed (${response.status})` };
|
|
110
|
+
}
|
|
111
|
+
let payload;
|
|
112
|
+
try {
|
|
113
|
+
payload = await response.json();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { ok: false, status: 502, error: "GitHub response was not JSON" };
|
|
117
|
+
}
|
|
118
|
+
if (!isPlainObject(payload) || typeof payload.title !== "string" || payload.title.length === 0) {
|
|
119
|
+
return { ok: false, status: 502, error: "GitHub issue is missing a title" };
|
|
120
|
+
}
|
|
121
|
+
const context = typeof payload.body === "string" ? payload.body : "";
|
|
122
|
+
const baseId = slugIssueTitle(payload.title, input.number);
|
|
123
|
+
const id = await allocateTaskId(projectRoot, directory, baseId);
|
|
124
|
+
if (!id) {
|
|
125
|
+
return { ok: false, status: 409, error: "Unable to allocate a task id" };
|
|
126
|
+
}
|
|
127
|
+
return createTask(projectRoot, {
|
|
128
|
+
directory: input.directory,
|
|
129
|
+
id,
|
|
130
|
+
goal: payload.title,
|
|
131
|
+
...(context.length > 0 ? { context } : {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type InstallSkillParseError = {
|
|
2
|
+
ok: false;
|
|
3
|
+
status: 400;
|
|
4
|
+
error: string;
|
|
5
|
+
};
|
|
6
|
+
export type ParsedInstallSkillRequest = {
|
|
7
|
+
command: string;
|
|
8
|
+
};
|
|
9
|
+
export type ParsedSkillsAddCommand = {
|
|
10
|
+
source: string;
|
|
11
|
+
skillName?: string;
|
|
12
|
+
};
|
|
13
|
+
export type SkillCloneFn = (cloneUrl: string, destDir: string) => Promise<void>;
|
|
14
|
+
export type InstallSkillFromCommandResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
skill_name: string;
|
|
17
|
+
dest_path: string;
|
|
18
|
+
project_root: string;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
error: string;
|
|
22
|
+
doctorFailed?: boolean;
|
|
23
|
+
status?: 400 | 422;
|
|
24
|
+
};
|
|
25
|
+
export declare function parseSkillsAddCommand(command: string): ParsedSkillsAddCommand | InstallSkillParseError;
|
|
26
|
+
export declare function parseInstallSkillBody(body: unknown): ParsedInstallSkillRequest | InstallSkillParseError;
|
|
27
|
+
export type LocatedSkill = {
|
|
28
|
+
name: string;
|
|
29
|
+
root: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function locateSkillDirectory(checkoutRoot: string, skillName?: string): Promise<LocatedSkill>;
|
|
32
|
+
export declare function resolveGitCloneUrl(source: string): string;
|
|
33
|
+
export declare function defaultCloneSkillRepo(cloneUrl: string, destDir: string): Promise<void>;
|
|
34
|
+
export declare function installSkillFromCommand(input: {
|
|
35
|
+
projectRoot: string;
|
|
36
|
+
command: string;
|
|
37
|
+
cloneRepo?: SkillCloneFn;
|
|
38
|
+
}): Promise<InstallSkillFromCommandResult>;
|
|
39
|
+
/** Test helper: build a file: command for a local skill tree. */
|
|
40
|
+
export declare function skillsAddFileCommand(fromPath: string, skillName?: string): string;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { access, mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { installProjectSkill, } from "../cli/skillsCommand.js";
|
|
6
|
+
import { runGit } from "../git/exec.js";
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function tokenizeCommand(command) {
|
|
11
|
+
return command.trim().split(/\s+/).filter((part) => part.length > 0);
|
|
12
|
+
}
|
|
13
|
+
export function parseSkillsAddCommand(command) {
|
|
14
|
+
const trimmed = command.trim();
|
|
15
|
+
if (trimmed.length === 0) {
|
|
16
|
+
return { ok: false, status: 400, error: "command must be a non-empty string" };
|
|
17
|
+
}
|
|
18
|
+
const tokens = tokenizeCommand(trimmed);
|
|
19
|
+
let rest;
|
|
20
|
+
if (tokens[0] === "npx" && tokens[1] === "skills" && tokens[2] === "add") {
|
|
21
|
+
rest = tokens.slice(3);
|
|
22
|
+
}
|
|
23
|
+
else if (tokens[0] === "skills" && tokens[1] === "add") {
|
|
24
|
+
rest = tokens.slice(2);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
status: 400,
|
|
30
|
+
error: "command must be 'npx skills add <source> [--skill <name>]' or 'skills add …'",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let source;
|
|
34
|
+
let skillName;
|
|
35
|
+
for (let i = 0; i < rest.length; i++) {
|
|
36
|
+
const arg = rest[i];
|
|
37
|
+
if (arg === "--skill") {
|
|
38
|
+
const value = rest[++i];
|
|
39
|
+
if (value === undefined || value.length === 0) {
|
|
40
|
+
return { ok: false, status: 400, error: "missing value for --skill" };
|
|
41
|
+
}
|
|
42
|
+
if (value.startsWith("-")) {
|
|
43
|
+
return { ok: false, status: 400, error: "missing value for --skill" };
|
|
44
|
+
}
|
|
45
|
+
skillName = value;
|
|
46
|
+
}
|
|
47
|
+
else if (arg.startsWith("-")) {
|
|
48
|
+
return { ok: false, status: 400, error: `unknown flag: ${arg}` };
|
|
49
|
+
}
|
|
50
|
+
else if (source === undefined) {
|
|
51
|
+
source = arg;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return { ok: false, status: 400, error: `unexpected argument: ${arg}` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (source === undefined) {
|
|
58
|
+
return { ok: false, status: 400, error: "missing skill source after 'add'" };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
source,
|
|
62
|
+
...(skillName !== undefined ? { skillName } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function parseInstallSkillBody(body) {
|
|
66
|
+
if (!isPlainObject(body)) {
|
|
67
|
+
return { ok: false, status: 400, error: "Request body must be an object" };
|
|
68
|
+
}
|
|
69
|
+
if (typeof body.command !== "string" || body.command.trim().length === 0) {
|
|
70
|
+
return { ok: false, status: 400, error: "command must be a non-empty string" };
|
|
71
|
+
}
|
|
72
|
+
const parsed = parseSkillsAddCommand(body.command);
|
|
73
|
+
if ("ok" in parsed) {
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
return { command: body.command.trim() };
|
|
77
|
+
}
|
|
78
|
+
async function pathExists(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
await access(filePath);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function readFrontmatterName(skillMdPath) {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await readFile(skillMdPath, "utf8");
|
|
90
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
91
|
+
if (!match)
|
|
92
|
+
return undefined;
|
|
93
|
+
const nameLine = /^name:\s*(.+)\s*$/m.exec(match[1] ?? "");
|
|
94
|
+
const value = nameLine?.[1]?.trim();
|
|
95
|
+
if (value === undefined || value.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function locateSkillDirectory(checkoutRoot, skillName) {
|
|
104
|
+
const candidates = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const pushCandidate = (name, root) => {
|
|
107
|
+
const key = path.resolve(root);
|
|
108
|
+
if (seen.has(key))
|
|
109
|
+
return;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
candidates.push({ name, root: key });
|
|
112
|
+
};
|
|
113
|
+
const rootSkillMd = path.join(checkoutRoot, "SKILL.md");
|
|
114
|
+
if (await pathExists(rootSkillMd)) {
|
|
115
|
+
const name = (await readFrontmatterName(rootSkillMd)) ?? path.basename(path.resolve(checkoutRoot));
|
|
116
|
+
pushCandidate(name, checkoutRoot);
|
|
117
|
+
}
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = await readdir(checkoutRoot, { withFileTypes: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error("skill source directory not found");
|
|
124
|
+
}
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!entry.isDirectory())
|
|
127
|
+
continue;
|
|
128
|
+
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "skills") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const skillMd = path.join(checkoutRoot, entry.name, "SKILL.md");
|
|
132
|
+
if (await pathExists(skillMd)) {
|
|
133
|
+
pushCandidate(entry.name, path.join(checkoutRoot, entry.name));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const skillsDir = path.join(checkoutRoot, "skills");
|
|
137
|
+
try {
|
|
138
|
+
const skillsStat = await stat(skillsDir);
|
|
139
|
+
if (skillsStat.isDirectory()) {
|
|
140
|
+
const skillEntries = await readdir(skillsDir, { withFileTypes: true });
|
|
141
|
+
for (const entry of skillEntries) {
|
|
142
|
+
if (!entry.isDirectory())
|
|
143
|
+
continue;
|
|
144
|
+
const skillMd = path.join(skillsDir, entry.name, "SKILL.md");
|
|
145
|
+
if (await pathExists(skillMd)) {
|
|
146
|
+
pushCandidate(entry.name, path.join(skillsDir, entry.name));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// no skills/ directory
|
|
153
|
+
}
|
|
154
|
+
if (skillName !== undefined) {
|
|
155
|
+
const namedPaths = [
|
|
156
|
+
path.join(checkoutRoot, skillName, "SKILL.md"),
|
|
157
|
+
path.join(checkoutRoot, "skills", skillName, "SKILL.md"),
|
|
158
|
+
];
|
|
159
|
+
for (const skillMd of namedPaths) {
|
|
160
|
+
if (await pathExists(skillMd)) {
|
|
161
|
+
return { name: skillName, root: path.dirname(skillMd) };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const match = candidates.find((candidate) => candidate.name === skillName);
|
|
165
|
+
if (match !== undefined) {
|
|
166
|
+
return match;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`skill "${skillName}" not found in source`);
|
|
169
|
+
}
|
|
170
|
+
if (candidates.length === 1) {
|
|
171
|
+
return candidates[0];
|
|
172
|
+
}
|
|
173
|
+
if (candidates.length === 0) {
|
|
174
|
+
throw new Error("no SKILL.md found in source");
|
|
175
|
+
}
|
|
176
|
+
const names = candidates.map((candidate) => candidate.name).join(", ");
|
|
177
|
+
throw new Error(`multiple skills found (${names}); pass --skill <name>`);
|
|
178
|
+
}
|
|
179
|
+
export function resolveGitCloneUrl(source) {
|
|
180
|
+
const githubHttps = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:\/|$)/i.exec(source);
|
|
181
|
+
if (githubHttps) {
|
|
182
|
+
return `https://github.com/${githubHttps[1]}/${githubHttps[2]}.git`;
|
|
183
|
+
}
|
|
184
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(source)) {
|
|
185
|
+
return `https://github.com/${source}.git`;
|
|
186
|
+
}
|
|
187
|
+
throw new Error("source must be a GitHub HTTPS URL, owner/repo slug, or file: URL");
|
|
188
|
+
}
|
|
189
|
+
export async function defaultCloneSkillRepo(cloneUrl, destDir) {
|
|
190
|
+
await runGit({
|
|
191
|
+
args: ["clone", "--depth", "1", "--single-branch", cloneUrl, destDir],
|
|
192
|
+
timeoutMs: 120_000,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function resolveFileSource(source) {
|
|
196
|
+
if (source.startsWith("file:")) {
|
|
197
|
+
return fileURLToPath(source);
|
|
198
|
+
}
|
|
199
|
+
throw new Error("source must be a GitHub HTTPS URL, owner/repo slug, or file: URL");
|
|
200
|
+
}
|
|
201
|
+
export async function installSkillFromCommand(input) {
|
|
202
|
+
const parsed = parseSkillsAddCommand(input.command);
|
|
203
|
+
if ("ok" in parsed) {
|
|
204
|
+
return { ok: false, error: parsed.error, status: parsed.status };
|
|
205
|
+
}
|
|
206
|
+
let cleanup;
|
|
207
|
+
try {
|
|
208
|
+
let checkoutRoot;
|
|
209
|
+
if (parsed.source.startsWith("file:")) {
|
|
210
|
+
checkoutRoot = resolveFileSource(parsed.source);
|
|
211
|
+
try {
|
|
212
|
+
const sourceStat = await stat(checkoutRoot);
|
|
213
|
+
if (!sourceStat.isDirectory()) {
|
|
214
|
+
return { ok: false, error: "skill source path not found", status: 400 };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return { ok: false, error: "skill source path not found", status: 400 };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
let cloneUrl;
|
|
223
|
+
try {
|
|
224
|
+
cloneUrl = resolveGitCloneUrl(parsed.source);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: err instanceof Error ? err.message : String(err),
|
|
230
|
+
status: 400,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "sf-skill-clone-"));
|
|
234
|
+
cleanup = async () => {
|
|
235
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
236
|
+
};
|
|
237
|
+
const cloneDest = path.join(tempRoot, "repo");
|
|
238
|
+
const cloneRepo = input.cloneRepo ?? defaultCloneSkillRepo;
|
|
239
|
+
await cloneRepo(cloneUrl, cloneDest);
|
|
240
|
+
checkoutRoot = cloneDest;
|
|
241
|
+
}
|
|
242
|
+
const located = await locateSkillDirectory(checkoutRoot, parsed.skillName);
|
|
243
|
+
const result = await installProjectSkill({
|
|
244
|
+
projectRoot: input.projectRoot,
|
|
245
|
+
fromPath: located.root,
|
|
246
|
+
skillName: located.name,
|
|
247
|
+
io: {
|
|
248
|
+
log: () => undefined,
|
|
249
|
+
error: () => undefined,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
if (!result.ok) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: result.error,
|
|
256
|
+
...(result.doctorFailed ? { doctorFailed: true, status: 422 } : { status: 400 }),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
skill_name: result.skillName,
|
|
262
|
+
dest_path: result.destPath,
|
|
263
|
+
project_root: input.projectRoot,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: err instanceof Error ? err.message : String(err),
|
|
270
|
+
status: 400,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
if (cleanup !== undefined) {
|
|
275
|
+
await cleanup();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Test helper: build a file: command for a local skill tree. */
|
|
280
|
+
export function skillsAddFileCommand(fromPath, skillName) {
|
|
281
|
+
const source = pathToFileURL(path.resolve(fromPath)).href;
|
|
282
|
+
if (skillName === undefined) {
|
|
283
|
+
return `npx skills add ${source}`;
|
|
284
|
+
}
|
|
285
|
+
return `npx skills add ${source} --skill ${skillName}`;
|
|
286
|
+
}
|