project-init 0.3.0__py3-none-any.whl
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.
- project_init/__init__.py +4 -0
- project_init/__main__.py +662 -0
- project_init/mcps.py +57 -0
- project_init/scaffold.py +374 -0
- project_init/templates/base/AGENTS.md.tmpl +50 -0
- project_init/templates/base/CLAUDE.md.tmpl +16 -0
- project_init/templates/base/CONTRIBUTING.md.tmpl +55 -0
- project_init/templates/base/GEMINI.md.tmpl +16 -0
- project_init/templates/base/LICENSE.tmpl +231 -0
- project_init/templates/base/SECURITY.md.tmpl +26 -0
- project_init/templates/base/docs/explanation/index.md +9 -0
- project_init/templates/base/docs/how-to/index.md +7 -0
- project_init/templates/base/docs/index.md.tmpl +20 -0
- project_init/templates/base/docs/reference/index.md +13 -0
- project_init/templates/base/docs/tutorials/index.md +7 -0
- project_init/templates/base/dot_claude/agents/README.md +30 -0
- project_init/templates/base/dot_claude/config.yaml.tmpl +31 -0
- project_init/templates/base/dot_claude/docs/README.md +26 -0
- project_init/templates/base/dot_claude/docs/adr/adr-001-memory-stack.md.tmpl +22 -0
- project_init/templates/base/dot_claude/docs/adr/adr-002-mcp-choices.md.tmpl +32 -0
- project_init/templates/base/dot_claude/docs/adr/adr-template.md +29 -0
- project_init/templates/base/dot_claude/docs/development/conventions.md.tmpl +31 -0
- project_init/templates/base/dot_claude/docs/development/testing.md +25 -0
- project_init/templates/base/dot_claude/docs/guides/developer-onboarding.md +110 -0
- project_init/templates/base/dot_claude/docs/guides/issue-metadata.md +27 -0
- project_init/templates/base/dot_claude/docs/guides/secrets.md +50 -0
- project_init/templates/base/dot_claude/docs/guides/using-memory.md +36 -0
- project_init/templates/base/dot_claude/hooks/README.md +15 -0
- project_init/templates/base/dot_claude/hooks/agent_guard_adapter.py.tmpl +64 -0
- project_init/templates/base/dot_claude/hooks/dag_workflow.py +610 -0
- project_init/templates/base/dot_claude/memory/MEMORY.md.tmpl +11 -0
- project_init/templates/base/dot_claude/memory/README.md +51 -0
- project_init/templates/base/dot_claude/memory/SCHEMA.md +52 -0
- project_init/templates/base/dot_claude/memory/feedback_conventions.md +11 -0
- project_init/templates/base/dot_claude/memory/project_context.md.tmpl +11 -0
- project_init/templates/base/dot_claude/memory/user_role.md +7 -0
- project_init/templates/base/dot_claude/project-init.md.tmpl +174 -0
- project_init/templates/base/dot_claude/rules/go.md +14 -0
- project_init/templates/base/dot_claude/rules/hooks.md +30 -0
- project_init/templates/base/dot_claude/rules/node.md +17 -0
- project_init/templates/base/dot_claude/rules/python.md +25 -0
- project_init/templates/base/dot_claude/scripts/README.md +15 -0
- project_init/templates/base/dot_claude/scripts/create_issue.sh +577 -0
- project_init/templates/base/dot_claude/scripts/create_nojira_pr.sh +3 -0
- project_init/templates/base/dot_claude/scripts/finish_pr.sh +3 -0
- project_init/templates/base/dot_claude/scripts/install_hooks.sh +55 -0
- project_init/templates/base/dot_claude/scripts/monitor_pr.sh +270 -0
- project_init/templates/base/dot_claude/scripts/promote_review.sh +3 -0
- project_init/templates/base/dot_claude/scripts/push_branch.sh +5 -0
- project_init/templates/base/dot_claude/scripts/push_wiki.sh +34 -0
- project_init/templates/base/dot_claude/scripts/setup_github.sh +219 -0
- project_init/templates/base/dot_claude/scripts/start_issue.sh +134 -0
- project_init/templates/base/dot_claude/settings.json.tmpl +83 -0
- project_init/templates/base/dot_claude/skills/README.md +12 -0
- project_init/templates/base/dot_claude/skills/plan/SKILL.md.tmpl +40 -0
- project_init/templates/base/dot_claude/vault/README.md +21 -0
- project_init/templates/base/dot_claude/vault/decisions/README.md +22 -0
- project_init/templates/base/dot_claude/vault/design/README.md +3 -0
- project_init/templates/base/dot_claude/vault/knowledge/README.md +5 -0
- project_init/templates/base/dot_claude/vault/sessions/README.md +5 -0
- project_init/templates/base/dot_devcontainer/devcontainer.json.tmpl +17 -0
- project_init/templates/base/dot_devcontainer/post-create.sh.tmpl +31 -0
- project_init/templates/base/dot_env.example.tmpl +13 -0
- project_init/templates/base/dot_github/CODEOWNERS.tmpl +12 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/bug.yml +98 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/chore.yml +82 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/config.yml +5 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/docs.yml +84 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/feature.yml +87 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/test.yml +90 -0
- project_init/templates/base/dot_github/copilot-instructions.md.tmpl +25 -0
- project_init/templates/base/dot_github/hooks/commit-msg +52 -0
- project_init/templates/base/dot_github/hooks/pre-commit +16 -0
- project_init/templates/base/dot_github/hooks/pre-push +51 -0
- project_init/templates/base/dot_github/pull_request_template.md +22 -0
- project_init/templates/base/dot_github/workflows/board-automation.yml +232 -0
- project_init/templates/base/dot_github/workflows/ci.yml.tmpl +204 -0
- project_init/templates/base/dot_github/workflows/docs.yml.tmpl +98 -0
- project_init/templates/base/dot_github/workflows/issue-validation.yml +72 -0
- project_init/templates/base/dot_github/workflows/review-status.yml +48 -0
- project_init/templates/base/dot_github/workflows/validate-pr.yml +103 -0
- project_init/templates/base/dot_gitignore.tmpl +41 -0
- project_init/templates/base/dot_golangci.yml.tmpl +20 -0
- project_init/templates/base/dot_vscode/extensions.json.tmpl +10 -0
- project_init/templates/base/dot_vscode/settings.json.tmpl +8 -0
- project_init/templates/base/eslint.config.mjs.tmpl +29 -0
- project_init/templates/base/justfile.tmpl +95 -0
- project_init/templates/base/mise.toml.tmpl +20 -0
- project_init/templates/base/mkdocs.yml.tmpl +32 -0
- project_init/templates/base/renovate.json +14 -0
- project_init/templates/base/ruff.toml.tmpl +31 -0
- project_init/templates/base/typedoc.json.tmpl +14 -0
- project_init/templates/codex/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init/templates/codex/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init/templates/codex/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init/templates/codex/dot_agents/skills/audit/SKILL.md +146 -0
- project_init/templates/codex/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init/templates/codex/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/codex/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init/templates/codex/dot_agents/skills/review/SKILL.md +17 -0
- project_init/templates/codex/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init/templates/codex/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init/templates/codex/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init/templates/codex/dot_agents/skills/status/SKILL.md +15 -0
- project_init/templates/codex/dot_codex/hooks.json.tmpl +17 -0
- project_init/templates/fallback/dot_claude/hooks/github_command_guard.sh +11 -0
- project_init/templates/fallback/dot_claude/hooks/post_edit_lint.sh +58 -0
- project_init/templates/fallback/dot_claude/hooks/pre_commit_gate.sh +81 -0
- project_init/templates/fallback/dot_claude/hooks/prod_guard.py +140 -0
- project_init/templates/fallback/dot_claude/hooks/session_setup.sh +62 -0
- project_init/templates/fallback/dot_claude/hooks/workflow_state_reminder.sh +72 -0
- project_init/templates/fallback/dot_claude/skills/INDEX.md +28 -0
- project_init/templates/fallback/dot_claude/skills/add_adr/SKILL.md +33 -0
- project_init/templates/fallback/dot_claude/skills/add_command/SKILL.md +63 -0
- project_init/templates/fallback/dot_claude/skills/add_hook/SKILL.md +112 -0
- project_init/templates/fallback/dot_claude/skills/audit/SKILL.md +146 -0
- project_init/templates/fallback/dot_claude/skills/create_issue/SKILL.md +59 -0
- project_init/templates/fallback/dot_claude/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/fallback/dot_claude/skills/request_review/SKILL.md +19 -0
- project_init/templates/fallback/dot_claude/skills/review/SKILL.md +17 -0
- project_init/templates/fallback/dot_claude/skills/save_memory/SKILL.md +17 -0
- project_init/templates/fallback/dot_claude/skills/session_summary/SKILL.md +35 -0
- project_init/templates/fallback/dot_claude/skills/start_task/SKILL.md +48 -0
- project_init/templates/fallback/dot_claude/skills/status/SKILL.md +15 -0
- project_init/templates/gemini/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init/templates/gemini/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init/templates/gemini/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init/templates/gemini/dot_agents/skills/audit/SKILL.md +146 -0
- project_init/templates/gemini/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init/templates/gemini/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/gemini/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init/templates/gemini/dot_agents/skills/review/SKILL.md +17 -0
- project_init/templates/gemini/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init/templates/gemini/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init/templates/gemini/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init/templates/gemini/dot_agents/skills/status/SKILL.md +15 -0
- project_init/templates/gemini/dot_claude/scripts/setup_gemini.sh.tmpl +16 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_adr.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_command.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_hook.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/audit.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/create_issue.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/github_workflow.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/request_review.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/review.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/save_memory.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/session_summary.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/start_task.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/status.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/gemini-extension.json.tmpl +6 -0
- project_init/templates/gemini/dot_gemini-extension/hooks/hooks.json.tmpl +18 -0
- project_init/templates/graphify/dot_claude/docs/guides/using-graphify.md +37 -0
- project_init/templates/graphify/dot_claude/rules/graphify.md +18 -0
- project_init/templates/graphify/dot_claude/scripts/setup_graphify.sh +40 -0
- project_init/templates/obsidian/dot_claude/scripts/lint_memory.sh +115 -0
- project_init/templates/obsidian/dot_claude/vault/decisions/adr-000-project-setup.md.tmpl +22 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/README.md +31 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/app.json +6 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/community-plugins.json +1 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/core-plugins.json +1 -0
- project_init/templates/obsidian/dot_claude/vault/log.md +6 -0
- project_init/templates/obsidian/dot_claude/vault/templates/decision.md +16 -0
- project_init/templates/obsidian/dot_claude/vault/templates/design-note.md +14 -0
- project_init/templates/obsidian/dot_claude/vault/templates/knowledge-note.md +12 -0
- project_init/templates/obsidian/dot_claude/vault/templates/session-note.md +16 -0
- project_init/templates/presets/obsidian-graphify.toml +16 -0
- project_init/templates/presets/obsidian-only.toml +14 -0
- project_init/upgrade.py +569 -0
- project_init-0.3.0.dist-info/METADATA +342 -0
- project_init-0.3.0.dist-info/RECORD +173 -0
- project_init-0.3.0.dist-info/WHEEL +4 -0
- project_init-0.3.0.dist-info/entry_points.txt +2 -0
- project_init-0.3.0.dist-info/licenses/LICENSE +201 -0
project_init/__init__.py
ADDED
project_init/__main__.py
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"""CLI entry point for `project-init` and `uvx project-init`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import date
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from project_init import __repo_url__, __version__
|
|
11
|
+
from project_init.mcps import (
|
|
12
|
+
DB_CATALOG,
|
|
13
|
+
MCP_CATALOG,
|
|
14
|
+
PLAYWRIGHT_MCP,
|
|
15
|
+
format_installed_mcps,
|
|
16
|
+
format_installed_mcps_yaml,
|
|
17
|
+
)
|
|
18
|
+
from project_init.scaffold import (
|
|
19
|
+
TemplateRenderError,
|
|
20
|
+
list_presets,
|
|
21
|
+
load_preset,
|
|
22
|
+
scaffold,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
p = argparse.ArgumentParser(
|
|
28
|
+
prog="project-init",
|
|
29
|
+
description="Scaffold agentic-development infrastructure into a project.",
|
|
30
|
+
epilog=(
|
|
31
|
+
"Subcommand: project-init upgrade [target] [--apply] — re-render "
|
|
32
|
+
"from the recorded config and report drift (PI-142)."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
p.add_argument(
|
|
36
|
+
"target",
|
|
37
|
+
nargs="?",
|
|
38
|
+
default=".",
|
|
39
|
+
help="Target directory (default: current directory)",
|
|
40
|
+
)
|
|
41
|
+
p.add_argument("--preset", help="Preset name (skip interactive selection)")
|
|
42
|
+
p.add_argument("--name", help="Project name")
|
|
43
|
+
p.add_argument("--description", help="One-line project description")
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--language",
|
|
46
|
+
choices=["python", "node", "go", "none"],
|
|
47
|
+
help="Primary language/runtime",
|
|
48
|
+
)
|
|
49
|
+
p.add_argument(
|
|
50
|
+
"--mcps",
|
|
51
|
+
default="",
|
|
52
|
+
help="Comma-separated MCP IDs from the core catalog (e.g. context7)",
|
|
53
|
+
)
|
|
54
|
+
p.add_argument(
|
|
55
|
+
"--db",
|
|
56
|
+
choices=["none", "postgres", "sqlite"],
|
|
57
|
+
default="none",
|
|
58
|
+
help="Database MCP to add (default: none)",
|
|
59
|
+
)
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--browser",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Add Playwright browser-automation MCP",
|
|
64
|
+
)
|
|
65
|
+
p.add_argument(
|
|
66
|
+
"--license",
|
|
67
|
+
choices=["mit", "apache-2.0", "proprietary", "none"],
|
|
68
|
+
default="none",
|
|
69
|
+
help="LICENSE file to render (default: none — no file)",
|
|
70
|
+
)
|
|
71
|
+
p.add_argument(
|
|
72
|
+
"--owner",
|
|
73
|
+
default="",
|
|
74
|
+
help=(
|
|
75
|
+
"Project owner/team: CODEOWNERS default owner (@user or "
|
|
76
|
+
"@org/team), SECURITY contact, and LICENSE copyright holder"
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
p.add_argument(
|
|
80
|
+
"--agents",
|
|
81
|
+
default="claude",
|
|
82
|
+
help=(
|
|
83
|
+
"Comma-separated agents the project supports: claude (always "
|
|
84
|
+
"included), codex, gemini, ollama. Codex/Gemini get native "
|
|
85
|
+
"wiring overlays; ollama is instructions-level only (PI-137)"
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
p.add_argument(
|
|
89
|
+
"--mise",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help=(
|
|
92
|
+
"Render mise.toml pinning toolchain versions (mise owns versions "
|
|
93
|
+
"only; uv/bun own deps, just owns commands, .env owns environment)"
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
p.add_argument(
|
|
97
|
+
"--vscode",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help=(
|
|
100
|
+
"Render .vscode/extensions.json + minimal settings.json "
|
|
101
|
+
"(format-on-save wired to the preset formatter; nothing personal)"
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
p.add_argument(
|
|
105
|
+
"--devcontainer",
|
|
106
|
+
action="store_true",
|
|
107
|
+
help=(
|
|
108
|
+
"Render .devcontainer/ (base image + toolchain bootstrap) for "
|
|
109
|
+
"Codespaces, fresh clones, and remote agent sessions"
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
p.add_argument(
|
|
113
|
+
"--no-plugin",
|
|
114
|
+
action="store_true",
|
|
115
|
+
help=(
|
|
116
|
+
"Copy hooks/skills into the project and wire them in settings "
|
|
117
|
+
"instead of relying on the project-init-workflow plugin "
|
|
118
|
+
"(offline / no-marketplace-trust fallback; ADR-010 cutover)"
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
p.add_argument(
|
|
122
|
+
"--non-interactive",
|
|
123
|
+
action="store_true",
|
|
124
|
+
help="Skip all prompts (requires --preset, --name, --description)",
|
|
125
|
+
)
|
|
126
|
+
p.add_argument(
|
|
127
|
+
"--strict",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Fail if any {{...}} placeholder survives rendering (PI-17)",
|
|
130
|
+
)
|
|
131
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
132
|
+
return p
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _prompt(label: str, default: str = "") -> str:
|
|
136
|
+
from rich.prompt import Prompt
|
|
137
|
+
|
|
138
|
+
return Prompt.ask(label, default=default) or default
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _choose_preset_interactive(presets: list[dict]) -> dict:
|
|
142
|
+
from rich.console import Console
|
|
143
|
+
from rich.prompt import IntPrompt
|
|
144
|
+
|
|
145
|
+
console = Console()
|
|
146
|
+
console.print("\n[bold]Available presets:[/bold]")
|
|
147
|
+
for i, p in enumerate(presets, 1):
|
|
148
|
+
console.print(f" [cyan]{i}[/cyan]. {p['name']} — {p['description']}")
|
|
149
|
+
console.print()
|
|
150
|
+
|
|
151
|
+
choice = IntPrompt.ask("Choose a preset", default=1)
|
|
152
|
+
if choice < 1 or choice > len(presets):
|
|
153
|
+
console.print("[red]Invalid choice. Using preset 1.[/red]")
|
|
154
|
+
choice = 1
|
|
155
|
+
return presets[choice - 1]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _choose_mcps_interactive(catalog: list[dict]) -> list[dict]:
|
|
159
|
+
from rich.console import Console
|
|
160
|
+
from rich.prompt import Prompt
|
|
161
|
+
|
|
162
|
+
console = Console()
|
|
163
|
+
console.print("\n[bold]MCPs to install:[/bold]")
|
|
164
|
+
for i, m in enumerate(catalog, 1):
|
|
165
|
+
console.print(f" [cyan]{i}[/cyan]. {m['name']} — {m['description']}")
|
|
166
|
+
console.print()
|
|
167
|
+
|
|
168
|
+
raw = Prompt.ask(
|
|
169
|
+
"Choose MCPs (comma-separated numbers, or Enter to skip)",
|
|
170
|
+
default="",
|
|
171
|
+
)
|
|
172
|
+
if not raw.strip():
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
selected = []
|
|
176
|
+
seen: set[str] = set()
|
|
177
|
+
for part in raw.split(","):
|
|
178
|
+
part = part.strip()
|
|
179
|
+
if part.isdigit():
|
|
180
|
+
idx = int(part) - 1
|
|
181
|
+
if 0 <= idx < len(catalog) and catalog[idx]["id"] not in seen:
|
|
182
|
+
selected.append(catalog[idx])
|
|
183
|
+
seen.add(catalog[idx]["id"])
|
|
184
|
+
return selected
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _choose_db_interactive() -> dict | None:
|
|
188
|
+
from rich.console import Console
|
|
189
|
+
from rich.prompt import IntPrompt
|
|
190
|
+
|
|
191
|
+
console = Console()
|
|
192
|
+
console.print("\n[bold]Database MCP:[/bold]")
|
|
193
|
+
console.print(" [cyan]1[/cyan]. None")
|
|
194
|
+
console.print(" [cyan]2[/cyan]. Postgres")
|
|
195
|
+
console.print(" [cyan]3[/cyan]. SQLite")
|
|
196
|
+
console.print()
|
|
197
|
+
|
|
198
|
+
choice = IntPrompt.ask("Choose", default=1)
|
|
199
|
+
if choice == 2:
|
|
200
|
+
return DB_CATALOG["postgres"]
|
|
201
|
+
if choice == 3:
|
|
202
|
+
return DB_CATALOG["sqlite"]
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _choose_browser_interactive() -> bool:
|
|
207
|
+
from rich.prompt import Confirm
|
|
208
|
+
|
|
209
|
+
return Confirm.ask("\nAdd Playwright (browser automation)?", default=False)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _resolve_mcps_non_interactive(
|
|
213
|
+
mcps_arg: str,
|
|
214
|
+
db_arg: str,
|
|
215
|
+
browser_arg: bool,
|
|
216
|
+
) -> list[dict]:
|
|
217
|
+
"""Parse non-interactive MCP flags into a flat list of selected MCPs.
|
|
218
|
+
|
|
219
|
+
Raises ValueError on unknown MCP IDs — silently ignoring them hides typos.
|
|
220
|
+
"""
|
|
221
|
+
catalog_by_id = {m["id"]: m for m in MCP_CATALOG}
|
|
222
|
+
selected: list[dict] = []
|
|
223
|
+
seen: set[str] = set()
|
|
224
|
+
unknown: list[str] = []
|
|
225
|
+
|
|
226
|
+
for raw_id in mcps_arg.split(","):
|
|
227
|
+
mcp_id = raw_id.strip().lower()
|
|
228
|
+
if not mcp_id:
|
|
229
|
+
continue
|
|
230
|
+
if mcp_id not in catalog_by_id:
|
|
231
|
+
unknown.append(mcp_id)
|
|
232
|
+
continue
|
|
233
|
+
if mcp_id in seen:
|
|
234
|
+
continue
|
|
235
|
+
selected.append(catalog_by_id[mcp_id])
|
|
236
|
+
seen.add(mcp_id)
|
|
237
|
+
|
|
238
|
+
if unknown:
|
|
239
|
+
valid = ", ".join(catalog_by_id.keys())
|
|
240
|
+
msg = f"unknown MCP id(s): {', '.join(unknown)}. Valid: {valid}"
|
|
241
|
+
raise ValueError(msg)
|
|
242
|
+
|
|
243
|
+
if db_arg and db_arg != "none" and db_arg in DB_CATALOG:
|
|
244
|
+
selected.append(DB_CATALOG[db_arg])
|
|
245
|
+
|
|
246
|
+
if browser_arg:
|
|
247
|
+
selected.append(PLAYWRIGHT_MCP)
|
|
248
|
+
|
|
249
|
+
return selected
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _print_summary(target: Path, created: list[Path], preset_name: str) -> None:
|
|
253
|
+
from rich.console import Console
|
|
254
|
+
from rich.panel import Panel
|
|
255
|
+
|
|
256
|
+
console = Console()
|
|
257
|
+
|
|
258
|
+
dirs = sorted({str(p.parent) for p in created if str(p.parent) != "."})
|
|
259
|
+
files_count = len(created)
|
|
260
|
+
|
|
261
|
+
body = f"[bold]Preset:[/bold] {preset_name}\n"
|
|
262
|
+
body += f"[bold]Files:[/bold] {files_count} created/updated\n"
|
|
263
|
+
body += f"[bold]Target:[/bold] {target.resolve()}\n\n"
|
|
264
|
+
body += "[bold]Directories:[/bold]\n"
|
|
265
|
+
for d in dirs[:15]:
|
|
266
|
+
body += f" {d}/\n"
|
|
267
|
+
if len(dirs) > 15:
|
|
268
|
+
body += f" ... and {len(dirs) - 15} more\n"
|
|
269
|
+
|
|
270
|
+
console.print()
|
|
271
|
+
console.print(Panel(body.rstrip(), title="project-init", border_style="green"))
|
|
272
|
+
console.print()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _print_conflicts(conflicts: list[tuple[Path, Path]]) -> None:
|
|
276
|
+
"""Warn that user-owned files were kept; renders landed as .new siblings."""
|
|
277
|
+
from rich.console import Console
|
|
278
|
+
from rich.panel import Panel
|
|
279
|
+
|
|
280
|
+
console = Console()
|
|
281
|
+
body = (
|
|
282
|
+
"Your existing files were [bold]not overwritten[/bold]. The new "
|
|
283
|
+
"project-init version of each was written alongside as a sibling — "
|
|
284
|
+
"review and merge what you want, then delete the sibling:\n\n"
|
|
285
|
+
)
|
|
286
|
+
body += "\n".join(f" {original} → {sibling}" for original, sibling in sorted(conflicts))
|
|
287
|
+
console.print(Panel(body, title="Existing files preserved", border_style="yellow"))
|
|
288
|
+
console.print()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _print_mcp_commands(selected: list[dict]) -> None:
|
|
292
|
+
"""Print the bare claude mcp add commands for the chosen MCPs."""
|
|
293
|
+
if not selected:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
from rich.console import Console
|
|
297
|
+
from rich.panel import Panel
|
|
298
|
+
|
|
299
|
+
console = Console()
|
|
300
|
+
body = "\n".join(m["command"] for m in selected)
|
|
301
|
+
console.print(
|
|
302
|
+
Panel(
|
|
303
|
+
body,
|
|
304
|
+
title="Next step — add MCPs (run in your project)",
|
|
305
|
+
border_style="cyan",
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
console.print()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _require_non_interactive_args(
|
|
312
|
+
args: argparse.Namespace, parser: argparse.ArgumentParser
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Fail fast when --non-interactive is missing one of its required flags."""
|
|
315
|
+
missing = []
|
|
316
|
+
if not args.preset:
|
|
317
|
+
missing.append("--preset")
|
|
318
|
+
if not args.name:
|
|
319
|
+
missing.append("--name")
|
|
320
|
+
if not args.description:
|
|
321
|
+
missing.append("--description")
|
|
322
|
+
if missing:
|
|
323
|
+
parser.error(f"--non-interactive requires: {', '.join(missing)}")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _select_preset(
|
|
327
|
+
args: argparse.Namespace, parser: argparse.ArgumentParser, presets: list[dict]
|
|
328
|
+
) -> dict:
|
|
329
|
+
"""Resolve the preset from flags or interactive choice (exits on bad --preset)."""
|
|
330
|
+
if args.preset:
|
|
331
|
+
try:
|
|
332
|
+
return load_preset(args.preset)
|
|
333
|
+
except ValueError as e:
|
|
334
|
+
parser.error(str(e))
|
|
335
|
+
if args.non_interactive:
|
|
336
|
+
return presets[0]
|
|
337
|
+
return _choose_preset_interactive(presets)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _gather_inputs_interactive(
|
|
341
|
+
default_name: str,
|
|
342
|
+
) -> tuple[str, str, str, list[dict], str, str, bool, bool, bool, list[str]]:
|
|
343
|
+
"""Prompt for project basics, MCPs, governance, and opt-in overlays."""
|
|
344
|
+
project_name = _prompt("Project name", default=default_name)
|
|
345
|
+
project_description = _prompt("Description", default="")
|
|
346
|
+
language = _prompt("Language (python/node/go/none)", default="none")
|
|
347
|
+
if language not in {"python", "node", "go", "none"}:
|
|
348
|
+
language = "none"
|
|
349
|
+
|
|
350
|
+
# MCP selection — three steps.
|
|
351
|
+
selected_mcps = _choose_mcps_interactive(MCP_CATALOG)
|
|
352
|
+
db_mcp = _choose_db_interactive()
|
|
353
|
+
if db_mcp:
|
|
354
|
+
selected_mcps = selected_mcps + [db_mcp]
|
|
355
|
+
if _choose_browser_interactive():
|
|
356
|
+
selected_mcps = selected_mcps + [PLAYWRIGHT_MCP]
|
|
357
|
+
|
|
358
|
+
# Governance (PI-145).
|
|
359
|
+
owner = _prompt("Owner/team for CODEOWNERS + LICENSE (e.g. @org/team)", default="")
|
|
360
|
+
license_choice = _prompt("License (mit/apache-2.0/proprietary/none)", default="none")
|
|
361
|
+
if license_choice not in {"mit", "apache-2.0", "proprietary", "none"}:
|
|
362
|
+
license_choice = "none"
|
|
363
|
+
|
|
364
|
+
from rich.prompt import Confirm
|
|
365
|
+
|
|
366
|
+
devcontainer = Confirm.ask(
|
|
367
|
+
"Add a devcontainer (Codespaces / remote agent sessions)?", default=False
|
|
368
|
+
)
|
|
369
|
+
mise = Confirm.ask("Pin toolchain versions with mise (mise.toml)?", default=False)
|
|
370
|
+
vscode = Confirm.ask("Add shared VS Code config (extensions + format-on-save)?", default=False)
|
|
371
|
+
while True:
|
|
372
|
+
agents_raw = _prompt(
|
|
373
|
+
"Agents to support (claude always; add codex/gemini/ollama, comma-separated)",
|
|
374
|
+
default="claude",
|
|
375
|
+
)
|
|
376
|
+
try:
|
|
377
|
+
agents = resolve_agents(agents_raw)
|
|
378
|
+
break
|
|
379
|
+
except ValueError as e:
|
|
380
|
+
from rich.console import Console
|
|
381
|
+
|
|
382
|
+
Console().print(f"[red]{e}[/red]")
|
|
383
|
+
return (
|
|
384
|
+
project_name,
|
|
385
|
+
project_description,
|
|
386
|
+
language,
|
|
387
|
+
selected_mcps,
|
|
388
|
+
owner,
|
|
389
|
+
license_choice,
|
|
390
|
+
devcontainer,
|
|
391
|
+
mise,
|
|
392
|
+
vscode,
|
|
393
|
+
agents,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
_VALID_AGENTS = ("claude", "codex", "gemini", "ollama")
|
|
398
|
+
# Agents whose native wiring ships as a template layer; ollama is
|
|
399
|
+
# instructions-level only (canonical AGENTS.md + portable scripts, PI-137).
|
|
400
|
+
_AGENT_LAYERS = ("codex", "gemini")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def resolve_agents(raw: str) -> list[str]:
|
|
404
|
+
"""Parse/validate an --agents value; claude is always included first."""
|
|
405
|
+
selected = [a.strip().lower() for a in raw.split(",") if a.strip()]
|
|
406
|
+
unknown = [a for a in selected if a not in _VALID_AGENTS]
|
|
407
|
+
if unknown:
|
|
408
|
+
msg = f"unknown agent(s): {', '.join(unknown)}. Valid: {', '.join(_VALID_AGENTS)}"
|
|
409
|
+
raise ValueError(msg)
|
|
410
|
+
ordered = ["claude"]
|
|
411
|
+
ordered += [a for a in _VALID_AGENTS if a != "claude" and a in selected]
|
|
412
|
+
return ordered
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def agent_layers(agents: list[str]) -> list[str]:
|
|
416
|
+
"""Template layers contributed by the selected agents."""
|
|
417
|
+
return [a for a in _AGENT_LAYERS if a in agents]
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# Per-language tooling commands (PI-16): (lint, format, test). Empty strings
|
|
421
|
+
# when no convention applies — templates should wrap usages in
|
|
422
|
+
# {{#if python}}/{{#if node}}/etc.
|
|
423
|
+
_LANGUAGE_COMMANDS: dict[str, tuple[str, str, str]] = {
|
|
424
|
+
"python": ("uv run ruff check .", "uv run ruff format .", "uv run pytest"),
|
|
425
|
+
# node recipes call the tools directly (PI-180): a freshly scaffolded
|
|
426
|
+
# project has no package.json scripts to back `bun run lint`/`format`.
|
|
427
|
+
"node": ("bunx eslint .", "bunx @biomejs/biome format --write .", "bun test"),
|
|
428
|
+
"go": ("golangci-lint run", "gofmt -w .", "go test ./..."),
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _upgrade_main(argv: list[str]) -> int:
|
|
433
|
+
"""Parse and run the `project-init upgrade` subcommand (PI-142)."""
|
|
434
|
+
from project_init.upgrade import run_upgrade
|
|
435
|
+
|
|
436
|
+
p = argparse.ArgumentParser(
|
|
437
|
+
prog="project-init upgrade",
|
|
438
|
+
description=(
|
|
439
|
+
"Re-render the recorded preset at the current template version "
|
|
440
|
+
"and report drift. Without --apply no files are touched."
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
p.add_argument(
|
|
444
|
+
"target",
|
|
445
|
+
nargs="?",
|
|
446
|
+
default=".",
|
|
447
|
+
help="Scaffolded project directory (default: current directory)",
|
|
448
|
+
)
|
|
449
|
+
p.add_argument(
|
|
450
|
+
"--apply",
|
|
451
|
+
action="store_true",
|
|
452
|
+
help="Apply non-conflicting changes; conflicts become .new siblings",
|
|
453
|
+
)
|
|
454
|
+
p.add_argument(
|
|
455
|
+
"--no-plugin",
|
|
456
|
+
action="store_true",
|
|
457
|
+
help=(
|
|
458
|
+
"Switch the project to the no-plugin fallback on this upgrade: "
|
|
459
|
+
"re-render with copied hooks/skills + local settings wiring"
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
p.add_argument(
|
|
463
|
+
"--non-interactive",
|
|
464
|
+
action="store_true",
|
|
465
|
+
help="Accepted for CLI symmetry — upgrade never prompts",
|
|
466
|
+
)
|
|
467
|
+
args = p.parse_args(argv)
|
|
468
|
+
return run_upgrade(Path(args.target).resolve(), apply=args.apply, no_plugin=args.no_plugin)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _build_variables( # noqa: PLR0913 — one variable per wizard input
|
|
472
|
+
preset: dict,
|
|
473
|
+
*,
|
|
474
|
+
project_name: str,
|
|
475
|
+
project_description: str,
|
|
476
|
+
language: str,
|
|
477
|
+
selected_mcps: list[dict],
|
|
478
|
+
owner: str,
|
|
479
|
+
license_choice: str,
|
|
480
|
+
devcontainer: bool,
|
|
481
|
+
mise: bool,
|
|
482
|
+
vscode: bool,
|
|
483
|
+
agents: list[str],
|
|
484
|
+
no_plugin: bool,
|
|
485
|
+
) -> dict[str, str]:
|
|
486
|
+
"""Assemble the template render context from the resolved inputs."""
|
|
487
|
+
is_graphify = "graphify" in preset.get("name", "")
|
|
488
|
+
has_obsidian = "obsidian" in preset.get("layers", [])
|
|
489
|
+
lint_command, format_command, test_command = _LANGUAGE_COMMANDS.get(language, ("", "", ""))
|
|
490
|
+
return {
|
|
491
|
+
"project_name": project_name,
|
|
492
|
+
"project_description": project_description,
|
|
493
|
+
"created_date": date.today().isoformat(),
|
|
494
|
+
"project_init_version": __version__,
|
|
495
|
+
"project_init_url": __repo_url__,
|
|
496
|
+
# owner/name slug for the same-repo plugin marketplace (ADR-010)
|
|
497
|
+
"project_init_repo": __repo_url__.removeprefix("https://github.com/"),
|
|
498
|
+
"language": language,
|
|
499
|
+
"memory_stack": preset.get("vars", {}).get("memory_stack", "obsidian-only"),
|
|
500
|
+
"installed_mcps": format_installed_mcps(selected_mcps),
|
|
501
|
+
"installed_mcps_yaml": format_installed_mcps_yaml(selected_mcps),
|
|
502
|
+
"lint_command": lint_command,
|
|
503
|
+
"format_command": format_command,
|
|
504
|
+
"test_command": test_command,
|
|
505
|
+
# Governance (PI-145). license_holder falls back to the project name
|
|
506
|
+
# so a LICENSE rendered without --owner still has a copyright line.
|
|
507
|
+
# The leading "@" is required for CODEOWNERS (project_owner) but is a
|
|
508
|
+
# GitHub-handle artifact in a legal copyright notice, so strip it for
|
|
509
|
+
# the license holder only (PI-181).
|
|
510
|
+
"project_owner": owner,
|
|
511
|
+
"license": license_choice,
|
|
512
|
+
"license_holder": (owner or project_name).removeprefix("@"),
|
|
513
|
+
"created_year": date.today().strftime("%Y"),
|
|
514
|
+
# Conditional block flags (truthy/falsy strings).
|
|
515
|
+
"python": "true" if language == "python" else "",
|
|
516
|
+
"node": "true" if language == "node" else "",
|
|
517
|
+
"go": "true" if language == "go" else "",
|
|
518
|
+
"justfile": "true" if language != "none" else "",
|
|
519
|
+
"devcontainer": "true" if devcontainer else "",
|
|
520
|
+
# Multi-agent support (PI-137): the agents list drives overlay layers
|
|
521
|
+
# on upgrade re-render; per-agent flags gate conditional blocks.
|
|
522
|
+
"agents": ",".join(agents),
|
|
523
|
+
"codex": "true" if "codex" in agents else "",
|
|
524
|
+
"gemini": "true" if "gemini" in agents else "",
|
|
525
|
+
"ollama": "true" if "ollama" in agents else "",
|
|
526
|
+
"multi_agent": "true" if ("codex" in agents or "gemini" in agents) else "",
|
|
527
|
+
"other_agents": "true" if len(agents) > 1 else "",
|
|
528
|
+
# Plugin cutover (PI-165): inverse pair, same pattern as vscode_off.
|
|
529
|
+
"plugin_mode": "" if no_plugin else "true",
|
|
530
|
+
"no_plugin": "true" if no_plugin else "",
|
|
531
|
+
"mise": "true" if mise else "",
|
|
532
|
+
"vscode": "true" if vscode else "",
|
|
533
|
+
# Inverse flag: the template engine has no else-branch, and without
|
|
534
|
+
# --vscode the gitignore must keep personal .vscode/ fully ignored.
|
|
535
|
+
"vscode_off": "" if vscode else "true",
|
|
536
|
+
"graphify": "true" if is_graphify else "",
|
|
537
|
+
"obsidian": "true" if has_obsidian else "",
|
|
538
|
+
"license_mit": "true" if license_choice == "mit" else "",
|
|
539
|
+
"license_apache": "true" if license_choice == "apache-2.0" else "",
|
|
540
|
+
"license_proprietary": "true" if license_choice == "proprietary" else "",
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _resolve_inputs(args, parser, target: Path) -> tuple | None:
|
|
545
|
+
"""Resolve all scaffold inputs from flags; None means prompt instead.
|
|
546
|
+
|
|
547
|
+
Validation errors call ``parser.error`` (exits) BEFORE the target dir is
|
|
548
|
+
created (PI-20), so a typo'd flag never leaves an empty dir behind.
|
|
549
|
+
"""
|
|
550
|
+
if not args.non_interactive:
|
|
551
|
+
return None
|
|
552
|
+
try:
|
|
553
|
+
selected_mcps = _resolve_mcps_non_interactive(args.mcps, args.db, args.browser)
|
|
554
|
+
agents = resolve_agents(args.agents)
|
|
555
|
+
except ValueError as e:
|
|
556
|
+
parser.error(str(e))
|
|
557
|
+
return (
|
|
558
|
+
args.name,
|
|
559
|
+
args.description,
|
|
560
|
+
args.language or "none",
|
|
561
|
+
selected_mcps,
|
|
562
|
+
args.owner,
|
|
563
|
+
args.license,
|
|
564
|
+
args.devcontainer,
|
|
565
|
+
args.mise,
|
|
566
|
+
args.vscode,
|
|
567
|
+
agents,
|
|
568
|
+
args.no_plugin,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def main(argv: list[str] | None = None) -> int:
|
|
573
|
+
"""Run the scaffolding CLI; return the process exit code."""
|
|
574
|
+
argv = list(sys.argv[1:]) if argv is None else list(argv)
|
|
575
|
+
if argv[:1] == ["upgrade"]:
|
|
576
|
+
return _upgrade_main(argv[1:])
|
|
577
|
+
parser = _build_parser()
|
|
578
|
+
args = parser.parse_args(argv)
|
|
579
|
+
|
|
580
|
+
if args.non_interactive:
|
|
581
|
+
_require_non_interactive_args(args, parser)
|
|
582
|
+
|
|
583
|
+
target = Path(args.target).resolve()
|
|
584
|
+
|
|
585
|
+
# Select preset BEFORE creating the target directory — a typo'd --preset
|
|
586
|
+
# should fail without leaving an empty dir behind.
|
|
587
|
+
presets = list_presets()
|
|
588
|
+
if not presets:
|
|
589
|
+
sys.stderr.write("error: no presets found in templates/presets/\n")
|
|
590
|
+
return 1
|
|
591
|
+
preset = _select_preset(args, parser, presets)
|
|
592
|
+
|
|
593
|
+
# Validate non-interactive args BEFORE creating the target directory
|
|
594
|
+
# (PI-20: a bad flag must not leave an empty dir behind).
|
|
595
|
+
inputs = _resolve_inputs(args, parser, target)
|
|
596
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
597
|
+
if inputs is None:
|
|
598
|
+
inputs = _gather_inputs_interactive(default_name=target.name) + (args.no_plugin,)
|
|
599
|
+
(
|
|
600
|
+
project_name,
|
|
601
|
+
project_description,
|
|
602
|
+
language,
|
|
603
|
+
selected_mcps,
|
|
604
|
+
owner,
|
|
605
|
+
license_choice,
|
|
606
|
+
devcontainer,
|
|
607
|
+
mise,
|
|
608
|
+
vscode,
|
|
609
|
+
agents,
|
|
610
|
+
no_plugin,
|
|
611
|
+
) = inputs
|
|
612
|
+
|
|
613
|
+
# Agent overlays append to the preset's layers (PI-137); --no-plugin
|
|
614
|
+
# restores the shared hooks/skills copies via the fallback layer
|
|
615
|
+
# (PI-165, ADR-010 cutover). The preset dict is copied so the loaded
|
|
616
|
+
# definition stays pristine.
|
|
617
|
+
extra_layers = agent_layers(agents)
|
|
618
|
+
if no_plugin:
|
|
619
|
+
extra_layers = ["fallback", *extra_layers]
|
|
620
|
+
if extra_layers:
|
|
621
|
+
preset = {**preset, "layers": list(preset["layers"]) + extra_layers}
|
|
622
|
+
|
|
623
|
+
variables = _build_variables(
|
|
624
|
+
preset,
|
|
625
|
+
project_name=project_name,
|
|
626
|
+
project_description=project_description,
|
|
627
|
+
language=language,
|
|
628
|
+
selected_mcps=selected_mcps,
|
|
629
|
+
owner=owner,
|
|
630
|
+
license_choice=license_choice,
|
|
631
|
+
devcontainer=devcontainer,
|
|
632
|
+
mise=mise,
|
|
633
|
+
vscode=vscode,
|
|
634
|
+
agents=agents,
|
|
635
|
+
no_plugin=no_plugin,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# Overwrite protection (PI-179): scaffold() decides per file whether it is
|
|
639
|
+
# user-owned (first scaffold, or an unresolved `.new` sibling still pending)
|
|
640
|
+
# and writes a `.new` sibling rather than clobbering it. Always pass the list
|
|
641
|
+
# so a re-run before the user merges a prior conflict stays protected too.
|
|
642
|
+
conflicts: list[tuple[Path, Path]] = []
|
|
643
|
+
try:
|
|
644
|
+
created = scaffold(target, preset, variables, strict=args.strict, conflicts=conflicts)
|
|
645
|
+
except TemplateRenderError as e:
|
|
646
|
+
sys.stderr.write(f"error: {e}\n")
|
|
647
|
+
return 2
|
|
648
|
+
|
|
649
|
+
# Record the scaffold inputs + rendered-content hashes so a later
|
|
650
|
+
# `project-init upgrade` can re-render faithfully and detect drift.
|
|
651
|
+
from project_init.upgrade import write_scaffold_record
|
|
652
|
+
|
|
653
|
+
write_scaffold_record(target, preset["name"], variables, created)
|
|
654
|
+
_print_summary(target, created, preset["name"])
|
|
655
|
+
if conflicts:
|
|
656
|
+
_print_conflicts(conflicts)
|
|
657
|
+
_print_mcp_commands(selected_mcps)
|
|
658
|
+
return 0
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
if __name__ == "__main__":
|
|
662
|
+
sys.exit(main())
|