toolroll 0.5.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/LICENSE +21 -0
- package/README.md +1113 -0
- package/THIRD_PARTY_NOTICES.md +449 -0
- package/dist/accent-colors.d.ts +48 -0
- package/dist/accent-colors.js +122 -0
- package/dist/action-ledger.d.ts +26 -0
- package/dist/action-ledger.js +75 -0
- package/dist/agent-fence.d.ts +42 -0
- package/dist/agent-fence.js +183 -0
- package/dist/agentconfig.d.ts +216 -0
- package/dist/agentconfig.js +454 -0
- package/dist/api-tokens.d.ts +16 -0
- package/dist/api-tokens.js +27 -0
- package/dist/approval-policy.d.ts +69 -0
- package/dist/approval-policy.js +137 -0
- package/dist/approval-rules-ui.d.ts +25 -0
- package/dist/approval-rules-ui.js +39 -0
- package/dist/assignment-adapters.d.ts +180 -0
- package/dist/assignment-adapters.js +239 -0
- package/dist/assignment-brief.d.ts +71 -0
- package/dist/assignment-brief.js +129 -0
- package/dist/assignment-delivery.d.ts +56 -0
- package/dist/assignment-delivery.js +160 -0
- package/dist/assignment-presentation.d.ts +26 -0
- package/dist/assignment-presentation.js +80 -0
- package/dist/assignment-status.d.ts +62 -0
- package/dist/assignment-status.js +152 -0
- package/dist/assignment-ui.d.ts +71 -0
- package/dist/assignment-ui.js +103 -0
- package/dist/assignment.d.ts +222 -0
- package/dist/assignment.js +399 -0
- package/dist/attest.d.ts +56 -0
- package/dist/attest.js +153 -0
- package/dist/backend.d.ts +97 -0
- package/dist/backend.js +166 -0
- package/dist/backup-ui.d.ts +22 -0
- package/dist/backup-ui.js +59 -0
- package/dist/backup.d.ts +84 -0
- package/dist/backup.js +421 -0
- package/dist/beads.d.ts +34 -0
- package/dist/beads.js +135 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22 -0
- package/dist/board.d.ts +150 -0
- package/dist/board.js +210 -0
- package/dist/boot-identity.d.ts +63 -0
- package/dist/boot-identity.js +99 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
- package/dist/browser/workspace.css +4 -0
- package/dist/browser/workspace.js +225 -0
- package/dist/browser-crew.d.ts +11 -0
- package/dist/browser-crew.js +50 -0
- package/dist/browser-shell.d.ts +8 -0
- package/dist/browser-shell.js +60 -0
- package/dist/browser-workspace.d.ts +780 -0
- package/dist/browser-workspace.js +47 -0
- package/dist/budget-alerts.d.ts +19 -0
- package/dist/budget-alerts.js +46 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.js +3262 -0
- package/dist/capscan.d.ts +35 -0
- package/dist/capscan.js +113 -0
- package/dist/chat-acceptance.d.ts +71 -0
- package/dist/chat-acceptance.js +120 -0
- package/dist/chat-actions.d.ts +265 -0
- package/dist/chat-actions.js +1273 -0
- package/dist/chat-channel.d.ts +109 -0
- package/dist/chat-channel.js +652 -0
- package/dist/chat-continuity.d.ts +32 -0
- package/dist/chat-continuity.js +317 -0
- package/dist/chat-controls.d.ts +113 -0
- package/dist/chat-controls.js +53 -0
- package/dist/chat-delivery-state.d.ts +159 -0
- package/dist/chat-delivery-state.js +309 -0
- package/dist/chat-delivery.d.ts +38 -0
- package/dist/chat-delivery.js +693 -0
- package/dist/chat-display.d.ts +1 -0
- package/dist/chat-display.js +18 -0
- package/dist/chat-evidence.d.ts +123 -0
- package/dist/chat-evidence.js +207 -0
- package/dist/chat-flow.d.ts +44 -0
- package/dist/chat-flow.js +215 -0
- package/dist/chat-inbox.d.ts +57 -0
- package/dist/chat-inbox.js +123 -0
- package/dist/chat-polish.d.ts +16 -0
- package/dist/chat-polish.js +135 -0
- package/dist/chat-review.d.ts +46 -0
- package/dist/chat-review.js +98 -0
- package/dist/chat-rooms.d.ts +123 -0
- package/dist/chat-rooms.js +216 -0
- package/dist/chat-task-actions.d.ts +44 -0
- package/dist/chat-task-actions.js +79 -0
- package/dist/check-progress.d.ts +51 -0
- package/dist/check-progress.js +290 -0
- package/dist/child-database.d.ts +12 -0
- package/dist/child-database.js +30 -0
- package/dist/claim.d.ts +688 -0
- package/dist/claim.js +1740 -0
- package/dist/cli.d.ts +137 -0
- package/dist/cli.js +1461 -0
- package/dist/codex-limits.d.ts +15 -0
- package/dist/codex-limits.js +145 -0
- package/dist/coding-context.d.ts +55 -0
- package/dist/coding-context.js +92 -0
- package/dist/coding-handoff.d.ts +57 -0
- package/dist/coding-handoff.js +277 -0
- package/dist/coding-provider.d.ts +72 -0
- package/dist/coding-provider.js +424 -0
- package/dist/coding-shipping-ui.d.ts +4 -0
- package/dist/coding-shipping-ui.js +12 -0
- package/dist/coding-types.d.ts +62 -0
- package/dist/coding-types.js +1 -0
- package/dist/coding-ui.d.ts +17 -0
- package/dist/coding-ui.js +364 -0
- package/dist/coding-update.d.ts +15 -0
- package/dist/coding-update.js +149 -0
- package/dist/coding-workspace.d.ts +124 -0
- package/dist/coding-workspace.js +768 -0
- package/dist/container-state.d.ts +5 -0
- package/dist/container-state.js +62 -0
- package/dist/containment.d.ts +197 -0
- package/dist/containment.js +559 -0
- package/dist/contest.d.ts +318 -0
- package/dist/contest.js +753 -0
- package/dist/control-setup.d.ts +35 -0
- package/dist/control-setup.js +40 -0
- package/dist/control-ui.d.ts +29 -0
- package/dist/control-ui.js +53 -0
- package/dist/controller-service.d.ts +1 -0
- package/dist/controller-service.js +19 -0
- package/dist/controller-supervisor.d.ts +30 -0
- package/dist/controller-supervisor.js +104 -0
- package/dist/converse.d.ts +304 -0
- package/dist/converse.js +849 -0
- package/dist/coordinator-proposals.d.ts +29 -0
- package/dist/coordinator-proposals.js +137 -0
- package/dist/coordinator.d.ts +186 -0
- package/dist/coordinator.js +447 -0
- package/dist/credentials-ui.d.ts +25 -0
- package/dist/credentials-ui.js +48 -0
- package/dist/daemon.d.ts +209 -0
- package/dist/daemon.js +604 -0
- package/dist/decision.d.ts +120 -0
- package/dist/decision.js +388 -0
- package/dist/demo.d.ts +55 -0
- package/dist/demo.js +992 -0
- package/dist/desktop-access.d.ts +28 -0
- package/dist/desktop-access.js +88 -0
- package/dist/desktop-bundle.d.ts +16 -0
- package/dist/desktop-bundle.js +74 -0
- package/dist/desktop-host.d.ts +54 -0
- package/dist/desktop-host.js +508 -0
- package/dist/desktop-update-gate.d.ts +12 -0
- package/dist/desktop-update-gate.js +91 -0
- package/dist/desktop-update-recovery.d.ts +14 -0
- package/dist/desktop-update-recovery.js +243 -0
- package/dist/desktop-update.d.ts +110 -0
- package/dist/desktop-update.js +740 -0
- package/dist/discord-api.d.ts +20 -0
- package/dist/discord-api.js +138 -0
- package/dist/discord-chat.d.ts +16 -0
- package/dist/discord-chat.js +381 -0
- package/dist/discord-settings.d.ts +7 -0
- package/dist/discord-settings.js +75 -0
- package/dist/discord.d.ts +10 -0
- package/dist/discord.js +169 -0
- package/dist/discover.d.ts +75 -0
- package/dist/discover.js +150 -0
- package/dist/dispatch.d.ts +100 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispose.d.ts +161 -0
- package/dist/dispose.js +803 -0
- package/dist/email-settings.d.ts +34 -0
- package/dist/email-settings.js +50 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.js +105 -0
- package/dist/evidence-pack.d.ts +185 -0
- package/dist/evidence-pack.js +258 -0
- package/dist/evidence.d.ts +345 -0
- package/dist/evidence.js +782 -0
- package/dist/exec.d.ts +285 -0
- package/dist/exec.js +1841 -0
- package/dist/exhaustion.d.ts +85 -0
- package/dist/exhaustion.js +141 -0
- package/dist/export-ui.d.ts +4 -0
- package/dist/export-ui.js +17 -0
- package/dist/export.d.ts +48 -0
- package/dist/export.js +305 -0
- package/dist/flow-actions.d.ts +77 -0
- package/dist/flow-actions.js +203 -0
- package/dist/flow-code.d.ts +50 -0
- package/dist/flow-code.js +159 -0
- package/dist/flow-draft.d.ts +38 -0
- package/dist/flow-draft.js +80 -0
- package/dist/flow-engine.d.ts +66 -0
- package/dist/flow-engine.js +416 -0
- package/dist/flow-insights.d.ts +95 -0
- package/dist/flow-insights.js +149 -0
- package/dist/flow-live.d.ts +24 -0
- package/dist/flow-live.js +150 -0
- package/dist/flow-people.d.ts +24 -0
- package/dist/flow-people.js +91 -0
- package/dist/flow-replies.d.ts +48 -0
- package/dist/flow-replies.js +113 -0
- package/dist/flow-scripts.d.ts +38 -0
- package/dist/flow-scripts.js +73 -0
- package/dist/flow-secrets.d.ts +13 -0
- package/dist/flow-secrets.js +45 -0
- package/dist/flow-sort.d.ts +82 -0
- package/dist/flow-sort.js +153 -0
- package/dist/flow-steps.d.ts +40 -0
- package/dist/flow-steps.js +408 -0
- package/dist/flow-triggers.d.ts +244 -0
- package/dist/flow-triggers.js +959 -0
- package/dist/flows-ui.d.ts +20 -0
- package/dist/flows-ui.js +175 -0
- package/dist/flows.d.ts +229 -0
- package/dist/flows.js +968 -0
- package/dist/fonts.d.ts +19 -0
- package/dist/fonts.js +19 -0
- package/dist/gaps.d.ts +35 -0
- package/dist/gaps.js +101 -0
- package/dist/gate-failure.d.ts +40 -0
- package/dist/gate-failure.js +66 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +94 -0
- package/dist/google-mail.d.ts +58 -0
- package/dist/google-mail.js +151 -0
- package/dist/grant.d.ts +136 -0
- package/dist/grant.js +238 -0
- package/dist/graph.d.ts +164 -0
- package/dist/graph.js +383 -0
- package/dist/guides.d.ts +22 -0
- package/dist/guides.js +363 -0
- package/dist/held.d.ts +150 -0
- package/dist/held.js +799 -0
- package/dist/invoke.d.ts +112 -0
- package/dist/invoke.js +780 -0
- package/dist/issues.d.ts +41 -0
- package/dist/issues.js +131 -0
- package/dist/job-object-helper.ps1 +252 -0
- package/dist/jsonl-discriminants.d.ts +11 -0
- package/dist/jsonl-discriminants.js +93 -0
- package/dist/keys.d.ts +104 -0
- package/dist/keys.js +229 -0
- package/dist/kits-ui.d.ts +17 -0
- package/dist/kits-ui.js +46 -0
- package/dist/kits.d.ts +86 -0
- package/dist/kits.js +215 -0
- package/dist/knowledge-cli.d.ts +101 -0
- package/dist/knowledge-cli.js +92 -0
- package/dist/knowledge-ui.d.ts +24 -0
- package/dist/knowledge-ui.js +40 -0
- package/dist/lead-context.d.ts +8 -0
- package/dist/lead-context.js +56 -0
- package/dist/lead-follow.d.ts +22 -0
- package/dist/lead-follow.js +167 -0
- package/dist/lead-status.d.ts +75 -0
- package/dist/lead-status.js +285 -0
- package/dist/ledger-chain.d.ts +46 -0
- package/dist/ledger-chain.js +187 -0
- package/dist/ledger-csv.d.ts +4 -0
- package/dist/ledger-csv.js +11 -0
- package/dist/ledger-view.d.ts +24 -0
- package/dist/ledger-view.js +58 -0
- package/dist/limits-ui.d.ts +26 -0
- package/dist/limits-ui.js +67 -0
- package/dist/link.d.ts +72 -0
- package/dist/link.js +217 -0
- package/dist/live.d.ts +96 -0
- package/dist/live.js +366 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +42 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +25 -0
- package/dist/mailbox.d.ts +57 -0
- package/dist/mailbox.js +150 -0
- package/dist/maintenance.d.ts +11 -0
- package/dist/maintenance.js +35 -0
- package/dist/mate-cli.d.ts +52 -0
- package/dist/mate-cli.js +345 -0
- package/dist/mate-contract.d.ts +10 -0
- package/dist/mate-contract.js +30 -0
- package/dist/mate-doors.d.ts +69 -0
- package/dist/mate-doors.js +548 -0
- package/dist/mate-progress.d.ts +29 -0
- package/dist/mate-progress.js +121 -0
- package/dist/mate-tools.d.ts +94 -0
- package/dist/mate-tools.js +1878 -0
- package/dist/mate.d.ts +92 -0
- package/dist/mate.js +435 -0
- package/dist/mcp-connect.d.ts +103 -0
- package/dist/mcp-connect.js +252 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +651 -0
- package/dist/memory-cli.d.ts +466 -0
- package/dist/memory-cli.js +111 -0
- package/dist/memory-pass.d.ts +160 -0
- package/dist/memory-pass.js +409 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +56 -0
- package/dist/mobile-viewport.d.ts +3 -0
- package/dist/mobile-viewport.js +37 -0
- package/dist/model-catalog.d.ts +118 -0
- package/dist/model-catalog.js +376 -0
- package/dist/models-cli.d.ts +102 -0
- package/dist/models-cli.js +66 -0
- package/dist/models-ui.d.ts +34 -0
- package/dist/models-ui.js +68 -0
- package/dist/modes.d.ts +80 -0
- package/dist/modes.js +182 -0
- package/dist/monitoring-settings.d.ts +44 -0
- package/dist/monitoring-settings.js +173 -0
- package/dist/monitoring-ui.d.ts +20 -0
- package/dist/monitoring-ui.js +51 -0
- package/dist/monitoring.d.ts +34 -0
- package/dist/monitoring.js +221 -0
- package/dist/names.d.ts +67 -0
- package/dist/names.js +102 -0
- package/dist/observations.d.ts +40 -0
- package/dist/observations.js +211 -0
- package/dist/oidc.d.ts +71 -0
- package/dist/oidc.js +182 -0
- package/dist/onboard.d.ts +108 -0
- package/dist/onboard.js +325 -0
- package/dist/openrouter-models.d.ts +30 -0
- package/dist/openrouter-models.js +120 -0
- package/dist/operate.d.ts +118 -0
- package/dist/operate.js +11321 -0
- package/dist/peek-cli.d.ts +97 -0
- package/dist/peek-cli.js +215 -0
- package/dist/peek.d.ts +134 -0
- package/dist/peek.js +578 -0
- package/dist/phase-routing.d.ts +307 -0
- package/dist/phase-routing.js +658 -0
- package/dist/plan-auto.d.ts +14 -0
- package/dist/plan-auto.js +92 -0
- package/dist/plan.d.ts +186 -0
- package/dist/plan.js +401 -0
- package/dist/planner-source.d.ts +251 -0
- package/dist/planner-source.js +460 -0
- package/dist/planner.d.ts +75 -0
- package/dist/planner.js +992 -0
- package/dist/policy-ui.d.ts +20 -0
- package/dist/policy-ui.js +41 -0
- package/dist/policy.d.ts +111 -0
- package/dist/policy.js +231 -0
- package/dist/prepared-evidence.d.ts +16 -0
- package/dist/prepared-evidence.js +96 -0
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.js +77 -0
- package/dist/principal.d.ts +42 -0
- package/dist/principal.js +82 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.js +79 -0
- package/dist/process-custody.d.ts +17 -0
- package/dist/process-custody.js +91 -0
- package/dist/process-liveness.d.ts +10 -0
- package/dist/process-liveness.js +81 -0
- package/dist/process-recovery-anchor.d.ts +75 -0
- package/dist/process-recovery-anchor.js +188 -0
- package/dist/process-recovery-coalition.d.ts +25 -0
- package/dist/process-recovery-coalition.js +105 -0
- package/dist/process-recovery-eligibility.d.ts +178 -0
- package/dist/process-recovery-eligibility.js +170 -0
- package/dist/process-recovery-native.d.ts +172 -0
- package/dist/process-recovery-native.js +592 -0
- package/dist/process-recovery-provenance.d.ts +163 -0
- package/dist/process-recovery-provenance.js +292 -0
- package/dist/process-recovery-services.d.ts +56 -0
- package/dist/process-recovery-services.js +191 -0
- package/dist/process-recovery-settlement.d.ts +32 -0
- package/dist/process-recovery-settlement.js +132 -0
- package/dist/process-recovery.d.ts +41 -0
- package/dist/process-recovery.js +91 -0
- package/dist/process-tree.d.ts +41 -0
- package/dist/process-tree.js +264 -0
- package/dist/project-access.d.ts +14 -0
- package/dist/project-access.js +34 -0
- package/dist/project-cli.d.ts +18 -0
- package/dist/project-cli.js +104 -0
- package/dist/project-delete-ui.d.ts +22 -0
- package/dist/project-delete-ui.js +48 -0
- package/dist/project-delete.d.ts +55 -0
- package/dist/project-delete.js +413 -0
- package/dist/project-knowledge.d.ts +111 -0
- package/dist/project-knowledge.js +241 -0
- package/dist/project-learning.d.ts +100 -0
- package/dist/project-learning.js +438 -0
- package/dist/project-memory.d.ts +81 -0
- package/dist/project-memory.js +264 -0
- package/dist/project-skills.d.ts +152 -0
- package/dist/project-skills.js +660 -0
- package/dist/project-tools.d.ts +219 -0
- package/dist/project-tools.js +796 -0
- package/dist/project.d.ts +61 -0
- package/dist/project.js +125 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/prompt.js +72 -0
- package/dist/proof.d.ts +513 -0
- package/dist/proof.js +1140 -0
- package/dist/proposal.d.ts +82 -0
- package/dist/proposal.js +210 -0
- package/dist/provider-connection.d.ts +22 -0
- package/dist/provider-connection.js +98 -0
- package/dist/provider-limits.d.ts +35 -0
- package/dist/provider-limits.js +95 -0
- package/dist/provider.d.ts +279 -0
- package/dist/provider.js +935 -0
- package/dist/publish.d.ts +107 -0
- package/dist/publish.js +650 -0
- package/dist/pulls.d.ts +118 -0
- package/dist/pulls.js +240 -0
- package/dist/push.d.ts +95 -0
- package/dist/push.js +353 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.js +6 -0
- package/dist/recipe-ui.d.ts +18 -0
- package/dist/recipe-ui.js +152 -0
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.js +332 -0
- package/dist/remote.d.ts +67 -0
- package/dist/remote.js +122 -0
- package/dist/render.d.ts +64 -0
- package/dist/render.js +587 -0
- package/dist/report-summary.d.ts +13 -0
- package/dist/report-summary.js +15 -0
- package/dist/repos.d.ts +65 -0
- package/dist/repos.js +277 -0
- package/dist/repository-context-ui.d.ts +2 -0
- package/dist/repository-context-ui.js +10 -0
- package/dist/repository-context.d.ts +67 -0
- package/dist/repository-context.js +376 -0
- package/dist/restart-certification.d.ts +134 -0
- package/dist/restart-certification.js +226 -0
- package/dist/result-actions.d.ts +28 -0
- package/dist/result-actions.js +270 -0
- package/dist/result-completion.d.ts +18 -0
- package/dist/result-completion.js +66 -0
- package/dist/result-review.d.ts +213 -0
- package/dist/result-review.js +430 -0
- package/dist/retention-ui.d.ts +12 -0
- package/dist/retention-ui.js +39 -0
- package/dist/retention.d.ts +72 -0
- package/dist/retention.js +288 -0
- package/dist/review-context.d.ts +243 -0
- package/dist/review-context.js +1043 -0
- package/dist/review-evidence.d.ts +33 -0
- package/dist/review-evidence.js +56 -0
- package/dist/reviewer.d.ts +97 -0
- package/dist/reviewer.js +202 -0
- package/dist/routine.d.ts +228 -0
- package/dist/routine.js +764 -0
- package/dist/runner.d.ts +266 -0
- package/dist/runner.js +399 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +92 -0
- package/dist/scope.d.ts +695 -0
- package/dist/scope.js +1176 -0
- package/dist/scout-report.d.ts +42 -0
- package/dist/scout-report.js +108 -0
- package/dist/scout.d.ts +69 -0
- package/dist/scout.js +351 -0
- package/dist/serve.d.ts +313 -0
- package/dist/serve.js +21776 -0
- package/dist/session-brief.d.ts +8 -0
- package/dist/session-brief.js +24 -0
- package/dist/session-cli.d.ts +13 -0
- package/dist/session-cli.js +284 -0
- package/dist/session-contract.d.ts +183 -0
- package/dist/session-contract.js +122 -0
- package/dist/session-http.d.ts +19 -0
- package/dist/session-http.js +154 -0
- package/dist/session-server.d.ts +12 -0
- package/dist/session-server.js +37 -0
- package/dist/session-service.d.ts +22 -0
- package/dist/session-service.js +157 -0
- package/dist/setup-guide.d.ts +39 -0
- package/dist/setup-guide.js +93 -0
- package/dist/sign-in-guard.d.ts +45 -0
- package/dist/sign-in-guard.js +74 -0
- package/dist/skills-ui.d.ts +11 -0
- package/dist/skills-ui.js +58 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.js +276 -0
- package/dist/slack-api.d.ts +55 -0
- package/dist/slack-api.js +229 -0
- package/dist/slack-chat.d.ts +28 -0
- package/dist/slack-chat.js +413 -0
- package/dist/slack-settings.d.ts +7 -0
- package/dist/slack-settings.js +75 -0
- package/dist/slack-state.d.ts +13 -0
- package/dist/slack-state.js +9 -0
- package/dist/slack.d.ts +13 -0
- package/dist/slack.js +167 -0
- package/dist/spend-ui.d.ts +28 -0
- package/dist/spend-ui.js +95 -0
- package/dist/spend.d.ts +142 -0
- package/dist/spend.js +313 -0
- package/dist/sqlite-runtime.d.ts +3 -0
- package/dist/sqlite-runtime.js +6 -0
- package/dist/sso-settings.d.ts +33 -0
- package/dist/sso-settings.js +103 -0
- package/dist/sso-ui.d.ts +22 -0
- package/dist/sso-ui.js +39 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +84 -0
- package/dist/store.d.ts +7754 -0
- package/dist/store.js +23279 -0
- package/dist/structured-output.d.ts +58 -0
- package/dist/structured-output.js +103 -0
- package/dist/style-asset.d.ts +7 -0
- package/dist/style-asset.js +41 -0
- package/dist/subscription-chat.d.ts +46 -0
- package/dist/subscription-chat.js +275 -0
- package/dist/summary.d.ts +33 -0
- package/dist/summary.js +71 -0
- package/dist/supervisor.mjs +297 -0
- package/dist/surface.d.ts +63 -0
- package/dist/surface.js +352 -0
- package/dist/sync.d.ts +94 -0
- package/dist/sync.js +279 -0
- package/dist/task-composer.d.ts +13 -0
- package/dist/task-composer.js +52 -0
- package/dist/task-control.d.ts +165 -0
- package/dist/task-control.js +214 -0
- package/dist/task-outcome-cli.d.ts +10 -0
- package/dist/task-outcome-cli.js +92 -0
- package/dist/task-text.d.ts +30 -0
- package/dist/task-text.js +41 -0
- package/dist/team-cli.d.ts +15 -0
- package/dist/team-cli.js +618 -0
- package/dist/team-contract.d.ts +92 -0
- package/dist/team-contract.js +1 -0
- package/dist/team-http.d.ts +17 -0
- package/dist/team-http.js +144 -0
- package/dist/team-leads.d.ts +81 -0
- package/dist/team-leads.js +565 -0
- package/dist/team-runtime.d.ts +27 -0
- package/dist/team-runtime.js +264 -0
- package/dist/team-ui.d.ts +3 -0
- package/dist/team-ui.js +9 -0
- package/dist/team-updates.d.ts +8 -0
- package/dist/team-updates.js +57 -0
- package/dist/teammate-admin.d.ts +53 -0
- package/dist/teammate-admin.js +111 -0
- package/dist/teammate-desk.d.ts +60 -0
- package/dist/teammate-desk.js +154 -0
- package/dist/teammate-memory.d.ts +64 -0
- package/dist/teammate-memory.js +196 -0
- package/dist/teammate-question.d.ts +59 -0
- package/dist/teammate-question.js +175 -0
- package/dist/teammate-tools.d.ts +108 -0
- package/dist/teammate-tools.js +271 -0
- package/dist/teammate-week.d.ts +68 -0
- package/dist/teammate-week.js +132 -0
- package/dist/teammate-work.d.ts +76 -0
- package/dist/teammate-work.js +280 -0
- package/dist/teammates-ui.d.ts +20 -0
- package/dist/teammates-ui.js +166 -0
- package/dist/teammates.d.ts +180 -0
- package/dist/teammates.js +253 -0
- package/dist/teams-api.d.ts +33 -0
- package/dist/teams-api.js +204 -0
- package/dist/teams-chat.d.ts +26 -0
- package/dist/teams-chat.js +215 -0
- package/dist/teams-settings.d.ts +12 -0
- package/dist/teams-settings.js +57 -0
- package/dist/teams.d.ts +21 -0
- package/dist/teams.js +124 -0
- package/dist/telegram-flow.d.ts +49 -0
- package/dist/telegram-flow.js +110 -0
- package/dist/telegram-mate.d.ts +137 -0
- package/dist/telegram-mate.js +707 -0
- package/dist/telegram-progress.d.ts +14 -0
- package/dist/telegram-progress.js +129 -0
- package/dist/telegram-settings.d.ts +9 -0
- package/dist/telegram-settings.js +36 -0
- package/dist/telegram-status.d.ts +75 -0
- package/dist/telegram-status.js +265 -0
- package/dist/telegram-team.d.ts +85 -0
- package/dist/telegram-team.js +359 -0
- package/dist/telegram.d.ts +230 -0
- package/dist/telegram.js +1931 -0
- package/dist/templates.d.ts +65 -0
- package/dist/templates.js +118 -0
- package/dist/tool-launcher.d.ts +1 -0
- package/dist/tool-launcher.js +101 -0
- package/dist/tools-ui.d.ts +30 -0
- package/dist/tools-ui.js +40 -0
- package/dist/transitions-recipes.d.ts +1 -0
- package/dist/transitions-recipes.js +203 -0
- package/dist/tree-proof.d.ts +34 -0
- package/dist/tree-proof.js +58 -0
- package/dist/verification-evidence.d.ts +44 -0
- package/dist/verification-evidence.js +238 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +283 -0
- package/dist/work-index.d.ts +68 -0
- package/dist/work-index.js +384 -0
- package/dist/work-summary.d.ts +57 -0
- package/dist/work-summary.js +116 -0
- package/dist/workspace-motion.d.ts +4 -0
- package/dist/workspace-motion.js +133 -0
- package/dist/workspace-revision.d.ts +19 -0
- package/dist/workspace-revision.js +92 -0
- package/dist/workspace-ui.d.ts +204 -0
- package/dist/workspace-ui.js +411 -0
- package/dist/worktree-notices.d.ts +13 -0
- package/dist/worktree-notices.js +17 -0
- package/dist/worktree.d.ts +249 -0
- package/dist/worktree.js +720 -0
- package/package.json +122 -0
- package/scripts/canary-assertions.mjs +42 -0
- package/scripts/crash-canary.mjs +253 -0
- package/scripts/fixtures/crash-process.mjs +74 -0
- package/scripts/fixtures/pilot-scenarios.mjs +85 -0
- package/scripts/fixtures/restart-service.mjs +27 -0
- package/scripts/launchd-certification.mjs +143 -0
- package/scripts/pilot.mjs +121 -0
- package/scripts/proof-preflight.mjs +140 -0
- package/scripts/provider-canary.mjs +379 -0
- package/scripts/recovery-canary.mjs +135 -0
- package/scripts/restart-certification.mjs +98 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The command.
|
|
4
|
+
*
|
|
5
|
+
* `toolroll` with no arguments reads the repositories below the working
|
|
6
|
+
* directory and prints what is in flight. It writes nothing, starts nothing,
|
|
7
|
+
* and asks nothing. Everything it knows comes from git and from the operator's
|
|
8
|
+
* own filesystem.
|
|
9
|
+
*
|
|
10
|
+
* `--json` exists because half the intended audience is an agent, and an agent
|
|
11
|
+
* should not have to parse a column layout.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, lstatSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
import { configPath, loadRepos, addRepos, removeRepos, updateRepos } from "./repos.js";
|
|
18
|
+
import { CAPABILITIES, ENVELOPE_VERSION, capturedEnvelope, envelopeJson, onEnvelopeCaptured, resetCapturedEnvelope } from "./envelope.js";
|
|
19
|
+
import { applyClaudeCodeInstall, applyInstall, contextBlock, defaultClaudeCodeSkillDir, planClaudeCodeInstall, planInstall, } from "./skills.js";
|
|
20
|
+
import { GUIDES, guideNamed } from "./guides.js";
|
|
21
|
+
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, isLargeRepo } from "./onboard.js";
|
|
22
|
+
import { run as execRun } from "./exec.js";
|
|
23
|
+
import { COMMAND_GUIDE, SURFACE_NOTES, SURFACE_SCHEMA_VERSION } from "./surface.js";
|
|
24
|
+
import { runSessionCommand, SESSION_CLI_ACTIONS } from "./session-cli.js";
|
|
25
|
+
import { maybeRunTeamCommand, TEAM_CLI_ACTIONS } from "./team-cli.js";
|
|
26
|
+
import { discover, inspectAll } from "./discover.js";
|
|
27
|
+
import { readPulls } from "./pulls.js";
|
|
28
|
+
import { detectGraphs, chooseBackend, setupOptions, LABELS as GRAPH_LABELS, } from "./graph.js";
|
|
29
|
+
import { openStore, openStoreReadOnly, databasePath } from "./store.js";
|
|
30
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
31
|
+
import { runOperate, OPERATE_HELP } from "./operate.js";
|
|
32
|
+
import { renderReport, renderPulls, renderGraph } from "./render.js";
|
|
33
|
+
import { readRemote } from "./remote.js";
|
|
34
|
+
import { DEFAULT_MAX_DEPTH } from "./scan.js";
|
|
35
|
+
import { BIN_NAME, chooseTarget, planLink, applyLink, planUnlink, applyUnlink, } from "./link.js";
|
|
36
|
+
const USAGE_EXIT = 2;
|
|
37
|
+
export const HELP = `toolroll — a control plane for unattended coding agents
|
|
38
|
+
|
|
39
|
+
Usage
|
|
40
|
+
toolroll [path...] report what is in flight
|
|
41
|
+
toolroll pulls report what is waiting on a person
|
|
42
|
+
toolroll graph report which work graph is already here
|
|
43
|
+
toolroll repos list connected repositories, and how to adjust
|
|
44
|
+
toolroll repos add <path> connect one (no path: the repo you are in)
|
|
45
|
+
toolroll repos remove <path>
|
|
46
|
+
toolroll repos add-from-github <owner/name> --root <dir>
|
|
47
|
+
preview, then clone and connect (--yes)
|
|
48
|
+
toolroll link put \`toolroll\` on your PATH
|
|
49
|
+
toolroll unlink take it off again
|
|
50
|
+
toolroll contract the machine contract: envelope version + capabilities
|
|
51
|
+
(--commands dumps the declared command guide)
|
|
52
|
+
toolroll skills install --claude-code [--dir <path>]
|
|
53
|
+
install this binary's guides for Claude Code (preview first)
|
|
54
|
+
toolroll skills install teach a repo's agents this queue exists (preview first)
|
|
55
|
+
toolroll skills list the guides this exact binary serves
|
|
56
|
+
toolroll skills get <name> print one guide (version-matched, never stale)
|
|
57
|
+
toolroll demo a seeded throwaway sandbox — see it working in 90 seconds
|
|
58
|
+
toolroll up app + builder for every saved project — the normal start
|
|
59
|
+
toolroll status running, queued, ready results, release check and plan windows
|
|
60
|
+
toolroll session native coding sessions through the running service
|
|
61
|
+
toolroll connect save a private connection to your central service
|
|
62
|
+
toolroll lead named leads on the connected service
|
|
63
|
+
toolroll conversation shared and private conversations on that service
|
|
64
|
+
toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)
|
|
65
|
+
|
|
66
|
+
Operating the queue — \`toolroll task\` prints the whole surface,
|
|
67
|
+
and any queue command + --help prints it too
|
|
68
|
+
toolroll approver add <name>
|
|
69
|
+
mint the credential that lets a person say yes
|
|
70
|
+
toolroll ready what could be dispatched right now
|
|
71
|
+
toolroll check-progress <run> current or final approved check progress
|
|
72
|
+
toolroll task wait <id> [--timeout <seconds>]
|
|
73
|
+
wait for this attempt to finish or need a person
|
|
74
|
+
toolroll task add <title> queue work
|
|
75
|
+
toolroll task scope <id> --goal <text>
|
|
76
|
+
state what success is; approval binds to it
|
|
77
|
+
toolroll task approve <id>
|
|
78
|
+
the yes — nothing builds without one
|
|
79
|
+
toolroll claim <id> --runner <name>
|
|
80
|
+
toolroll heartbeat <lease> / release <lease> / reap
|
|
81
|
+
toolroll tick --runner <name> --token <t> --repo <path>
|
|
82
|
+
one unattended pass over the ready set
|
|
83
|
+
toolroll chat --as <you> --token <t>
|
|
84
|
+
talk to the mate: one conversation across every
|
|
85
|
+
project; it proposes, you confirm (--say "…" for one turn)
|
|
86
|
+
toolroll serve --repo <path> advanced: run only the console
|
|
87
|
+
(--editor vscode with --runner: file links
|
|
88
|
+
open in VS Code on the device you browse from)
|
|
89
|
+
toolroll watch / daemon install advanced: run only the builder
|
|
90
|
+
toolroll reconcile recover what the last stretch left behind
|
|
91
|
+
|
|
92
|
+
With nothing connected it reports everything it can find below the working
|
|
93
|
+
directory. Once you connect repositories it reports those instead.
|
|
94
|
+
|
|
95
|
+
Options
|
|
96
|
+
--all report everything discoverable, ignoring connected repos
|
|
97
|
+
--depth <n> directory levels to descend (default: ${DEFAULT_MAX_DEPTH})
|
|
98
|
+
--hidden include dot-directories, which are skipped by default
|
|
99
|
+
--dirty also read each working tree for uncommitted files
|
|
100
|
+
--local branches only: no network, no pull requests, no issues
|
|
101
|
+
--json emit a machine-readable envelope instead of a report
|
|
102
|
+
-o <file> also write that envelope to a file (requires --json)
|
|
103
|
+
-h, --help show this
|
|
104
|
+
|
|
105
|
+
Nothing is written, installed, or configured. Every repository is read
|
|
106
|
+
through git with --no-optional-locks, so a scan never contends for the
|
|
107
|
+
index lock with an editor you have open.
|
|
108
|
+
|
|
109
|
+
--dirty is off by default because reading a working tree is unbounded
|
|
110
|
+
work: on a large or cloud-synced repo \`git status\` can take minutes,
|
|
111
|
+
where listing branches takes milliseconds.
|
|
112
|
+
|
|
113
|
+
link shows what it would do and changes nothing; add --yes to apply it,
|
|
114
|
+
or --to <dir> to choose where. It never uses sudo, never leaves your
|
|
115
|
+
home directory, and never touches a file it did not create.`;
|
|
116
|
+
export function parseArgs(argv) {
|
|
117
|
+
const roots = [];
|
|
118
|
+
const rawRoots = [];
|
|
119
|
+
const options = {
|
|
120
|
+
roots: [],
|
|
121
|
+
rawRoots: [],
|
|
122
|
+
rootsGiven: false,
|
|
123
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
124
|
+
json: false,
|
|
125
|
+
includeHidden: false,
|
|
126
|
+
all: false,
|
|
127
|
+
dirty: false,
|
|
128
|
+
local: false,
|
|
129
|
+
help: false,
|
|
130
|
+
};
|
|
131
|
+
for (let index = 0; index < argv.length; index++) {
|
|
132
|
+
const argument = argv[index];
|
|
133
|
+
if (argument === "--json") {
|
|
134
|
+
options.json = true;
|
|
135
|
+
}
|
|
136
|
+
else if (argument === "--hidden") {
|
|
137
|
+
options.includeHidden = true;
|
|
138
|
+
}
|
|
139
|
+
else if (argument === "--all") {
|
|
140
|
+
options.all = true;
|
|
141
|
+
}
|
|
142
|
+
else if (argument === "--dirty") {
|
|
143
|
+
options.dirty = true;
|
|
144
|
+
}
|
|
145
|
+
else if (argument === "--local") {
|
|
146
|
+
options.local = true;
|
|
147
|
+
}
|
|
148
|
+
else if (argument === "--help" || argument === "-h") {
|
|
149
|
+
options.help = true;
|
|
150
|
+
}
|
|
151
|
+
else if (argument === "--depth") {
|
|
152
|
+
const depth = Number(argv[++index]);
|
|
153
|
+
if (!Number.isInteger(depth) || depth < 0) {
|
|
154
|
+
return { error: "--depth needs a whole number, for example `--depth 8`" };
|
|
155
|
+
}
|
|
156
|
+
options.maxDepth = depth;
|
|
157
|
+
}
|
|
158
|
+
else if (argument.startsWith("-")) {
|
|
159
|
+
return { error: `unknown option ${argument} — try \`toolroll --help\`` };
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
roots.push(resolve(argument));
|
|
163
|
+
rawRoots.push(argument);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
options: {
|
|
168
|
+
...options,
|
|
169
|
+
roots: roots.length > 0 ? roots : [process.cwd()],
|
|
170
|
+
rawRoots,
|
|
171
|
+
rootsGiven: roots.length > 0,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** The commands that operate the queue rather than report on the world. */
|
|
176
|
+
/** Every verb the CLI routes OUTSIDE runOperate. Exported (with
|
|
177
|
+
* OPERATE_COMMANDS) so the declared command guide's drift tests compare
|
|
178
|
+
* exact sets instead of regex-reading source. "" is the no-verb report,
|
|
179
|
+
* which answers as `scan`. */
|
|
180
|
+
export const TOP_LEVEL_COMMANDS = [
|
|
181
|
+
"", "pulls", "graph", "repos", "repos add", "repos remove", "repos add-from-github",
|
|
182
|
+
"link", "unlink", "contract", "skills list", "skills get", "skills install", "demo",
|
|
183
|
+
...SESSION_CLI_ACTIONS.map(action => `session ${action}`),
|
|
184
|
+
...TEAM_CLI_ACTIONS,
|
|
185
|
+
];
|
|
186
|
+
export const OPERATE_COMMANDS = new Set([
|
|
187
|
+
"up",
|
|
188
|
+
"status",
|
|
189
|
+
"ready",
|
|
190
|
+
"task",
|
|
191
|
+
"project",
|
|
192
|
+
"ledger",
|
|
193
|
+
"storage",
|
|
194
|
+
"monitoring",
|
|
195
|
+
"check-progress",
|
|
196
|
+
"spend",
|
|
197
|
+
"budget",
|
|
198
|
+
"retention",
|
|
199
|
+
"backup",
|
|
200
|
+
"restore",
|
|
201
|
+
"export",
|
|
202
|
+
"policy",
|
|
203
|
+
"assignment",
|
|
204
|
+
"knowledge",
|
|
205
|
+
"memory",
|
|
206
|
+
"models",
|
|
207
|
+
"claim",
|
|
208
|
+
"heartbeat",
|
|
209
|
+
"release",
|
|
210
|
+
"reap",
|
|
211
|
+
"enroll",
|
|
212
|
+
"grants",
|
|
213
|
+
"revoke",
|
|
214
|
+
"runner",
|
|
215
|
+
"approver",
|
|
216
|
+
"coordinator",
|
|
217
|
+
"mcp",
|
|
218
|
+
"build",
|
|
219
|
+
"tick",
|
|
220
|
+
"cap",
|
|
221
|
+
"gaps",
|
|
222
|
+
"outbox",
|
|
223
|
+
"peek",
|
|
224
|
+
"brief",
|
|
225
|
+
"decide",
|
|
226
|
+
"incident",
|
|
227
|
+
"serve",
|
|
228
|
+
"watch",
|
|
229
|
+
"daemon",
|
|
230
|
+
"bridge",
|
|
231
|
+
"publish",
|
|
232
|
+
"reconcile",
|
|
233
|
+
"routine",
|
|
234
|
+
"config", "chat", "proposals", "mode", "people", "keys",
|
|
235
|
+
"setup",
|
|
236
|
+
"verify",
|
|
237
|
+
"intake",
|
|
238
|
+
"providers",
|
|
239
|
+
"template",
|
|
240
|
+
"contest",
|
|
241
|
+
"webhook",
|
|
242
|
+
"sync",
|
|
243
|
+
]);
|
|
244
|
+
/**
|
|
245
|
+
* Strip `-o <file>` / `--output <file>` before dispatch. The flag belongs to
|
|
246
|
+
* the entry point, not to any one command: after the command runs, whatever
|
|
247
|
+
* envelope it serialized is copied to the file, so an agent reads its answer
|
|
248
|
+
* from a path instead of scraping a terminal. Requires `--json`, because the
|
|
249
|
+
* file receives the machine envelope and nothing else does.
|
|
250
|
+
*/
|
|
251
|
+
export function extractOutputFlag(argv) {
|
|
252
|
+
const args = [];
|
|
253
|
+
let outputFile;
|
|
254
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
255
|
+
const arg = argv[i];
|
|
256
|
+
if (arg === "-o" || arg === "--output") {
|
|
257
|
+
const value = argv[i + 1];
|
|
258
|
+
if (value === undefined || value.startsWith("-")) {
|
|
259
|
+
return { error: `${arg} takes a file path` };
|
|
260
|
+
}
|
|
261
|
+
outputFile = value;
|
|
262
|
+
i += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
args.push(arg);
|
|
266
|
+
}
|
|
267
|
+
return { args, ...(outputFile === undefined ? {} : { outputFile }) };
|
|
268
|
+
}
|
|
269
|
+
export async function main(argv, write = line => console.log(line), mainOptions = {}) {
|
|
270
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
271
|
+
write(PACKAGE_VERSION);
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
const extracted = extractOutputFlag(argv);
|
|
275
|
+
if ("error" in extracted) {
|
|
276
|
+
write(extracted.error);
|
|
277
|
+
return USAGE_EXIT;
|
|
278
|
+
}
|
|
279
|
+
const { args, outputFile } = extracted;
|
|
280
|
+
if (outputFile !== undefined && !args.includes("--json")) {
|
|
281
|
+
write("-o requires --json — the output file receives the machine envelope");
|
|
282
|
+
return USAGE_EXIT;
|
|
283
|
+
}
|
|
284
|
+
resetCapturedEnvelope();
|
|
285
|
+
// Long-running commands (serve, up) emit their ONE envelope at startup,
|
|
286
|
+
// when its URL is useful — so -o is preflighted BEFORE dispatch (an
|
|
287
|
+
// unwritable target refuses up front) and written the moment the
|
|
288
|
+
// envelope is captured, through a hook that never throws into the
|
|
289
|
+
// command (arc 2 findings 23/30).
|
|
290
|
+
const firstWord = args.find(one => !one.startsWith("-"));
|
|
291
|
+
const earlyFlush = outputFile !== undefined && (firstWord === "serve" || firstWord === "up");
|
|
292
|
+
let earlyFlushProblem = null;
|
|
293
|
+
if (earlyFlush && outputFile !== undefined) {
|
|
294
|
+
try {
|
|
295
|
+
const existing = lstatSync(outputFile, { throwIfNoEntry: false });
|
|
296
|
+
if (existing !== undefined) {
|
|
297
|
+
if (!existing.isFile()) {
|
|
298
|
+
write(`-o refuses ${outputFile}: not a regular file`);
|
|
299
|
+
return USAGE_EXIT;
|
|
300
|
+
}
|
|
301
|
+
unlinkSync(outputFile);
|
|
302
|
+
}
|
|
303
|
+
writeFileSync(outputFile, "", { mode: 0o600, flag: "wx" });
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
write(`-o could not prepare ${outputFile}: ${String(error.message ?? error)}`);
|
|
307
|
+
return USAGE_EXIT;
|
|
308
|
+
}
|
|
309
|
+
onEnvelopeCaptured(body => {
|
|
310
|
+
try {
|
|
311
|
+
writeFileSync(outputFile, `${body}\n`, { mode: 0o600 });
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
earlyFlushProblem = String(error.message ?? error);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
let code;
|
|
319
|
+
try {
|
|
320
|
+
code = await dispatch(args, write, mainOptions);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
if (earlyFlush)
|
|
324
|
+
onEnvelopeCaptured(null);
|
|
325
|
+
}
|
|
326
|
+
if (earlyFlushProblem !== null) {
|
|
327
|
+
process.stderr.write(`-o could not write ${outputFile}: ${earlyFlushProblem}\n`);
|
|
328
|
+
return code === 0 ? 1 : code;
|
|
329
|
+
}
|
|
330
|
+
if (outputFile !== undefined && !earlyFlush) {
|
|
331
|
+
const captured = capturedEnvelope();
|
|
332
|
+
if (captured !== null) {
|
|
333
|
+
// Envelopes can carry one-time credentials (runner register, approver
|
|
334
|
+
// add), so the file is written 0600 and never through a symlink or
|
|
335
|
+
// onto a non-regular target (Codex M5-M8 audit, IV-8).
|
|
336
|
+
try {
|
|
337
|
+
const existing = lstatSync(outputFile, { throwIfNoEntry: false });
|
|
338
|
+
if (existing !== undefined) {
|
|
339
|
+
if (!existing.isFile()) {
|
|
340
|
+
write(`-o refuses ${outputFile}: not a regular file`);
|
|
341
|
+
return USAGE_EXIT;
|
|
342
|
+
}
|
|
343
|
+
unlinkSync(outputFile);
|
|
344
|
+
}
|
|
345
|
+
writeFileSync(outputFile, `${captured}\n`, { mode: 0o600, flag: "wx" });
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
write(`-o could not write ${outputFile}: ${String(error.message ?? error)}`);
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return code;
|
|
354
|
+
}
|
|
355
|
+
async function dispatch(argv, write, mainOptions) {
|
|
356
|
+
const team = await maybeRunTeamCommand(argv, write, mainOptions.team);
|
|
357
|
+
if (team !== null)
|
|
358
|
+
return team;
|
|
359
|
+
if ((argv[0] === "chat" || argv[0] === "brief") && argv.includes("--local"))
|
|
360
|
+
argv = argv.filter(arg => arg !== "--local");
|
|
361
|
+
const [first, ...rest] = argv;
|
|
362
|
+
if (first === "help") {
|
|
363
|
+
// The bare word, because somebody will type it — never scanned as a
|
|
364
|
+
// path named "help".
|
|
365
|
+
write(HELP);
|
|
366
|
+
return 0;
|
|
367
|
+
}
|
|
368
|
+
if (first === "link" || first === "unlink") {
|
|
369
|
+
return runLinkCommand(first, rest, write, mainOptions.binSource);
|
|
370
|
+
}
|
|
371
|
+
if (first === "contract")
|
|
372
|
+
return runContractCommand(rest, write);
|
|
373
|
+
if (first === "session")
|
|
374
|
+
return runSessionCommand(rest, write, mainOptions.session);
|
|
375
|
+
if (first === "demo")
|
|
376
|
+
return runDemoCommand(rest, write);
|
|
377
|
+
if (first === "skills")
|
|
378
|
+
return runSkillsCommand(rest, write);
|
|
379
|
+
if (first === "repos")
|
|
380
|
+
return runReposCommand(rest, write, mainOptions.onboard);
|
|
381
|
+
if (first === "pulls")
|
|
382
|
+
return runPullsCommand(rest, write);
|
|
383
|
+
if (first === "graph")
|
|
384
|
+
return runGraphCommand(rest, write);
|
|
385
|
+
if (first !== undefined && OPERATE_COMMANDS.has(first)) {
|
|
386
|
+
return runOperate(first, rest, write, mainOptions.operate ?? {});
|
|
387
|
+
}
|
|
388
|
+
const parsed = parseArgs(argv);
|
|
389
|
+
if ("error" in parsed) {
|
|
390
|
+
write(parsed.error);
|
|
391
|
+
return USAGE_EXIT;
|
|
392
|
+
}
|
|
393
|
+
const { options } = parsed;
|
|
394
|
+
if (options.help) {
|
|
395
|
+
write(HELP);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
const now = new Date();
|
|
399
|
+
// An enrolled list is a decision the operator already made; honour it unless
|
|
400
|
+
// they asked otherwise by naming paths or passing --all.
|
|
401
|
+
const enrolled = await loadRepos(configFile());
|
|
402
|
+
if ("error" in enrolled) {
|
|
403
|
+
write(enrolled.error);
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
if (!options.all && !options.rootsGiven && enrolled.repos.length > 0) {
|
|
407
|
+
return reportEnrolled(enrolled.repos, options, now, write);
|
|
408
|
+
}
|
|
409
|
+
const { present, missing } = partitionRoots(options.roots, existsSync);
|
|
410
|
+
// A path that does not exist is a typo, not an empty search, and saying
|
|
411
|
+
// "no repositories found" about it would be answering a question we never
|
|
412
|
+
// asked. Warnings go inside the envelope in JSON mode so it stays
|
|
413
|
+
// parseable — and when NOTHING was scannable the envelope says ok:false
|
|
414
|
+
// (aligned with `pulls`), because an all-missing scan that reported
|
|
415
|
+
// ok:true let a typo'd command read as success.
|
|
416
|
+
if (present.length === 0) {
|
|
417
|
+
write(options.json
|
|
418
|
+
? renderMissingJson(options.roots, missing, now)
|
|
419
|
+
: describeMissing(missing, options.rawRoots, options.roots));
|
|
420
|
+
return USAGE_EXIT;
|
|
421
|
+
}
|
|
422
|
+
if (missing.length > 0 && !options.json) {
|
|
423
|
+
write(describeMissing(missing, options.rawRoots, options.roots));
|
|
424
|
+
write("");
|
|
425
|
+
}
|
|
426
|
+
const repos = await discover(present, {
|
|
427
|
+
scan: { maxDepth: options.maxDepth, includeHidden: options.includeHidden },
|
|
428
|
+
dirty: options.dirty,
|
|
429
|
+
});
|
|
430
|
+
const remote = await maybeReadRemote(repos, options);
|
|
431
|
+
const output = options.json
|
|
432
|
+
? renderJson(repos, present, missing, now, remote)
|
|
433
|
+
: renderReport(repos, { now, roots: present, ...(remote === undefined ? {} : { remote }) });
|
|
434
|
+
write(output);
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Pull requests and issues, unless the operator asked to stay local.
|
|
439
|
+
*
|
|
440
|
+
* Reading them is the difference between "every branch in flight" and "every
|
|
441
|
+
* branch, pull request and issue in flight", which is what the milestone asks
|
|
442
|
+
* for — but it is also the only part of this report that touches the network,
|
|
443
|
+
* so it is bounded and `--local` turns it off entirely.
|
|
444
|
+
*/
|
|
445
|
+
async function maybeReadRemote(repos, options) {
|
|
446
|
+
if (options.local)
|
|
447
|
+
return undefined;
|
|
448
|
+
return readRemote(repos.map(repo => ({ path: repo.path, remoteUrl: repo.remoteUrl })));
|
|
449
|
+
}
|
|
450
|
+
function configFile() {
|
|
451
|
+
return configPath(process.env, homedir());
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Report the enrolled repositories directly. A path that has since been moved
|
|
455
|
+
* or deleted is named rather than silently dropped — a list that quietly
|
|
456
|
+
* shrinks is worse than one that tells you it is stale.
|
|
457
|
+
*/
|
|
458
|
+
async function reportEnrolled(repos, options, now, write) {
|
|
459
|
+
const { present, missing } = partitionRoots(repos, existsSync);
|
|
460
|
+
const snapshots = await inspectAll(present, { dirty: options.dirty });
|
|
461
|
+
const remote = await maybeReadRemote(snapshots, options);
|
|
462
|
+
if (options.json) {
|
|
463
|
+
write(renderJson(snapshots, present, missing, now, remote));
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
if (missing.length > 0) {
|
|
467
|
+
for (const repo of missing)
|
|
468
|
+
write(`${repo} is enrolled but is no longer there.`);
|
|
469
|
+
write(`Drop it with \`toolroll repos remove ${missing[0]}\`.`);
|
|
470
|
+
write("");
|
|
471
|
+
}
|
|
472
|
+
write(renderReport(snapshots, {
|
|
473
|
+
now,
|
|
474
|
+
roots: present,
|
|
475
|
+
...(remote === undefined ? {} : { remote }),
|
|
476
|
+
}));
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
export function partitionRoots(roots, exists) {
|
|
480
|
+
return {
|
|
481
|
+
present: roots.filter(exists),
|
|
482
|
+
missing: roots.filter(root => !exists(root)),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* A missing root that was typed as a bare word — no separator, no leading
|
|
487
|
+
* dot, no home shorthand — is as likely a mistyped COMMAND as a missing
|
|
488
|
+
* folder, and after `resolve()` the two are indistinguishable. The message
|
|
489
|
+
* names both readings instead of guessing (Codex round-4 finding 18);
|
|
490
|
+
* path-shaped roots keep the plain missing-path sentence.
|
|
491
|
+
*/
|
|
492
|
+
function describeMissing(missing, rawRoots = [], roots = []) {
|
|
493
|
+
const rawOf = new Map();
|
|
494
|
+
roots.forEach((resolved, index) => {
|
|
495
|
+
const raw = rawRoots[index];
|
|
496
|
+
if (raw !== undefined)
|
|
497
|
+
rawOf.set(resolved, raw);
|
|
498
|
+
});
|
|
499
|
+
const bareWord = (raw) => raw !== undefined && !/[/\\~]/.test(raw) && !raw.startsWith(".");
|
|
500
|
+
return missing
|
|
501
|
+
.map(root => bareWord(rawOf.get(root))
|
|
502
|
+
? `${root} does not exist — if you meant a command, \`toolroll --help\` lists them; to scan a folder, give a path that exists.`
|
|
503
|
+
: `${root} does not exist — check the path.`)
|
|
504
|
+
.join("\n");
|
|
505
|
+
}
|
|
506
|
+
/** The all-missing scan envelope: honest ok:false with the scan fields kept. */
|
|
507
|
+
function renderMissingJson(roots, missingRoots, now) {
|
|
508
|
+
return envelopeJson({
|
|
509
|
+
ok: false,
|
|
510
|
+
command: "scan",
|
|
511
|
+
reason: "no-repositories",
|
|
512
|
+
message: "none of the given paths exist — nothing was scanned",
|
|
513
|
+
scannedAt: now.toISOString(),
|
|
514
|
+
roots,
|
|
515
|
+
missingRoots,
|
|
516
|
+
remoteRead: false,
|
|
517
|
+
repos: [],
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* `repos` with no arguments lists what is connected and shows how to change
|
|
522
|
+
* it, so the way to adjust the list is visible at the moment you are looking
|
|
523
|
+
* at it — rather than being something you have to remember a flag for.
|
|
524
|
+
*/
|
|
525
|
+
async function runReposCommand(argv, write, onboard) {
|
|
526
|
+
// `repos` honors the machine contract like everything else (audit TG-3):
|
|
527
|
+
// --json answers with one envelope, whatever the outcome.
|
|
528
|
+
const json = argv.includes("--json");
|
|
529
|
+
const bare = argv.filter(argument => argument !== "--json");
|
|
530
|
+
const [action, ...paths] = bare;
|
|
531
|
+
const file = configFile();
|
|
532
|
+
// Dispatched BEFORE the registry load (verification finding 3): a
|
|
533
|
+
// malformed registry must not mask this command's own parsing, its
|
|
534
|
+
// read-only preview, or its taxonomy — enrollment reads the registry
|
|
535
|
+
// through the locked primitive at the moment it writes.
|
|
536
|
+
if (action === "add-from-github") {
|
|
537
|
+
return addFromGithubCommand(bare.slice(1), json, file, write, onboard);
|
|
538
|
+
}
|
|
539
|
+
const loaded = await loadRepos(file);
|
|
540
|
+
if ("error" in loaded) {
|
|
541
|
+
write(json ? envelopeJson({ ok: false, command: "repos", reason: "unreadable", message: loaded.error }) : loaded.error);
|
|
542
|
+
return 1;
|
|
543
|
+
}
|
|
544
|
+
if (action === undefined) {
|
|
545
|
+
if (json) {
|
|
546
|
+
write(envelopeJson({ ok: true, command: "repos", count: loaded.repos.length, repos: loaded.repos, file }));
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
return listRepos(loaded.repos, file, write);
|
|
550
|
+
}
|
|
551
|
+
if (action !== "add" && action !== "remove") {
|
|
552
|
+
const message = `unknown command \`repos ${action}\` — try \`repos\`, \`repos add\`, \`repos remove\`, or \`repos add-from-github <owner/name>\``;
|
|
553
|
+
write(json ? envelopeJson({ ok: false, command: "repos", reason: "usage", message }) : message);
|
|
554
|
+
return USAGE_EXIT;
|
|
555
|
+
}
|
|
556
|
+
// `repos add` with no path means the repository you are standing in.
|
|
557
|
+
const targets = (paths.length > 0 ? paths : [process.cwd()]).map(path => resolve(path));
|
|
558
|
+
if (json) {
|
|
559
|
+
const lines = [];
|
|
560
|
+
const said = {};
|
|
561
|
+
const code = action === "add"
|
|
562
|
+
? await addToRepos(loaded.repos, targets, file, line => lines.push(line), said)
|
|
563
|
+
: await removeFromRepos(loaded.repos, targets, file, line => lines.push(line), said);
|
|
564
|
+
write(envelopeJson({
|
|
565
|
+
ok: code === 0,
|
|
566
|
+
command: `repos ${action}`,
|
|
567
|
+
...(code === 0 ? {} : { reason: said.reason ?? "usage" }),
|
|
568
|
+
message: lines.join(" "),
|
|
569
|
+
targets,
|
|
570
|
+
}));
|
|
571
|
+
return code;
|
|
572
|
+
}
|
|
573
|
+
return action === "add"
|
|
574
|
+
? addToRepos(loaded.repos, targets, file, write)
|
|
575
|
+
: removeFromRepos(loaded.repos, targets, file, write);
|
|
576
|
+
}
|
|
577
|
+
function listRepos(repos, file, write) {
|
|
578
|
+
if (repos.length === 0) {
|
|
579
|
+
write("No repositories connected. Toolroll reports on everything it can find.");
|
|
580
|
+
write("");
|
|
581
|
+
write("Connect the ones you actually work in:");
|
|
582
|
+
write(" toolroll repos add ~/code/thing");
|
|
583
|
+
write(" toolroll repos add # the repo you are standing in");
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
write(`${repos.length === 1 ? "1 repository" : `${repos.length} repositories`} connected:`);
|
|
587
|
+
for (const repo of repos)
|
|
588
|
+
write(` ${repo}${existsSync(repo) ? "" : " (missing)"}`);
|
|
589
|
+
write("");
|
|
590
|
+
write(" toolroll repos add <path> connect another");
|
|
591
|
+
write(" toolroll repos remove <path> disconnect one");
|
|
592
|
+
write(" toolroll repos add-from-github <owner/name> --root <dir> clone from GitHub and connect");
|
|
593
|
+
write(" toolroll --all report everything, ignoring this list");
|
|
594
|
+
write(` ${file}`);
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
async function addToRepos(existing, targets, file, write, said) {
|
|
598
|
+
// Enrolling something that is not a repository would fail later and further
|
|
599
|
+
// away, so it fails here instead.
|
|
600
|
+
const rejected = targets.filter(path => !existsSync(join(path, ".git")));
|
|
601
|
+
if (rejected.length > 0) {
|
|
602
|
+
for (const path of rejected)
|
|
603
|
+
write(`${path} is not a git repository.`);
|
|
604
|
+
if (said !== undefined)
|
|
605
|
+
said.reason = "usage";
|
|
606
|
+
return USAGE_EXIT;
|
|
607
|
+
}
|
|
608
|
+
const added = targets.filter(path => !existing.includes(path));
|
|
609
|
+
const wrote = await updateRepos(file, repos => addRepos(repos, targets));
|
|
610
|
+
if (!wrote.ok) {
|
|
611
|
+
// The updater's own taxonomy survives (verification finding 4): a held
|
|
612
|
+
// lock or a malformed registry is an operational fact, never "usage".
|
|
613
|
+
if (said !== undefined)
|
|
614
|
+
said.reason = wrote.reason;
|
|
615
|
+
write(`could not update the connected list — ${wrote.message}`);
|
|
616
|
+
return 1;
|
|
617
|
+
}
|
|
618
|
+
if (added.length === 0) {
|
|
619
|
+
write(`Already connected: ${targets.join(", ")}`);
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
for (const path of added)
|
|
623
|
+
write(`Connected ${path}`);
|
|
624
|
+
write("");
|
|
625
|
+
write("`toolroll` now reports these. `toolroll --all` still shows everything.");
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
async function removeFromRepos(existing, targets, file, write, said) {
|
|
629
|
+
const removed = targets.filter(path => existing.includes(path));
|
|
630
|
+
if (removed.length === 0) {
|
|
631
|
+
write(`Not connected: ${targets.join(", ")}`);
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
const wrote = await updateRepos(file, repos => removeRepos(repos, targets));
|
|
635
|
+
if (!wrote.ok) {
|
|
636
|
+
if (said !== undefined)
|
|
637
|
+
said.reason = wrote.reason;
|
|
638
|
+
write(`could not update the connected list — ${wrote.message}`);
|
|
639
|
+
return 1;
|
|
640
|
+
}
|
|
641
|
+
for (const path of removed)
|
|
642
|
+
write(`Disconnected ${path}`);
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* `repos add-from-github <owner/name>` — the console onboarding ceremony's
|
|
647
|
+
* CLI twin (onboarding rounds 1-4; named in the ledger as the companion):
|
|
648
|
+
* strict parse, gh preview BEFORE any write, the same large-repo gate, the
|
|
649
|
+
* claim-first clone, and enrollment through the ONE locked registry
|
|
650
|
+
* primitive. Preview by default; --yes clones. Windows refuses like the
|
|
651
|
+
* console does — tree death cannot be proven there.
|
|
652
|
+
*/
|
|
653
|
+
async function addFromGithubCommand(argv, json, file, write, onboard) {
|
|
654
|
+
const command = "repos add-from-github";
|
|
655
|
+
const answer = (reason, message, code, extra = {}) => {
|
|
656
|
+
write(json ? envelopeJson({ ok: false, command, reason, message, ...extra }) : message);
|
|
657
|
+
return code;
|
|
658
|
+
};
|
|
659
|
+
const usage = "`toolroll repos add-from-github <owner/name | github.com link> --root <dir> [--large-ok] [--yes] [--json]`";
|
|
660
|
+
let spec;
|
|
661
|
+
let rootGiven;
|
|
662
|
+
let yes = false;
|
|
663
|
+
let largeOk = false;
|
|
664
|
+
for (let index = 0; index < argv.length; index++) {
|
|
665
|
+
const argument = argv[index];
|
|
666
|
+
if (!argument.startsWith("-")) {
|
|
667
|
+
if (spec !== undefined)
|
|
668
|
+
return answer("usage", "one repository at a time", USAGE_EXIT);
|
|
669
|
+
spec = argument;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (argument === "--yes") {
|
|
673
|
+
yes = true;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (argument === "--large-ok") {
|
|
677
|
+
largeOk = true;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (argument === "--root") {
|
|
681
|
+
const value = argv[index + 1];
|
|
682
|
+
if (value === undefined || value.startsWith("-"))
|
|
683
|
+
return answer("usage", "--root needs a directory", USAGE_EXIT);
|
|
684
|
+
rootGiven = value;
|
|
685
|
+
index += 1;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (argument === "-h" || argument === "--help")
|
|
689
|
+
return answer("usage", usage, USAGE_EXIT);
|
|
690
|
+
return answer("usage", `unknown option ${argument} for \`repos add-from-github\``, USAGE_EXIT);
|
|
691
|
+
}
|
|
692
|
+
if (spec === undefined)
|
|
693
|
+
return answer("usage", usage, USAGE_EXIT);
|
|
694
|
+
const shape = parseGithubRepo(spec);
|
|
695
|
+
if (!shape.ok)
|
|
696
|
+
return answer("usage", shape.problem, USAGE_EXIT);
|
|
697
|
+
if (rootGiven === undefined)
|
|
698
|
+
return answer("usage", "--root <dir> names where the clone lands (an existing directory)", USAGE_EXIT);
|
|
699
|
+
let root;
|
|
700
|
+
try {
|
|
701
|
+
root = realpathSync(resolve(rootGiven));
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return answer("usage", `--root ${rootGiven} does not exist`, USAGE_EXIT);
|
|
705
|
+
}
|
|
706
|
+
if (!lstatSync(root).isDirectory())
|
|
707
|
+
return answer("usage", `--root ${rootGiven} is not a directory`, USAGE_EXIT);
|
|
708
|
+
// The platform refusal comes AFTER the invocation is proved well-formed
|
|
709
|
+
// (verification finding 3): a Windows user still learns their typo first.
|
|
710
|
+
if (process.platform === "win32") {
|
|
711
|
+
return answer("platform", "adding from GitHub is not supported on Windows yet — the clone's process tree cannot be proven dead there", 1);
|
|
712
|
+
}
|
|
713
|
+
const previewed = await (onboard?.preview ?? previewGithubRepo)(shape.owner, shape.name);
|
|
714
|
+
if (!previewed.ok)
|
|
715
|
+
return answer(previewed.reason, previewed.message, 1);
|
|
716
|
+
const reparsed = parseGithubRepo(previewed.preview.nameWithOwner);
|
|
717
|
+
if (!reparsed.ok)
|
|
718
|
+
return answer("malformed", "GitHub named a repository shape this command refuses — nothing was written", 1);
|
|
719
|
+
const target = join(root, reparsed.name);
|
|
720
|
+
if (dirname(target) !== root)
|
|
721
|
+
return answer("usage", "the target escaped its root — refused; nothing was written", USAGE_EXIT);
|
|
722
|
+
const diskUsageKib = typeof previewed.preview.diskUsageKib === "number" && Number.isFinite(previewed.preview.diskUsageKib) && previewed.preview.diskUsageKib >= 0
|
|
723
|
+
? previewed.preview.diskUsageKib
|
|
724
|
+
: null;
|
|
725
|
+
const large = isLargeRepo({ ...previewed.preview, diskUsageKib });
|
|
726
|
+
const size = diskUsageKib === null ? "size unknown" : `${Math.max(1, Math.round(diskUsageKib / 1024))} MiB`;
|
|
727
|
+
if (!yes) {
|
|
728
|
+
if (json) {
|
|
729
|
+
write(envelopeJson({
|
|
730
|
+
ok: false, command, reason: "unconfirmed",
|
|
731
|
+
preview: { nameWithOwner: previewed.preview.nameWithOwner, visibility: previewed.preview.visibility, diskUsageKib, large, target },
|
|
732
|
+
}));
|
|
733
|
+
return 3;
|
|
734
|
+
}
|
|
735
|
+
write("Would clone, exactly:");
|
|
736
|
+
write(` ${previewed.preview.nameWithOwner} (${previewed.preview.visibility} · ${size}${large ? " — LARGE or unknown; --large-ok will be required" : ""})`);
|
|
737
|
+
write(` into ${target}, then connect it`);
|
|
738
|
+
write("");
|
|
739
|
+
write("Nothing was written. Re-run with --yes to apply.");
|
|
740
|
+
return 3;
|
|
741
|
+
}
|
|
742
|
+
if (large && !largeOk) {
|
|
743
|
+
return answer("large", `${previewed.preview.nameWithOwner} is large or of unknown size (${size}) — add --large-ok to clone it anyway; nothing was written`, 3);
|
|
744
|
+
}
|
|
745
|
+
const cloned = await (onboard?.clone ?? cloneGithubRepo)(previewed.preview.nameWithOwner, root, async (cwd) => {
|
|
746
|
+
const proof = await execRun("git", ["rev-parse", "--show-toplevel"], { cwd, timeoutMs: 15_000 });
|
|
747
|
+
return { code: proof.code, stdout: proof.stdout };
|
|
748
|
+
});
|
|
749
|
+
if (!cloned.ok)
|
|
750
|
+
return answer(cloned.reason, cloned.message, 1);
|
|
751
|
+
if (cloned.target !== target) {
|
|
752
|
+
return answer("malformed", "the clone answered with a different path than the preview promised — refused; nothing was connected", 1);
|
|
753
|
+
}
|
|
754
|
+
const enrolled = await updateRepos(file, repos => addRepos(repos, [cloned.target]));
|
|
755
|
+
if (!enrolled.ok) {
|
|
756
|
+
return answer(enrolled.reason, `${cloned.target} is cloned but not connected — ${enrolled.message}; connect it with \`repos add ${cloned.target}\``, 1);
|
|
757
|
+
}
|
|
758
|
+
if (json) {
|
|
759
|
+
write(envelopeJson({ ok: true, command, target: cloned.target, nameWithOwner: previewed.preview.nameWithOwner }));
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
write(`Cloned ${previewed.preview.nameWithOwner} into ${cloned.target} and connected it.`);
|
|
763
|
+
write("Large-file objects were not downloaded — run `git lfs pull` in the repository when you need them.");
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* `pulls` reads the same repositories the default report does, because the
|
|
768
|
+
* question "what is waiting on me" is asked about the same set of work. It is
|
|
769
|
+
* a separate command rather than part of the report because it costs a network
|
|
770
|
+
* round trip per repository, and the default report is meant to stay local.
|
|
771
|
+
*/
|
|
772
|
+
async function runPullsCommand(argv, write) {
|
|
773
|
+
const json = argv.includes("--json");
|
|
774
|
+
const unknown = argv.find(argument => argument.startsWith("-") && argument !== "--json");
|
|
775
|
+
if (unknown !== undefined) {
|
|
776
|
+
write(`unknown option ${unknown} — \`pulls\` takes paths and --json`);
|
|
777
|
+
return USAGE_EXIT;
|
|
778
|
+
}
|
|
779
|
+
const named = argv.filter(argument => !argument.startsWith("-")).map(path => resolve(path));
|
|
780
|
+
const targets = named.length > 0 ? named : await defaultPullTargets(write);
|
|
781
|
+
if (targets === null)
|
|
782
|
+
return 1;
|
|
783
|
+
const { present, missing } = partitionRoots(targets, existsSync);
|
|
784
|
+
if (present.length === 0) {
|
|
785
|
+
write(json ? envelopeJson({ ok: false, command: "pulls", reason: "no-repositories", message: describeMissing(missing), groups: [] }) : describeMissing(missing));
|
|
786
|
+
return USAGE_EXIT;
|
|
787
|
+
}
|
|
788
|
+
const groups = await Promise.all(present.map(async (path) => {
|
|
789
|
+
const read = await readPulls(path);
|
|
790
|
+
return { repo: basename(path), pulls: read.pulls, problems: read.problems };
|
|
791
|
+
}));
|
|
792
|
+
write(json
|
|
793
|
+
? envelopeJson({ ok: true, command: "pulls", count: groups.length, groups })
|
|
794
|
+
: renderPulls(groups, { now: new Date() }));
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* `graph` answers "what work graph is already here", which is the question
|
|
799
|
+
* standing between discovery and anything autonomous. It reads the same
|
|
800
|
+
* repositories the report does, and like everything else in M0 it writes
|
|
801
|
+
* nothing: detection is not authorization.
|
|
802
|
+
*/
|
|
803
|
+
async function runGraphCommand(argv, write) {
|
|
804
|
+
const json = argv.includes("--json");
|
|
805
|
+
const unknown = argv.find(argument => argument.startsWith("-") && argument !== "--json");
|
|
806
|
+
if (unknown !== undefined) {
|
|
807
|
+
write(`unknown option ${unknown} — \`graph\` takes paths and --json`);
|
|
808
|
+
return USAGE_EXIT;
|
|
809
|
+
}
|
|
810
|
+
const named = argv.filter(argument => !argument.startsWith("-")).map(path => resolve(path));
|
|
811
|
+
const targets = named.length > 0 ? named : await defaultPullTargets(write);
|
|
812
|
+
if (targets === null)
|
|
813
|
+
return 1;
|
|
814
|
+
const { present, missing } = partitionRoots(targets, existsSync);
|
|
815
|
+
if (present.length === 0) {
|
|
816
|
+
write(json ? envelopeJson({ ok: false, command: "graph", reason: "no-repositories", message: describeMissing(missing), detections: [] }) : describeMissing(missing));
|
|
817
|
+
return USAGE_EXIT;
|
|
818
|
+
}
|
|
819
|
+
const detections = await detectGraphs(present);
|
|
820
|
+
// An enrolled backend outranks anything detection would have picked — the
|
|
821
|
+
// ladder's first rung, and now reachable because grants are persisted. An
|
|
822
|
+
// operator who already said which one must not be re-asked because a second
|
|
823
|
+
// tracker turned up somewhere else on the machine.
|
|
824
|
+
const enrolled = enrolledBackend(present);
|
|
825
|
+
// The envelope carries the choice and the setup commands, not just the raw
|
|
826
|
+
// detections: an agent reading this has the same question a person does —
|
|
827
|
+
// what is here, what would you use, and what would I have to run.
|
|
828
|
+
const envelope = {
|
|
829
|
+
detections,
|
|
830
|
+
choice: chooseBackend(detections, enrolled === undefined ? {} : { enrolled }),
|
|
831
|
+
setup: setupOptions(),
|
|
832
|
+
};
|
|
833
|
+
write(json
|
|
834
|
+
? envelopeJson({ ok: true, command: "graph", ...envelope })
|
|
835
|
+
: renderGraph(detections, enrolled === undefined ? {} : { enrolled }));
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* The backend enrolled for any of these repositories, if one is.
|
|
840
|
+
*
|
|
841
|
+
* Read-only and failure-tolerant on purpose: `graph` is a reporting command,
|
|
842
|
+
* and a queue that cannot be opened must degrade to plain detection rather
|
|
843
|
+
* than take the report down with it.
|
|
844
|
+
*/
|
|
845
|
+
function enrolledBackend(repos) {
|
|
846
|
+
const store = openStoreReadOnly(databasePath(process.env, homedir()));
|
|
847
|
+
if (store === null)
|
|
848
|
+
return undefined;
|
|
849
|
+
try {
|
|
850
|
+
const wanted = new Set(repos);
|
|
851
|
+
const grant = store.listGrants().find((one) => wanted.has(one.repo));
|
|
852
|
+
// A grant may name the built-in store, which detection never reports, so
|
|
853
|
+
// only a backend the graph report knows about is passed through.
|
|
854
|
+
return grant !== undefined && isBackendKind(grant.backend) ? grant.backend : undefined;
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
// An older or incomplete layout degrades to detection, never a migration.
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
store.close();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function isBackendKind(name) {
|
|
865
|
+
return Object.prototype.hasOwnProperty.call(GRAPH_LABELS, name);
|
|
866
|
+
}
|
|
867
|
+
/** null means the configuration could not be read, and was already reported. */
|
|
868
|
+
async function defaultPullTargets(write) {
|
|
869
|
+
const enrolled = await loadRepos(configFile());
|
|
870
|
+
if ("error" in enrolled) {
|
|
871
|
+
write(enrolled.error);
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return enrolled.repos.length > 0 ? enrolled.repos : [process.cwd()];
|
|
875
|
+
}
|
|
876
|
+
export function parseLinkArgs(argv) {
|
|
877
|
+
const args = { yes: false };
|
|
878
|
+
for (let index = 0; index < argv.length; index++) {
|
|
879
|
+
const argument = argv[index];
|
|
880
|
+
if (argument === "--yes" || argument === "-y") {
|
|
881
|
+
args.yes = true;
|
|
882
|
+
}
|
|
883
|
+
else if (argument === "--to") {
|
|
884
|
+
const dir = argv[++index];
|
|
885
|
+
if (dir === undefined)
|
|
886
|
+
return { error: "--to needs a directory" };
|
|
887
|
+
args.to = dir;
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
return { error: `unknown option ${argument} — try \`toolroll --help\`` };
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return { args };
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Both halves show their work before doing it. Nothing changes without --yes,
|
|
897
|
+
* which is the same courtesy this tool extends to `bd init`.
|
|
898
|
+
*/
|
|
899
|
+
async function runLinkCommand(command, argv, write, binSource) {
|
|
900
|
+
const parsed = parseLinkArgs(argv);
|
|
901
|
+
if ("error" in parsed) {
|
|
902
|
+
write(parsed.error);
|
|
903
|
+
return USAGE_EXIT;
|
|
904
|
+
}
|
|
905
|
+
const { to, yes } = parsed.args;
|
|
906
|
+
const choice = chooseTarget({
|
|
907
|
+
pathEntries: (process.env["PATH"] ?? "").split(delimiter).filter(Boolean),
|
|
908
|
+
home: homedir(),
|
|
909
|
+
...(to === undefined ? {} : { override: to }),
|
|
910
|
+
});
|
|
911
|
+
// Only `link` needs something to point at. Demanding a build before you are
|
|
912
|
+
// allowed to uninstall would strand anyone whose dist/ has been cleaned.
|
|
913
|
+
if (command === "unlink")
|
|
914
|
+
return runUnlink(choice.dir, yes, write);
|
|
915
|
+
const resolved = binSource === undefined ? resolveBinSource() : { source: binSource };
|
|
916
|
+
if ("error" in resolved) {
|
|
917
|
+
write(resolved.error);
|
|
918
|
+
return USAGE_EXIT;
|
|
919
|
+
}
|
|
920
|
+
return runLink(resolved.source, choice, yes, write);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* A link must point at the built command. Running through tsx puts us in
|
|
924
|
+
* src/, so resolve across to dist/ rather than linking a file node cannot run.
|
|
925
|
+
*/
|
|
926
|
+
function resolveBinSource() {
|
|
927
|
+
const here = fileURLToPath(import.meta.url);
|
|
928
|
+
const source = here.endsWith(".ts") ? resolve(dirname(here), "..", "dist", "cli.js") : here;
|
|
929
|
+
if (!existsSync(source)) {
|
|
930
|
+
return { error: "Run `npm run build` first — there is no built command to link to yet." };
|
|
931
|
+
}
|
|
932
|
+
return { source };
|
|
933
|
+
}
|
|
934
|
+
async function runLink(source, choice, yes, write) {
|
|
935
|
+
const plan = await planLink(source, choice.dir);
|
|
936
|
+
if (plan.action === "conflict") {
|
|
937
|
+
write(`${plan.target}: ${plan.reason}.`);
|
|
938
|
+
write("Remove it yourself if you want it gone, or use --to <dir> to link elsewhere.");
|
|
939
|
+
return 1;
|
|
940
|
+
}
|
|
941
|
+
if (plan.action === "already") {
|
|
942
|
+
write(`Already linked: ${plan.target} → ${plan.source}`);
|
|
943
|
+
return 0;
|
|
944
|
+
}
|
|
945
|
+
write(plan.action === "replace" ? `Will replace ${plan.target}` : `Will create ${plan.target}`);
|
|
946
|
+
write(` → ${plan.source}`);
|
|
947
|
+
if (plan.action === "replace")
|
|
948
|
+
write(` (it currently points at ${plan.current})`);
|
|
949
|
+
if (!yes) {
|
|
950
|
+
write("");
|
|
951
|
+
write("Nothing has been written. Re-run with --yes to do it.");
|
|
952
|
+
return 0;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
await applyLink(plan);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
write(error instanceof Error ? error.message : String(error));
|
|
959
|
+
return 1;
|
|
960
|
+
}
|
|
961
|
+
write("");
|
|
962
|
+
write(`Linked. \`${BIN_NAME}\` is now a command.`);
|
|
963
|
+
if (!choice.onPath) {
|
|
964
|
+
write("");
|
|
965
|
+
write(`${choice.dir} is not on your PATH. ${describePathFix(choice.dir)}`);
|
|
966
|
+
}
|
|
967
|
+
return 0;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* The instruction has to be one the operator can paste into the shell they are
|
|
971
|
+
* actually in. `export` is meaningless in PowerShell, and the separator between
|
|
972
|
+
* PATH entries is not `:` everywhere.
|
|
973
|
+
*/
|
|
974
|
+
function describePathFix(dir) {
|
|
975
|
+
if (process.platform === "win32") {
|
|
976
|
+
return [
|
|
977
|
+
"Add it for this session:",
|
|
978
|
+
` $env:PATH = "${dir}${delimiter}$env:PATH"`,
|
|
979
|
+
"or permanently, for your user only:",
|
|
980
|
+
` setx PATH "${dir}${delimiter}%PATH%"`,
|
|
981
|
+
].join("\n");
|
|
982
|
+
}
|
|
983
|
+
return ["Add this to your shell profile:", ` export PATH="${dir}${delimiter}$PATH"`].join("\n");
|
|
984
|
+
}
|
|
985
|
+
async function runUnlink(dir, yes, write) {
|
|
986
|
+
const plan = await planUnlink(dir);
|
|
987
|
+
if (plan.action === "absent") {
|
|
988
|
+
write(`Nothing to remove: ${plan.target} does not exist.`);
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
991
|
+
if (plan.action === "foreign") {
|
|
992
|
+
write(`${plan.target}: ${plan.reason}. Leaving it alone.`);
|
|
993
|
+
return 1;
|
|
994
|
+
}
|
|
995
|
+
write(`Will remove ${plan.target} → ${plan.current}`);
|
|
996
|
+
if (!yes) {
|
|
997
|
+
write("");
|
|
998
|
+
write("Nothing has been removed. Re-run with --yes to do it.");
|
|
999
|
+
return 0;
|
|
1000
|
+
}
|
|
1001
|
+
await applyUnlink(plan);
|
|
1002
|
+
write("");
|
|
1003
|
+
write("Removed.");
|
|
1004
|
+
return 0;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* `skills install`: with --claude-code, install this binary's lead guides
|
|
1008
|
+
* in the user's Claude Code skills directory. Without it, retain the older
|
|
1009
|
+
* repository-local Agent Skills entry and optional managed AGENTS.md block.
|
|
1010
|
+
* Both paths preview by default, write only with --yes, and refuse files
|
|
1011
|
+
* they do not own.
|
|
1012
|
+
*/
|
|
1013
|
+
/** The skills subcommands and their EXACT flag vocabularies — consulted
|
|
1014
|
+
* by the parser above and compared verbatim by the command guide's tests
|
|
1015
|
+
* (arc-5 review, finding 5). */
|
|
1016
|
+
export const SKILLS_ACTIONS = ["install", "list", "get"];
|
|
1017
|
+
export const SKILLS_FLAGS = {
|
|
1018
|
+
install: { json: "flag", yes: "flag", "claude-code": "flag", dir: "value", "write-context": "flag", repo: "value" },
|
|
1019
|
+
list: { json: "flag" },
|
|
1020
|
+
get: { json: "flag" },
|
|
1021
|
+
};
|
|
1022
|
+
function runSkillsCommand(argv, write) {
|
|
1023
|
+
const json = argv.includes("--json");
|
|
1024
|
+
const action = argv.find(argument => !argument.startsWith("-"));
|
|
1025
|
+
const usage = (message) => {
|
|
1026
|
+
write(json ? envelopeJson({ ok: false, command: action === undefined ? "skills" : `skills ${action}`, reason: "usage", message }) : message);
|
|
1027
|
+
return USAGE_EXIT;
|
|
1028
|
+
};
|
|
1029
|
+
if (action === undefined || !SKILLS_ACTIONS.includes(action)) {
|
|
1030
|
+
return usage("`toolroll skills install --claude-code [--dir <path>] [--yes]` · `skills install [--repo <path>] [--write-context] [--yes]` · `skills list` · `skills get <name>` — all take --json");
|
|
1031
|
+
}
|
|
1032
|
+
// Exact per-action vocabularies (arc-5 review, finding 5): an unknown
|
|
1033
|
+
// flag, a flagless --repo, or a stray positional is a usage refusal
|
|
1034
|
+
// here, never a silent ignore.
|
|
1035
|
+
const allowed = SKILLS_FLAGS[action];
|
|
1036
|
+
const positionals = [];
|
|
1037
|
+
for (let index = 0; index < argv.length; index++) {
|
|
1038
|
+
const argument = argv[index];
|
|
1039
|
+
if (!argument.startsWith("-")) {
|
|
1040
|
+
positionals.push(argument);
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const name = argument.replace(/^--?/, "");
|
|
1044
|
+
if (name === "h" || name === "help")
|
|
1045
|
+
return usage(`\`toolroll skills ${action}\` — flags: ${Object.keys(allowed).map(one => `--${one}`).join(" ")}`);
|
|
1046
|
+
const arity = allowed[name];
|
|
1047
|
+
if (arity === undefined)
|
|
1048
|
+
return usage(`unknown option --${name} for \`skills ${action}\``);
|
|
1049
|
+
if (arity === "value") {
|
|
1050
|
+
const value = argv[index + 1];
|
|
1051
|
+
if (value === undefined || value.startsWith("-"))
|
|
1052
|
+
return usage(`--${name} needs a value`);
|
|
1053
|
+
index += 1;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (action === "list") {
|
|
1057
|
+
if (positionals.length > 1)
|
|
1058
|
+
return usage("`skills list` takes no arguments");
|
|
1059
|
+
if (json) {
|
|
1060
|
+
write(envelopeJson({ ok: true, command: "skills list", guides: GUIDES.map(one => ({ name: one.name, title: one.title, oneLiner: one.oneLiner })) }));
|
|
1061
|
+
return 0;
|
|
1062
|
+
}
|
|
1063
|
+
write("Guides this exact binary serves — `toolroll skills get <name>`:");
|
|
1064
|
+
for (const guide of GUIDES)
|
|
1065
|
+
write(` ${guide.name.padEnd(14)} ${guide.oneLiner}`);
|
|
1066
|
+
return 0;
|
|
1067
|
+
}
|
|
1068
|
+
if (action === "get") {
|
|
1069
|
+
const name = positionals[1];
|
|
1070
|
+
if (name === undefined || positionals.length > 2)
|
|
1071
|
+
return usage("`toolroll skills get <name>` — one guide name from `skills list`");
|
|
1072
|
+
const guide = guideNamed(name);
|
|
1073
|
+
if (guide === null) {
|
|
1074
|
+
// The command was valid and the named guide is absent: exit 3, the
|
|
1075
|
+
// answer is no — same shape as an unknown task or template.
|
|
1076
|
+
write(json
|
|
1077
|
+
? envelopeJson({ ok: false, command: "skills get", reason: "unknown-skill", message: `no guide named ${name} — the guides are: ${GUIDES.map(one => one.name).join(", ")}` })
|
|
1078
|
+
: `no guide named ${name} — the guides are: ${GUIDES.map(one => one.name).join(", ")}`);
|
|
1079
|
+
return 3;
|
|
1080
|
+
}
|
|
1081
|
+
if (json) {
|
|
1082
|
+
write(envelopeJson({ ok: true, command: "skills get", name: guide.name, title: guide.title, content: guide.content }));
|
|
1083
|
+
return 0;
|
|
1084
|
+
}
|
|
1085
|
+
// Raw markdown alone — an agent pipes or reads it; no banner.
|
|
1086
|
+
write(guide.content);
|
|
1087
|
+
return 0;
|
|
1088
|
+
}
|
|
1089
|
+
// install
|
|
1090
|
+
const claudeCode = argv.includes("--claude-code");
|
|
1091
|
+
const confirmed = argv.includes("--yes");
|
|
1092
|
+
if (positionals.length > 1)
|
|
1093
|
+
return usage("`skills install` takes no arguments beyond its flags");
|
|
1094
|
+
if (claudeCode) {
|
|
1095
|
+
if (argv.includes("--repo") || argv.includes("--write-context")) {
|
|
1096
|
+
return usage("--claude-code uses --dir, not --repo or --write-context");
|
|
1097
|
+
}
|
|
1098
|
+
const dirAt = argv.indexOf("--dir");
|
|
1099
|
+
const directory = dirAt === -1
|
|
1100
|
+
? defaultClaudeCodeSkillDir()
|
|
1101
|
+
: resolve(argv[dirAt + 1]);
|
|
1102
|
+
const plan = planClaudeCodeInstall(directory);
|
|
1103
|
+
if (!confirmed) {
|
|
1104
|
+
const blocked = plan.files.some(file => file.action === "refuse-foreign");
|
|
1105
|
+
const message = blocked
|
|
1106
|
+
? "Nothing was written. Move the existing file you want to keep, or choose another --dir."
|
|
1107
|
+
: "Nothing was written. Re-run with --yes to install the Claude Code skill.";
|
|
1108
|
+
if (json) {
|
|
1109
|
+
write(envelopeJson({
|
|
1110
|
+
ok: false,
|
|
1111
|
+
command: "skills install",
|
|
1112
|
+
reason: "unconfirmed",
|
|
1113
|
+
message,
|
|
1114
|
+
plan,
|
|
1115
|
+
}));
|
|
1116
|
+
return 3;
|
|
1117
|
+
}
|
|
1118
|
+
write(`Would install the Claude Code skill in ${plan.directory}:`);
|
|
1119
|
+
for (const file of plan.files) {
|
|
1120
|
+
const action = file.action === "refuse-foreign"
|
|
1121
|
+
? "REFUSED — this file was not written by the installer"
|
|
1122
|
+
: file.action;
|
|
1123
|
+
write(` ${file.path} (${action})`);
|
|
1124
|
+
}
|
|
1125
|
+
for (const path of plan.legacyFiles)
|
|
1126
|
+
write(` ${path} (remove — replaced by the new copy)`);
|
|
1127
|
+
write("");
|
|
1128
|
+
write(message);
|
|
1129
|
+
return 3;
|
|
1130
|
+
}
|
|
1131
|
+
const result = applyClaudeCodeInstall(directory);
|
|
1132
|
+
if (!result.ok) {
|
|
1133
|
+
write(json
|
|
1134
|
+
? envelopeJson({ ok: false, command: "skills install", reason: result.reason, message: result.message, plan: result.plan })
|
|
1135
|
+
: result.message);
|
|
1136
|
+
return 1;
|
|
1137
|
+
}
|
|
1138
|
+
if (json) {
|
|
1139
|
+
write(envelopeJson({ ok: true, command: "skills install", target: "claude-code", wrote: result.wrote, plan: result.plan }));
|
|
1140
|
+
return 0;
|
|
1141
|
+
}
|
|
1142
|
+
for (const file of result.wrote)
|
|
1143
|
+
write(`wrote ${file}`);
|
|
1144
|
+
for (const path of result.plan.legacyFiles)
|
|
1145
|
+
write(`removed ${path}`);
|
|
1146
|
+
write("Claude Code will use the refreshed Toolroll guides in its next session.");
|
|
1147
|
+
return 0;
|
|
1148
|
+
}
|
|
1149
|
+
if (argv.includes("--dir"))
|
|
1150
|
+
return usage("--dir belongs with --claude-code");
|
|
1151
|
+
const repoAt = argv.indexOf("--repo");
|
|
1152
|
+
const repo = repoAt !== -1 ? resolve(argv[repoAt + 1]) : process.cwd();
|
|
1153
|
+
const writeContext = argv.includes("--write-context");
|
|
1154
|
+
if (!existsSync(join(repo, ".git"))) {
|
|
1155
|
+
write(json ? envelopeJson({ ok: false, command: "skills install", reason: "not-a-repo", message: `${repo} has no .git — name the repository with --repo` }) : `${repo} has no .git — name the repository with --repo`);
|
|
1156
|
+
return USAGE_EXIT;
|
|
1157
|
+
}
|
|
1158
|
+
const plan = planInstall(repo, writeContext);
|
|
1159
|
+
if (!confirmed) {
|
|
1160
|
+
if (json) {
|
|
1161
|
+
write(envelopeJson({ ok: false, command: "skills install", reason: "unconfirmed", plan }));
|
|
1162
|
+
return 3;
|
|
1163
|
+
}
|
|
1164
|
+
write(`Would write, exactly:`);
|
|
1165
|
+
write(` ${plan.skillPath} (${plan.skillAction === "refuse-foreign" ? "REFUSED — a skill this installer did not write is already there" : plan.skillAction})`);
|
|
1166
|
+
if (plan.legacySkillPath !== null)
|
|
1167
|
+
write(` ${plan.legacySkillPath} (remove — replaced by the new copy)`);
|
|
1168
|
+
if (plan.contextPath !== null) {
|
|
1169
|
+
write(` ${plan.contextPath} (${plan.contextAction} — only the marked block is ever touched)`);
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
write(` AGENTS.md untouched — add --write-context to install the managed block, or paste it yourself:`);
|
|
1173
|
+
write("");
|
|
1174
|
+
for (const line of contextBlock().split("\n"))
|
|
1175
|
+
write(` ${line}`);
|
|
1176
|
+
}
|
|
1177
|
+
write("");
|
|
1178
|
+
write("Nothing was written. Re-run with --yes to apply.");
|
|
1179
|
+
return 3;
|
|
1180
|
+
}
|
|
1181
|
+
const result = applyInstall(repo, writeContext);
|
|
1182
|
+
if (!result.ok) {
|
|
1183
|
+
write(json ? envelopeJson({ ok: false, command: "skills install", reason: result.reason, message: result.message }) : result.message);
|
|
1184
|
+
return 1;
|
|
1185
|
+
}
|
|
1186
|
+
if (json) {
|
|
1187
|
+
write(envelopeJson({ ok: true, command: "skills install", wrote: result.wrote, plan: result.plan }));
|
|
1188
|
+
return 0;
|
|
1189
|
+
}
|
|
1190
|
+
for (const file of result.wrote)
|
|
1191
|
+
write(`wrote ${file}`);
|
|
1192
|
+
if (result.plan.legacySkillPath !== null)
|
|
1193
|
+
write(`removed ${result.plan.legacySkillPath}`);
|
|
1194
|
+
write("Agents that read Agent Skills (claude, codex, gemini, opencode) will discover the queue on their next session here.");
|
|
1195
|
+
return 0;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* `toolroll demo` (adoption track, step 4): a seeded throwaway
|
|
1199
|
+
* sandbox served on localhost — ninety seconds from npx to seeing the
|
|
1200
|
+
* product mid-flight. Everything under one temp directory; the database
|
|
1201
|
+
* is stamped `demo` before any row exists, so every spending or
|
|
1202
|
+
* external-effect command fails closed on it forever, kept or not. The
|
|
1203
|
+
* throwaway password prints to the terminal and to a 0600 file in the
|
|
1204
|
+
* sandbox — never into the --json envelope.
|
|
1205
|
+
*/
|
|
1206
|
+
async function runDemoCommand(argv, write) {
|
|
1207
|
+
const json = argv.includes("--json");
|
|
1208
|
+
const keep = argv.includes("--keep");
|
|
1209
|
+
const portIndex = argv.indexOf("--port");
|
|
1210
|
+
const portGiven = portIndex === -1 ? "0" : (argv[portIndex + 1] ?? "");
|
|
1211
|
+
const port = Number(portGiven);
|
|
1212
|
+
const DEMO_USAGE = "`toolroll demo [--port <n>] [--host <addr>] [--allow-host name:port,…] [--keep]` — the port is a number under 65536; absent, a free one is picked";
|
|
1213
|
+
// Asking how it works never starts a sandbox.
|
|
1214
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1215
|
+
write(json ? envelopeJson({ ok: true, command: "demo", usage: DEMO_USAGE }) : `${DEMO_USAGE}\n\nA throwaway sandbox with a seeded fleet and flows mid-flight. It never spends and never reaches outside; Ctrl-C removes it unless --keep.`);
|
|
1216
|
+
return 0;
|
|
1217
|
+
}
|
|
1218
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
1219
|
+
write(json ? envelopeJson({ ok: false, command: "demo", reason: "usage", message: DEMO_USAGE }) : DEMO_USAGE);
|
|
1220
|
+
return 2;
|
|
1221
|
+
}
|
|
1222
|
+
// Reachability, same flags as `serve`: --host binds beyond localhost
|
|
1223
|
+
// (a tailnet, a LAN), and --allow-host names the Host headers the
|
|
1224
|
+
// console will answer to there — a bind on 0.0.0.0 alone still refuses
|
|
1225
|
+
// any name it was not told. Nothing here loosens the demo fence: the
|
|
1226
|
+
// database stays stamped, spend stays refused.
|
|
1227
|
+
const hostIndex = argv.indexOf("--host");
|
|
1228
|
+
const host = hostIndex === -1 ? "127.0.0.1" : (argv[hostIndex + 1] ?? "");
|
|
1229
|
+
const allowIndex = argv.indexOf("--allow-host");
|
|
1230
|
+
const allowedHosts = allowIndex === -1 ? [] : (argv[allowIndex + 1] ?? "").split(",").map(one => one.trim()).filter(one => one !== "");
|
|
1231
|
+
if (host === "" || host.startsWith("--") || (allowIndex !== -1 && allowedHosts.length === 0)) {
|
|
1232
|
+
write(json ? envelopeJson({ ok: false, command: "demo", reason: "usage", message: DEMO_USAGE }) : DEMO_USAGE);
|
|
1233
|
+
return 2;
|
|
1234
|
+
}
|
|
1235
|
+
const { createDemoSandbox } = await import("./demo.js");
|
|
1236
|
+
const { createDecisionServer } = await import("./serve.js");
|
|
1237
|
+
const now = new Date();
|
|
1238
|
+
const { sandbox, store, seed, evidenceRoot, passwordFile } = createDemoSandbox(now);
|
|
1239
|
+
const server = createDecisionServer({
|
|
1240
|
+
store, evidenceRoot, clock: () => new Date(), repos: seed.repos,
|
|
1241
|
+
...(allowedHosts.length === 0 ? {} : { allowedHosts }),
|
|
1242
|
+
});
|
|
1243
|
+
await new Promise((ready, failed) => {
|
|
1244
|
+
server.once("error", failed);
|
|
1245
|
+
server.listen(port, host, ready);
|
|
1246
|
+
});
|
|
1247
|
+
const address = server.address();
|
|
1248
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
1249
|
+
// The greeting names a road that will actually answer: the first allowed
|
|
1250
|
+
// name when one was given, else the loopback.
|
|
1251
|
+
const url = allowedHosts.length > 0 ? `http://${allowedHosts[0]}` : `http://127.0.0.1:${boundPort}`;
|
|
1252
|
+
if (json) {
|
|
1253
|
+
// The password is deliberately NOT here (adoption review, finding 9):
|
|
1254
|
+
// envelopes land in logs. It lives in the 0600 file and on the TTY.
|
|
1255
|
+
write(envelopeJson({ ok: true, command: "demo", url, sandbox, login: { name: seed.login.name, passwordFile }, keep }));
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
write("Demo sandbox is up — seeded fleet, zero spend, zero remotes.");
|
|
1259
|
+
write("");
|
|
1260
|
+
write(` open ${url}`);
|
|
1261
|
+
write(` login ${seed.login.name} / ${seed.login.password}`);
|
|
1262
|
+
write(` sandbox ${sandbox}`);
|
|
1263
|
+
write("");
|
|
1264
|
+
write("Two projects are seeded. The inbox works project-free; the board and");
|
|
1265
|
+
write("routines ask you to open one first — pick payments-api.");
|
|
1266
|
+
write("");
|
|
1267
|
+
write(keep
|
|
1268
|
+
? "Ctrl-C stops the server; the sandbox is KEPT. It stays a demo forever — every spending command refuses it."
|
|
1269
|
+
: "Ctrl-C stops the server and deletes the sandbox. --keep preserves it (still fenced from spending).");
|
|
1270
|
+
}
|
|
1271
|
+
await new Promise(done => {
|
|
1272
|
+
const stop = () => {
|
|
1273
|
+
process.off("SIGINT", stop);
|
|
1274
|
+
process.off("SIGTERM", stop);
|
|
1275
|
+
done();
|
|
1276
|
+
};
|
|
1277
|
+
process.on("SIGINT", stop);
|
|
1278
|
+
process.on("SIGTERM", stop);
|
|
1279
|
+
});
|
|
1280
|
+
await new Promise(closed => server.close(() => closed()));
|
|
1281
|
+
store.close();
|
|
1282
|
+
if (!keep) {
|
|
1283
|
+
const { rmSync } = await import("node:fs");
|
|
1284
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
1285
|
+
if (!json)
|
|
1286
|
+
write("Sandbox deleted.");
|
|
1287
|
+
}
|
|
1288
|
+
else if (!json) {
|
|
1289
|
+
write(`Sandbox kept at ${sandbox} — reopen it any time: toolroll serve --db ${join(sandbox, "orders.db")}`);
|
|
1290
|
+
write("Ready for your own repository? `toolroll up --repo <path>` starts the real thing.");
|
|
1291
|
+
}
|
|
1292
|
+
return 0;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* `contract` is the machine contract, stated by the machine: the envelope
|
|
1296
|
+
* version and the capability tokens an agent consumer can feature-detect on.
|
|
1297
|
+
* Behaviors, not versions — consumers ignore tokens they do not recognize.
|
|
1298
|
+
*/
|
|
1299
|
+
/** contract's EXACT flags — consulted below and compared by the command
|
|
1300
|
+
* guide's tests (arc-5 review, finding 5). */
|
|
1301
|
+
export const CONTRACT_FLAGS = ["json", "commands"];
|
|
1302
|
+
function runContractCommand(argv, write) {
|
|
1303
|
+
const json = argv.includes("--json");
|
|
1304
|
+
// Exact parsing (arc-5 review, finding 5): unknown flags and stray
|
|
1305
|
+
// positionals refuse instead of being silently ignored.
|
|
1306
|
+
for (const argument of argv) {
|
|
1307
|
+
const bad = (message) => {
|
|
1308
|
+
write(json ? envelopeJson({ ok: false, command: "contract", reason: "usage", message }) : message);
|
|
1309
|
+
return USAGE_EXIT;
|
|
1310
|
+
};
|
|
1311
|
+
if (!argument.startsWith("-"))
|
|
1312
|
+
return bad(`\`toolroll contract\` takes no arguments — flags: ${CONTRACT_FLAGS.map(one => `--${one}`).join(" ")}`);
|
|
1313
|
+
const name = argument.replace(/^--?/, "");
|
|
1314
|
+
if (name === "h" || name === "help")
|
|
1315
|
+
return bad(`\`toolroll contract [--commands] [--json]\``);
|
|
1316
|
+
if (!CONTRACT_FLAGS.includes(name))
|
|
1317
|
+
return bad(`unknown option ${argument} for \`contract\``);
|
|
1318
|
+
}
|
|
1319
|
+
const commands = argv.includes("--commands");
|
|
1320
|
+
if (commands) {
|
|
1321
|
+
if (json) {
|
|
1322
|
+
write(envelopeJson({
|
|
1323
|
+
ok: true,
|
|
1324
|
+
command: "contract",
|
|
1325
|
+
schemaVersion: SURFACE_SCHEMA_VERSION,
|
|
1326
|
+
notes: SURFACE_NOTES,
|
|
1327
|
+
commands: COMMAND_GUIDE,
|
|
1328
|
+
}));
|
|
1329
|
+
return 0;
|
|
1330
|
+
}
|
|
1331
|
+
write(`toolroll declared command guide — schema v${SURFACE_SCHEMA_VERSION}`);
|
|
1332
|
+
write("");
|
|
1333
|
+
write(`authority: ${SURFACE_NOTES.authority}`);
|
|
1334
|
+
write(`flags: ${SURFACE_NOTES.flags}`);
|
|
1335
|
+
write(`reasons: ${SURFACE_NOTES.reasons}`);
|
|
1336
|
+
write("");
|
|
1337
|
+
const agentRows = COMMAND_GUIDE.filter(row => row.agentMayInvoke);
|
|
1338
|
+
const operatorRows = COMMAND_GUIDE.filter(row => !row.agentMayInvoke);
|
|
1339
|
+
write("Agent surface:");
|
|
1340
|
+
for (const row of agentRows) {
|
|
1341
|
+
const name = row.invocation === "" ? "(no verb)" : row.invocation;
|
|
1342
|
+
write(` ${name.padEnd(18)} ${row.synopsis} [${row.mutation}]`);
|
|
1343
|
+
for (const flag of row.flags ?? []) {
|
|
1344
|
+
write(` --${flag.name}${flag.takesValue ? " <value>" : ""} ${flag.meaning}`);
|
|
1345
|
+
}
|
|
1346
|
+
if (row.notableReasons !== undefined && row.notableReasons.length > 0) {
|
|
1347
|
+
write(` reasons: ${row.notableReasons.join(", ")}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
write("");
|
|
1351
|
+
write("Operator ceremonies and infrastructure — an agent must not invoke");
|
|
1352
|
+
write("these, even when credentials are within reach; the credential IS the");
|
|
1353
|
+
write("person:");
|
|
1354
|
+
for (const row of operatorRows)
|
|
1355
|
+
write(` ${row.invocation.padEnd(18)} ${row.synopsis}`);
|
|
1356
|
+
return 0;
|
|
1357
|
+
}
|
|
1358
|
+
if (json) {
|
|
1359
|
+
write(envelopeJson({ ok: true, command: "contract", capabilities: [...CAPABILITIES] }));
|
|
1360
|
+
return 0;
|
|
1361
|
+
}
|
|
1362
|
+
write(`toolroll machine contract — envelope v${ENVELOPE_VERSION}`);
|
|
1363
|
+
write("");
|
|
1364
|
+
write("Every --json answer is one envelope on stdout: { envelopeVersion, ok,");
|
|
1365
|
+
write("command, ... } — failures add a stable `reason` token and a human");
|
|
1366
|
+
write("`message`. Consumers must ignore keys and capabilities they do not");
|
|
1367
|
+
write("recognize.");
|
|
1368
|
+
write("");
|
|
1369
|
+
write("Capabilities:");
|
|
1370
|
+
for (const capability of CAPABILITIES)
|
|
1371
|
+
write(` ${capability}`);
|
|
1372
|
+
write("");
|
|
1373
|
+
write("`contract --commands` dumps the declared command guide.");
|
|
1374
|
+
return 0;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* The machine-readable envelope. Remote state is folded into each repository
|
|
1378
|
+
* rather than sitting in a parallel map, so a consumer never has to join two
|
|
1379
|
+
* collections to answer "what is happening in this repo" — and `remoteRead`
|
|
1380
|
+
* says whether the absence of pull requests means none or means we did not ask.
|
|
1381
|
+
*/
|
|
1382
|
+
function renderJson(repos, roots, missingRoots, now, remote) {
|
|
1383
|
+
const withRemote = repos.map(repo => {
|
|
1384
|
+
const state = remote?.get(repo.path);
|
|
1385
|
+
return {
|
|
1386
|
+
...repo,
|
|
1387
|
+
pulls: state?.pulls ?? [],
|
|
1388
|
+
issues: state?.issues ?? [],
|
|
1389
|
+
// Per repository, not only at the top level. An empty `pulls` means
|
|
1390
|
+
// three different things — none open, not asked, or asked and failed —
|
|
1391
|
+
// and a consumer should not have to join two parts of the envelope to
|
|
1392
|
+
// find out which.
|
|
1393
|
+
pullsRead: state?.pullsRead ?? false,
|
|
1394
|
+
issuesRead: state?.issuesRead ?? false,
|
|
1395
|
+
remoteSkipped: state?.skipped ?? false,
|
|
1396
|
+
remoteProblems: state?.problems ?? [],
|
|
1397
|
+
};
|
|
1398
|
+
});
|
|
1399
|
+
return envelopeJson({
|
|
1400
|
+
ok: true,
|
|
1401
|
+
command: "scan",
|
|
1402
|
+
scannedAt: now.toISOString(),
|
|
1403
|
+
roots,
|
|
1404
|
+
missingRoots,
|
|
1405
|
+
remoteRead: remote !== undefined,
|
|
1406
|
+
repos: withRemote,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Whether this module is the program being run, rather than an import.
|
|
1411
|
+
*
|
|
1412
|
+
* Node resolves a module to its real path while argv[1] keeps whatever name it
|
|
1413
|
+
* was invoked by, so a symlinked `toolroll` on PATH compares unequal to
|
|
1414
|
+
* itself unless both sides are resolved. Getting this wrong is silent: the
|
|
1415
|
+
* command runs, prints nothing, and exits 0.
|
|
1416
|
+
*/
|
|
1417
|
+
export function isDirectInvocation(moduleUrl, entry, toRealPath = realpathSync) {
|
|
1418
|
+
if (entry === undefined)
|
|
1419
|
+
return false;
|
|
1420
|
+
try {
|
|
1421
|
+
return moduleUrl === pathToFileURL(toRealPath(entry)).href;
|
|
1422
|
+
}
|
|
1423
|
+
catch {
|
|
1424
|
+
return false;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Node prints `ExperimentalWarning: SQLite is an experimental feature` to
|
|
1429
|
+
* stderr the first time the store is opened. One line is fair warning; one
|
|
1430
|
+
* line per invocation, in a dispatch loop that runs the command thousands of
|
|
1431
|
+
* times overnight, is a log nobody can read.
|
|
1432
|
+
*
|
|
1433
|
+
* It is filtered here and nowhere else. This runs only when the CLI is the
|
|
1434
|
+
* program — a process it legitimately owns — so importing any of these modules
|
|
1435
|
+
* as a library still leaves the host's warnings exactly as they were. The
|
|
1436
|
+
* mechanism matters too: wrapping `process.emitWarning`, which is what Node's
|
|
1437
|
+
* own experimental notice goes through, leaves every `warning` listener anyone
|
|
1438
|
+
* else registered working normally. An earlier attempt swapped the listeners
|
|
1439
|
+
* themselves and quietly made everybody's `once` permanent.
|
|
1440
|
+
*/
|
|
1441
|
+
function hushExperimentalSqlite() {
|
|
1442
|
+
const original = process.emitWarning.bind(process);
|
|
1443
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
1444
|
+
const type = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
|
|
1445
|
+
const text = warning instanceof Error ? warning.message : String(warning);
|
|
1446
|
+
if (type === "ExperimentalWarning" && text.includes("SQLite"))
|
|
1447
|
+
return;
|
|
1448
|
+
original(warning, ...rest);
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
/** The process entry the bin calls: the SQLite notice hushed, the exit code set. */
|
|
1452
|
+
export async function runCliProcess(argv) {
|
|
1453
|
+
hushExperimentalSqlite();
|
|
1454
|
+
process.exitCode = await main(argv);
|
|
1455
|
+
}
|
|
1456
|
+
if (isDirectInvocation(import.meta.url, process.argv[1])) {
|
|
1457
|
+
hushExperimentalSqlite();
|
|
1458
|
+
main(process.argv.slice(2)).then(code => {
|
|
1459
|
+
process.exitCode = code;
|
|
1460
|
+
});
|
|
1461
|
+
}
|