nobrainer-tech-flow 0.0.0-stage → 2.4.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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.codex-plugin/plugin.json +47 -0
- package/.opencode/INSTALL.md +37 -0
- package/.opencode/plugins/nobrainer-tech-skills.js +66 -0
- package/.pi/extensions/nobrainer-tech-skills.js +64 -0
- package/AGENTS.md +204 -0
- package/CLAUDE.md +204 -0
- package/CONTRIBUTING.md +70 -0
- package/GEMINI.md +1 -0
- package/LICENSE +21 -0
- package/README.md +658 -2
- package/SECURITY.md +33 -0
- package/adapters/bootstrap.md +27 -0
- package/assets/nobrainer-tech-logo.svg +1 -0
- package/assets/nobrainer-workflow.svg +155 -0
- package/assets/plugin-logo.png +0 -0
- package/bin/nobrainer-tech-flow.mjs +48 -0
- package/docs/BOUNDED_RUNNER.md +60 -0
- package/docs/CHAT_PLUGINS.md +29 -0
- package/docs/COMPATIBILITY.md +371 -0
- package/docs/INSTALL.md +626 -0
- package/docs/MIGRATION_TO_FLOW.md +117 -0
- package/docs/NAMING.md +39 -0
- package/docs/PRIVACY.md +9 -0
- package/docs/SESSION_RESTART.md +70 -0
- package/docs/SKILL_CURATION.md +124 -0
- package/docs/TESTING.md +232 -0
- package/docs/TRY_IT.md +109 -0
- package/docs/releases/v1.0.0.md +81 -0
- package/docs/releases/v1.1.0.md +85 -0
- package/docs/releases/v1.10.0.md +34 -0
- package/docs/releases/v1.11.0-publication-readback.md +25 -0
- package/docs/releases/v1.11.0.md +42 -0
- package/docs/releases/v1.12.0.md +30 -0
- package/docs/releases/v1.13.0.md +46 -0
- package/docs/releases/v1.14.0.md +43 -0
- package/docs/releases/v1.14.1.md +21 -0
- package/docs/releases/v1.2.0.md +87 -0
- package/docs/releases/v1.2.1.md +92 -0
- package/docs/releases/v1.3.0-publication-readback.md +69 -0
- package/docs/releases/v1.3.0.md +66 -0
- package/docs/releases/v1.3.1-publication-readback.md +76 -0
- package/docs/releases/v1.3.1.md +59 -0
- package/docs/releases/v1.4.0-publication-readback.md +81 -0
- package/docs/releases/v1.4.0.md +43 -0
- package/docs/releases/v1.5.0-publication-readback.md +76 -0
- package/docs/releases/v1.5.0.md +54 -0
- package/docs/releases/v1.6.0.md +64 -0
- package/docs/releases/v1.6.1-publication-readback.md +53 -0
- package/docs/releases/v1.6.1.md +82 -0
- package/docs/releases/v1.7.0.md +42 -0
- package/docs/releases/v1.7.1-publication-readback.md +64 -0
- package/docs/releases/v1.7.1.md +49 -0
- package/docs/releases/v1.8.0.md +63 -0
- package/docs/releases/v1.8.1.md +32 -0
- package/docs/releases/v1.9.0.md +33 -0
- package/docs/releases/v2.0.0-candidate.md +28 -0
- package/docs/releases/v2.0.0.md +53 -0
- package/docs/releases/v2.0.1.md +135 -0
- package/docs/releases/v2.0.2.md +189 -0
- package/docs/releases/v2.1.0.md +64 -0
- package/docs/releases/v2.1.1.md +43 -0
- package/docs/releases/v2.2.0.md +64 -0
- package/docs/releases/v2.2.1.md +11 -0
- package/docs/releases/v2.3.0.md +97 -0
- package/docs/releases/v2.3.1.md +71 -0
- package/docs/releases/v2.3.2.md +242 -0
- package/docs/releases/v2.4.0.md +126 -0
- package/gemini-extension.json +6 -0
- package/hooks/claude-hooks.json +17 -0
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/run-hook.cmd +36 -0
- package/hooks/session-start +38 -0
- package/package.json +69 -4
- package/plugin.json +51 -0
- package/scripts/build_plugin.py +50 -0
- package/scripts/check_flow_update.py +18 -0
- package/scripts/check_public_commit.py +86 -0
- package/scripts/fetch_public_refs.py +69 -0
- package/scripts/install.py +600 -0
- package/scripts/install.sh +1392 -0
- package/scripts/install_guards.py +279 -0
- package/scripts/install_personalization.py +527 -0
- package/scripts/install_skills.py +951 -0
- package/scripts/npm_install.py +85 -0
- package/scripts/recommend_flow_setup.py +1522 -0
- package/scripts/validate_skills.py +465 -0
- package/skills/nobrainer-auto-fine-tune/SKILL.md +79 -0
- package/skills/nobrainer-auto-fine-tune/references/capacity-audit.md +49 -0
- package/skills/nobrainer-autoimprove/SKILL.md +212 -0
- package/skills/nobrainer-autoimprove/scripts/count_words.py +22 -0
- package/skills/nobrainer-browser/SKILL.md +234 -0
- package/skills/nobrainer-browser/references/cdp-profile-restart.md +93 -0
- package/skills/nobrainer-build/SKILL.md +173 -0
- package/skills/nobrainer-codex-context/SKILL.md +111 -0
- package/skills/nobrainer-codex-context/scripts/codex_context.py +175 -0
- package/skills/nobrainer-decide/SKILL.md +198 -0
- package/skills/nobrainer-dispatcher/SKILL.md +256 -0
- package/skills/nobrainer-dispatcher/agents/openai.yaml +4 -0
- package/skills/nobrainer-rca/SKILL.md +163 -0
- package/skills/nobrainer-research/SKILL.md +131 -0
- package/skills/nobrainer-review/SKILL.md +195 -0
- package/skills/nobrainer-security/SKILL.md +166 -0
- package/skills/nobrainer-sessions/SKILL.md +223 -0
- package/skills/nobrainer-sessions/references/evidence-receipt.md +55 -0
- package/skills/nobrainer-sessions/references/protocol.md +337 -0
- package/skills/nobrainer-sessions/references/session-restart.md +255 -0
- package/skills/nobrainer-sessions/scripts/evidence_receipt.py +373 -0
- package/skills/nobrainer-sessions/scripts/restart_gate.py +140 -0
- package/skills/nobrainer-sessions/scripts/run_bounded.py +390 -0
- package/skills/nobrainer-sessions/scripts/session_title.py +43 -0
- package/skills/nobrainer-skill-doctor/SKILL.md +149 -0
- package/skills/nobrainer-skill-doctor/references/astra.md +19 -0
- package/skills/nobrainer-skill-doctor/scripts/audit.py +100 -0
- package/skills/nobrainer-skill-doctor/scripts/context_candidates.py +133 -0
- package/skills/nobrainer-spec-driven-development/SKILL.md +132 -0
- package/skills/nobrainer-spec-driven-development/references/spec-template.md +114 -0
- package/skills/nobrainer-team/SKILL.md +165 -0
- package/skills/nobrainer-team/references/team-plan.md +64 -0
- package/skills/nobrainer-tech-flow/SKILL.md +239 -0
- package/skills/nobrainer-tech-flow/VERSION +1 -0
- package/skills/nobrainer-tech-flow/references/autopilot.md +72 -0
- package/skills/nobrainer-tech-flow/references/codex-routing.md +21 -0
- package/skills/nobrainer-tech-flow/references/communication.md +108 -0
- package/skills/nobrainer-tech-flow/references/correction-hooks.md +110 -0
- package/skills/nobrainer-tech-flow/references/daily-update.md +28 -0
- package/skills/nobrainer-tech-flow/references/delivery.md +243 -0
- package/skills/nobrainer-tech-flow/references/idea-to-delivery.md +82 -0
- package/skills/nobrainer-tech-flow/references/long-run-state.md +209 -0
- package/skills/nobrainer-tech-flow/references/model-routing.md +83 -0
- package/skills/nobrainer-tech-flow/references/optional-decisions.md +83 -0
- package/skills/nobrainer-tech-flow/references/project-start.md +22 -0
- package/skills/nobrainer-tech-flow/references/routing.md +125 -0
- package/skills/nobrainer-tech-flow/references/setup.md +180 -0
- package/skills/nobrainer-tech-flow/references/worktrees.md +138 -0
- package/skills/nobrainer-tech-flow/scripts/check_flow_update.py +298 -0
- package/skills/nobrainer-tech-flow/scripts/cleanup_worktree.py +327 -0
- package/skills/nobrainer-tech-flow/scripts/decision_config.py +107 -0
- package/skills/nobrainer-tech-flow/scripts/flow_guard.py +252 -0
- package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +210 -0
- package/skills/nobrainer-wiki/SKILL.md +164 -0
- package/skills/nobrainer-wiki/references/setup.md +88 -0
- package/skills/nobrainer-writing/SKILL.md +272 -0
- package/skills/nobrainer-writing/agents/openai.yaml +4 -0
- package/skills/nobrainer-writing/references/brief-artifacts.md +357 -0
- package/skills/nobrainer-writing/references/plain-technical-english.md +76 -0
- package/skills/nobrainer-writing/references/research.md +86 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Preview, add or remove the optional nobrainer-tech-flow guards for Claude Code.
|
|
3
|
+
|
|
4
|
+
The guards are three hooks in the Claude Code user settings.json: a PreToolUse hook
|
|
5
|
+
that asks before common risky shell commands, and Stop and SubagentStop hooks that
|
|
6
|
+
ask for a proof line once. Every run is a preview until --apply. The hooks point at
|
|
7
|
+
flow_guard.py through the skill link that scripts/install.py created, so they keep
|
|
8
|
+
working when the checkout is updated. Our entries are the hooks whose command is
|
|
9
|
+
exactly one this script generates; --undo --apply removes those and nothing else.
|
|
10
|
+
The preview names only those hooks and the settings path, never other settings,
|
|
11
|
+
which can hold tokens.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import importlib.util
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import shlex
|
|
21
|
+
import shutil
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
29
|
+
INSTALL_SKILLS = ROOT / "scripts" / "install_skills.py"
|
|
30
|
+
GUARD = Path("nobrainer-tech-flow") / "scripts" / "flow_guard.py"
|
|
31
|
+
EVENTS = (("PreToolUse", "Bash", "pre-tool"), ("Stop", None, "stop"), ("SubagentStop", None, "stop"))
|
|
32
|
+
TIMEOUT = 10
|
|
33
|
+
DISCLAIMER = (
|
|
34
|
+
"NOTE: a seatbelt: it asks before common risky commands; it is not a security "
|
|
35
|
+
"boundary, and sh -c or variables bypass it."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Refused(Exception):
|
|
40
|
+
"""The run stopped; the reason is the message and nothing was written."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def client_destinations():
|
|
44
|
+
spec = importlib.util.spec_from_file_location("_flow_install_skills", INSTALL_SKILLS)
|
|
45
|
+
if spec is None or spec.loader is None:
|
|
46
|
+
raise RuntimeError(f"cannot load {INSTALL_SKILLS}")
|
|
47
|
+
module = importlib.util.module_from_spec(spec)
|
|
48
|
+
spec.loader.exec_module(module)
|
|
49
|
+
return module.client_destinations
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def locations(home: Path | None) -> tuple[Path, Path]:
|
|
53
|
+
"""(settings.json, flow_guard.py through the skill link) for Claude Code.
|
|
54
|
+
|
|
55
|
+
An explicit home selects ~/.claude under it and ignores CLAUDE_CONFIG_DIR, as
|
|
56
|
+
scripts/install.py does.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
environ = {} if home is not None else os.environ
|
|
60
|
+
skills = client_destinations()(environ, home or Path.home(), "claude")["claude"]
|
|
61
|
+
return skills.parent / "settings.json", skills / GUARD
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def command(guard: Path, mode: str) -> str:
|
|
65
|
+
# A missing or failing guard (the skills were unlinked, no python3) must stay silent:
|
|
66
|
+
# exit 2 from a hook would block every shell command or every stop.
|
|
67
|
+
path = shlex.quote(str(guard))
|
|
68
|
+
return f"if [ -f {path} ]; then python3 {path} {mode} || true; fi"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ours(hook: object, guard: Path) -> bool:
|
|
72
|
+
# The exact generated text, not a path substring: the path may be shell-quoted, and
|
|
73
|
+
# another hook that merely mentions the path is not ours.
|
|
74
|
+
return isinstance(hook, dict) and hook.get("command") in {command(guard, mode) for _, _, mode in EVENTS}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def read_settings(path: Path) -> tuple[str | None, dict]:
|
|
78
|
+
if not path.exists():
|
|
79
|
+
return None, {}
|
|
80
|
+
text = path.read_text(encoding="utf-8")
|
|
81
|
+
try:
|
|
82
|
+
settings = json.loads(text)
|
|
83
|
+
except ValueError as exc:
|
|
84
|
+
raise Refused(f"{path} is not valid JSON ({exc}); fix it first") from exc
|
|
85
|
+
if not isinstance(settings, dict):
|
|
86
|
+
raise Refused(f"{path} does not hold a JSON object")
|
|
87
|
+
hooks = settings.get("hooks", {})
|
|
88
|
+
if not isinstance(hooks, dict):
|
|
89
|
+
raise Refused(f"{path}: \"hooks\" is not an object")
|
|
90
|
+
for event, groups in hooks.items():
|
|
91
|
+
if not isinstance(groups, list) or not all(
|
|
92
|
+
isinstance(group, dict) and isinstance(group.get("hooks", []), list) for group in groups
|
|
93
|
+
):
|
|
94
|
+
raise Refused(f"{path}: hooks.{event} is not a list of matcher groups")
|
|
95
|
+
return text, settings
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def without_ours(settings: dict, guard: Path) -> dict:
|
|
99
|
+
"""SETTINGS with our hooks removed, and the groups and events they leave empty."""
|
|
100
|
+
|
|
101
|
+
result = json.loads(json.dumps(settings))
|
|
102
|
+
hooks = result.get("hooks")
|
|
103
|
+
if not isinstance(hooks, dict):
|
|
104
|
+
return result
|
|
105
|
+
for event in list(hooks):
|
|
106
|
+
groups = []
|
|
107
|
+
for group in hooks[event]:
|
|
108
|
+
kept = [hook for hook in group.get("hooks", []) if not ours(hook, guard)]
|
|
109
|
+
if kept or not group.get("hooks"):
|
|
110
|
+
groups.append({**group, "hooks": kept} if "hooks" in group else group)
|
|
111
|
+
if groups or not hooks[event]:
|
|
112
|
+
hooks[event] = groups
|
|
113
|
+
else:
|
|
114
|
+
del hooks[event]
|
|
115
|
+
if not hooks and "hooks" in settings and settings["hooks"]:
|
|
116
|
+
del result["hooks"]
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def with_ours(settings: dict, guard: Path) -> dict:
|
|
121
|
+
result = without_ours(settings, guard)
|
|
122
|
+
hooks = result.setdefault("hooks", {})
|
|
123
|
+
for event, matcher, mode in EVENTS:
|
|
124
|
+
group: dict = {"matcher": matcher} if matcher else {}
|
|
125
|
+
group["hooks"] = [{"type": "command", "command": command(guard, mode), "timeout": TIMEOUT}]
|
|
126
|
+
hooks.setdefault(event, []).append(group)
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def render(settings: dict) -> str:
|
|
131
|
+
return json.dumps(settings, indent=2) + "\n"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def listed(settings: dict, guard: Path) -> list[str]:
|
|
135
|
+
"""One line per hook of ours in SETTINGS: its event, matcher and command."""
|
|
136
|
+
|
|
137
|
+
lines = []
|
|
138
|
+
for event, groups in settings.get("hooks", {}).items():
|
|
139
|
+
for group in groups:
|
|
140
|
+
matcher = f" (matcher {group['matcher']})" if group.get("matcher") else ""
|
|
141
|
+
lines.extend(f" {event}{matcher}: {hook['command']}" for hook in group.get("hooks", []) if ours(hook, guard))
|
|
142
|
+
return lines
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def backup_copy(target: Path) -> Path:
|
|
146
|
+
"""Copy TARGET to a new backup file; an existing file of that name is never replaced."""
|
|
147
|
+
|
|
148
|
+
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
149
|
+
for counter in range(1000):
|
|
150
|
+
suffix = f"-{counter}" if counter else ""
|
|
151
|
+
backup = target.with_name(f"{target.name}.flow-guards-{stamp}{suffix}.bak")
|
|
152
|
+
try:
|
|
153
|
+
# Exclusive creation also refuses a symlink planted at the name. The settings can
|
|
154
|
+
# hold tokens, so the copy is private from its first byte whatever the umask is.
|
|
155
|
+
descriptor = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
156
|
+
except FileExistsError:
|
|
157
|
+
continue
|
|
158
|
+
try:
|
|
159
|
+
with os.fdopen(descriptor, "wb") as handle, target.open("rb") as source:
|
|
160
|
+
shutil.copyfileobj(source, handle)
|
|
161
|
+
# Only the times are copied: copying the mode would undo the privacy above.
|
|
162
|
+
status = target.stat()
|
|
163
|
+
os.utime(backup, ns=(status.st_atime_ns, status.st_mtime_ns))
|
|
164
|
+
except BaseException:
|
|
165
|
+
backup.unlink(missing_ok=True)
|
|
166
|
+
raise
|
|
167
|
+
return backup
|
|
168
|
+
raise RuntimeError(f"no free backup name for {target}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def write(path: Path, before: str | None, after: str) -> Path | None:
|
|
172
|
+
"""Write AFTER atomically, keeping a backup of BEFORE; the backup path or None."""
|
|
173
|
+
|
|
174
|
+
target = path.resolve() if path.is_symlink() else path
|
|
175
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
backup = None
|
|
177
|
+
if before is not None:
|
|
178
|
+
backup = backup_copy(target)
|
|
179
|
+
descriptor, staged = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
|
|
180
|
+
try:
|
|
181
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
182
|
+
handle.write(after)
|
|
183
|
+
handle.flush()
|
|
184
|
+
os.fsync(handle.fileno())
|
|
185
|
+
if before is not None:
|
|
186
|
+
shutil.copymode(target, staged)
|
|
187
|
+
os.replace(staged, target)
|
|
188
|
+
except BaseException:
|
|
189
|
+
Path(staged).unlink(missing_ok=True)
|
|
190
|
+
raise
|
|
191
|
+
if target.read_text(encoding="utf-8") != after:
|
|
192
|
+
raise RuntimeError(f"readback of {target} does not match what was written")
|
|
193
|
+
return backup
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def installed(settings_path: Path, guard: Path) -> bool:
|
|
197
|
+
"""Whether SETTINGS_PATH holds any of our hooks; unreadable settings count as no."""
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
_, settings = read_settings(settings_path)
|
|
201
|
+
except (OSError, Refused):
|
|
202
|
+
return False
|
|
203
|
+
return without_ours(settings, guard) != settings
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def run(apply: bool, undo: bool, home: Path | None, rerun: str = "python3 scripts/install_guards.py {flags}") -> int:
|
|
207
|
+
"""RERUN is the command that continues this run, with {flags} where --apply goes."""
|
|
208
|
+
|
|
209
|
+
settings_path, guard = locations(home)
|
|
210
|
+
if not undo and not guard.is_file():
|
|
211
|
+
print(
|
|
212
|
+
f"UNSUPPORTED: no linked nobrainer-tech-flow skill at {guard.parent.parent}. The guards "
|
|
213
|
+
"point at the skill that scripts/install.py --client claude links; a plugin-only "
|
|
214
|
+
"install is not supported in this version. Install the skills first, then rerun."
|
|
215
|
+
)
|
|
216
|
+
return 3
|
|
217
|
+
before, settings = read_settings(settings_path)
|
|
218
|
+
updated = without_ours(settings, guard) if undo else with_ours(settings, guard)
|
|
219
|
+
if updated == settings:
|
|
220
|
+
if undo:
|
|
221
|
+
print(f"NOTHING TO UNDO: no Flow guards in {settings_path}; nothing was changed")
|
|
222
|
+
else:
|
|
223
|
+
print(f"UP TO DATE: the Flow guards are already in {settings_path}; nothing changed")
|
|
224
|
+
return 0
|
|
225
|
+
after = render(updated)
|
|
226
|
+
if not apply:
|
|
227
|
+
hooks = listed(settings if undo else updated, guard)
|
|
228
|
+
verb = f"remove {len(hooks)} hooks from" if undo else f"add {len(hooks)} hooks to"
|
|
229
|
+
print(f"PREVIEW: would {verb} {settings_path}; other settings keep their values:")
|
|
230
|
+
print("\n".join(hooks))
|
|
231
|
+
if not undo:
|
|
232
|
+
print(DISCLAIMER)
|
|
233
|
+
flags = "--undo --apply" if undo else "--apply"
|
|
234
|
+
print(f"NO_CHANGES: preview only; {'remove' if undo else 'add'} them with: {rerun.replace('{flags}', flags)}")
|
|
235
|
+
return 0
|
|
236
|
+
backup = write(settings_path, before, after)
|
|
237
|
+
print(f"{'REMOVED' if undo else 'INSTALLED'}: Flow guards in {settings_path}")
|
|
238
|
+
if backup:
|
|
239
|
+
print(f"BACKUP: {backup}")
|
|
240
|
+
if not undo:
|
|
241
|
+
print(DISCLAIMER)
|
|
242
|
+
print(f"UNDO: {rerun.replace('{flags}', '--undo --apply')}")
|
|
243
|
+
print("NEXT: restart Claude Code so it loads the changed hooks.")
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
248
|
+
parser = argparse.ArgumentParser(
|
|
249
|
+
description="Add the optional Flow guards to the Claude Code settings.json. Without --apply this only previews."
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument("--apply", action="store_true", help="write the change; the default is a preview")
|
|
252
|
+
parser.add_argument("--undo", action="store_true", help="remove the Flow guards (add --apply to do it)")
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"--home",
|
|
255
|
+
type=Path,
|
|
256
|
+
help="profile home for ~/.claude; when omitted, CLAUDE_CONFIG_DIR is honoured",
|
|
257
|
+
)
|
|
258
|
+
return parser.parse_args(argv)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main(argv: list[str] | None = None) -> int:
|
|
262
|
+
args = parse_args(argv)
|
|
263
|
+
if os.name == "nt":
|
|
264
|
+
print("UNSUPPORTED: the Flow guards are POSIX-only in this version.")
|
|
265
|
+
return 3
|
|
266
|
+
home = Path(os.path.abspath(args.home.expanduser())) if args.home else None
|
|
267
|
+
rerun = "python3 scripts/install_guards.py {flags}" + (f" --home {shlex.quote(str(home))}" if home else "")
|
|
268
|
+
try:
|
|
269
|
+
return run(args.apply, args.undo, home, rerun)
|
|
270
|
+
except Refused as refused:
|
|
271
|
+
print(f"REFUSED: {refused}; nothing was changed", file=sys.stderr)
|
|
272
|
+
return 3
|
|
273
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
274
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
275
|
+
return 2
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
raise SystemExit(main())
|