feat: add production init wizard UX

This commit is contained in:
thanhnv
2026-07-24 12:30:37 +07:00
parent eb3525456f
commit bfebfa9d39
7 changed files with 597 additions and 255 deletions
+113 -33
View File
@@ -17,9 +17,9 @@ What init writes into the target repo:
optional self-contained Core (--runtime vendored)
`verify` recomputes the resolved harness gate-code hash and compares it to
version.lock — the pin+VERIFY half. Drift/tamper of the global harness relative
to what the project pinned is caught here (preserves the Plan-16 "gates are
trusted code" guarantee even though the harness lives outside the repo).
version.lock — the pin+VERIFY half. Drift/tamper of the managed or vendored
runtime relative to what the project pinned is caught here (preserves the
Plan-16 "gates are trusted code" guarantee).
stdlib-only. Resolves the harness via CASAN_HARNESS_ROOT (set by the global
launcher) or --harness.
@@ -64,6 +64,7 @@ CLIENT_LABELS = {
"codex": "Codex",
"vscode-copilot": "VS Code / @casan",
}
RUNTIME_MODES = ("managed", "vendored")
def _color(code, text):
@@ -806,25 +807,61 @@ def migrate_vendored_prompt_contract(target, clients, backups):
}
def select_clients(values, interactive):
"""Normalize repeatable/comma-separated selections.
def _normalize_runtime_mode(value):
aliases = {
"1": "managed",
"global": "managed",
"shared": "managed",
"2": "vendored",
"local": "vendored",
"offline": "vendored",
"self-contained": "vendored",
"selfcontained": "vendored",
}
normalized = aliases.get(str(value).strip().lower(),
str(value).strip().lower())
if normalized not in RUNTIME_MODES:
raise ValueError(
"unknown runtime %r (choose managed or vendored)" % value)
return normalized
A plain interactive `casan init` presents the requested IDE menu. In
non-interactive automation, the historical Claude+Codex default is kept.
def select_runtime_mode(explicit, previous, interactive):
"""Resolve Core placement without surprising existing projects.
An explicit flag always wins. Re-init keeps a valid existing selection.
Only a new interactive adoption opens the placement wizard; automation
defaults to managed and never waits for input.
"""
if not values:
if interactive:
if explicit:
return _normalize_runtime_mode(explicit)
if previous in RUNTIME_MODES:
return previous
if not interactive:
return "managed"
while True:
sys.stderr.write(
"\nCore runtime placement\n\n"
" 1) Managed (Recommended)\n"
" Use the shared CASAN installation, pinned by version and hash.\n"
" Best for developer workstations and managed CI.\n\n"
" 2) Vendored\n"
" Copy production-only Core into .casan/runtime/casan-core.\n"
" Best for offline, air-gapped, or self-contained repositories.\n\n"
"Select runtime [1]: ")
sys.stderr.flush()
answer = sys.stdin.readline()
if not answer or not answer.strip():
return "managed"
try:
return _normalize_runtime_mode(answer)
except ValueError:
sys.stderr.write(
"\nEnable CASAN integrations (comma-separated numbers):\n"
" 1) Claude Code (CLI + official VS Code extension)\n"
" 2) Codex (CLI + official VS Code extension)\n"
" 3) GitHub Copilot in VS Code via explicit @casan route\n"
"Selection [1,2]: ")
sys.stderr.flush()
answer = sys.stdin.readline().strip() or "1,2"
values = [answer]
else:
values = ["claude,codex"]
"Invalid selection. Enter 1 for Managed or 2 for Vendored.\n")
def _normalize_clients(values):
aliases = {
"1": "claude",
"2": "codex",
@@ -852,6 +889,35 @@ def select_clients(values, interactive):
return selected
def select_clients(values, interactive):
"""Normalize repeatable/comma-separated selections.
A plain interactive `casan init` presents the requested IDE menu. In
non-interactive automation, the historical Claude+Codex default is kept.
"""
if values:
return _normalize_clients(values)
if not interactive:
return _normalize_clients(["claude,codex"])
while True:
sys.stderr.write(
"\nClient integrations\n\n"
" 1) Claude Code (CLI + official VS Code extension)\n"
" 2) Codex (CLI + official VS Code extension)\n"
" 3) GitHub Copilot in VS Code via explicit @casan route\n\n"
"Select clients (comma-separated) [1,2]: ")
sys.stderr.flush()
answer = sys.stdin.readline()
values = [answer.strip() or "1,2"] if answer else ["1,2"]
try:
return _normalize_clients(values)
except ValueError:
sys.stderr.write(
"Invalid selection. Enter 1, 2, 3, a comma-separated list, "
"`all`, or `none`.\n")
def merge_vscode_recommendations(target_file, clients, backups):
existed = os.path.exists(target_file)
doc = _load_json_or(target_file, {}) if existed else {}
@@ -965,20 +1031,18 @@ def cmd_init(args):
preview = (lvl == 3) # platform is a separate preview SERVICE; init applies the L2 base
apply_devkit = (lvl >= 2)
try:
clients = select_clients(args.client, sys.stdin.isatty() and not args.non_interactive)
except ValueError as error:
sys.stderr.write("casan init: %s\n" % error)
return 64
previous_config = _load_json_or(
os.path.join(target, ".casan", "config.json"), {})
previous_runtime_mode = previous_config.get("runtime_mode")
runtime_mode = (
args.runtime or
(previous_runtime_mode
if previous_runtime_mode in ("managed", "vendored") else "managed")
)
interactive = (
sys.stdin.isatty() and not args.non_interactive and not args.json)
try:
runtime_mode = select_runtime_mode(
args.runtime, previous_runtime_mode, interactive)
clients = select_clients(args.client, interactive)
except ValueError as error:
sys.stderr.write("casan init: %s\n" % error)
return 64
runtime_removed = False
runtime_files = 0
if runtime_mode == "vendored":
@@ -1734,10 +1798,25 @@ def cmd_uninstall(args):
def main(argv=None):
parser = argparse.ArgumentParser(prog="casan-init", description="CASAN hybrid adoption")
parser = argparse.ArgumentParser(
prog="casan-init",
description="Adopt CASAN governance into an existing project")
sub = parser.add_subparsers(dest="cmd")
pi = sub.add_parser("init", help="adopt CASAN into the current project (config only)")
pi = sub.add_parser(
"init",
help="adopt or reconfigure CASAN in the current project",
description=(
"Adopt CASAN governance into an existing project. New interactive "
"projects are guided through runtime and client selection; existing "
"projects preserve their current runtime unless --runtime is set."),
epilog=(
"examples:\n"
" casan init\n"
" casan init --runtime managed --client claude,codex\n"
" casan init --runtime vendored --client codex\n"
" casan init --non-interactive --runtime managed --client none"),
formatter_class=argparse.RawDescriptionHelpFormatter)
pi.add_argument("--target", help="project root (default: cwd)")
pi.add_argument("--project", help="project id (^[a-z][a-z0-9-]{1,62}$; default: dir name)")
pi.add_argument(
@@ -1746,7 +1825,8 @@ def main(argv=None):
"vscode-copilot, all, none. Interactive init shows a menu."))
pi.add_argument(
"--non-interactive", action="store_true",
help="do not prompt; defaults to the backward-compatible claude,codex set")
help=("do not prompt; new projects default to managed runtime and the "
"backward-compatible claude,codex client set"))
pi.add_argument(
"--vscode-install", choices=["auto", "yes", "no"], default="auto",
help=("install the local CASAN @casan VSIX when vscode-copilot is selected "