Skip to content

Commit 03e29e0

Browse files
chenchaoyiclaude
andauthored
chore(openspec): archive installable-cli (#38)
Move openspec/changes/installable-cli/ under openspec/changes/archive/2026-05-13-installable-cli/ and fold the spec deltas into the canonical specs: - installation (new capability) - macos-helper (MODIFIED Requirement: helper auto-detect now lists ~/Library/Application Support/diting/diting-tianer.app as the fifth search location, with the in-repo dev build still pinned first) Brings the canonical-specs count to 20. openspec validate --specs --strict passes. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 96ee5ab commit 03e29e0

8 files changed

Lines changed: 180 additions & 12 deletions

File tree

openspec/changes/installable-cli/.openspec.yaml renamed to openspec/changes/archive/2026-05-13-installable-cli/.openspec.yaml

File renamed without changes.
File renamed without changes.

openspec/changes/installable-cli/proposal.md renamed to openspec/changes/archive/2026-05-13-installable-cli/proposal.md

File renamed without changes.

openspec/changes/installable-cli/specs/installation/spec.md renamed to openspec/changes/archive/2026-05-13-installable-cli/specs/installation/spec.md

File renamed without changes.

openspec/changes/installable-cli/specs/macos-helper/spec.md renamed to openspec/changes/archive/2026-05-13-installable-cli/specs/macos-helper/spec.md

File renamed without changes.
File renamed without changes.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# installation Specification
2+
3+
## Purpose
4+
TBD - created by archiving change installable-cli. Update Purpose after archive.
5+
## Requirements
6+
### Requirement: A one-line installer SHALL drop a working `diting` onto a user's macOS without requiring Python, uv, or Xcode
7+
A first-time end-user SHALL be able to install diting by running a
8+
single shell command:
9+
10+
```bash
11+
curl -fsSL https://raw.githubusercontent.com/chenchaoyi/diting/main/install.sh | bash
12+
```
13+
14+
The installer MUST NOT require the user to have Python, `uv`,
15+
`brew`, or Xcode Command Line Tools installed beforehand. It MUST
16+
NOT require sudo. On completion, `diting` SHALL be on the user's
17+
PATH (directly, or via a hint the script prints to stderr) and the
18+
Swift helper bundle SHALL be in place at
19+
`~/Library/Application Support/diting/diting-tianer.app`.
20+
21+
#### Scenario: First-time install on Apple Silicon
22+
- **WHEN** a user on a macOS 14+ arm64 Mac runs the curl-bash one-liner with no diting toolchain previously installed
23+
- **THEN** `~/.local/bin/diting` exists and is executable; `~/Library/Application Support/diting/diting-tianer.app` exists; `diting --help` exits 0 after the user adds `~/.local/bin` to PATH (or follows the printed hint to do so)
24+
25+
#### Scenario: First-time install on Intel
26+
- **WHEN** a user on a macOS 13+ x86_64 Mac runs the same one-liner
27+
- **THEN** the installer downloads the `darwin-x86_64` tarball variant (not arm64), and the rest of the flow proceeds identically
28+
29+
#### Scenario: Repeat install / upgrade
30+
- **WHEN** the user runs the installer a second time on the same machine
31+
- **THEN** the installer downloads the current latest release, replaces `~/.local/share/diting/`, refreshes the helper at `~/Library/Application Support/diting/`, and prints "upgraded from <old> to <new>"
32+
33+
### Requirement: The installer SHALL refuse to run on non-macOS hosts with a clear error
34+
The installer MUST detect non-Darwin / non-arm64-or-x86_64 hosts
35+
and exit with a non-zero status and a one-line error message
36+
naming the unsupported OS / arch. It MUST NOT partially install,
37+
leave files behind, or attempt to "best-effort" the install.
38+
39+
#### Scenario: Run on Linux
40+
- **WHEN** the user pipes the install.sh through bash on a Linux machine
41+
- **THEN** the script exits non-zero with stderr containing "diting is macOS-only" and no files are written outside `/tmp`
42+
43+
#### Scenario: Run on Apple Silicon iPad-on-Mac emulation or other unsupported uname
44+
- **WHEN** `uname -s` is not `Darwin`
45+
- **THEN** the script exits non-zero with a clear error and writes no files
46+
47+
### Requirement: The installer SHALL fetch the matching release tarball from GitHub Releases and verify its SHA256
48+
The installer SHALL download the platform-matching tarball from the project's GitHub Releases and SHALL refuse to proceed when the SHA256 does not match the published hash. For each supported arch (`darwin-arm64`, `darwin-x86_64`) a GitHub Release SHALL provide:
49+
50+
- `diting-X.Y.Z-darwin-{arch}.tar.gz` — the platform tarball
51+
- `SHASUMS256.txt` — a sibling asset listing SHA256 hashes for
52+
each tarball, one per line, in `sha256sum`-compatible format
53+
54+
The installer SHALL fetch the latest release via the GitHub API
55+
(`api.github.com/repos/chenchaoyi/diting/releases/latest`),
56+
download the right tarball, compute its SHA256, and abort if the
57+
hash does not match the `SHASUMS256.txt` entry.
58+
59+
#### Scenario: Tarball matches SHASUMS256.txt
60+
- **WHEN** the download completes and the computed SHA256 matches the entry in SHASUMS256.txt
61+
- **THEN** the installer proceeds to extract and install
62+
63+
#### Scenario: Tarball SHA256 mismatch
64+
- **WHEN** the computed SHA256 does NOT match the recorded entry
65+
- **THEN** the installer aborts with a non-zero exit and stderr message naming the expected vs actual hash; no files are extracted
66+
67+
#### Scenario: Tag-locked install via DITING_VERSION env var
68+
- **WHEN** the user runs `DITING_VERSION=v0.10.0 curl … | bash`
69+
- **THEN** the installer fetches `v0.10.0` specifically rather than the latest tag
70+
71+
### Requirement: The installer SHALL extract the tarball under `~/.local/share/diting/` and symlink the binary at `~/.local/bin/diting`
72+
The tarball layout MUST be:
73+
74+
```
75+
diting-X.Y.Z/
76+
├── bin/diting
77+
└── share/diting-tianer.app/
78+
└── Contents/
79+
├── Info.plist
80+
└── MacOS/diting-tianer
81+
```
82+
83+
The installer SHALL extract into `~/.local/share/diting/` (creating
84+
parents as needed), atomically replace any prior install, and
85+
symlink `~/.local/bin/diting` to `~/.local/share/diting/bin/diting`.
86+
The installer MUST NOT use sudo.
87+
88+
#### Scenario: Fresh install with no prior `~/.local/share/diting/`
89+
- **WHEN** the user has no prior diting install
90+
- **THEN** the tarball extracts to `~/.local/share/diting/`, `~/.local/bin/diting` is a symlink to `bin/diting`, and `diting --help` succeeds from a shell where `~/.local/bin` is on PATH
91+
92+
#### Scenario: Upgrade replacing an older install
93+
- **WHEN** an older `~/.local/share/diting/` already exists from a previous install
94+
- **THEN** the installer moves the existing directory aside (rename to `~/.local/share/diting.old/`), extracts the new tarball, removes `.old` only after the new symlink target verifies as executable; on failure mid-flight the old directory is restored
95+
96+
### Requirement: The installer SHALL place the Swift helper bundle under `~/Library/Application Support/diting/` and prime it for TCC
97+
After extracting the tarball, the installer SHALL copy
98+
`share/diting-tianer.app/` to
99+
`~/Library/Application Support/diting/diting-tianer.app`, strip
100+
the quarantine xattr so Gatekeeper does not block first launch,
101+
and `open` the bundle once so macOS surfaces the Location Services
102+
and Bluetooth TCC prompts to the user.
103+
104+
#### Scenario: First install primes TCC
105+
- **WHEN** the helper bundle did not exist at the target path before this install
106+
- **THEN** after the installer runs, the bundle exists at the target path, has no `com.apple.quarantine` xattr, and a brief background `open` invocation has fired so the user sees TCC prompts on their next interaction
107+
108+
#### Scenario: Subsequent installs preserve granted permissions
109+
- **WHEN** the user has already granted Location Services and Bluetooth in a prior install and re-runs the installer with a same-cdhash helper binary
110+
- **THEN** the new copy lands at the same path; TCC keys by cdhash so the grants persist with no re-prompt
111+
112+
### Requirement: The installer SHALL print a PATH-update hint when `~/.local/bin` is not on PATH
113+
After installing, the installer SHALL check whether
114+
`~/.local/bin` is on the user's `PATH`. If not, it SHALL print a
115+
single-line hint suggesting the appropriate `export PATH` addition
116+
for the user's detected shell (`zsh`, `bash`, or `fish`).
117+
118+
#### Scenario: PATH already includes `~/.local/bin`
119+
- **WHEN** the user's shell already has `~/.local/bin` on PATH
120+
- **THEN** the installer prints "diting is on your PATH — run `diting`" and no hint
121+
122+
#### Scenario: PATH does NOT include `~/.local/bin` on zsh
123+
- **WHEN** the detected shell is zsh and the PATH check fails
124+
- **THEN** the installer prints exactly: `Add to ~/.zshrc: export PATH="$HOME/.local/bin:$PATH"`
125+
126+
### Requirement: The frozen-binary install path SHALL coexist with the `uv run diting` developer workflow
127+
Installing via the curl-bash one-liner MUST NOT break the
128+
contributor flow (`git clone` + `uv sync` + `make helper` +
129+
`uv run diting`). A developer can have both installed at once —
130+
the frozen binary on PATH and a working repo checkout — without
131+
the two paths interfering. In particular, `make helper` in the
132+
repo MUST keep working, and `find_helper()` MUST keep finding the
133+
in-repo dev build when invoked through `uv run`.
134+
135+
#### Scenario: Developer has both paths installed
136+
- **WHEN** a contributor has run the curl-bash installer AND has a repo checkout where they run `uv run diting`
137+
- **THEN** `uv run diting` picks up the in-repo helper (priority in `find_helper()` ordering), and standalone `diting` picks up the Application Support helper — neither shadows the other
138+
139+
#### Scenario: Developer uninstalls the frozen binary
140+
- **WHEN** the contributor deletes `~/.local/bin/diting` and `~/.local/share/diting/`
141+
- **THEN** `uv run diting` in the repo continues to work; the in-repo helper bundle is unaffected
142+

openspec/specs/macos-helper/spec.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,44 @@ the peripherals users most want to see in the BLE panel.
123123

124124
### Requirement: The helper SHALL be auto-detectable from the Python side without configuration
125125
The Python `_helper.find_helper()` SHALL locate the helper bundle
126-
under `helper/diting-tianer.app/Contents/MacOS/diting-tianer`
127-
relative to the diting source root (editable install) or the
128-
installed package path (pip install). The user SHALL NOT need to set
129-
`PATH` or env vars for the helper to be found.
130-
131-
#### Scenario: Editable install
132-
- **WHEN** diting is installed via `uv pip install -e .`
133-
- **THEN** `find_helper()` returns the path to the in-tree helper bundle
134-
135-
#### Scenario: Pip install without source
136-
- **WHEN** diting is installed via `pip install diting` and the helper bundle is not present
137-
- **THEN** `find_helper()` returns `None`, and the BLE / Wi-Fi paths fall back to direct CoreWLAN (which produces redacted scan results until the user runs `diting-build-helper`)
126+
without the user having to set `PATH` or env vars. The function
127+
SHALL search the following locations in order, returning the first
128+
hit:
129+
130+
1. `DITING_HELPER` env var (full path to the bundle OR the binary
131+
inside it) — escape hatch for contributors testing a non-default
132+
build location
133+
2. `<repo>/helper/diting-tianer.app` relative to the source root —
134+
in-place developer build picked up automatically when `diting`
135+
is run via `uv run` from a repo checkout
136+
3. `/Applications/diting-tianer.app` — back-compat for users who
137+
moved the bundle into `/Applications` before the in-place flow
138+
was the recommended developer path
139+
4. `~/Applications/diting-tianer.app` — same back-compat for
140+
users who installed to their personal Applications folder
141+
5. `~/Library/Application Support/diting/diting-tianer.app`
142+
the install location used by the curl-bash one-line installer
143+
144+
Search order MUST keep the in-repo dev build first so contributors
145+
running `uv run diting` from a checkout always pick up their
146+
freshly-`make helper`ed bundle even if they also have the
147+
one-line installer's copy in place.
148+
149+
#### Scenario: Developer with both a repo checkout and a one-line install
150+
- **WHEN** a contributor has both `<repo>/helper/diting-tianer.app` (from `make helper`) and `~/Library/Application Support/diting/diting-tianer.app` (from the curl-bash installer)
151+
- **THEN** `find_helper()` returns the in-repo path; the Application Support copy is shadowed
152+
153+
#### Scenario: End user with only the one-line install
154+
- **WHEN** a user has no repo checkout, no /Applications copy, only `~/Library/Application Support/diting/diting-tianer.app`
155+
- **THEN** `find_helper()` returns the Application Support path
156+
157+
#### Scenario: Pip install without source AND without a one-line install
158+
- **WHEN** diting is installed via `pip install diting` (no source tree) and the helper bundle is not present at any of the five search locations
159+
- **THEN** `find_helper()` returns `None`, and the BLE / Wi-Fi paths fall back to direct CoreWLAN (which produces redacted scan results until the user installs the helper bundle)
160+
161+
#### Scenario: `DITING_HELPER` env override
162+
- **WHEN** `DITING_HELPER=/Volumes/Builds/diting-tianer.app` is set
163+
- **THEN** `find_helper()` returns that path, ignoring every other location
138164

139165
### Requirement: The helper SHALL fail fast and loud on TCC denial
140166
The helper SHALL exit with code 3 and emit a single line

0 commit comments

Comments
 (0)