feat: add production init wizard UX
This commit is contained in:
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user