@deftai/directive-content 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-push +10 -9
- package/Taskfile.yml +57 -67
- package/UPGRADING.md +1 -1
- package/docs/assets/directive-lifecycle-diagram.png +0 -0
- package/docs/directive-lifecycle.md +73 -0
- package/docs/getting-started.md +5 -1
- package/package.json +3 -3
- package/packs/rules/rules-pack-0.1.json +3 -3
- package/packs/skills/skills-pack-0.1.json +22 -22
- package/scm/github.md +20 -2
- package/tasks/change.yml +16 -31
- package/tasks/ci.yml +8 -0
- package/tasks/commit.yml +12 -19
- package/tasks/core.yml +10 -0
- package/tasks/engine.yml +42 -0
- package/tasks/framework.yml +3 -0
- package/tasks/install.yml +20 -19
- package/tasks/migrate.yml +26 -15
- package/tasks/project.yml +16 -0
- package/tasks/relocate.yml +18 -48
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/templates/agents-entry.md +1 -2
- package/scripts/_agents_md.py +0 -494
- package/scripts/_cache_fetch.py +0 -635
- package/scripts/_cache_quota.py +0 -529
- package/scripts/_cache_refresh.py +0 -163
- package/scripts/_cache_validate.py +0 -209
- package/scripts/_content_root.py +0 -42
- package/scripts/_doctor_state.py +0 -277
- package/scripts/_event_detect.py +0 -305
- package/scripts/_events.py +0 -514
- package/scripts/_lifecycle_hygiene.py +0 -568
- package/scripts/_pathspec.py +0 -91
- package/scripts/_policy_show_cli.py +0 -266
- package/scripts/_precutover.py +0 -92
- package/scripts/_project_context.py +0 -224
- package/scripts/_project_definition_io.py +0 -164
- package/scripts/_relocate_snapshot.py +0 -209
- package/scripts/_relocate_states.py +0 -343
- package/scripts/_resolve_preflight_path.py +0 -152
- package/scripts/_safe_subprocess.py +0 -167
- package/scripts/_session_start_hook.py +0 -205
- package/scripts/_sor_gate_diff.py +0 -365
- package/scripts/_stdio_utf8.py +0 -59
- package/scripts/_triage_bootstrap_gitignore.py +0 -904
- package/scripts/_triage_classify_cli.py +0 -122
- package/scripts/_triage_queue_cli.py +0 -625
- package/scripts/_triage_scope_cli.py +0 -343
- package/scripts/_triage_scope_drift_cli.py +0 -121
- package/scripts/_triage_scope_ignores.py +0 -286
- package/scripts/_triage_scope_milestone.py +0 -432
- package/scripts/_triage_scope_mutations.py +0 -337
- package/scripts/_triage_scope_renderers.py +0 -207
- package/scripts/_triage_smoketest_stages.py +0 -674
- package/scripts/_triage_subscribe_cli.py +0 -140
- package/scripts/_triage_welcome_cli.py +0 -421
- package/scripts/_vbrief_build.py +0 -239
- package/scripts/_vbrief_fidelity.py +0 -479
- package/scripts/_vbrief_legacy.py +0 -589
- package/scripts/_vbrief_reconciliation.py +0 -883
- package/scripts/_vbrief_routing.py +0 -277
- package/scripts/_vbrief_safety.py +0 -778
- package/scripts/_vbrief_sources.py +0 -312
- package/scripts/_vbrief_speckit.py +0 -262
- package/scripts/_vbrief_story_quality.py +0 -353
- package/scripts/_vbrief_validation.py +0 -299
- package/scripts/build_dist.py +0 -412
- package/scripts/cache.py +0 -1078
- package/scripts/cache_scanner.py +0 -745
- package/scripts/candidates_log.py +0 -432
- package/scripts/capacity_backfill.py +0 -680
- package/scripts/capacity_show.py +0 -653
- package/scripts/ci_local.py +0 -689
- package/scripts/code_structure_validate.py +0 -765
- package/scripts/codebase_default_extractor.py +0 -495
- package/scripts/codebase_map.py +0 -304
- package/scripts/codebase_map_fresh.py +0 -104
- package/scripts/codebase_projection_registry.py +0 -94
- package/scripts/codebase_provider.py +0 -582
- package/scripts/doctor.py +0 -2551
- package/scripts/framework_commands.py +0 -505
- package/scripts/gh_rest.py +0 -882
- package/scripts/github_auth_modes.py +0 -437
- package/scripts/github_body.py +0 -292
- package/scripts/ip_risk.py +0 -531
- package/scripts/issue_emit.py +0 -670
- package/scripts/issue_ingest.py +0 -1064
- package/scripts/migrate_preflight.py +0 -418
- package/scripts/migrate_vbrief.py +0 -2677
- package/scripts/monitor_pr.py +0 -401
- package/scripts/pack_migrate_lessons.py +0 -336
- package/scripts/pack_migrate_patterns.py +0 -254
- package/scripts/pack_migrate_rules.py +0 -350
- package/scripts/pack_migrate_skills.py +0 -423
- package/scripts/pack_migrate_strategies.py +0 -311
- package/scripts/pack_migrate_swarm_spec.py +0 -250
- package/scripts/pack_render.py +0 -434
- package/scripts/packs_slice.py +0 -712
- package/scripts/platform_capabilities.py +0 -336
- package/scripts/policy.py +0 -2826
- package/scripts/policy_set.py +0 -324
- package/scripts/pr_check_closing_keywords.py +0 -524
- package/scripts/pr_check_protected_issues.py +0 -267
- package/scripts/pr_merge_readiness.py +0 -1004
- package/scripts/pr_wait_mergeable.py +0 -669
- package/scripts/prd_render.py +0 -159
- package/scripts/preflight_architecture_sor.py +0 -974
- package/scripts/preflight_branch.py +0 -289
- package/scripts/preflight_cache.py +0 -974
- package/scripts/preflight_gh.py +0 -721
- package/scripts/preflight_implementation.py +0 -272
- package/scripts/preflight_story_start.py +0 -838
- package/scripts/preflight_wip_cap.py +0 -149
- package/scripts/probe_session.py +0 -545
- package/scripts/project_render.py +0 -293
- package/scripts/quarantine_ext.py +0 -237
- package/scripts/reconcile_issues.py +0 -1442
- package/scripts/refresh-path.ps1 +0 -107
- package/scripts/release.py +0 -2030
- package/scripts/release_e2e.py +0 -1011
- package/scripts/release_publish.py +0 -486
- package/scripts/release_rollback.py +0 -980
- package/scripts/relocate.py +0 -1034
- package/scripts/resolve_changelog_unreleased.py +0 -667
- package/scripts/resolve_version.py +0 -490
- package/scripts/resume_conditions.py +0 -706
- package/scripts/ritual_sentinel.py +0 -609
- package/scripts/roadmap_render.py +0 -635
- package/scripts/rule_ownership_lint.py +0 -325
- package/scripts/scm.py +0 -591
- package/scripts/scope_audit_log.py +0 -387
- package/scripts/scope_decompose.py +0 -654
- package/scripts/scope_demote.py +0 -509
- package/scripts/scope_lifecycle.py +0 -1126
- package/scripts/scope_undo.py +0 -772
- package/scripts/session_start.py +0 -406
- package/scripts/setup_ghx.py +0 -339
- package/scripts/setup_windows.ps1 +0 -220
- package/scripts/slice_audit.py +0 -585
- package/scripts/slice_record.py +0 -530
- package/scripts/slice_record_existing.py +0 -692
- package/scripts/slug_normalize.py +0 -178
- package/scripts/spec_render.py +0 -477
- package/scripts/spec_validate.py +0 -238
- package/scripts/subagent_monitor.py +0 -658
- package/scripts/swarm_complete_cohort.py +0 -644
- package/scripts/swarm_launch.py +0 -1206
- package/scripts/swarm_readiness.py +0 -554
- package/scripts/swarm_verify_review_clean.py +0 -438
- package/scripts/swarm_worktrees.py +0 -497
- package/scripts/toolchain-check.py +0 -52
- package/scripts/triage_actions.py +0 -871
- package/scripts/triage_bootstrap.py +0 -1153
- package/scripts/triage_bulk.py +0 -630
- package/scripts/triage_classify.py +0 -932
- package/scripts/triage_help.py +0 -1685
- package/scripts/triage_queue.py +0 -1944
- package/scripts/triage_reconcile.py +0 -581
- package/scripts/triage_refresh.py +0 -643
- package/scripts/triage_scope.py +0 -999
- package/scripts/triage_scope_drift.py +0 -575
- package/scripts/triage_smoketest.py +0 -396
- package/scripts/triage_subscribe.py +0 -399
- package/scripts/triage_summary.py +0 -1011
- package/scripts/triage_welcome.py +0 -1178
- package/scripts/ts_check_lane.py +0 -86
- package/scripts/validate-links.py +0 -64
- package/scripts/validate_strategy_output.py +0 -212
- package/scripts/vbrief_activate.py +0 -228
- package/scripts/vbrief_migrate_conformance.py +0 -368
- package/scripts/vbrief_reconcile_graph.py +0 -306
- package/scripts/vbrief_reconcile_labels.py +0 -460
- package/scripts/vbrief_reconcile_umbrellas.py +0 -741
- package/scripts/vbrief_validate.py +0 -1144
- package/scripts/verify-stubs.py +0 -61
- package/scripts/verify_capacity.py +0 -160
- package/scripts/verify_encoding.py +0 -699
- package/scripts/verify_hooks_installed.py +0 -206
- package/scripts/verify_investigation.py +0 -360
- package/scripts/verify_judgment_gates.py +0 -827
- package/scripts/verify_no_task_runtime.py +0 -171
- package/scripts/verify_scm_boundary.py +0 -509
- package/scripts/verify_session_ritual.py +0 -389
- package/scripts/verify_tools.py +0 -426
- package/scripts/verify_vbrief_conformance.py +0 -478
package/scripts/setup_ghx.py
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""setup_ghx.py -- consent-gated ghx proxy installer for `task setup` (#884).
|
|
3
|
-
|
|
4
|
-
Wraps the `brunoborges/ghx <https://github.com/brunoborges/ghx>`_ caching
|
|
5
|
-
proxy installer behind an explicit-consent prompt so the maintainer's
|
|
6
|
-
``task setup`` never auto-installs network-fetched binaries by default. ghx
|
|
7
|
-
is the recommended drop-in proxy for ``gh`` -- it caches read-only API
|
|
8
|
-
calls so multi-agent swarms (and the deft ``scm:*`` task surface) do not
|
|
9
|
-
hammer the GitHub rate limiter; v0.26.0's ``scm:*`` stub already prefers
|
|
10
|
-
``ghx`` over ``gh`` at runtime via :mod:`scripts.scm`'s
|
|
11
|
-
``_BINARY_PREFERENCE`` ladder when ``ghx`` is on PATH. It is maintainer and
|
|
12
|
-
swarm tooling, not a consumer installer prerequisite: consumer installs require
|
|
13
|
-
``gh`` for GitHub-backed workflows and transparently ignore ``ghx`` when absent.
|
|
14
|
-
|
|
15
|
-
Behaviour matrix:
|
|
16
|
-
|
|
17
|
-
- ``ghx`` already on PATH -> print a one-line acknowledgement, exit 0.
|
|
18
|
-
- ``ghx`` missing, default (interactive) -> prompt for explicit consent
|
|
19
|
-
(default *no*); on decline print a one-line "recommended for speed"
|
|
20
|
-
note and exit 0.
|
|
21
|
-
- ``ghx`` missing, ``--yes`` flag -> skip the prompt and install
|
|
22
|
-
unconditionally (CI / scripted approval path).
|
|
23
|
-
- ``ghx`` missing, ``--check`` flag -> never install, never prompt;
|
|
24
|
-
print a one-line note when missing and exit 0. Used by the Taskfile
|
|
25
|
-
step so ``task setup`` is non-interactive on a clean re-run.
|
|
26
|
-
|
|
27
|
-
Install dispatch is host-platform aware:
|
|
28
|
-
|
|
29
|
-
- Windows -> ``pwsh -Command "irm <install.ps1> | iex"``
|
|
30
|
-
- macOS / Linux -> ``curl -fsSL <install.sh> | bash``
|
|
31
|
-
|
|
32
|
-
The upstream URLs come from the ghx README; both installers honour the
|
|
33
|
-
upstream's documented contract. Network failures during install are
|
|
34
|
-
surfaced as exit 1 (the script does NOT retry).
|
|
35
|
-
|
|
36
|
-
Three-state exit (mirrors :mod:`scripts.preflight_branch` (#747) and
|
|
37
|
-
:mod:`scripts.migrate_preflight` (#793)):
|
|
38
|
-
|
|
39
|
-
- ``0`` -- ghx already present, user declined, or install succeeded.
|
|
40
|
-
- ``1`` -- install failure (subprocess non-zero, network error, or no
|
|
41
|
-
install method available for the detected host).
|
|
42
|
-
- ``2`` -- config error (e.g. ``--yes`` and ``--check`` combined).
|
|
43
|
-
|
|
44
|
-
This script is intentionally pure-stdlib + ``subprocess`` so it can be
|
|
45
|
-
invoked from a fresh maintainer worktree before ``uv sync`` has run.
|
|
46
|
-
|
|
47
|
-
Refs #884.
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
from __future__ import annotations
|
|
51
|
-
|
|
52
|
-
import argparse
|
|
53
|
-
import os
|
|
54
|
-
import platform
|
|
55
|
-
import shutil
|
|
56
|
-
import subprocess
|
|
57
|
-
import sys
|
|
58
|
-
from collections.abc import Sequence
|
|
59
|
-
|
|
60
|
-
# ---------------------------------------------------------------------------
|
|
61
|
-
# Constants
|
|
62
|
-
# ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
#: Pinned ghx version. CI workflows reference the same constant via env-var
|
|
65
|
-
#: indirection (see ``.github/workflows/ci.yml``); bump both surfaces in
|
|
66
|
-
#: lockstep so a future ghx security advisory only requires one edit.
|
|
67
|
-
GHX_VERSION: str = "v1.5.1"
|
|
68
|
-
|
|
69
|
-
#: Upstream installer URLs, pinned to :data:`GHX_VERSION` so the script the
|
|
70
|
-
#: pipe-trampoline executes is the script as it existed at the pinned tag
|
|
71
|
-
#: (closes Greptile #950 P2). The PowerShell installer drops binaries under
|
|
72
|
-
#: ``%LOCALAPPDATA%\\ghx\\bin`` and adds them to the user PATH; the bash
|
|
73
|
-
#: installer drops them under ``/usr/local/bin`` (override via
|
|
74
|
-
#: ``INSTALL_DIR=...``). Pinning the URL by tag rather than ``main``
|
|
75
|
-
#: prevents an upstream regression -- or a hypothetical compromise of the
|
|
76
|
-
#: default branch between when CI runs and when an operator runs
|
|
77
|
-
#: ``task setup:ghx`` -- from silently feeding altered shell into either
|
|
78
|
-
#: trampoline. Bump in lockstep with ``.github/workflows/ci.yml``
|
|
79
|
-
#: ``env.GHX_VERSION`` and the URLs under each ``Install ghx`` step.
|
|
80
|
-
INSTALL_PS1_URL: str = (
|
|
81
|
-
f"https://raw.githubusercontent.com/brunoborges/ghx/{GHX_VERSION}/install.ps1"
|
|
82
|
-
)
|
|
83
|
-
INSTALL_SH_URL: str = (
|
|
84
|
-
f"https://raw.githubusercontent.com/brunoborges/ghx/{GHX_VERSION}/install.sh"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# ---------------------------------------------------------------------------
|
|
89
|
-
# Detection
|
|
90
|
-
# ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def ghx_present() -> bool:
|
|
94
|
-
"""Return True when ``ghx`` (or ``ghx.exe`` on Windows) is on PATH.
|
|
95
|
-
|
|
96
|
-
Mirrors :func:`scripts.scm.resolve_binary` so the Taskfile-side check
|
|
97
|
-
and the ``scm:*`` runtime ladder agree on the detection contract.
|
|
98
|
-
"""
|
|
99
|
-
return shutil.which("ghx") is not None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def detect_host() -> str:
|
|
103
|
-
"""Return the canonical host tag: ``windows`` / ``darwin`` / ``linux``.
|
|
104
|
-
|
|
105
|
-
Falls back to ``platform.system().lower()`` for anything else; the
|
|
106
|
-
install-dispatch branch raises a friendly error in that case.
|
|
107
|
-
"""
|
|
108
|
-
system = platform.system().lower()
|
|
109
|
-
# Normalise the macOS reporting (``platform.system()`` returns
|
|
110
|
-
# ``Darwin``); other hosts come through with sensible names already.
|
|
111
|
-
if system == "darwin":
|
|
112
|
-
return "darwin"
|
|
113
|
-
if system == "windows":
|
|
114
|
-
return "windows"
|
|
115
|
-
if system == "linux":
|
|
116
|
-
return "linux"
|
|
117
|
-
return system
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# ---------------------------------------------------------------------------
|
|
121
|
-
# Consent prompt
|
|
122
|
-
# ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def prompt_consent(stream_in: object | None = None, stream_out: object | None = None) -> bool:
|
|
126
|
-
"""Render an interactive y/N consent prompt; default *no*.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
stream_in: Optional input stream override (tests inject
|
|
130
|
-
``io.StringIO``). Defaults to ``sys.stdin``.
|
|
131
|
-
stream_out: Optional output stream override. Defaults to
|
|
132
|
-
``sys.stdout``.
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
True when the operator typed ``y`` / ``yes`` (case-insensitive);
|
|
136
|
-
False on empty / EOF / anything else. Default-deny matches the
|
|
137
|
-
#884 constraint that install MUST require explicit consent.
|
|
138
|
-
"""
|
|
139
|
-
sin = stream_in if stream_in is not None else sys.stdin
|
|
140
|
-
sout = stream_out if stream_out is not None else sys.stdout
|
|
141
|
-
print(
|
|
142
|
-
"\n[setup_ghx] ghx is the recommended GitHub CLI cache proxy for deft "
|
|
143
|
-
"maintainers (prevents rate-limiting in multi-agent swarms; speeds up "
|
|
144
|
-
"scm:* calls). Consumer projects only require gh.",
|
|
145
|
-
file=sout,
|
|
146
|
-
)
|
|
147
|
-
print(f"[setup_ghx] Upstream: https://github.com/brunoborges/ghx ({GHX_VERSION})", file=sout)
|
|
148
|
-
print("[setup_ghx] Install ghx via the upstream installer? [y/N]: ", end="", file=sout)
|
|
149
|
-
sout.flush()
|
|
150
|
-
try:
|
|
151
|
-
# ``readline`` returns ``""`` on EOF (e.g. piped non-tty); treat
|
|
152
|
-
# as decline so a non-interactive ``task setup`` never installs by
|
|
153
|
-
# accident -- ``--yes`` is the explicit non-interactive path.
|
|
154
|
-
line = sin.readline()
|
|
155
|
-
except (EOFError, KeyboardInterrupt):
|
|
156
|
-
return False
|
|
157
|
-
answer = (line or "").strip().lower()
|
|
158
|
-
return answer in ("y", "yes")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# ---------------------------------------------------------------------------
|
|
162
|
-
# Install dispatch
|
|
163
|
-
# ---------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def build_install_command(host: str) -> list[str]:
|
|
167
|
-
"""Return the argv that will fetch + run the upstream installer.
|
|
168
|
-
|
|
169
|
-
Tests assert against the returned shape so a regression that changed
|
|
170
|
-
the installer URL or the shell trampoline would fail loudly here
|
|
171
|
-
rather than silently dispatching the wrong network call.
|
|
172
|
-
|
|
173
|
-
Args:
|
|
174
|
-
host: One of ``windows`` / ``darwin`` / ``linux``. Anything else
|
|
175
|
-
raises :class:`RuntimeError`.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
Argv list ready for :func:`subprocess.run`.
|
|
179
|
-
"""
|
|
180
|
-
if host == "windows":
|
|
181
|
-
# Use ``pwsh`` (PS 7+) when available, falling back to
|
|
182
|
-
# ``powershell`` (Windows PS 5.1). The installer itself is
|
|
183
|
-
# ASCII-only per the upstream README so PS 5.1's cp1252 default
|
|
184
|
-
# does not corrupt the script body.
|
|
185
|
-
ps_bin = shutil.which("pwsh") or shutil.which("powershell") or "powershell"
|
|
186
|
-
return [
|
|
187
|
-
ps_bin,
|
|
188
|
-
"-NoProfile",
|
|
189
|
-
"-ExecutionPolicy",
|
|
190
|
-
"Bypass",
|
|
191
|
-
"-Command",
|
|
192
|
-
f"irm {INSTALL_PS1_URL} | iex",
|
|
193
|
-
]
|
|
194
|
-
if host in ("darwin", "linux"):
|
|
195
|
-
# ``curl | bash`` mirrors the upstream README's "Quick install
|
|
196
|
-
# script" path. ``-fsSL`` makes curl fail loud on HTTP 4xx/5xx
|
|
197
|
-
# rather than piping an HTML error page into bash.
|
|
198
|
-
return ["bash", "-c", f"curl -fsSL {INSTALL_SH_URL} | bash"]
|
|
199
|
-
raise RuntimeError(
|
|
200
|
-
f"no upstream ghx installer available for host {host!r}; "
|
|
201
|
-
"see https://github.com/brunoborges/ghx#install for manual options"
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def install_ghx(host: str, *, runner: object | None = None) -> int:
|
|
206
|
-
"""Invoke the upstream installer. Returns the subprocess exit code.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
host: The detected host tag (see :func:`detect_host`).
|
|
210
|
-
runner: Optional ``subprocess.run``-compatible callable for
|
|
211
|
-
test injection. Defaults to :func:`subprocess.run`.
|
|
212
|
-
|
|
213
|
-
Returns:
|
|
214
|
-
The installer's exit code (0 on success).
|
|
215
|
-
|
|
216
|
-
Closes Greptile #950 P1: ``GHX_VERSION`` MUST be injected into the
|
|
217
|
-
subprocess environment because the upstream ``install.sh`` /
|
|
218
|
-
``install.ps1`` honour ``${GHX_VERSION}`` as the version-pin hook.
|
|
219
|
-
Without this, the version constant in this module was a no-op at
|
|
220
|
-
install time -- the operator-side ``task setup:ghx`` could install a
|
|
221
|
-
different binary version than the CI pre-install step despite the
|
|
222
|
-
documented lockstep contract.
|
|
223
|
-
"""
|
|
224
|
-
cmd = build_install_command(host)
|
|
225
|
-
run = runner if runner is not None else subprocess.run
|
|
226
|
-
print(f"[setup_ghx] Invoking upstream installer: {' '.join(cmd)}", file=sys.stderr)
|
|
227
|
-
install_env = {**os.environ, "GHX_VERSION": GHX_VERSION}
|
|
228
|
-
proc = run(cmd, check=False, env=install_env)
|
|
229
|
-
return int(getattr(proc, "returncode", 1))
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# ---------------------------------------------------------------------------
|
|
233
|
-
# CLI
|
|
234
|
-
# ---------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def _build_parser() -> argparse.ArgumentParser:
|
|
238
|
-
parser = argparse.ArgumentParser(
|
|
239
|
-
prog="setup_ghx.py",
|
|
240
|
-
description=(
|
|
241
|
-
"Consent-gated installer for the ghx GitHub CLI cache proxy "
|
|
242
|
-
"(brunoborges/ghx). See #884 for the adoption rationale."
|
|
243
|
-
),
|
|
244
|
-
)
|
|
245
|
-
parser.add_argument(
|
|
246
|
-
"--yes",
|
|
247
|
-
action="store_true",
|
|
248
|
-
help=(
|
|
249
|
-
"Non-interactive consent (CI / scripted approval). Skip the y/N "
|
|
250
|
-
"prompt and install unconditionally when ghx is missing."
|
|
251
|
-
),
|
|
252
|
-
)
|
|
253
|
-
parser.add_argument(
|
|
254
|
-
"--check",
|
|
255
|
-
action="store_true",
|
|
256
|
-
help=(
|
|
257
|
-
"Detection-only mode: print whether ghx is on PATH, then exit 0. "
|
|
258
|
-
"Never prompt, never install. Used by the Taskfile step so "
|
|
259
|
-
"`task setup` stays non-interactive on a clean re-run."
|
|
260
|
-
),
|
|
261
|
-
)
|
|
262
|
-
return parser
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def main(argv: Sequence[str] | None = None) -> int:
|
|
266
|
-
"""Entry point. See module docstring for the exit-code contract."""
|
|
267
|
-
parser = _build_parser()
|
|
268
|
-
args = parser.parse_args(argv)
|
|
269
|
-
|
|
270
|
-
if args.yes and args.check:
|
|
271
|
-
print(
|
|
272
|
-
"[setup_ghx] error: --yes and --check are mutually exclusive.",
|
|
273
|
-
file=sys.stderr,
|
|
274
|
-
)
|
|
275
|
-
return 2
|
|
276
|
-
|
|
277
|
-
if ghx_present():
|
|
278
|
-
# ASCII-only success line so PS 5.1 cp1252 stdout never corrupts
|
|
279
|
-
# the output. The leading tag mirrors `[setup_windows] ...` in
|
|
280
|
-
# scripts/setup_windows.ps1 so operators see one consistent
|
|
281
|
-
# provenance prefix across the setup surface.
|
|
282
|
-
print("[setup_ghx] ghx already on PATH -- skipping install.")
|
|
283
|
-
return 0
|
|
284
|
-
|
|
285
|
-
if args.check:
|
|
286
|
-
print(
|
|
287
|
-
"[setup_ghx] ghx not on PATH; recommended for speed -- "
|
|
288
|
-
"run `task setup` (without --check) to opt in. Consumer projects "
|
|
289
|
-
"only require gh. Refs #884."
|
|
290
|
-
)
|
|
291
|
-
return 0
|
|
292
|
-
|
|
293
|
-
consent: bool
|
|
294
|
-
if args.yes:
|
|
295
|
-
consent = True
|
|
296
|
-
print("[setup_ghx] --yes provided; skipping interactive consent prompt.")
|
|
297
|
-
else:
|
|
298
|
-
# Honour the documented opt-out env-var so non-interactive shells
|
|
299
|
-
# (CI hooks, dotfile bootstraps) can suppress the prompt without
|
|
300
|
-
# passing --check explicitly. This is purely additive -- the
|
|
301
|
-
# default still requires explicit consent.
|
|
302
|
-
if os.environ.get("DEFT_SETUP_GHX_SKIP", "").strip() in ("1", "true", "yes"):
|
|
303
|
-
print(
|
|
304
|
-
"[setup_ghx] DEFT_SETUP_GHX_SKIP set; skipping ghx install. "
|
|
305
|
-
"Refs #884."
|
|
306
|
-
)
|
|
307
|
-
return 0
|
|
308
|
-
consent = prompt_consent()
|
|
309
|
-
|
|
310
|
-
if not consent:
|
|
311
|
-
print(
|
|
312
|
-
"[setup_ghx] Skipping ghx install. ghx is recommended for speed "
|
|
313
|
-
"for maintainers and swarm runs; consumer projects only require gh "
|
|
314
|
-
"(see https://github.com/brunoborges/ghx, #884)."
|
|
315
|
-
)
|
|
316
|
-
return 0
|
|
317
|
-
|
|
318
|
-
host = detect_host()
|
|
319
|
-
try:
|
|
320
|
-
rc = install_ghx(host)
|
|
321
|
-
except RuntimeError as exc:
|
|
322
|
-
print(f"[setup_ghx] error: {exc}", file=sys.stderr)
|
|
323
|
-
return 1
|
|
324
|
-
if rc != 0:
|
|
325
|
-
print(
|
|
326
|
-
f"[setup_ghx] error: upstream installer exited {rc}. "
|
|
327
|
-
"See https://github.com/brunoborges/ghx#install for manual options.",
|
|
328
|
-
file=sys.stderr,
|
|
329
|
-
)
|
|
330
|
-
return 1
|
|
331
|
-
print(
|
|
332
|
-
"[setup_ghx] ghx installed. Open a fresh shell so the updated PATH "
|
|
333
|
-
"takes effect, then re-run `task setup` to verify."
|
|
334
|
-
)
|
|
335
|
-
return 0
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if __name__ == "__main__":
|
|
339
|
-
raise SystemExit(main())
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
# setup_windows.ps1 -- idempotent winget bootstrap for the deft Windows
|
|
2
|
-
# maintainer toolchain (Go, Python 3.12+, uv, Task, GitHub CLI).
|
|
3
|
-
#
|
|
4
|
-
# Probes each tool via Get-Command first; only invokes `winget install` when
|
|
5
|
-
# the tool is missing. After installs, dot-sources scripts/refresh-path.ps1 so
|
|
6
|
-
# the running session sees the newly-installed binaries without requiring a
|
|
7
|
-
# fresh shell.
|
|
8
|
-
#
|
|
9
|
-
# Usage:
|
|
10
|
-
# pwsh -ExecutionPolicy Bypass -File scripts\setup_windows.ps1
|
|
11
|
-
# # or, via the parent Taskfile alias:
|
|
12
|
-
# task setup:toolchain
|
|
13
|
-
#
|
|
14
|
-
# Tests: tests/scripts/test_setup_windows.ps1
|
|
15
|
-
# Companion: scripts/refresh-path.ps1
|
|
16
|
-
# Issue: #902
|
|
17
|
-
#
|
|
18
|
-
# ASCII-only by policy (AGENTS.md PowerShell rule). Do not introduce em
|
|
19
|
-
# dashes, smart quotes, arrows, or other non-ASCII glyphs in this file.
|
|
20
|
-
|
|
21
|
-
[CmdletBinding()]
|
|
22
|
-
param(
|
|
23
|
-
# When set, the script only reports what it would do without invoking
|
|
24
|
-
# winget. Useful for dry-run validation in tests and CI.
|
|
25
|
-
[switch] $WhatIfOnly,
|
|
26
|
-
|
|
27
|
-
# Test seam: list of probe names to treat as missing regardless of the
|
|
28
|
-
# actual host PATH. Lets the regression suite exercise the
|
|
29
|
-
# "winget install" branch without mutating the host.
|
|
30
|
-
[string[]] $ForceMissing = @(),
|
|
31
|
-
|
|
32
|
-
# Test seam: scriptblock invoked instead of winget for each missing tool.
|
|
33
|
-
# The block is invoked with the canonical winget package id as its single
|
|
34
|
-
# argument. When unset, the script invokes `winget install` directly.
|
|
35
|
-
[scriptblock] $InstallOverride,
|
|
36
|
-
|
|
37
|
-
# Test seam: when set, skips the post-install dot-source of
|
|
38
|
-
# refresh-path.ps1. Tests use this to keep $env:PATH stable.
|
|
39
|
-
[switch] $SkipRefresh
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# NOTE: $ErrorActionPreference is set INSIDE Invoke-DeftWindowsSetup so it
|
|
43
|
-
# scopes to the function body and never leaks into a dot-source caller's
|
|
44
|
-
# scope (e.g. the Pester Describe blocks that dot-source this file via
|
|
45
|
-
# BeforeAll). See #909 cycle-3 P1 finding.
|
|
46
|
-
|
|
47
|
-
# Capture $PSScriptRoot at script load time (before any function definitions)
|
|
48
|
-
# so the refresh-path.ps1 lookup inside Invoke-DeftWindowsSetup remains
|
|
49
|
-
# correct when the script is dot-sourced from a different directory. The
|
|
50
|
-
# $script: scope qualifier ensures the value persists across the function-
|
|
51
|
-
# definition / function-call boundary. See #909 cycle-3 P1 finding.
|
|
52
|
-
$script:DeftSetupScriptRoot = $PSScriptRoot
|
|
53
|
-
|
|
54
|
-
# Tool registry. Each entry maps a probe command (the binary name resolved
|
|
55
|
-
# via Get-Command) to its canonical winget package id. The id list is the
|
|
56
|
-
# acceptance criterion in #902 plus the GitHub.cli sibling.
|
|
57
|
-
$DeftWindowsTools = @(
|
|
58
|
-
[pscustomobject]@{ Name = 'go'; Probe = 'go'; WingetId = 'GoLang.Go' },
|
|
59
|
-
[pscustomobject]@{ Name = 'python'; Probe = 'python'; WingetId = 'Python.Python.3.12' },
|
|
60
|
-
[pscustomobject]@{ Name = 'uv'; Probe = 'uv'; WingetId = 'astral-sh.uv' },
|
|
61
|
-
[pscustomobject]@{ Name = 'task'; Probe = 'task'; WingetId = 'Task.Task' },
|
|
62
|
-
[pscustomobject]@{ Name = 'gh'; Probe = 'gh'; WingetId = 'GitHub.cli' }
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
function Test-DeftWindowsAppsStub {
|
|
66
|
-
[CmdletBinding()]
|
|
67
|
-
[OutputType([bool])]
|
|
68
|
-
param(
|
|
69
|
-
[Parameter(Mandatory)]
|
|
70
|
-
[AllowNull()]
|
|
71
|
-
[object] $Command
|
|
72
|
-
)
|
|
73
|
-
# Windows App Installer ships %LOCALAPPDATA%\Microsoft\WindowsApps\<name>.exe
|
|
74
|
-
# stubs (notably python.exe) that redirect to the Microsoft Store rather
|
|
75
|
-
# than launching a real interpreter. Get-Command resolves these stubs, so
|
|
76
|
-
# a naive presence check causes `winget install` to be skipped silently.
|
|
77
|
-
# Treat any binary whose Source path is anchored under WindowsApps as a
|
|
78
|
-
# stub so the install branch fires on stock Windows 10/11 hosts.
|
|
79
|
-
if ($null -eq $Command) { return $false }
|
|
80
|
-
if (-not $Command.Source) { return $false }
|
|
81
|
-
return ($Command.Source -match '\\WindowsApps\\')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function Test-DeftToolPresent {
|
|
85
|
-
[CmdletBinding()]
|
|
86
|
-
[OutputType([bool])]
|
|
87
|
-
param(
|
|
88
|
-
[Parameter(Mandatory)]
|
|
89
|
-
[string] $Probe,
|
|
90
|
-
|
|
91
|
-
[string[]] $ForceMissing = @()
|
|
92
|
-
)
|
|
93
|
-
if ($ForceMissing -contains $Probe) { return $false }
|
|
94
|
-
$cmd = Get-Command -Name $Probe -ErrorAction SilentlyContinue
|
|
95
|
-
if ($null -eq $cmd) { return $false }
|
|
96
|
-
if (Test-DeftWindowsAppsStub -Command $cmd) { return $false }
|
|
97
|
-
return $true
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function Test-DeftWingetSuccess {
|
|
101
|
-
[CmdletBinding()]
|
|
102
|
-
[OutputType([bool])]
|
|
103
|
-
param(
|
|
104
|
-
[Parameter(Mandatory)]
|
|
105
|
-
[int] $ExitCode
|
|
106
|
-
)
|
|
107
|
-
# 3010 = ERROR_SUCCESS_REBOOT_REQUIRED -- the install succeeded but a
|
|
108
|
-
# reboot is needed (Python's MSI, Go's installer, etc. propagate this
|
|
109
|
-
# via winget). Treating it as a failure causes the script to add the
|
|
110
|
-
# tool to $failed and exit 1 even though the binary is installed --
|
|
111
|
-
# which means a fresh-machine bootstrap visibly "fails" on first run.
|
|
112
|
-
# The downstream PATH refresh handles the session PATH; an actual
|
|
113
|
-
# reboot is only required for kernel-level changes that this toolchain
|
|
114
|
-
# does not produce. See #909 cycle-4 P1 finding.
|
|
115
|
-
return ($ExitCode -eq 0 -or $ExitCode -eq 3010)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function Invoke-DeftWingetInstall {
|
|
119
|
-
[CmdletBinding()]
|
|
120
|
-
param(
|
|
121
|
-
[Parameter(Mandatory)]
|
|
122
|
-
[string] $WingetId
|
|
123
|
-
)
|
|
124
|
-
$wingetArgs = @(
|
|
125
|
-
'install',
|
|
126
|
-
'--id', $WingetId,
|
|
127
|
-
'-e',
|
|
128
|
-
'--silent',
|
|
129
|
-
'--accept-source-agreements',
|
|
130
|
-
'--accept-package-agreements'
|
|
131
|
-
)
|
|
132
|
-
& winget @wingetArgs
|
|
133
|
-
if (-not (Test-DeftWingetSuccess -ExitCode $LASTEXITCODE)) {
|
|
134
|
-
throw ("winget install --id {0} exited with code {1}" -f $WingetId, $LASTEXITCODE)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function Invoke-DeftWindowsSetup {
|
|
139
|
-
[CmdletBinding()]
|
|
140
|
-
[OutputType([pscustomobject])]
|
|
141
|
-
param(
|
|
142
|
-
[switch] $WhatIfOnly,
|
|
143
|
-
[string[]] $ForceMissing = @(),
|
|
144
|
-
[scriptblock] $InstallOverride,
|
|
145
|
-
[switch] $SkipRefresh
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
# Scope $ErrorActionPreference to the function body so it does not
|
|
149
|
-
# mutate the caller's scope when this file is dot-sourced. See #909.
|
|
150
|
-
$ErrorActionPreference = 'Stop'
|
|
151
|
-
|
|
152
|
-
$installed = New-Object System.Collections.ArrayList
|
|
153
|
-
$alreadyPresent = New-Object System.Collections.ArrayList
|
|
154
|
-
$failed = New-Object System.Collections.ArrayList
|
|
155
|
-
|
|
156
|
-
foreach ($tool in $DeftWindowsTools) {
|
|
157
|
-
$present = Test-DeftToolPresent -Probe $tool.Probe -ForceMissing $ForceMissing
|
|
158
|
-
if ($present) {
|
|
159
|
-
[void]$alreadyPresent.Add($tool.Name)
|
|
160
|
-
Write-Host ("[setup_windows] {0}: present (skip)" -f $tool.Name)
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
Write-Host ("[setup_windows] {0}: missing -- installing {1}" -f $tool.Name, $tool.WingetId)
|
|
165
|
-
if ($WhatIfOnly) {
|
|
166
|
-
[void]$installed.Add($tool.Name)
|
|
167
|
-
continue
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
if ($null -ne $InstallOverride) {
|
|
172
|
-
& $InstallOverride $tool.WingetId
|
|
173
|
-
} else {
|
|
174
|
-
Invoke-DeftWingetInstall -WingetId $tool.WingetId
|
|
175
|
-
}
|
|
176
|
-
[void]$installed.Add($tool.Name)
|
|
177
|
-
} catch {
|
|
178
|
-
Write-Warning ("[setup_windows] failed to install {0}: {1}" -f $tool.Name, $_)
|
|
179
|
-
[void]$failed.Add($tool.Name)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (-not $SkipRefresh -and -not $WhatIfOnly -and $installed.Count -gt 0) {
|
|
184
|
-
# Use the script-scope variable captured at dot-source time. Bare
|
|
185
|
-
# $PSScriptRoot here would resolve to the caller's directory when
|
|
186
|
-
# this function is invoked from a dot-sourced context. See #909.
|
|
187
|
-
$refreshScript = Join-Path $script:DeftSetupScriptRoot 'refresh-path.ps1'
|
|
188
|
-
if (Test-Path -LiteralPath $refreshScript) {
|
|
189
|
-
. $refreshScript
|
|
190
|
-
} else {
|
|
191
|
-
Write-Warning ("[setup_windows] refresh-path.ps1 not found at {0}" -f $refreshScript)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
$installedStr = if ($installed.Count -gt 0) { $installed -join ', ' } else { 'none' }
|
|
196
|
-
$presentStr = if ($alreadyPresent.Count -gt 0) { $alreadyPresent -join ', ' } else { 'none' }
|
|
197
|
-
Write-Host ("[setup_windows] Installed: {0}. Already present: {1}." -f $installedStr, $presentStr)
|
|
198
|
-
if ($failed.Count -gt 0) {
|
|
199
|
-
Write-Warning ("[setup_windows] Failed: {0}" -f ($failed -join ', '))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return [pscustomobject]@{
|
|
203
|
-
Installed = @($installed)
|
|
204
|
-
AlreadyPresent = @($alreadyPresent)
|
|
205
|
-
Failed = @($failed)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# Run the bootstrap unless the script was dot-sourced (the test suite dot-
|
|
210
|
-
# sources to access the helper functions without triggering the main flow).
|
|
211
|
-
if ($MyInvocation.InvocationName -ne '.') {
|
|
212
|
-
$result = Invoke-DeftWindowsSetup `
|
|
213
|
-
-WhatIfOnly:$WhatIfOnly `
|
|
214
|
-
-ForceMissing $ForceMissing `
|
|
215
|
-
-InstallOverride $InstallOverride `
|
|
216
|
-
-SkipRefresh:$SkipRefresh
|
|
217
|
-
if ($result.Failed.Count -gt 0) {
|
|
218
|
-
exit 1
|
|
219
|
-
}
|
|
220
|
-
}
|