simt-dev 0.2.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.
- python/simt_dev/__init__.py +32 -0
- python/simt_dev/cli.py +225 -0
- simt_dev/__init__.py +32 -0
- simt_dev/cli.py +225 -0
- simt_dev/vendored/CHANGELOG.md +166 -0
- simt_dev/vendored/bin/simt-dev.mjs +28 -0
- simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
- simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
- simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
- simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
- simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
- simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
- simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
- simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
- simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
- simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
- simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
- simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
- simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
- simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
- simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
- simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
- simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
- simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
- simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
- simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
- simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
- simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
- simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
- simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
- simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
- simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
- simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
- simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
- simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
- simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
- simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- simt_dev/vendored/docs/decisions/README.md +530 -0
- simt_dev/vendored/graph/skills.graph.json +1622 -0
- simt_dev/vendored/package.json +43 -0
- simt_dev/vendored/rules/core.md +76 -0
- simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
- simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
- simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
- simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
- simt_dev/vendored/rules/stacks/android.md +63 -0
- simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
- simt_dev/vendored/rules/stacks/connectiq.md +62 -0
- simt_dev/vendored/rules/stacks/container.md +31 -0
- simt_dev/vendored/rules/stacks/jvm.md +54 -0
- simt_dev/vendored/rules/stacks/node.md +45 -0
- simt_dev/vendored/rules/stacks/python.md +48 -0
- simt_dev/vendored/rules/stacks/react-native.md +58 -0
- simt_dev/vendored/rules/stacks/react-web.md +61 -0
- simt_dev/vendored/rules/stacks/typescript.md +21 -0
- simt_dev/vendored/rules/stacks/uv.md +32 -0
- simt_dev/vendored/src/argv.mjs +116 -0
- simt_dev/vendored/src/ci/dependabot.mjs +268 -0
- simt_dev/vendored/src/ci/workflow.mjs +360 -0
- simt_dev/vendored/src/commands/ci.mjs +285 -0
- simt_dev/vendored/src/commands/compare.mjs +195 -0
- simt_dev/vendored/src/commands/graph.mjs +73 -0
- simt_dev/vendored/src/commands/help.mjs +108 -0
- simt_dev/vendored/src/commands/hooks.mjs +471 -0
- simt_dev/vendored/src/commands/index.mjs +241 -0
- simt_dev/vendored/src/commands/init.mjs +632 -0
- simt_dev/vendored/src/commands/mcp.mjs +63 -0
- simt_dev/vendored/src/commands/rules.mjs +374 -0
- simt_dev/vendored/src/commands/skills.mjs +647 -0
- simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
- simt_dev/vendored/src/commands/sync.mjs +331 -0
- simt_dev/vendored/src/commands/upgrade.mjs +202 -0
- simt_dev/vendored/src/compare/discover.mjs +212 -0
- simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
- simt_dev/vendored/src/compare/row.mjs +299 -0
- simt_dev/vendored/src/decisions/index.mjs +536 -0
- simt_dev/vendored/src/graph/load.mjs +541 -0
- simt_dev/vendored/src/hooks/capability.mjs +129 -0
- simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
- simt_dev/vendored/src/hooks/gates.mjs +144 -0
- simt_dev/vendored/src/hooks/resolve.mjs +194 -0
- simt_dev/vendored/src/hooks/shim.mjs +140 -0
- simt_dev/vendored/src/hooks/where.mjs +118 -0
- simt_dev/vendored/src/identity.mjs +218 -0
- simt_dev/vendored/src/locate.mjs +75 -0
- simt_dev/vendored/src/mcp/server.mjs +167 -0
- simt_dev/vendored/src/mcp/tools.mjs +410 -0
- simt_dev/vendored/src/name.mjs +95 -0
- simt_dev/vendored/src/output/index.mjs +96 -0
- simt_dev/vendored/src/projection/baselines.mjs +209 -0
- simt_dev/vendored/src/projection/conflicts.mjs +205 -0
- simt_dev/vendored/src/projection/manifest.mjs +478 -0
- simt_dev/vendored/src/projection/markers.mjs +349 -0
- simt_dev/vendored/src/projection/mdc.mjs +327 -0
- simt_dev/vendored/src/projection/project.mjs +504 -0
- simt_dev/vendored/src/projection/rules.mjs +640 -0
- simt_dev/vendored/src/projection/surfaces.mjs +398 -0
- simt_dev/vendored/src/projection/targets.mjs +221 -0
- simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
- simt_dev/vendored/src/resolve/near.mjs +61 -0
- simt_dev/vendored/src/resolve/resolve.mjs +457 -0
- simt_dev/vendored/src/resolve/source.mjs +105 -0
- simt_dev/vendored/src/toolchain.mjs +202 -0
- simt_dev/vendored/src/tsconfig.mjs +314 -0
- simt_dev/vendored/src/upgrade/plan.mjs +203 -0
- simt_dev/vendored/src/workspace.mjs +289 -0
- simt_dev-0.2.0.dist-info/METADATA +150 -0
- simt_dev-0.2.0.dist-info/RECORD +138 -0
- simt_dev-0.2.0.dist-info/WHEEL +4 -0
- simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
- simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""simt-dev, installed from PyPI.
|
|
2
|
+
|
|
3
|
+
The package this wheel ships is a Node program. See {@link simt_dev.cli} for
|
|
4
|
+
what this file is for and why it works the way it does; nothing here needs to
|
|
5
|
+
be remembered twice.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cli import find_node, main, vendored_root
|
|
9
|
+
|
|
10
|
+
__all__ = ["main", "find_node", "vendored_root", "__version__"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _version():
|
|
14
|
+
"""The version of the vendored tool, read from its own manifest.
|
|
15
|
+
|
|
16
|
+
Not from this wheel's distribution metadata. The two are asserted equal by
|
|
17
|
+
a test, and reading the vendored one here is what makes a disagreement
|
|
18
|
+
visible to a reader: `simt --version` reporting the bytes that actually
|
|
19
|
+
shipped, rather than a number that was correct at packaging time.
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
path = os.path.join(vendored_root(), "package.json")
|
|
25
|
+
try:
|
|
26
|
+
with open(path, encoding="utf-8") as handle:
|
|
27
|
+
return json.load(handle)["version"]
|
|
28
|
+
except (OSError, ValueError, KeyError):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__version__ = _version()
|
python/simt_dev/cli.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""The ``simt`` command, installed from PyPI.
|
|
2
|
+
|
|
3
|
+
simt-dev is a Node program. It has zero runtime dependencies and it does not
|
|
4
|
+
need a build step, which means the whole thing is a directory of `.mjs` files
|
|
5
|
+
and a `package.json` - about 1.3 MB of text. So the wheel carries those bytes
|
|
6
|
+
directly, alongside this file, and the only question left is *which node runs
|
|
7
|
+
them*.
|
|
8
|
+
|
|
9
|
+
That question is the whole content of {@link find_node}, and it is asked in an
|
|
10
|
+
order rather than answered with a guess:
|
|
11
|
+
|
|
12
|
+
SIMT_DEV_NODE an explicit override, for anyone who wants to say
|
|
13
|
+
nodejs_wheel the bundled node, if the [node] extra was installed
|
|
14
|
+
PATH whatever node this machine already has
|
|
15
|
+
|
|
16
|
+
and then a refusal, not a fallback to something invented. A placeholder node
|
|
17
|
+
path is worse than no path: it produces "file not found" from a depth that has
|
|
18
|
+
nothing to do with the problem, when the problem is that node is not installed
|
|
19
|
+
and the reader can fix that in one command.
|
|
20
|
+
|
|
21
|
+
This file is also why the process is *replaced* rather than *spawned*.
|
|
22
|
+
``os.execv`` hands the file descriptors over, so terminal width, a tty, stdin,
|
|
23
|
+
Ctrl-C, and the exit code all arrive unchanged. A subprocess wrapper has to
|
|
24
|
+
reimplement every one of those, and the day one of them is forgotten is the day
|
|
25
|
+
somebody's editor-integrated ``simt`` loses its colours.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
__all__ = ["main", "find_node", "vendored_root"]
|
|
33
|
+
|
|
34
|
+
# The env var that names a node binary. SIMT_DEV_NODE rather than SIMT_DEV_BIN,
|
|
35
|
+
# because SIMT_DEV_BIN already means something and means it well: it names the
|
|
36
|
+
# *tool* (an executable, or a .mjs entry point to hand to node), and the hook
|
|
37
|
+
# shims and the CI workflow both read it. This names the *runtime*. Two names
|
|
38
|
+
# for two questions; reusing one would make the value's meaning depend on which
|
|
39
|
+
# of the two files is reading it.
|
|
40
|
+
ENV_NODE = "SIMT_DEV_NODE"
|
|
41
|
+
|
|
42
|
+
# The env var that names the program to run instead of the one this wheel ships.
|
|
43
|
+
# Same meaning it has in the shim: a path to a .mjs entry point, so somebody
|
|
44
|
+
# working on simt-dev itself can `pip install -e .` and still point at their
|
|
45
|
+
# checkout without editing anything.
|
|
46
|
+
ENV_BIN = "SIMT_DEV_BIN"
|
|
47
|
+
|
|
48
|
+
BIN_REL = os.path.join("bin", "simt-dev.mjs")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def vendored_root():
|
|
52
|
+
"""The directory this wheel placed the tool's own source in.
|
|
53
|
+
|
|
54
|
+
Resolved from ``__file__``, because that is the one location that is correct
|
|
55
|
+
in a checkout, in a wheel, in a virtualenv and in a system install alike. A
|
|
56
|
+
path derived from the current working directory would be wrong the moment
|
|
57
|
+
a hook ran this from inside a repository - which is the normal case.
|
|
58
|
+
|
|
59
|
+
One level up from this file, not two: this module is `simt_dev/cli.py` and
|
|
60
|
+
the vendored tree sits beside it at `simt_dev/vendored/`. (It was written as
|
|
61
|
+
two while the package layout was still a guess, and the symptom was an
|
|
62
|
+
install that worked and a command that reported its own wheel as
|
|
63
|
+
incomplete.)
|
|
64
|
+
"""
|
|
65
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendored")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _executable(candidate):
|
|
69
|
+
"""Whether `candidate` names a file this user may execute."""
|
|
70
|
+
return candidate is not None and os.path.isfile(candidate) and os.access(candidate, os.X_OK)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _from_env():
|
|
74
|
+
"""`SIMT_DEV_NODE`, as a path or as a name to look up.
|
|
75
|
+
|
|
76
|
+
Accepted in both spellings because the reader who sets it has one in mind: a
|
|
77
|
+
Homebrew prefix or an nvm path is a path, and `node` is a name. Deciding by
|
|
78
|
+
the presence of a separator is the usual way this is done and the wrong test
|
|
79
|
+
to be clever about - `./node` and `node` are both things people type.
|
|
80
|
+
"""
|
|
81
|
+
value = os.environ.get(ENV_NODE)
|
|
82
|
+
if not value:
|
|
83
|
+
return None
|
|
84
|
+
if os.sep in value or (os.altsep and os.altsep in value):
|
|
85
|
+
return value if _executable(value) else None
|
|
86
|
+
return shutil.which(value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _from_bundled():
|
|
90
|
+
"""The node that ships with the `[node]` extra, if that extra is installed.
|
|
91
|
+
|
|
92
|
+
Imported lazily and guarded, because the default install does not have it:
|
|
93
|
+
`pip install simt-dev` must not pay for, or fail on, an optional wheel it
|
|
94
|
+
did not ask for. An ImportError here means "no bundled node", which is an
|
|
95
|
+
ordinary outcome and not an error to report.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import nodejs_wheel # noqa: PLC0415 the whole point is that it may be absent
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
return nodejs_wheel.node()
|
|
103
|
+
except Exception:
|
|
104
|
+
# Present but unusable - a broken install, a platform with no wheel.
|
|
105
|
+
# Reported rather than swallowed by returning a path that does not run.
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _from_path():
|
|
110
|
+
"""Whatever node this machine already has.
|
|
111
|
+
|
|
112
|
+
`shutil.which` rather than a list of well-known install directories. A
|
|
113
|
+
hardcoded `/opt/homebrew/bin/node` list is three guesses and a surprise:
|
|
114
|
+
it starts preferring a guess over the reader's own PATH, and it is already
|
|
115
|
+
out of date for anyone who installs node differently.
|
|
116
|
+
"""
|
|
117
|
+
return shutil.which("node")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def find_node(env=None):
|
|
121
|
+
"""The node binary that should run the tool, or None if there is not one.
|
|
122
|
+
|
|
123
|
+
The order is load-bearing and not aesthetic. An explicit override wins, so
|
|
124
|
+
a reader who has a reason can say so. The bundled node comes next rather
|
|
125
|
+
than before `PATH`, because a machine that already has node should keep
|
|
126
|
+
using the node it uses everywhere else - installing the `[node]` extra on
|
|
127
|
+
such a machine is meant to add a fallback, not to silently switch the
|
|
128
|
+
runtime underneath an otherwise-working setup.
|
|
129
|
+
|
|
130
|
+
@param env an environment mapping; defaults to `os.environ`
|
|
131
|
+
@returns {str|None} a path to a node binary, or None
|
|
132
|
+
"""
|
|
133
|
+
env = os.environ if env is None else env
|
|
134
|
+
|
|
135
|
+
explicit = env.get(ENV_NODE)
|
|
136
|
+
if explicit:
|
|
137
|
+
candidate = _from_env()
|
|
138
|
+
if candidate is not None:
|
|
139
|
+
return candidate
|
|
140
|
+
# Falling through on a bad override would run a *different* node than
|
|
141
|
+
# the one asked for, which is the one outcome worse than failing.
|
|
142
|
+
sys.stderr.write(
|
|
143
|
+
f"simt: {ENV_NODE} names {explicit!r}, which is not an executable node.\n"
|
|
144
|
+
f" set {ENV_NODE} to a node binary or a name on PATH, or unset it to let simt find one.\n"
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
bundled = _from_bundled()
|
|
149
|
+
if bundled is not None:
|
|
150
|
+
return bundled
|
|
151
|
+
|
|
152
|
+
return _from_path()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _program(vendored):
|
|
156
|
+
"""Which `.mjs` entry point to run, honouring SIMT_DEV_BIN the way the shim does."""
|
|
157
|
+
override = os.environ.get(ENV_BIN)
|
|
158
|
+
if override:
|
|
159
|
+
if " " in override:
|
|
160
|
+
# The shim's rule, kept: the value names one program. A value with a
|
|
161
|
+
# space in it is either a command line, which there is no safe way to
|
|
162
|
+
# split here, or a genuine path with a space in it - and refusing
|
|
163
|
+
# both costs nothing, while accepting the first is a shell injection.
|
|
164
|
+
sys.stderr.write(
|
|
165
|
+
f"simt: {ENV_BIN} names a command line, not a program.\n"
|
|
166
|
+
f" set it to a single executable or a .mjs path, e.g.\n"
|
|
167
|
+
f" {ENV_BIN}=/path/to/checkout/bin/simt-dev.mjs\n"
|
|
168
|
+
)
|
|
169
|
+
return None
|
|
170
|
+
return override if override.endswith(".mjs") else None
|
|
171
|
+
return os.path.join(vendored, BIN_REL)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _remedy():
|
|
175
|
+
"""What to do about the missing node, in a form the reader can act on.
|
|
176
|
+
|
|
177
|
+
The `[node]` extra first because it is the one remedy that is the same
|
|
178
|
+
command on every platform. Platform package managers are deliberately not
|
|
179
|
+
listed: `apt`, `dnf`, `pacman` and `brew` differ, getting one wrong is
|
|
180
|
+
itself a broken remedy, and a wrong remedy is a second problem stacked on
|
|
181
|
+
the first - which is the failure this project's rules exist to prevent.
|
|
182
|
+
"""
|
|
183
|
+
return (
|
|
184
|
+
" REMEDY: pip install simt-dev[node] (installs a bundled node, and works on every platform)\n"
|
|
185
|
+
" or install Node.js yourself and make sure `node` is on PATH\n"
|
|
186
|
+
" or set SIMT_DEV_NODE=/path/to/node"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main(argv=None):
|
|
191
|
+
"""Replace this process with `node <entry point> <args>`.
|
|
192
|
+
|
|
193
|
+
@param argv arguments to forward; defaults to `sys.argv[1:]` so an explicit
|
|
194
|
+
caller gets a slash list while an installed entry point gets the shell's
|
|
195
|
+
@returns {int} only ever 2 (an error before exec); otherwise this does not return
|
|
196
|
+
"""
|
|
197
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
198
|
+
|
|
199
|
+
node = find_node()
|
|
200
|
+
if node is None:
|
|
201
|
+
sys.stderr.write(
|
|
202
|
+
"simt: cannot find node, which is the runtime this tool is written in.\n"
|
|
203
|
+
f"{_remedy()}\n"
|
|
204
|
+
)
|
|
205
|
+
return 2
|
|
206
|
+
|
|
207
|
+
vendored = vendored_root()
|
|
208
|
+
program = _program(vendored)
|
|
209
|
+
if program is None:
|
|
210
|
+
# `_program` has already said which env var was wrong and how.
|
|
211
|
+
return 2
|
|
212
|
+
if not os.path.isfile(program):
|
|
213
|
+
sys.stderr.write(
|
|
214
|
+
f"simt: {program} is not in this installation.\n"
|
|
215
|
+
" this usually means the wheel is incomplete - reinstall with:\n"
|
|
216
|
+
" pip install --force-reinstall --no-cache-dir simt-dev\n"
|
|
217
|
+
)
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
os.execv(node, [node, program, *argv])
|
|
221
|
+
raise AssertionError("unreachable: execv does not return")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
sys.exit(main())
|
simt_dev/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""simt-dev, installed from PyPI.
|
|
2
|
+
|
|
3
|
+
The package this wheel ships is a Node program. See {@link simt_dev.cli} for
|
|
4
|
+
what this file is for and why it works the way it does; nothing here needs to
|
|
5
|
+
be remembered twice.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cli import find_node, main, vendored_root
|
|
9
|
+
|
|
10
|
+
__all__ = ["main", "find_node", "vendored_root", "__version__"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _version():
|
|
14
|
+
"""The version of the vendored tool, read from its own manifest.
|
|
15
|
+
|
|
16
|
+
Not from this wheel's distribution metadata. The two are asserted equal by
|
|
17
|
+
a test, and reading the vendored one here is what makes a disagreement
|
|
18
|
+
visible to a reader: `simt --version` reporting the bytes that actually
|
|
19
|
+
shipped, rather than a number that was correct at packaging time.
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
path = os.path.join(vendored_root(), "package.json")
|
|
25
|
+
try:
|
|
26
|
+
with open(path, encoding="utf-8") as handle:
|
|
27
|
+
return json.load(handle)["version"]
|
|
28
|
+
except (OSError, ValueError, KeyError):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__version__ = _version()
|
simt_dev/cli.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""The ``simt`` command, installed from PyPI.
|
|
2
|
+
|
|
3
|
+
simt-dev is a Node program. It has zero runtime dependencies and it does not
|
|
4
|
+
need a build step, which means the whole thing is a directory of `.mjs` files
|
|
5
|
+
and a `package.json` - about 1.3 MB of text. So the wheel carries those bytes
|
|
6
|
+
directly, alongside this file, and the only question left is *which node runs
|
|
7
|
+
them*.
|
|
8
|
+
|
|
9
|
+
That question is the whole content of {@link find_node}, and it is asked in an
|
|
10
|
+
order rather than answered with a guess:
|
|
11
|
+
|
|
12
|
+
SIMT_DEV_NODE an explicit override, for anyone who wants to say
|
|
13
|
+
nodejs_wheel the bundled node, if the [node] extra was installed
|
|
14
|
+
PATH whatever node this machine already has
|
|
15
|
+
|
|
16
|
+
and then a refusal, not a fallback to something invented. A placeholder node
|
|
17
|
+
path is worse than no path: it produces "file not found" from a depth that has
|
|
18
|
+
nothing to do with the problem, when the problem is that node is not installed
|
|
19
|
+
and the reader can fix that in one command.
|
|
20
|
+
|
|
21
|
+
This file is also why the process is *replaced* rather than *spawned*.
|
|
22
|
+
``os.execv`` hands the file descriptors over, so terminal width, a tty, stdin,
|
|
23
|
+
Ctrl-C, and the exit code all arrive unchanged. A subprocess wrapper has to
|
|
24
|
+
reimplement every one of those, and the day one of them is forgotten is the day
|
|
25
|
+
somebody's editor-integrated ``simt`` loses its colours.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
__all__ = ["main", "find_node", "vendored_root"]
|
|
33
|
+
|
|
34
|
+
# The env var that names a node binary. SIMT_DEV_NODE rather than SIMT_DEV_BIN,
|
|
35
|
+
# because SIMT_DEV_BIN already means something and means it well: it names the
|
|
36
|
+
# *tool* (an executable, or a .mjs entry point to hand to node), and the hook
|
|
37
|
+
# shims and the CI workflow both read it. This names the *runtime*. Two names
|
|
38
|
+
# for two questions; reusing one would make the value's meaning depend on which
|
|
39
|
+
# of the two files is reading it.
|
|
40
|
+
ENV_NODE = "SIMT_DEV_NODE"
|
|
41
|
+
|
|
42
|
+
# The env var that names the program to run instead of the one this wheel ships.
|
|
43
|
+
# Same meaning it has in the shim: a path to a .mjs entry point, so somebody
|
|
44
|
+
# working on simt-dev itself can `pip install -e .` and still point at their
|
|
45
|
+
# checkout without editing anything.
|
|
46
|
+
ENV_BIN = "SIMT_DEV_BIN"
|
|
47
|
+
|
|
48
|
+
BIN_REL = os.path.join("bin", "simt-dev.mjs")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def vendored_root():
|
|
52
|
+
"""The directory this wheel placed the tool's own source in.
|
|
53
|
+
|
|
54
|
+
Resolved from ``__file__``, because that is the one location that is correct
|
|
55
|
+
in a checkout, in a wheel, in a virtualenv and in a system install alike. A
|
|
56
|
+
path derived from the current working directory would be wrong the moment
|
|
57
|
+
a hook ran this from inside a repository - which is the normal case.
|
|
58
|
+
|
|
59
|
+
One level up from this file, not two: this module is `simt_dev/cli.py` and
|
|
60
|
+
the vendored tree sits beside it at `simt_dev/vendored/`. (It was written as
|
|
61
|
+
two while the package layout was still a guess, and the symptom was an
|
|
62
|
+
install that worked and a command that reported its own wheel as
|
|
63
|
+
incomplete.)
|
|
64
|
+
"""
|
|
65
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendored")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _executable(candidate):
|
|
69
|
+
"""Whether `candidate` names a file this user may execute."""
|
|
70
|
+
return candidate is not None and os.path.isfile(candidate) and os.access(candidate, os.X_OK)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _from_env():
|
|
74
|
+
"""`SIMT_DEV_NODE`, as a path or as a name to look up.
|
|
75
|
+
|
|
76
|
+
Accepted in both spellings because the reader who sets it has one in mind: a
|
|
77
|
+
Homebrew prefix or an nvm path is a path, and `node` is a name. Deciding by
|
|
78
|
+
the presence of a separator is the usual way this is done and the wrong test
|
|
79
|
+
to be clever about - `./node` and `node` are both things people type.
|
|
80
|
+
"""
|
|
81
|
+
value = os.environ.get(ENV_NODE)
|
|
82
|
+
if not value:
|
|
83
|
+
return None
|
|
84
|
+
if os.sep in value or (os.altsep and os.altsep in value):
|
|
85
|
+
return value if _executable(value) else None
|
|
86
|
+
return shutil.which(value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _from_bundled():
|
|
90
|
+
"""The node that ships with the `[node]` extra, if that extra is installed.
|
|
91
|
+
|
|
92
|
+
Imported lazily and guarded, because the default install does not have it:
|
|
93
|
+
`pip install simt-dev` must not pay for, or fail on, an optional wheel it
|
|
94
|
+
did not ask for. An ImportError here means "no bundled node", which is an
|
|
95
|
+
ordinary outcome and not an error to report.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import nodejs_wheel # noqa: PLC0415 the whole point is that it may be absent
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
return nodejs_wheel.node()
|
|
103
|
+
except Exception:
|
|
104
|
+
# Present but unusable - a broken install, a platform with no wheel.
|
|
105
|
+
# Reported rather than swallowed by returning a path that does not run.
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _from_path():
|
|
110
|
+
"""Whatever node this machine already has.
|
|
111
|
+
|
|
112
|
+
`shutil.which` rather than a list of well-known install directories. A
|
|
113
|
+
hardcoded `/opt/homebrew/bin/node` list is three guesses and a surprise:
|
|
114
|
+
it starts preferring a guess over the reader's own PATH, and it is already
|
|
115
|
+
out of date for anyone who installs node differently.
|
|
116
|
+
"""
|
|
117
|
+
return shutil.which("node")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def find_node(env=None):
|
|
121
|
+
"""The node binary that should run the tool, or None if there is not one.
|
|
122
|
+
|
|
123
|
+
The order is load-bearing and not aesthetic. An explicit override wins, so
|
|
124
|
+
a reader who has a reason can say so. The bundled node comes next rather
|
|
125
|
+
than before `PATH`, because a machine that already has node should keep
|
|
126
|
+
using the node it uses everywhere else - installing the `[node]` extra on
|
|
127
|
+
such a machine is meant to add a fallback, not to silently switch the
|
|
128
|
+
runtime underneath an otherwise-working setup.
|
|
129
|
+
|
|
130
|
+
@param env an environment mapping; defaults to `os.environ`
|
|
131
|
+
@returns {str|None} a path to a node binary, or None
|
|
132
|
+
"""
|
|
133
|
+
env = os.environ if env is None else env
|
|
134
|
+
|
|
135
|
+
explicit = env.get(ENV_NODE)
|
|
136
|
+
if explicit:
|
|
137
|
+
candidate = _from_env()
|
|
138
|
+
if candidate is not None:
|
|
139
|
+
return candidate
|
|
140
|
+
# Falling through on a bad override would run a *different* node than
|
|
141
|
+
# the one asked for, which is the one outcome worse than failing.
|
|
142
|
+
sys.stderr.write(
|
|
143
|
+
f"simt: {ENV_NODE} names {explicit!r}, which is not an executable node.\n"
|
|
144
|
+
f" set {ENV_NODE} to a node binary or a name on PATH, or unset it to let simt find one.\n"
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
bundled = _from_bundled()
|
|
149
|
+
if bundled is not None:
|
|
150
|
+
return bundled
|
|
151
|
+
|
|
152
|
+
return _from_path()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _program(vendored):
|
|
156
|
+
"""Which `.mjs` entry point to run, honouring SIMT_DEV_BIN the way the shim does."""
|
|
157
|
+
override = os.environ.get(ENV_BIN)
|
|
158
|
+
if override:
|
|
159
|
+
if " " in override:
|
|
160
|
+
# The shim's rule, kept: the value names one program. A value with a
|
|
161
|
+
# space in it is either a command line, which there is no safe way to
|
|
162
|
+
# split here, or a genuine path with a space in it - and refusing
|
|
163
|
+
# both costs nothing, while accepting the first is a shell injection.
|
|
164
|
+
sys.stderr.write(
|
|
165
|
+
f"simt: {ENV_BIN} names a command line, not a program.\n"
|
|
166
|
+
f" set it to a single executable or a .mjs path, e.g.\n"
|
|
167
|
+
f" {ENV_BIN}=/path/to/checkout/bin/simt-dev.mjs\n"
|
|
168
|
+
)
|
|
169
|
+
return None
|
|
170
|
+
return override if override.endswith(".mjs") else None
|
|
171
|
+
return os.path.join(vendored, BIN_REL)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _remedy():
|
|
175
|
+
"""What to do about the missing node, in a form the reader can act on.
|
|
176
|
+
|
|
177
|
+
The `[node]` extra first because it is the one remedy that is the same
|
|
178
|
+
command on every platform. Platform package managers are deliberately not
|
|
179
|
+
listed: `apt`, `dnf`, `pacman` and `brew` differ, getting one wrong is
|
|
180
|
+
itself a broken remedy, and a wrong remedy is a second problem stacked on
|
|
181
|
+
the first - which is the failure this project's rules exist to prevent.
|
|
182
|
+
"""
|
|
183
|
+
return (
|
|
184
|
+
" REMEDY: pip install simt-dev[node] (installs a bundled node, and works on every platform)\n"
|
|
185
|
+
" or install Node.js yourself and make sure `node` is on PATH\n"
|
|
186
|
+
" or set SIMT_DEV_NODE=/path/to/node"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main(argv=None):
|
|
191
|
+
"""Replace this process with `node <entry point> <args>`.
|
|
192
|
+
|
|
193
|
+
@param argv arguments to forward; defaults to `sys.argv[1:]` so an explicit
|
|
194
|
+
caller gets a slash list while an installed entry point gets the shell's
|
|
195
|
+
@returns {int} only ever 2 (an error before exec); otherwise this does not return
|
|
196
|
+
"""
|
|
197
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
198
|
+
|
|
199
|
+
node = find_node()
|
|
200
|
+
if node is None:
|
|
201
|
+
sys.stderr.write(
|
|
202
|
+
"simt: cannot find node, which is the runtime this tool is written in.\n"
|
|
203
|
+
f"{_remedy()}\n"
|
|
204
|
+
)
|
|
205
|
+
return 2
|
|
206
|
+
|
|
207
|
+
vendored = vendored_root()
|
|
208
|
+
program = _program(vendored)
|
|
209
|
+
if program is None:
|
|
210
|
+
# `_program` has already said which env var was wrong and how.
|
|
211
|
+
return 2
|
|
212
|
+
if not os.path.isfile(program):
|
|
213
|
+
sys.stderr.write(
|
|
214
|
+
f"simt: {program} is not in this installation.\n"
|
|
215
|
+
" this usually means the wheel is incomplete - reinstall with:\n"
|
|
216
|
+
" pip install --force-reinstall --no-cache-dir simt-dev\n"
|
|
217
|
+
)
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
os.execv(node, [node, program, *argv])
|
|
221
|
+
raise AssertionError("unreachable: execv does not return")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
sys.exit(main())
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] — 2026-10-11
|
|
8
|
+
|
|
9
|
+
### Publishable from PyPI and Homebrew as well as npm
|
|
10
|
+
|
|
11
|
+
simt-dev is a *repository* tool, and the people who adopt a repository are not
|
|
12
|
+
all Node developers. Somebody running `simt init` inside a repository with no
|
|
13
|
+
`package.json` has no reason to install a package manager, and a Homebrew user
|
|
14
|
+
has no reason to install Node to install a tool that needs Node.
|
|
15
|
+
|
|
16
|
+
So it now publishes to three channels, from one version number:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm i -D simt-dev # a tarball of .mjs files
|
|
20
|
+
pip install simt-dev # a wheel carrying those same bytes
|
|
21
|
+
brew install QuazarMedia/tap/simt-dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The wheel is not a wrapper around `npm install`. It carries the tool's own
|
|
25
|
+
source, and `scripts/verify-wheel.mjs` builds both artefacts and hashes every
|
|
26
|
+
file in both — a release fails when a single byte differs between them. That
|
|
27
|
+
check exists because the obvious defect in this arrangement is the wheel and the
|
|
28
|
+
tarball quietly shipping different code.
|
|
29
|
+
|
|
30
|
+
`simt` is the one command, on every channel. Where node comes from is
|
|
31
|
+
`SIMT_DEV_NODE`, then the bundled node from `pip install "simt-dev[node]"`,
|
|
32
|
+
then `node` on `PATH` — and if none of those resolve you get the remedy rather
|
|
33
|
+
than a placeholder path. There are no required dependencies on any channel.
|
|
34
|
+
*(ADR-058)*
|
|
35
|
+
|
|
36
|
+
The channels are published in a fixed order rather than in parallel, and the
|
|
37
|
+
reason is not taste: the Homebrew formula downloads the npm tarball at install
|
|
38
|
+
time, so a formula naming a version still sitting in npm's staging area
|
|
39
|
+
installs a 404. Both jobs fail with that stated, and are run again after
|
|
40
|
+
`npm stage approve`.
|
|
41
|
+
|
|
42
|
+
## [0.1.1] — 2026-10-11
|
|
43
|
+
|
|
44
|
+
### A foreign hook on an event this repo does not gate is chained
|
|
45
|
+
|
|
46
|
+
`0.1.0` chained the wrong set of hooks and is worth replacing rather than
|
|
47
|
+
recommending. It chained on the event *vocabulary* — anything that was not
|
|
48
|
+
`pre-commit` or `pre-push` — when the question worth asking is whether **this
|
|
49
|
+
repository** declares a gate for that event.
|
|
50
|
+
|
|
51
|
+
The difference is `simt-app`, whose `pre-push` is `git-lfs`. `pre-push` is an
|
|
52
|
+
event this tool gates in general, so `0.1.0` displaced that hook; and because
|
|
53
|
+
`simt-app` declares no `pre-push` gate there was no shim for the event either, so
|
|
54
|
+
git would have found no `pre-push` hook at all. LFS objects would have stopped
|
|
55
|
+
being uploaded, with nothing to notice it — the exact silent failure chaining
|
|
56
|
+
exists to prevent, reached by fixing the wrong half of it.
|
|
57
|
+
|
|
58
|
+
`0.1.1` asks the manifest. An event the repo declares a gate for is left to
|
|
59
|
+
`gates.commands`, where the hook runs as a gate and its verdict appears in the
|
|
60
|
+
report; every other foreign hook is chained. That also picks up
|
|
61
|
+
`simt-landing`'s Lighthouse pre-push, which declares no gates at all.
|
|
62
|
+
*(ADR-018, as amended)*
|
|
63
|
+
|
|
64
|
+
Upgrade with `npm i -D simt-dev@0.1.1`, then `simt hooks install --yes`. Any
|
|
65
|
+
repository already routed with `0.1.0` should re-run it: the shims it writes
|
|
66
|
+
will differ where a gated event was previously displaced.
|
|
67
|
+
|
|
68
|
+
## [0.1.0] — 2026-10-11
|
|
69
|
+
|
|
70
|
+
The first release with real contents in it. `1.0.0` was an empty reservation of the name
|
|
71
|
+
and `0.0.0-stage` was its rehearsal; neither shipped a byte anyone could run, so this entry
|
|
72
|
+
is the whole history. The decisions behind it are readable in the package under
|
|
73
|
+
`docs/decisions/`, and from any checkout with `simt rules why <id>`.
|
|
74
|
+
|
|
75
|
+
### The command is `simt`
|
|
76
|
+
|
|
77
|
+
The package is `simt-dev` and the command is `simt`. They are not the same thing and were
|
|
78
|
+
never required to be: the command appears only in text the tool itself writes, and that
|
|
79
|
+
text migrates on the next write. The package name, the `.simt-hooks/` directory and the
|
|
80
|
+
`simt-dev-check.yml` gate already exist in every repository that adopted this tool, and
|
|
81
|
+
they did not move. The manifest and the region markers did — see below.
|
|
82
|
+
|
|
83
|
+
### The manifest is `simt.json`
|
|
84
|
+
|
|
85
|
+
The manifest was called `simt-dev.json`; it is now `simt.json`. The move is a
|
|
86
|
+
read-both/write-new, so nothing in an adopting repository has to be done:
|
|
87
|
+
|
|
88
|
+
- Every read takes `simt.json` if it exists, else `simt-dev.json`, so a repository that
|
|
89
|
+
has not written since the upgrade is still seen as adopted.
|
|
90
|
+
- Every write goes to `simt.json`, and a lone `simt-dev.json` is moved, not shadowed.
|
|
91
|
+
- The old name keeps working until the first `init` or manifest write in each repository,
|
|
92
|
+
and that write closes it. *(ADR-056)*
|
|
93
|
+
|
|
94
|
+
### The region markers are `simt:`
|
|
95
|
+
|
|
96
|
+
The generated region was stamped `<!-- simt-dev:begin -->` / `<!-- simt-dev:end -->` — and
|
|
97
|
+
so was the provenance header inside it; every one of those lines is now `simt:`. The move
|
|
98
|
+
is the same read-both/write-new as the manifest:
|
|
99
|
+
|
|
100
|
+
- Every read accepts either spelling, so a repository that has not written since the
|
|
101
|
+
upgrade is still seen as adopted.
|
|
102
|
+
- Every write emits `simt:` only, so the first `sync` in each repository migrates its
|
|
103
|
+
regions in place, and that write closes the window.
|
|
104
|
+
- A file holding two regions — one of each spelling — is a state only a hand edit can
|
|
105
|
+
make, and it is reported malformed rather than merged. *(ADR-056)*
|
|
106
|
+
|
|
107
|
+
### A hook you already had keeps running
|
|
108
|
+
|
|
109
|
+
`hooks install` points `core.hooksPath` at `.simt-hooks/`, and git reads **every** hook from
|
|
110
|
+
that one directory — not just the two events this tool gates. So adopting a repository that
|
|
111
|
+
had its own `post-commit`, `post-checkout` or `post-merge` would have quietly stopped them:
|
|
112
|
+
nothing fails, nothing is logged, and the hook is still on disk looking intact.
|
|
113
|
+
|
|
114
|
+
Those hooks are now **chained**. Install writes a shim for them that runs the repository's
|
|
115
|
+
own hook and forwards git's arguments, so nothing is lost and the reason is in the file. If
|
|
116
|
+
the hook is gone, the shim refuses and names the path instead of passing quietly. A hook on
|
|
117
|
+
an event this tool *does* gate (`pre-commit`, `pre-push`) is deliberately not chained —
|
|
118
|
+
declare it under `gates.commands` so the dispatcher runs it and its verdict appears in the
|
|
119
|
+
gate report. *(ADR-018, as amended)*
|
|
120
|
+
|
|
121
|
+
### What a first release has to say out loud
|
|
122
|
+
|
|
123
|
+
These are the behaviours a reader is most likely to trip over. Each one is deliberate,
|
|
124
|
+
and each has a numbered decision record behind it — but a behaviour nobody can discover
|
|
125
|
+
without reading the source is not a documented one.
|
|
126
|
+
|
|
127
|
+
- **`sync` and `init` write nothing without `--yes`.** Both print a plan and exit. `sync`
|
|
128
|
+
exits **1** when the plan is non-empty, so bare `sync` doubles as a check. Neither ever
|
|
129
|
+
commits or pushes. *(ADR-035, ADR-036)*
|
|
130
|
+
- **`upgrade` moves a pin and nothing else.** It never adopts a repository, never repairs
|
|
131
|
+
one, and never rewrites a manifest. Every consumer moves to the same version as one set.
|
|
132
|
+
- **A pre-commit hook with no reachable binary fails closed.** The refusal is the correct
|
|
133
|
+
behaviour, not a bug; the remedy it prints is the command that obtains one.
|
|
134
|
+
*(ADR-010, ADR-018)*
|
|
135
|
+
- **A gate whose tool is not installed is dropped, with a remedy naming the package.**
|
|
136
|
+
It is never declared and silently unrunnable. *(ADR-017)*
|
|
137
|
+
- **Two rules are never loaded twice.** Each rule projects to exactly one place, chosen by
|
|
138
|
+
what the harness actually reads. *(ADR-021, ADR-046)*
|
|
139
|
+
- **macOS and Linux only.** Windows `npm i` fails with npm's `EBADPLATFORM`, which npm
|
|
140
|
+
will not let a package explain; use WSL2. See the platform section of the README.
|
|
141
|
+
*(ADR-012)*
|
|
142
|
+
|
|
143
|
+
### Packaging
|
|
144
|
+
|
|
145
|
+
- Licence set to Apache-2.0; the `LICENSE` file is the canonical, unmodified text.
|
|
146
|
+
- `files` no longer lists `skills`, `agents`, `tooling` or `hooks`, none of which exist,
|
|
147
|
+
and no longer ships `docs/plans`. The tarball is the tool and the decisions behind it,
|
|
148
|
+
and nothing else.
|
|
149
|
+
- The package `description` no longer names an internal ecosystem. It is the first line
|
|
150
|
+
anybody outside the project reads.
|
|
151
|
+
- Unscoped, so the install command is `npm i -D simt-dev` rather than a name under an
|
|
152
|
+
organisation. The `@simt` and `@quazarmedia` scopes were both free; an unscoped name is
|
|
153
|
+
the one a reader can guess.
|
|
154
|
+
|
|
155
|
+
### Notes for anyone upgrading a hook
|
|
156
|
+
|
|
157
|
+
A shim installed by an earlier version looks for `SIMT_DEV_BIN`, then for
|
|
158
|
+
`node_modules/simt-dev/bin/simt-dev.mjs`, then for a program named `simt` on `PATH`. The
|
|
159
|
+
last of those is new, so a machine that installed the package before this release and
|
|
160
|
+
never ran `simt hooks install` again will find nothing until the dependency is refreshed
|
|
161
|
+
and `simt hooks install --yes` is re-run. That is a real upgrade step and it is called out
|
|
162
|
+
here rather than discovered.
|
|
163
|
+
|
|
164
|
+
[0.2.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.0
|
|
165
|
+
[0.1.1]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.1
|
|
166
|
+
[0.1.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.0
|