@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4
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 +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1061 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +48 -4
- package/src/adapters/antigravity/index.js +631 -0
- package/src/adapters/claude/index.js +548 -0
- package/src/adapters/claude/mcp.js +94 -0
- package/src/adapters/codex/agents.js +208 -0
- package/src/adapters/codex/config.js +272 -0
- package/src/adapters/codex/hooks.js +203 -0
- package/src/adapters/codex/index.js +707 -0
- package/src/adapters/codex/mcp.js +271 -0
- package/src/adapters/codex/native-projection.js +34 -0
- package/src/adapters/copilot/index.js +413 -0
- package/src/adapters/copy-tree.js +620 -0
- package/src/adapters/gemini/hooks.js +210 -0
- package/src/adapters/gemini/index.js +390 -0
- package/src/adapters/hook-commands.js +60 -0
- package/src/adapters/index.js +106 -0
- package/src/adapters/kiro/index.js +310 -0
- package/src/adapters/mcp-entries.js +331 -0
- package/src/adapters/opencode/index.js +466 -0
- package/src/adapters/pi/index.js +279 -0
- package/src/adapters/pi/json-members.js +185 -0
- package/src/adapters/pi/mcp.js +373 -0
- package/src/adapters/resolve-source.js +19 -0
- package/src/cli/commands/install.js +218 -0
- package/src/cli/commands/list-backups.js +21 -0
- package/src/cli/commands/reconcile.js +120 -0
- package/src/cli/commands/remove.js +82 -0
- package/src/cli/commands/rollback.js +172 -0
- package/src/cli/commands/self-update.js +33 -0
- package/src/cli/commands/status.js +138 -0
- package/src/cli/commands/tools.js +92 -0
- package/src/cli/commands/update.js +162 -0
- package/src/cli/index.js +568 -0
- package/src/cli/runtime-commands.js +281 -0
- package/src/cli/shared.js +218 -0
- package/src/helper/bash-helper.js +34 -0
- package/src/helper/git.js +24 -0
- package/src/helper/guardrails.js +49 -0
- package/src/helper/harness-paths.js +140 -0
- package/src/helper/marker-merge.js +116 -0
- package/src/helper/prompt.js +61 -0
- package/src/helper/repo-root.js +29 -0
- package/src/helper/settings-merge.js +151 -0
- package/src/helper/settings-scope.js +33 -0
- package/src/helper/toml.js +244 -0
- package/src/install/backup-set.js +149 -0
- package/src/install/backup.js +724 -0
- package/src/install/context.js +58 -0
- package/src/install/manifest.js +145 -0
- package/src/install/mcp.js +254 -0
- package/src/install/paths.js +84 -0
- package/src/install/targets.js +90 -0
- package/src/install/tool-lifecycle.js +151 -0
- package/src/lifecycle/bash-availability.js +25 -0
- package/src/lifecycle/index.js +812 -0
- package/src/lifecycle/mcp-index.js +28 -0
- package/src/lifecycle/policies.js +73 -0
- package/src/lifecycle/view.js +175 -0
- package/src/registry/index.js +611 -0
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/sync-plugin-versions.js +66 -0
- package/src/runtime/advice/cli.js +283 -0
- package/src/runtime/advice/client.js +324 -0
- package/src/runtime/advice/config.js +189 -0
- package/src/runtime/advice/doctor.js +169 -0
- package/src/runtime/advice/endpoint-guard.js +134 -0
- package/src/runtime/advice/kinds.js +154 -0
- package/src/runtime/advice/providers/ollama-systemone.js +209 -0
- package/src/runtime/advice/record.js +166 -0
- package/src/runtime/advice/registry.js +170 -0
- package/src/runtime/advice/strength.js +66 -0
- package/src/runtime/brief-tools.js +342 -0
- package/src/runtime/c4-project.js +371 -0
- package/src/runtime/capability-router.js +484 -0
- package/src/runtime/checkouts.js +170 -0
- package/src/runtime/claims.js +468 -0
- package/src/runtime/cli-result.js +55 -0
- package/src/runtime/cli.js +678 -0
- package/src/runtime/command-detect.js +516 -0
- package/src/runtime/context-pack.js +492 -0
- package/src/runtime/decision-register.js +493 -0
- package/src/runtime/evidence-ledger.js +338 -0
- package/src/runtime/failure/capture.js +272 -0
- package/src/runtime/failure/classifier.js +68 -0
- package/src/runtime/failure/cli.js +178 -0
- package/src/runtime/failure/home.js +64 -0
- package/src/runtime/failure/store.js +226 -0
- package/src/runtime/feature-resolve.js +79 -0
- package/src/runtime/freshness.js +177 -0
- package/src/runtime/health.js +863 -0
- package/src/runtime/history-compactor.js +237 -0
- package/src/runtime/implementation-gate.js +321 -0
- package/src/runtime/inventory/coverage.js +218 -0
- package/src/runtime/inventory/identity.js +65 -0
- package/src/runtime/inventory/index.js +705 -0
- package/src/runtime/inventory/precedence.js +134 -0
- package/src/runtime/inventory/read-scopes.js +131 -0
- package/src/runtime/inventory/siblings.js +192 -0
- package/src/runtime/knowledge/chunker.js +124 -0
- package/src/runtime/knowledge/hybrid.js +149 -0
- package/src/runtime/knowledge/index-store.js +145 -0
- package/src/runtime/knowledge/retrieval.js +194 -0
- package/src/runtime/leak-scan.js +185 -0
- package/src/runtime/lifecycle/cli.js +276 -0
- package/src/runtime/lifecycle/event-store.js +340 -0
- package/src/runtime/lifecycle/fold.js +353 -0
- package/src/runtime/lifecycle/followup.js +432 -0
- package/src/runtime/lifecycle/goal.js +249 -0
- package/src/runtime/lifecycle/intent-writer.js +86 -0
- package/src/runtime/lifecycle/overview.js +292 -0
- package/src/runtime/lifecycle/release.js +241 -0
- package/src/runtime/lifecycle/report-store.js +221 -0
- package/src/runtime/lifecycle/retention.js +212 -0
- package/src/runtime/lifecycle/root.js +71 -0
- package/src/runtime/lifecycle/status.js +353 -0
- package/src/runtime/lifecycle/store-upkeep.js +393 -0
- package/src/runtime/locator-resolve.js +158 -0
- package/src/runtime/mask.js +210 -0
- package/src/runtime/mcp-consultations.js +412 -0
- package/src/runtime/mcp-reach.js +157 -0
- package/src/runtime/model-router.js +153 -0
- package/src/runtime/outcome.js +467 -0
- package/src/runtime/reach.js +148 -0
- package/src/runtime/readiness-record.js +150 -0
- package/src/runtime/readiness.js +435 -0
- package/src/runtime/recovery.js +282 -0
- package/src/runtime/research-request.js +231 -0
- package/src/runtime/retrieval-plan.js +578 -0
- package/src/runtime/scaffold/artifacts.js +217 -0
- package/src/runtime/scaffold/fingerprint.js +56 -0
- package/src/runtime/scaffold/generate.js +911 -0
- package/src/runtime/scaffold/languages.js +245 -0
- package/src/runtime/storage/cli.js +64 -0
- package/src/runtime/storage/engine.js +249 -0
- package/src/runtime/storage/registry.js +130 -0
- package/src/runtime/storage/roots.js +63 -0
- package/src/runtime/storage/rules.js +282 -0
- package/src/runtime/storage/upkeep.js +243 -0
- package/src/runtime/storage/work-status.js +326 -0
- package/src/runtime/task-attempts.js +415 -0
- package/src/runtime/task-classifier.js +550 -0
- package/src/runtime/task-scope.js +130 -0
- package/src/runtime/task-state.js +176 -0
- package/src/runtime/trace/indicators.js +235 -0
- package/src/runtime/trace/ledger.js +626 -0
- package/src/runtime/trace/render.js +76 -0
- package/src/runtime/trace/views.js +459 -0
- package/src/runtime/verification/contract-runner.js +341 -0
- package/src/runtime/verification/engine.js +1461 -0
- package/src/runtime/verification/registry.js +130 -0
- package/src/runtime/verification/scope-bound.js +418 -0
- package/src/runtime/verification/test-selection.js +172 -0
- package/src/runtime/workflow-engine.js +588 -0
- package/src/runtime/workflow-orchestrator.js +953 -0
- package/src/runtime/worktree.js +340 -0
- package/src/state/index.js +193 -0
- package/src/state/lockfile.js +136 -0
- package/src/state/run-lock.js +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Pi MCP projection: DoFlow's stdio servers merged per server into the mcpServers map of Pi's
|
|
6
|
+
// mcp.json. Ownership is one ledger row per server and file, whose fingerprint is the entry value
|
|
7
|
+
// DoFlow last verified there. A same-named entry DoFlow holds no row for is the user's and is never
|
|
8
|
+
// touched; an owned entry whose value no longer matches its row was edited by the user and is
|
|
9
|
+
// released, not overwritten or removed. The file itself is edited by splicing members into its
|
|
10
|
+
// original text (./json-members.ts), so every byte DoFlow does not own survives.
|
|
11
|
+
//
|
|
12
|
+
// Plan changes carry DoFlow's own entry and a hash of the text plan read, never the file's text:
|
|
13
|
+
// the lifecycle copies every change into the recovery record, and a user's mcp.json may hold secrets.
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const crypto = require('node:crypto');
|
|
17
|
+
|
|
18
|
+
const { fingerprint } = require('../copy-tree.js');
|
|
19
|
+
const { SKELETON, readDocument, insertMember, replaceValue, removeMember, replaceInner, renderNewDocument } = require('./json-members.js');
|
|
20
|
+
|
|
21
|
+
const HARNESS = 'pi';
|
|
22
|
+
const ASSET_ID = 'guidance.codex-pointer';
|
|
23
|
+
const RENDERER = 'pi-mcp';
|
|
24
|
+
const SERVER_NAME = /^[A-Za-z0-9_-]+$/;
|
|
25
|
+
const ABSENT_DOCUMENT = Object.freeze({ ok: true, root: null, servers: null });
|
|
26
|
+
|
|
27
|
+
const NOTICES = Object.freeze({
|
|
28
|
+
surface: 'MCP: stdio servers are registered in the mcpServers map of Pi\'s mcp.json for its built-in MCP (Pi 0.99.0 or later; a project entry replaces a user entry of the same name).',
|
|
29
|
+
extension: 'MCP: an installed extension that registers /mcp, such as pi-mcp-adapter, replaces Pi\'s built-in MCP, and Pi then does not read mcp.json.',
|
|
30
|
+
trust: 'MCP: Pi reads .pi/mcp.json only after this project is trusted (/trust or --approve); DoFlow does not grant trust.',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function sha256(text ) { return crypto.createHash('sha256').update(text).digest('hex'); }
|
|
34
|
+
|
|
35
|
+
function shorten(name ) { return name.length > 40 ? `${name.slice(0, 37)}...` : name; }
|
|
36
|
+
|
|
37
|
+
function collisionNotice(key , id ) {
|
|
38
|
+
return `MCP: kept your own entry '${shorten(key)}' and did not register DoFlow's '${shorten(id)}'; rename or remove yours to let DoFlow manage it.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function releasedNotice(id ) {
|
|
42
|
+
return `MCP: entry '${shorten(id)}' was changed outside DoFlow; it is yours now and DoFlow no longer updates or removes it.`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pi treats two server names that differ only in `-` versus `_` as one server. */
|
|
46
|
+
function piServerKey(name ) { return name.replace(/_/g, '-'); }
|
|
47
|
+
|
|
48
|
+
function refusalFor(file , reason ) { return `Pi MCP: ${file}: ${reason}; DoFlow did not change the file`; }
|
|
49
|
+
|
|
50
|
+
function untouchedNotice(file ) {
|
|
51
|
+
const shown = file.length > 80 ? `...${file.slice(-77)}` : file;
|
|
52
|
+
return `MCP: left ${shown} untouched because it cannot be edited safely; DoFlow no longer manages the entries in it.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** DoFlow's entry for one catalog server, or the reason Pi cannot take it. */
|
|
56
|
+
function renderPiEntry(server ) {
|
|
57
|
+
if (server.transport !== 'stdio' || typeof server.command !== 'string' || !server.command) {
|
|
58
|
+
return { refusal: `Pi MCP: server '${server.id}' is not a stdio server; DoFlow writes only stdio entries for Pi` };
|
|
59
|
+
}
|
|
60
|
+
if (typeof server.id !== 'string' || !SERVER_NAME.test(server.id)) {
|
|
61
|
+
return { refusal: `Pi MCP: server id '${server.id}' is not a valid Pi server name` };
|
|
62
|
+
}
|
|
63
|
+
const entry = { command: server.command };
|
|
64
|
+
if (Array.isArray(server.args) && server.args.length) entry.args = [...server.args];
|
|
65
|
+
if (server.env && typeof server.env === 'object' && Object.keys(server.env).length) entry.env = { ...server.env };
|
|
66
|
+
return { entry };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function mcpRows(ledger ) {
|
|
70
|
+
return (ledger?.resources || []).filter((row ) => row.harness === HARNESS && row.kind === 'mcp-server');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readSnapshotFile(file , fsImpl ) {
|
|
74
|
+
if (!fsImpl.existsSync(file)) return { exists: false, text: '', hash: sha256(''), doc: ABSENT_DOCUMENT };
|
|
75
|
+
const bytes = fsImpl.readFileSync(file);
|
|
76
|
+
const text = bytes.toString('utf8');
|
|
77
|
+
// A splice writes the decoded text back, so bytes that do not survive a UTF-8 round trip would be
|
|
78
|
+
// rewritten; such a file is refused like any other one DoFlow cannot edit safely.
|
|
79
|
+
const doc = Buffer.from(text, 'utf8').equals(bytes) ? readDocument(text) : { ok: false, reason: 'file is not valid UTF-8' };
|
|
80
|
+
return { exists: true, text, hash: sha256(bytes), doc };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Read the selected server file and every file a ledger row names. Nothing is read when there is
|
|
84
|
+
* neither a selection nor a row, so a run with no Pi MCP work touches no MCP file. */
|
|
85
|
+
function discoverPiMcp({ selected = [], rows = [], file, fsImpl = fs } ) {
|
|
86
|
+
const files = new Set (rows.map((row ) => row.target));
|
|
87
|
+
if (selected.length) files.add(file);
|
|
88
|
+
return Object.fromEntries([...files].map((target) => [target, readSnapshotFile(target, fsImpl)]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function memberOf(doc , key ) { return doc.servers?.members.find((member ) => member.key === key) ?? null; }
|
|
92
|
+
|
|
93
|
+
/** The servers DoFlow owns in Pi's mcp.json now: rows whose entry still holds the value they record. */
|
|
94
|
+
function ownedPiMcpIds({ rows = [], snapshot = {} } ) {
|
|
95
|
+
const owned = rows.filter((row ) => {
|
|
96
|
+
const doc = snapshot[row.target]?.doc;
|
|
97
|
+
const member = doc?.ok ? memberOf(doc, row.identity) : null;
|
|
98
|
+
return member !== null && fingerprint(member.value) === row.fingerprint;
|
|
99
|
+
});
|
|
100
|
+
return [...new Set (owned.map((row ) => row.identity))];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function twinOf(doc , key ) {
|
|
104
|
+
return doc.servers?.members.find((member ) => member.key !== key && piServerKey(member.key) === piServerKey(key)) ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sameValue(a , b ) { return JSON.stringify(a) === JSON.stringify(b); }
|
|
108
|
+
|
|
109
|
+
function baseChange({ target, operation, id, snapshot } ) {
|
|
110
|
+
return {
|
|
111
|
+
assetId: ASSET_ID, target, operation,
|
|
112
|
+
ownershipIdentity: `doflow:${HARNESS}:mcp-server:${id}`,
|
|
113
|
+
kind: 'mcp-server', identity: id, baseHash: snapshot[target]?.hash ?? sha256(''),
|
|
114
|
+
sourceVersion: 'registry-v1', projection: { renderer: RENDERER },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeChange({ target, operation, id, entry, snapshot } ) {
|
|
119
|
+
return { ...baseChange({ target, operation, id, snapshot }), entry, fingerprint: fingerprint(entry), afterFingerprint: fingerprint(entry) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function releaseChange({ row, snapshot } ) {
|
|
123
|
+
const id = row.identity;
|
|
124
|
+
return { ...baseChange({ target: row.target, operation: 'remove', id, snapshot }), release: true, fingerprint: row.fingerprint };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function planPiMcp({ selected = [], rows = [], file, scope, removing = false, snapshot = {}, assets = [] } ) {
|
|
128
|
+
const changes = [];
|
|
129
|
+
const conflicts = [];
|
|
130
|
+
const notices = [];
|
|
131
|
+
if (!removing && !selected.length && !rows.length) return { changes, conflicts, notices };
|
|
132
|
+
if (removing && !rows.length) return { changes, conflicts, notices };
|
|
133
|
+
if (!assets.some((asset ) => asset.id === ASSET_ID)) {
|
|
134
|
+
return { changes, conflicts: [`Pi MCP needs the ${ASSET_ID} asset to record ownership`], notices };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// An install or update refuses a file it cannot edit; a removal must still uninstall everything
|
|
138
|
+
// else, so it releases the rows for that file and leaves the file as it is.
|
|
139
|
+
const refused = new Set ();
|
|
140
|
+
for (const [target, entry] of Object.entries(snapshot) ) {
|
|
141
|
+
if (entry.doc.ok) continue;
|
|
142
|
+
refused.add(target);
|
|
143
|
+
if (removing) notices.push(untouchedNotice(target));
|
|
144
|
+
else conflicts.push(refusalFor(target, entry.doc.reason));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const matched = new Set ();
|
|
148
|
+
if (!removing) {
|
|
149
|
+
if (selected.length) {
|
|
150
|
+
notices.push(NOTICES.surface, NOTICES.extension);
|
|
151
|
+
if (scope === 'project') notices.push(NOTICES.trust);
|
|
152
|
+
}
|
|
153
|
+
const doc = snapshot[file]?.doc ?? ABSENT_DOCUMENT;
|
|
154
|
+
for (const server of selected) {
|
|
155
|
+
const { entry: desired, refusal } = renderPiEntry(server);
|
|
156
|
+
if (refusal) { conflicts.push(refusal); continue; }
|
|
157
|
+
const row = rows.find((item ) => item.identity === server.id && item.target === file);
|
|
158
|
+
if (row) matched.add(row);
|
|
159
|
+
const member = memberOf(doc, server.id);
|
|
160
|
+
const twin = twinOf(doc, server.id);
|
|
161
|
+
if (row && member) {
|
|
162
|
+
if (fingerprint(member.value) !== row.fingerprint) {
|
|
163
|
+
changes.push(releaseChange({ row, snapshot }));
|
|
164
|
+
notices.push(releasedNotice(server.id));
|
|
165
|
+
} else if (!sameValue(member.value, desired)) {
|
|
166
|
+
changes.push(writeChange({ target: file, operation: 'update', id: server.id, entry: desired, snapshot }));
|
|
167
|
+
}
|
|
168
|
+
} else if (member || twin) {
|
|
169
|
+
// The user's entry holds the name. An owned row whose own entry is gone is released with it,
|
|
170
|
+
// because re-creating DoFlow's entry beside the user's twin would give Pi two of one server.
|
|
171
|
+
if (row) changes.push(releaseChange({ row, snapshot }));
|
|
172
|
+
notices.push(collisionNotice((member ?? twin).key, server.id));
|
|
173
|
+
} else {
|
|
174
|
+
changes.push(writeChange({ target: file, operation: 'create', id: server.id, entry: desired, snapshot }));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const row of rows) {
|
|
180
|
+
if (matched.has(row)) continue;
|
|
181
|
+
const id = row.identity;
|
|
182
|
+
const member = memberOf(snapshot[row.target]?.doc ?? ABSENT_DOCUMENT, id);
|
|
183
|
+
if (member && fingerprint(member.value) === row.fingerprint) {
|
|
184
|
+
changes.push({ ...baseChange({ target: row.target, operation: 'remove', id, snapshot }), fingerprint: row.fingerprint,
|
|
185
|
+
...(row.origin ? { origin: row.origin } : {}) });
|
|
186
|
+
} else {
|
|
187
|
+
changes.push(releaseChange({ row, snapshot }));
|
|
188
|
+
if (member) notices.push(releasedNotice(id));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { changes: changes.filter((change ) => !refused.has(change.target) || (removing && change.release)), conflicts, notices };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isSymlink(file , fsImpl ) {
|
|
196
|
+
try { return fsImpl.lstatSync(file).isSymbolicLink(); } catch { return false; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Where a write to `file` lands: a symlinked mcp.json is written through to the file it points at,
|
|
200
|
+
* so the link stays a link and its target receives the entries. */
|
|
201
|
+
function writePath(file , fsImpl ) {
|
|
202
|
+
if (!isSymlink(file, fsImpl)) return file;
|
|
203
|
+
try { return fsImpl.realpathSync(file); } catch { return path.resolve(path.dirname(file), fsImpl.readlinkSync(file)); }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function atomicWrite(file , content , fsImpl ) {
|
|
207
|
+
const dest = writePath(file, fsImpl);
|
|
208
|
+
fsImpl.mkdirSync(path.dirname(dest), { recursive: true });
|
|
209
|
+
// The rename gives the file the temp file's mode; an mcp.json kept private for its tokens stays so.
|
|
210
|
+
const mode = fsImpl.existsSync(dest) ? fsImpl.statSync(dest).mode & 0o7777 : null;
|
|
211
|
+
const temp = path.join(path.dirname(dest), `.${path.basename(dest)}.${process.pid}.${Date.now()}.tmp`);
|
|
212
|
+
try {
|
|
213
|
+
fsImpl.writeFileSync(temp, content, { encoding: 'utf8', flag: 'wx', ...(mode === null ? {} : { mode }) });
|
|
214
|
+
if (mode !== null) fsImpl.chmodSync(temp, mode);
|
|
215
|
+
fsImpl.renameSync(temp, dest);
|
|
216
|
+
} finally {
|
|
217
|
+
if (fsImpl.existsSync(temp)) fsImpl.rmSync(temp, { force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Replay one file's edits against the text read now, each against a fresh parse of the last result. */
|
|
222
|
+
function editedText(text , group ) {
|
|
223
|
+
const parse = (current ) => {
|
|
224
|
+
const doc = readDocument(current);
|
|
225
|
+
if (!doc.ok) throw new Error(`Pi MCP: cannot edit the planned file: ${doc.reason}`);
|
|
226
|
+
return doc;
|
|
227
|
+
};
|
|
228
|
+
let next = text;
|
|
229
|
+
for (const change of group.filter((item ) => item.operation === 'remove')) {
|
|
230
|
+
const doc = parse(next);
|
|
231
|
+
const member = memberOf(doc, change.identity);
|
|
232
|
+
if (member) next = removeMember(next, doc.servers, member);
|
|
233
|
+
}
|
|
234
|
+
for (const change of group.filter((item ) => item.operation === 'update')) {
|
|
235
|
+
const doc = parse(next);
|
|
236
|
+
const member = memberOf(doc, change.identity);
|
|
237
|
+
if (member) next = replaceValue(next, member, change.entry, doc);
|
|
238
|
+
}
|
|
239
|
+
const creates = group.filter((item ) => item.operation === 'create');
|
|
240
|
+
for (const [index, change] of creates.entries()) {
|
|
241
|
+
const doc = parse(next);
|
|
242
|
+
if (!doc.servers) {
|
|
243
|
+
const entries = Object.fromEntries(creates.slice(index).map((item ) => [item.identity, item.entry]));
|
|
244
|
+
return insertMember(next, doc.root, 'mcpServers', entries, doc);
|
|
245
|
+
}
|
|
246
|
+
next = insertMember(next, doc.servers, change.identity, change.entry, doc);
|
|
247
|
+
}
|
|
248
|
+
return next;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Undo what DoFlow added around its entries once the last of them is gone: the mcpServers member
|
|
252
|
+
* it inserted, or the inner whitespace of the container it first wrote into. `origin` is recorded
|
|
253
|
+
* on the ledger rows (see originFor); without it the emptied container is left as it is. */
|
|
254
|
+
function restoredText(text , origin ) {
|
|
255
|
+
const doc = readDocument(text);
|
|
256
|
+
if (!doc.ok || !doc.servers || doc.servers.members.length) return text;
|
|
257
|
+
if (origin.created === 'member') return replaceInner(text, doc.servers, origin.inner);
|
|
258
|
+
const next = removeMember(text, doc.root, doc.root.members.find((member ) => member.key === 'mcpServers'));
|
|
259
|
+
if (origin.inner === undefined) return next;
|
|
260
|
+
const after = readDocument(next);
|
|
261
|
+
return after.root.members.length ? next : replaceInner(next, after.root, origin.inner);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function applyPiMcp(changes , { fsImpl = fs } = {}) {
|
|
265
|
+
const groups = new Map ();
|
|
266
|
+
for (const change of changes) {
|
|
267
|
+
if (!groups.has(change.target)) groups.set(change.target, []);
|
|
268
|
+
groups.get(change.target) .push(change);
|
|
269
|
+
}
|
|
270
|
+
let applied = 0;
|
|
271
|
+
for (const [target, all] of groups) {
|
|
272
|
+
const group = all.filter((change ) => !change.release);
|
|
273
|
+
if (!group.length) continue;
|
|
274
|
+
const exists = fsImpl.existsSync(target);
|
|
275
|
+
const bytes = exists ? fsImpl.readFileSync(target) : Buffer.alloc(0);
|
|
276
|
+
if (sha256(bytes) !== group[0].baseHash) throw new Error(`Pi MCP: ${target} changed after planning; nothing was written, re-run the command`);
|
|
277
|
+
const text = bytes.toString('utf8');
|
|
278
|
+
const creates = group.filter((change ) => change.operation === 'create');
|
|
279
|
+
const origin = group.find((change ) => change.origin)?.origin;
|
|
280
|
+
let next = exists ? editedText(text, group) : renderNewDocument(creates.map((change ) => [change.identity, change.entry]));
|
|
281
|
+
// Only a file DoFlow created is deleted, and never through a symlink; a user's own file that
|
|
282
|
+
// happens to equal the skeleton stays.
|
|
283
|
+
if (exists && origin?.created === 'file' && next === SKELETON) {
|
|
284
|
+
if (!isSymlink(target, fsImpl)) { fsImpl.rmSync(target); applied += 1; continue; }
|
|
285
|
+
} else if (exists && origin && !creates.length) next = restoredText(next, origin);
|
|
286
|
+
if (next !== text) {
|
|
287
|
+
atomicWrite(target, next, fsImpl);
|
|
288
|
+
applied += 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { applied };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function statusFor({ id, target, status } ) {
|
|
295
|
+
return { harness: HARNESS, assetId: ASSET_ID, capability: 'mcp', identity: id, target,
|
|
296
|
+
ownershipIdentity: `doflow:${HARNESS}:mcp-server:${id}`, status };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resourceFor({ id, target, value, origin } ) {
|
|
300
|
+
return { assetId: ASSET_ID, target, ownershipIdentity: `doflow:${HARNESS}:mcp-server:${id}`, kind: 'mcp-server', identity: id,
|
|
301
|
+
fingerprint: fingerprint(value), sourceVersion: 'registry-v1', projection: { renderer: RENDERER }, ...(origin ? { origin } : {}) };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** What DoFlow added to `target` besides its entries, so a later removal can take it out again:
|
|
305
|
+
* the whole file, the mcpServers member, or the first member of an empty mcpServers (with the
|
|
306
|
+
* container's original inner whitespace). A row already carrying it passes it on; otherwise it is
|
|
307
|
+
* read from the file as plan saw it. Only whitespace is recorded, never file content. */
|
|
308
|
+
function originFor(target , rows , snapshot ) {
|
|
309
|
+
const carried = rows.find((row ) => row.target === target && row.origin)?.origin;
|
|
310
|
+
if (carried) return carried;
|
|
311
|
+
const before = snapshot[target];
|
|
312
|
+
if (!before) return undefined;
|
|
313
|
+
if (!before.exists) return { created: 'file' };
|
|
314
|
+
const { doc, text } = before;
|
|
315
|
+
if (!doc.ok) return undefined;
|
|
316
|
+
const inner = (node ) => text.slice(node.open + 1, node.close);
|
|
317
|
+
if (!doc.servers) return doc.root.members.length ? { created: 'key' } : { created: 'key', inner: inner(doc.root) };
|
|
318
|
+
return doc.servers.members.length ? undefined : { created: 'member', inner: inner(doc.servers) };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Statuses and ledger resources for the Pi MCP entries. `snapshot` is what plan read before apply:
|
|
322
|
+
* an entry it held that DoFlow did not own then, or held with a value its row no longer matched, is
|
|
323
|
+
* the user's, whatever it holds now. */
|
|
324
|
+
function verifyPiMcp({ selected = [], rows = [], file, snapshot = {}, removing = false, fsImpl = fs } ) {
|
|
325
|
+
const statuses = [];
|
|
326
|
+
const resources = [];
|
|
327
|
+
const conflicts = [];
|
|
328
|
+
const current = {};
|
|
329
|
+
for (const target of new Set ([...(selected.length ? [file] : []), ...rows.map((row ) => row.target)])) {
|
|
330
|
+
current[target] = readSnapshotFile(target, fsImpl).doc;
|
|
331
|
+
if (!current[target].ok && !removing) conflicts.push(refusalFor(target, current[target].reason));
|
|
332
|
+
}
|
|
333
|
+
const memberNow = (target , id ) => (current[target]?.ok ? memberOf(current[target], id) : null);
|
|
334
|
+
const foreign = (target , id ) => {
|
|
335
|
+
const before = snapshot[target]?.doc;
|
|
336
|
+
if (!before?.ok) return false;
|
|
337
|
+
const row = rows.find((item ) => item.identity === id && item.target === target);
|
|
338
|
+
const member = memberOf(before, id);
|
|
339
|
+
if (!member) return Boolean(twinOf(before, id));
|
|
340
|
+
return !row || fingerprint(member.value) !== row.fingerprint;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const verified = new Set ();
|
|
344
|
+
if (!removing) {
|
|
345
|
+
for (const server of selected) {
|
|
346
|
+
const { entry: desired } = renderPiEntry(server);
|
|
347
|
+
if (!desired) continue;
|
|
348
|
+
verified.add(`${file}\u0000${server.id}`);
|
|
349
|
+
const member = memberNow(file, server.id);
|
|
350
|
+
if (foreign(file, server.id)) statuses.push(statusFor({ id: server.id, target: file, status: 'not-managed' }));
|
|
351
|
+
else if (member && sameValue(member.value, desired)) {
|
|
352
|
+
statuses.push(statusFor({ id: server.id, target: file, status: 'managed' }));
|
|
353
|
+
resources.push(resourceFor({ id: server.id, target: file, value: member.value, origin: originFor(file, rows, snapshot) }));
|
|
354
|
+
} else statuses.push(statusFor({ id: server.id, target: file, status: 'missing' }));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const row of rows) {
|
|
359
|
+
const id = row.identity;
|
|
360
|
+
if (verified.has(`${row.target}\u0000${id}`)) continue;
|
|
361
|
+
const member = memberNow(row.target, id);
|
|
362
|
+
let status ;
|
|
363
|
+
if (!current[row.target].ok) status = 'not-managed';
|
|
364
|
+
else if (!member) status = 'absent';
|
|
365
|
+
else if (foreign(row.target, id) || fingerprint(member.value) !== row.fingerprint) status = 'not-managed';
|
|
366
|
+
else status = 'retained';
|
|
367
|
+
statuses.push(statusFor({ id, target: row.target, status }));
|
|
368
|
+
if (status === 'managed') resources.push(resourceFor({ id, target: row.target, value: member.value, origin: originFor(row.target, rows, snapshot) }));
|
|
369
|
+
}
|
|
370
|
+
return { statuses, resources, conflicts };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
module.exports = { NOTICES, renderPiEntry, piServerKey, mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
/** Resolve a registry-declared source path against the repo root, refusing escapes. Shared by the
|
|
8
|
+
* common projection module and by per-adapter native-projection normalizers, which is why it lives
|
|
9
|
+
* in its own module rather than in index.js — a normalizer importing index.js would be a cycle. */
|
|
10
|
+
function resolveRegistrySource(registry , source , label ) {
|
|
11
|
+
if (typeof registry.repoRoot !== 'string' || !registry.repoRoot) throw new Error(`Registry repoRoot is required for ${label}`);
|
|
12
|
+
if (typeof source !== 'string' || !source) throw new Error(`Registry ${label} source is required`);
|
|
13
|
+
const root = path.resolve(registry.repoRoot);
|
|
14
|
+
const resolved = path.resolve(root, source);
|
|
15
|
+
if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error(`Registry ${label} source escapes repository: ${source}`);
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { resolveRegistrySource };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// `doflow install` — plan the full lifecycle across the requested targets, back up, apply, and
|
|
3
|
+
// pin what was chosen in doflow.lock. Supports --dry-run previews and MCP selection.
|
|
4
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { resolveTargets, toolDirs } = require('../../install/targets.js');
|
|
8
|
+
const { resolveContext, printContext } = require('../../install/context.js');
|
|
9
|
+
const {
|
|
10
|
+
BackupError, DEFAULT_BACKUP_RETENTION, createFileBackup, applyRetention, formatBytes, retentionNote,
|
|
11
|
+
} = require('../../install/backup.js');
|
|
12
|
+
const { backupSetFromPlan } = require('../../install/backup-set.js');
|
|
13
|
+
const { ledgerPath } = require('../../state/index.js');
|
|
14
|
+
const { writeManifest, readInstallManifest } = require('../../install/manifest.js');
|
|
15
|
+
const { confirm } = require('../../helper/prompt.js');
|
|
16
|
+
const { sourceCommit } = require('../../helper/git.js');
|
|
17
|
+
const { chmodHooksExecutable } = require('../../helper/settings-scope.js');
|
|
18
|
+
const { promptMcpCheckbox, resolveMcpSelections } = require('../../install/mcp.js');
|
|
19
|
+
const { loadRegistry } = require('../../registry/index.js');
|
|
20
|
+
const { storageAfterInstall } = require('../../runtime/storage/upkeep.js');
|
|
21
|
+
const { applyLifecycle, applyMcpIndex, recordMcpOwnership } = require('../../lifecycle/index.js');
|
|
22
|
+
const {
|
|
23
|
+
codexScope, registryLifecycleView, printRegistryLifecycle, printPlanNotices, assertSafeRegistryPlan,
|
|
24
|
+
lockDocument, recordLock,
|
|
25
|
+
} = require('../../lifecycle/view.js');
|
|
26
|
+
const {
|
|
27
|
+
REPO_ROOT, SCRIPT_DIR, pkg, scopeOf, installPaths, reportRetiredMcp, scopeSelectionState, printMcpSelection, plannedMcpSelections,
|
|
28
|
+
printRecordedMcpOwnership, buildAdapterRegistry, refuseHomeRootedProject, holdRunLock, checkpointRunLock,
|
|
29
|
+
} = require('../shared.js');
|
|
30
|
+
|
|
31
|
+
function cmdInstall(o ) {
|
|
32
|
+
refuseHomeRootedProject(o, 'install');
|
|
33
|
+
const targets = resolveTargets(o.targets);
|
|
34
|
+
const scope = scopeOf(o);
|
|
35
|
+
const hold = holdRunLock(o, scope, 'install');
|
|
36
|
+
try {
|
|
37
|
+
const dirs = toolDirs(scope);
|
|
38
|
+
const lifecyclePaths = installPaths(scope);
|
|
39
|
+
const backupRoot = lifecyclePaths.backupRoot;
|
|
40
|
+
// Resolved once per invocation and threaded into resolveContext/createFileBackup/writeManifest below
|
|
41
|
+
// — those three used to each spawn their own `git rev-parse` for the identical value.
|
|
42
|
+
const commit = sourceCommit(SCRIPT_DIR);
|
|
43
|
+
|
|
44
|
+
printContext(resolveContext({ repoRoot: REPO_ROOT, targets, dirs, sourceCommit: commit, ...scope }));
|
|
45
|
+
|
|
46
|
+
const registry = loadRegistry({ repoRoot: REPO_ROOT });
|
|
47
|
+
const existingManifest = readInstallManifest({ scopeRoot: lifecyclePaths.scopeRoot });
|
|
48
|
+
const { lock, ledger } = scopeSelectionState(scope);
|
|
49
|
+
// Each MCP-capable target gets its own selection; a real terminal is asked once for all of them.
|
|
50
|
+
const selection = resolveMcpSelections({
|
|
51
|
+
cmd: 'install', requested: o.mcp, targets, registry, lock, ledger, manifestServers: existingManifest?.mcpServers ?? null,
|
|
52
|
+
interactive: !o.dryRun && !o.force && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY),
|
|
53
|
+
promptFn: promptMcpCheckbox, onStale: reportRetiredMcp,
|
|
54
|
+
});
|
|
55
|
+
// One lifecycle view across every requested target — computed unconditionally (not only under
|
|
56
|
+
// --dry-run) so its safety gate and its plan are the exact same object the real apply below uses.
|
|
57
|
+
const lifecycleView = registryLifecycleView({ registry, repoRoot: REPO_ROOT, scope, dirs, targets,
|
|
58
|
+
mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds,
|
|
59
|
+
force: o.force, adopt: o.adopt === true, permissions: o.permissions === true, statusline: o.statusline === true });
|
|
60
|
+
if (!lifecycleView.plan.safe) { assertSafeRegistryPlan(lifecycleView); return; }
|
|
61
|
+
|
|
62
|
+
printMcpSelection(lifecycleView, selection.sources, { requested: o.mcp, prefix: o.dryRun ? '[DRY]' : '[INFO]' });
|
|
63
|
+
// Safe-by-default surfaced, not silent: a first-ever non-interactive install selects zero MCP
|
|
64
|
+
// servers. Anyone scripting installs must opt in explicitly; interactive users never see this
|
|
65
|
+
// because the checkbox is the discovery path.
|
|
66
|
+
if (!o.mcp && !o.dryRun && !process.stdin.isTTY && Object.values(selection.sources).includes('default')) {
|
|
67
|
+
console.log('[INFO] MCP: none selected by default in non-interactive mode — pass --mcp all or --mcp <names> to include servers.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The files this run's apply can overwrite or delete; DoFlow's own records are never among them.
|
|
71
|
+
const buildBackupSet = () => backupSetFromPlan({
|
|
72
|
+
plan: lifecycleView.plan, scope: scope.global ? 'global' : 'project', scopeRoot: lifecyclePaths.scopeRoot,
|
|
73
|
+
exclude: [backupRoot, lifecycleView.stateRoot, lifecyclePaths.manifestPath, path.join(lifecyclePaths.doflowRoot, 'doflow.lock')],
|
|
74
|
+
// The ownership ledger, and nothing else from the state root (runs, evidence and recovery
|
|
75
|
+
// records written after the backup must survive a rollback): a rollback then restores the
|
|
76
|
+
// rows that describe the files it restores.
|
|
77
|
+
records: [ledgerPath(lifecycleView.stateRoot)],
|
|
78
|
+
});
|
|
79
|
+
const keep = o.prune ?? DEFAULT_BACKUP_RETENTION;
|
|
80
|
+
|
|
81
|
+
if (o.dryRun) {
|
|
82
|
+
console.log(`[INFO] Install targets: ${targets.join(' ')}`);
|
|
83
|
+
printRegistryLifecycle(lifecycleView, '[DRY]');
|
|
84
|
+
const set = o.noBackup ? null : backupStep(buildBackupSet);
|
|
85
|
+
if (set) printBackupDryRun(set, `${backupRoot}/install_<timestamp>`);
|
|
86
|
+
const retention = applyRetention({ backupRoot, keep, dryRun: true, reserve: set?.count ? 1 : 0 });
|
|
87
|
+
console.log(`[DRY] Backups: would keep ${retention.kept}, would remove ${retention.wouldRemove.length} (${retentionNote(o.prune, keep)})`);
|
|
88
|
+
console.log(`[DRY] Would write manifest: ${lifecyclePaths.manifestPath}`);
|
|
89
|
+
console.log('[DRY] Dry run complete — no changes written');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!confirm(`Install configs to: ${targets.join(' ')}?`, o.force)) {
|
|
94
|
+
// Exit 1, not 0: a declined prompt is a decision, and it must not share an exit code with a
|
|
95
|
+
// completed run. With no stdin the prompt auto-declines, so `doflow install <path>` in a script
|
|
96
|
+
// or CI step printed "Aborted.", wrote zero files, and reported success. It also silently
|
|
97
|
+
// corrupted a set of install-timing measurements during the D.4 sweep, which is how it surfaced.
|
|
98
|
+
console.error('[INFO] Aborted.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
checkpointRunLock(hold);
|
|
102
|
+
|
|
103
|
+
let bid = '';
|
|
104
|
+
if (!o.noBackup) {
|
|
105
|
+
const backup = backupStep(() => createFileBackup({ operation: 'install', set: buildBackupSet(), backupRoot, repoRoot: SCRIPT_DIR,
|
|
106
|
+
sourceCommit: commit, version: pkg.version, date: new Date() }));
|
|
107
|
+
bid = reportBackup(backup);
|
|
108
|
+
} else {
|
|
109
|
+
console.error('[WARN] Skipping backup (--no-backup)');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The ledger the lock is pinned against: the one this run leaves.
|
|
113
|
+
let ledgerAfter = lifecycleView.ledger;
|
|
114
|
+
if (lifecycleView.plan.changes.length) {
|
|
115
|
+
const result = applyLifecycle({ plan: lifecycleView.plan, registry: lifecycleView.registry,
|
|
116
|
+
adapters: buildAdapterRegistry(),
|
|
117
|
+
stateRoot: lifecycleView.stateRoot, ledger: lifecycleView.ledger });
|
|
118
|
+
for (const target of lifecycleView.plan.targets) {
|
|
119
|
+
if (target.skipped || !target.changes.length) continue;
|
|
120
|
+
const owned = result.ledger.resources.filter((resource ) => resource.harness === target.harness).length;
|
|
121
|
+
console.log(`[INFO] ${target.harness}: lifecycle verified (${owned} owned resource(s))`);
|
|
122
|
+
}
|
|
123
|
+
ledgerAfter = result.ledger;
|
|
124
|
+
} else {
|
|
125
|
+
// MCP_INDEX.md is generated, not a tracked resource, so it never appears in plan.changes and
|
|
126
|
+
// applyLifecycle — which owns the only call that writes it — is skipped entirely when nothing
|
|
127
|
+
// else changed. Without this branch the index is rewritten only as a side effect of some
|
|
128
|
+
// unrelated asset changing, so a change to the renderer, to a server's `doc`/`shortFlag`, or
|
|
129
|
+
// to the resolved selection silently does nothing whenever the rest of the tree is current.
|
|
130
|
+
applyMcpIndex({ scopeRoot: lifecycleView.plan.scopeRoot, selectedMcp: lifecycleView.plan.mcp, mode: 'apply' });
|
|
131
|
+
const owned = recordMcpOwnership({ plan: lifecycleView.plan, registry: lifecycleView.registry, adapters: lifecycleView.adapters,
|
|
132
|
+
stateRoot: lifecycleView.stateRoot, ledger: lifecycleView.ledger });
|
|
133
|
+
printRecordedMcpOwnership(owned.recorded);
|
|
134
|
+
ledgerAfter = owned.ledger;
|
|
135
|
+
}
|
|
136
|
+
printPlanNotices(lifecycleView);
|
|
137
|
+
|
|
138
|
+
if (targets.includes('claude')) {
|
|
139
|
+
// A npm-packaged tarball does not reliably preserve the executable bit on arbitrary files
|
|
140
|
+
// (unlike git checkouts, which usually do) — copy-tree's mode-preserving copy only carries
|
|
141
|
+
// over whatever bit the source file actually has on this machine, so this runs after
|
|
142
|
+
// applyLifecycle (once the hook scripts are actually on disk) as a final, unconditional +x.
|
|
143
|
+
chmodHooksExecutable(dirs.claude);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
writeManifest({ scopeRoot: lifecyclePaths.scopeRoot, scriptVersion: pkg.version, operation: 'install', repoRoot: SCRIPT_DIR, sourceCommit: commit, backupId: bid, tools: targets, date: new Date() });
|
|
147
|
+
|
|
148
|
+
// Pin what this install CHOSE, beside what earlier runs pinned. The ledger owns ownership; the
|
|
149
|
+
// lock owns selection — together they make the next update's delta a reviewable fact instead of a
|
|
150
|
+
// surprise.
|
|
151
|
+
const lockResult = recordLock(
|
|
152
|
+
scope.global ? { scope: 'global', homeDir: os.homedir() } : { scope: 'project', projectRoot: path.resolve(scope.projectRoot) },
|
|
153
|
+
lockDocument({
|
|
154
|
+
registry, scope: codexScope(scope), scopeRoot: scope.global ? os.homedir() : path.resolve(scope.projectRoot),
|
|
155
|
+
previous: lock, ledger: ledgerAfter, plannedTargets: targets, mcpSelections: plannedMcpSelections(lifecycleView),
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
console.log(`[INFO] doflow.lock: ${lockResult.summary}`);
|
|
159
|
+
|
|
160
|
+
applyBackupRetention({ backupRoot, prune: o.prune, keep, bid });
|
|
161
|
+
storageAfterInstall({ scope: scope.global ? 'global' : 'project', scopeRoot: scope.global ? os.homedir() : path.resolve(scope.projectRoot) });
|
|
162
|
+
|
|
163
|
+
console.log('[OK] Installation complete!');
|
|
164
|
+
} finally {
|
|
165
|
+
hold?.release();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Run a backup step; a BackupError stops the command before anything is changed. */
|
|
170
|
+
function backupStep(step ) {
|
|
171
|
+
try {
|
|
172
|
+
return step();
|
|
173
|
+
} catch (error ) {
|
|
174
|
+
if (!(error instanceof BackupError)) throw error;
|
|
175
|
+
console.error(`[ERROR] Backup failed, nothing was changed: ${error.message}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Print what a backup step wrote and return its id ('' when there was nothing to back up). */
|
|
181
|
+
function reportBackup(backup ) {
|
|
182
|
+
if (!backup) {
|
|
183
|
+
console.error('[INFO] Backup: nothing to back up (no file changes)');
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
console.error(`[INFO] Backup created: ${backup.id} (${backup.files} file(s), ${formatBytes(backup.bytes)}; ${backup.absent} not present before the run)`);
|
|
187
|
+
return backup.id;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The dry-run backup lines for a set the real run would back up. */
|
|
191
|
+
function printBackupDryRun(set , destination ) {
|
|
192
|
+
if (set.count === 0) {
|
|
193
|
+
console.log('[DRY] Backup: nothing to back up (no file changes)');
|
|
194
|
+
} else {
|
|
195
|
+
console.log(`[DRY] Would back up ${set.count} file(s), ${formatBytes(set.bytes)} (${set.bytes} bytes; ${set.absent} not present yet) to ${destination}`);
|
|
196
|
+
}
|
|
197
|
+
const mcpOnly = set.excluded.filter((item ) => item.reason === 'mcp-entries-only').length;
|
|
198
|
+
if (mcpOnly > 0) console.log(`[DRY] Not backed up: ${mcpOnly} file(s) holding only MCP entries`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Keep the newest backups after a run that changed files; never changes the exit code. */
|
|
202
|
+
function applyBackupRetention({ backupRoot, prune, keep, bid } ) {
|
|
203
|
+
let retention;
|
|
204
|
+
try {
|
|
205
|
+
retention = applyRetention({ backupRoot, keep, protect: bid ? [bid] : [] });
|
|
206
|
+
} catch (error ) {
|
|
207
|
+
console.error(`[WARN] Backups: could not apply retention: ${error.message}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
for (const failure of retention.failed) console.error(`[WARN] Backups: could not remove ${failure.id}: ${failure.error}`);
|
|
211
|
+
console.error(`[INFO] Backups: kept ${retention.kept}, removed ${retention.removed.length} (${retentionNote(prune, keep)})`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = cmdInstall;
|
|
215
|
+
module.exports.backupStep = backupStep;
|
|
216
|
+
module.exports.reportBackup = reportBackup;
|
|
217
|
+
module.exports.printBackupDryRun = printBackupDryRun;
|
|
218
|
+
module.exports.applyBackupRetention = applyBackupRetention;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// `doflow list-backups` — the backup table for this scope. Takes no project-path positional
|
|
3
|
+
// (rollback's one positional slot is the backup id); scope is -g/--global vs cwd-rooted project.
|
|
4
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
5
|
+
const { listBackups } = require('../../install/backup.js');
|
|
6
|
+
const { installPaths, printBackupTable } = require('../shared.js');
|
|
7
|
+
|
|
8
|
+
function cmdListBackups(o ) {
|
|
9
|
+
// rollback/list-backups don't take a project-path positional (their one positional slot is the
|
|
10
|
+
// backup id for rollback) — scope is -g/--global vs default project rooted at cwd.
|
|
11
|
+
const scope = { global: o.global, projectRoot: '.' };
|
|
12
|
+
const lifecyclePaths = installPaths(scope);
|
|
13
|
+
const rows = listBackups(lifecyclePaths.backupRoot);
|
|
14
|
+
// listBackups now reads the canonical root AND the legacy pre-.doflow one, so the footer must name
|
|
15
|
+
// the roots the rows actually came from: reporting restore points as living in a directory that
|
|
16
|
+
// does not exist on this machine sends a user looking for recovery material to the wrong place.
|
|
17
|
+
const roots = [...new Set(rows.map((r ) => r.backupRoot))];
|
|
18
|
+
printBackupTable(rows, roots.length ? roots.join(', ') : lifecyclePaths.backupRoot);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = cmdListBackups;
|