feature-flow-cli 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {feature_flow_cli-0.1.4/feature_flow_cli.egg-info → feature_flow_cli-0.1.6}/PKG-INFO +4 -2
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/README.md +3 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/adapters/codex/feature-flow/SKILL.md +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/__init__.py +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/command.py +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/git.py +36 -8
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/install.py +174 -24
- feature_flow_cli-0.1.6/feature_flow/released.py +90 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6/feature_flow_cli.egg-info}/PKG-INFO +4 -2
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow_cli.egg-info/SOURCES.txt +1 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/build.md +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/install.py +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/install.sh +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/skills/feature-flow/SKILL.md +1 -1
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/LICENSE +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/MANIFEST.in +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/adapters/codex/feature-flow/agents/openai.yaml +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/agents/feature-planner.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/agents/plan-reviewer.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/agents/ticket-builder.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/agents/ticket-reviewer.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/__main__.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/checks.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/cli.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/conductor.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/floorguard.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/gate.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/prompts.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/state.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/status.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/suggest.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/tickets.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow/view.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow_cli.egg-info/dependency_links.txt +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow_cli.egg-info/entry_points.txt +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow_cli.egg-info/top_level.txt +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/brief.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/plan-review.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/plan.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/review.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/show.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/templates/commands.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/templates/learnings.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/templates/map.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/templates/spec.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/templates/ticket.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/guides/templates/ui-mockup.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/pyproject.toml +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/scripts/floor-guard.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/scripts/flow-status.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/scripts/flow-view.html +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/scripts/flow-view.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/scripts/flow.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/scripts/gate.py +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/setup.cfg +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/skills/architect-review/SKILL.md +0 -0
- {feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/skills/automation-design/SKILL.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: feature-flow-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
|
|
@@ -53,7 +53,9 @@ uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run fea
|
|
|
53
53
|
|
|
54
54
|
The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
|
|
55
55
|
|
|
56
|
-
The installer lists what it wrote in `.feature-flow/installed.txt`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes.
|
|
56
|
+
The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
|
|
57
|
+
|
|
58
|
+
To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
|
|
57
59
|
|
|
58
60
|
Then, in your repo, in the agent:
|
|
59
61
|
|
|
@@ -34,7 +34,9 @@ uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run fea
|
|
|
34
34
|
|
|
35
35
|
The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
|
|
36
36
|
|
|
37
|
-
The installer lists what it wrote in `.feature-flow/installed.txt`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes.
|
|
37
|
+
The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
|
|
38
|
+
|
|
39
|
+
To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
|
|
38
40
|
|
|
39
41
|
Then, in your repo, in the agent:
|
|
40
42
|
|
|
@@ -31,7 +31,7 @@ To add tickets to a plan that exists, edit it by hand following the ticket rules
|
|
|
31
31
|
|
|
32
32
|
With a plan present, check these before `start`, and stop at the first that fails:
|
|
33
33
|
|
|
34
|
-
- `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and
|
|
34
|
+
- `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and files listed in `.feature-flow/installed.txt` and that list itself: those are feature-flow's own install or upgrade, so never stop for them (`next` still stops if one of them was edited). Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
|
|
35
35
|
- `python3 scripts/flow-status.py <feature> --check` prints `OK`.
|
|
36
36
|
- `.agents/flow-roles/ticket-builder.md` and `.agents/flow-roles/ticket-reviewer.md` exist. If not, say to run `uvx feature-flow-cli install . --agent codex` in this repo (or `python3 install.py <repo> --agent codex` from a feature-flow clone).
|
|
37
37
|
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from feature_flow import __version__, cli, install, status, suggest, view
|
|
13
13
|
|
|
14
14
|
USAGE = """\
|
|
15
|
-
usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
|
|
15
|
+
usage: feature-flow install [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
|
|
16
16
|
feature-flow status <feature> [--next | --counts | --check | --mermaid [plain] | --json]
|
|
17
17
|
feature-flow view <feature> [--watch] [--no-open] [--out FILE]
|
|
18
18
|
feature-flow reset <feature> [NN]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""The few git calls the conductor needs, each one an argument list, never a shell string."""
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -30,17 +31,30 @@ def head():
|
|
|
30
31
|
return _git("rev-parse", "HEAD").stdout.strip()
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
# the installer's record of what it wrote: the paths, and the sha256 of each as it wrote it
|
|
35
|
+
INSTALLED = Path(".feature-flow") / "installed.txt"
|
|
36
|
+
HASHES = Path(".feature-flow") / "installed.sha256"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_lines(path):
|
|
40
|
+
try:
|
|
41
|
+
return path.read_text(encoding="utf-8").splitlines()
|
|
42
|
+
except (OSError, UnicodeDecodeError):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
|
|
33
46
|
def changes():
|
|
34
47
|
"""Every changed path, untracked files included, as `git status --porcelain` sees them, except
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
feature-flow's own install: a file still exactly as the installer wrote it, an untracked file it
|
|
49
|
+
lists without a hash (an install from before it kept hashes), and its two record files, which every
|
|
50
|
+
install rewrites; and untracked Python bytecode, which the flow's own Python runs leave behind."""
|
|
51
|
+
listed, hashes = set(), {}
|
|
38
52
|
top = toplevel()
|
|
39
53
|
if top is not None:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
listed = set(_read_lines(top / INSTALLED))
|
|
55
|
+
for line in _read_lines(top / HASHES):
|
|
56
|
+
digest, _, name = line.partition(" ")
|
|
57
|
+
hashes[name] = digest
|
|
44
58
|
entries = _git("status", "--porcelain", "-z", "-uall").stdout.split("\0")
|
|
45
59
|
found = []
|
|
46
60
|
i = 0
|
|
@@ -52,12 +66,26 @@ def changes():
|
|
|
52
66
|
code, path = entry[:2], entry[3:]
|
|
53
67
|
if code[0] in "RC":
|
|
54
68
|
i += 1 # the rename's source path follows
|
|
55
|
-
if
|
|
69
|
+
if path in (INSTALLED.as_posix(), HASHES.as_posix()):
|
|
70
|
+
continue
|
|
71
|
+
if path in hashes:
|
|
72
|
+
# only a worktree change: a staged version would ride along in the next commit
|
|
73
|
+
if (code[0] in " ?" and code[1] != "T" and top is not None and not (top / path).is_symlink()
|
|
74
|
+
and _sha256(top / path) == hashes[path]):
|
|
75
|
+
continue
|
|
76
|
+
elif code == "??" and (path in listed or _bytecode(path)):
|
|
56
77
|
continue
|
|
57
78
|
found.append(path)
|
|
58
79
|
return found
|
|
59
80
|
|
|
60
81
|
|
|
82
|
+
def _sha256(path):
|
|
83
|
+
try:
|
|
84
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
85
|
+
except OSError:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
61
89
|
def _bytecode(path):
|
|
62
90
|
return path.endswith(".pyc") or "__pycache__/" in "/" + path
|
|
63
91
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""The installer, ported from install.sh with the same report and exit codes.
|
|
2
2
|
|
|
3
|
-
usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
|
|
3
|
+
usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
|
|
4
4
|
copies the feature-flow skill, its roles, scripts and guides into a repo so the flow works there.
|
|
5
|
-
a file that already exists and differs is kept and reported, unless --force is given
|
|
5
|
+
a file that already exists and differs is kept and reported, unless --force is given or nobody
|
|
6
|
+
edited it since an earlier feature-flow install wrote it (then it is updated).
|
|
6
7
|
nothing is ever deleted. CLAUDE_HOME overrides ~/.claude and AGENTS_HOME overrides ~/.agents for --user.
|
|
7
8
|
|
|
8
9
|
Files are compared and written as bytes, and the two text transforms (the Codex skill header and
|
|
@@ -11,15 +12,18 @@ so the installed tree is the same byte for byte. Files are listed in byte order,
|
|
|
11
12
|
with LC_ALL=C.
|
|
12
13
|
"""
|
|
13
14
|
|
|
15
|
+
import hashlib
|
|
14
16
|
import os
|
|
15
17
|
import re
|
|
16
18
|
import shutil
|
|
17
19
|
import subprocess
|
|
18
20
|
import sys
|
|
19
21
|
|
|
22
|
+
from feature_flow.released import RELEASED
|
|
23
|
+
|
|
20
24
|
# the help text
|
|
21
25
|
USAGE = """\
|
|
22
|
-
usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
|
|
26
|
+
usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
|
|
23
27
|
copies the feature-flow skill, its roles, scripts and guides into a repo so the flow works there.
|
|
24
28
|
--agent claude (default)
|
|
25
29
|
skills go to <target>/.claude/skills/ (or ~/.claude/skills/ with --user)
|
|
@@ -30,7 +34,10 @@ copies the feature-flow skill, its roles, scripts and guides into a repo so the
|
|
|
30
34
|
--agent all does both.
|
|
31
35
|
scripts go to <target>/scripts/ and guides, roles and the Python package to <target>/.feature-flow/
|
|
32
36
|
(always: the skill and scripts/flow.py read them from the repo)
|
|
33
|
-
a file that already exists and differs is kept and reported, unless --force is given
|
|
37
|
+
a file that already exists and differs is kept and reported, unless --force is given or it is
|
|
38
|
+
still exactly what an earlier feature-flow install wrote (then it is updated).
|
|
39
|
+
--private keeps the install out of git: its paths go into .git/info/exclude, which is never
|
|
40
|
+
committed, so only the plans and tickets are. Later installs keep that list up to date.
|
|
34
41
|
nothing is ever deleted. CLAUDE_HOME overrides ~/.claude and AGENTS_HOME overrides ~/.agents for --user.
|
|
35
42
|
"""
|
|
36
43
|
|
|
@@ -42,6 +49,19 @@ SKILLS = ("feature-flow", "architect-review", "automation-design")
|
|
|
42
49
|
# the in-repo files an install wrote, one path per line, relative to the target. The conductor does not
|
|
43
50
|
# count them as a dirty tree while they are untracked, and `git add --pathspec-from-file` commits them.
|
|
44
51
|
INSTALLED = ".feature-flow/installed.txt"
|
|
52
|
+
# the sha256 of each of those files as the install wrote it, `<sha256> <path>` per line. A later install
|
|
53
|
+
# replaces a file that still has its recorded bytes: nobody edited it, so it is not the user's version.
|
|
54
|
+
HASHES = ".feature-flow/installed.sha256"
|
|
55
|
+
# the same record for a --user install, in ~/.claude and ~/.agents, paths relative to that folder
|
|
56
|
+
USER_HASHES = "feature-flow.sha256"
|
|
57
|
+
# the lines a --private install keeps in .git/info/exclude, between these two markers
|
|
58
|
+
EXCLUDE_BEGIN = "# feature-flow install (--private): kept out of git"
|
|
59
|
+
EXCLUDE_END = "# end of feature-flow install"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sha256(data):
|
|
63
|
+
return hashlib.sha256(data).hexdigest()
|
|
64
|
+
|
|
45
65
|
|
|
46
66
|
# an old skill that runs the ticket script (the bash one or the Python one) or ends a review with a verdict
|
|
47
67
|
LEFTOVER = re.compile(rb"scripts/flow-status\.(?:sh|py)|REVIEW: PASS")
|
|
@@ -152,13 +172,14 @@ def _logical_cwd():
|
|
|
152
172
|
|
|
153
173
|
|
|
154
174
|
class Installer:
|
|
155
|
-
def __init__(self, here, target, agent, user_level, force, dry, out):
|
|
175
|
+
def __init__(self, here, target, agent, user_level, force, dry, out, private=False):
|
|
156
176
|
self.here = here
|
|
157
177
|
self.target = target
|
|
158
178
|
self.agent = agent
|
|
159
179
|
self.force = force
|
|
160
180
|
self.dry = dry
|
|
161
181
|
self.out = out
|
|
182
|
+
self.private = private
|
|
162
183
|
if user_level:
|
|
163
184
|
home = os.environ.get("HOME", "")
|
|
164
185
|
self.claude_dir = os.environ.get("CLAUDE_HOME") or home + "/.claude"
|
|
@@ -172,7 +193,14 @@ class Installer:
|
|
|
172
193
|
package = os.path.dirname(os.path.abspath(__file__))
|
|
173
194
|
self.package = package
|
|
174
195
|
self.added = self.updated = self.same = self.kept = 0
|
|
175
|
-
self.written =
|
|
196
|
+
self.written = {}
|
|
197
|
+
# each folder this installs into, with its hash record; the target's own comes first
|
|
198
|
+
self.records = [(target, HASHES)]
|
|
199
|
+
if user_level and agent in ("claude", "all"):
|
|
200
|
+
self.records.append((self.claude_dir, USER_HASHES))
|
|
201
|
+
if user_level and agent in ("codex", "all"):
|
|
202
|
+
self.records.append((self.agents_dir, USER_HASHES))
|
|
203
|
+
self.recorded = dict((root, self._read_hashes(root + "/" + record)) for root, record in self.records)
|
|
176
204
|
|
|
177
205
|
def place(self, file, dest, data=None):
|
|
178
206
|
"""One file: add it, replace it (--force), leave it alone (same) or keep the user's version.
|
|
@@ -181,23 +209,55 @@ class Installer:
|
|
|
181
209
|
if not os.path.exists(dest):
|
|
182
210
|
self.out(" add " + dest)
|
|
183
211
|
self.added += 1
|
|
184
|
-
self.written
|
|
212
|
+
self.written[dest] = new
|
|
185
213
|
if not self.dry:
|
|
186
214
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
187
215
|
self._copy(file, dest, data)
|
|
188
216
|
elif new is not None and _read(dest) == new:
|
|
189
217
|
self.same += 1
|
|
190
|
-
self.written
|
|
191
|
-
elif self.force:
|
|
218
|
+
self.written[dest] = new
|
|
219
|
+
elif self.force or self.untouched(dest):
|
|
192
220
|
self.out(" update " + dest)
|
|
193
221
|
self.updated += 1
|
|
194
|
-
self.written
|
|
222
|
+
self.written[dest] = new
|
|
195
223
|
if not self.dry:
|
|
196
224
|
self._copy(file, dest, data)
|
|
197
225
|
else:
|
|
198
226
|
self.out(" kept " + dest + " (differs from this version, use --force to replace it)")
|
|
199
227
|
self.kept += 1
|
|
200
228
|
|
|
229
|
+
def locate(self, dest):
|
|
230
|
+
"""The install folder dest is in and dest relative to it, or (None, None)."""
|
|
231
|
+
for root, _ in sorted(self.records, key=lambda r: -len(r[0])):
|
|
232
|
+
if dest.startswith(root + "/"):
|
|
233
|
+
return root, dest[len(root) + 1:]
|
|
234
|
+
return None, None
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _read_hashes(path):
|
|
238
|
+
recorded = {}
|
|
239
|
+
try:
|
|
240
|
+
with open(path, encoding="utf-8") as f:
|
|
241
|
+
for line in f:
|
|
242
|
+
digest, _, name = line.rstrip("\n").partition(" ")
|
|
243
|
+
if name:
|
|
244
|
+
recorded[name] = digest
|
|
245
|
+
except (OSError, UnicodeDecodeError):
|
|
246
|
+
pass
|
|
247
|
+
return recorded
|
|
248
|
+
|
|
249
|
+
def untouched(self, dest):
|
|
250
|
+
"""dest still holds the bytes an earlier feature-flow install wrote there. Never a symlink or a
|
|
251
|
+
file with other hard links: writing through it would change the file it shares bytes with."""
|
|
252
|
+
if os.path.islink(dest) or os.stat(dest).st_nlink > 1:
|
|
253
|
+
return False
|
|
254
|
+
old = _read(dest)
|
|
255
|
+
if old is None:
|
|
256
|
+
return False
|
|
257
|
+
digest = _sha256(old)
|
|
258
|
+
root, rel = self.locate(dest)
|
|
259
|
+
return digest in RELEASED or self.recorded.get(root, {}).get(rel) == digest
|
|
260
|
+
|
|
201
261
|
@staticmethod
|
|
202
262
|
def _copy(file, dest, data):
|
|
203
263
|
"""cp -p: contents, mode and times."""
|
|
@@ -243,21 +303,92 @@ class Installer:
|
|
|
243
303
|
role_body(data if data is not None else b""))
|
|
244
304
|
|
|
245
305
|
def write_installed(self):
|
|
246
|
-
"""Add this run's in-repo files to the list,
|
|
247
|
-
|
|
306
|
+
"""Add this run's in-repo files to the list, and every file's hash to its folder's record, keeping
|
|
307
|
+
the earlier runs' entries for files that still exist. True when the target's list or record changed."""
|
|
248
308
|
names = set()
|
|
249
309
|
try:
|
|
250
|
-
with open(
|
|
310
|
+
with open(self.target + "/" + INSTALLED, encoding="utf-8") as old:
|
|
251
311
|
names.update(line.strip() for line in old)
|
|
252
312
|
except OSError:
|
|
253
313
|
pass
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
314
|
+
hashes = dict((root, dict(self.recorded[root])) for root, _ in self.records)
|
|
315
|
+
for dest, data in self.written.items():
|
|
316
|
+
root, rel = self.locate(dest)
|
|
317
|
+
if root == self.target:
|
|
318
|
+
names.add(rel)
|
|
319
|
+
if root is not None and data is not None:
|
|
320
|
+
hashes[root][rel] = _sha256(data)
|
|
321
|
+
names.update((INSTALLED, HASHES))
|
|
322
|
+
names = sorted(n for n in names
|
|
323
|
+
if n in (INSTALLED, HASHES) or (n and os.path.isfile(self.target + "/" + n)))
|
|
324
|
+
changed = self._write(self.target + "/" + INSTALLED, "".join(n + "\n" for n in names))
|
|
325
|
+
for root, record in self.records:
|
|
326
|
+
kept = names if root == self.target else sorted(
|
|
327
|
+
n for n in hashes[root] if os.path.isfile(root + "/" + n))
|
|
328
|
+
text = "".join("%s %s\n" % (hashes[root][n], n) for n in kept if n in hashes[root])
|
|
329
|
+
if self._write(root + "/" + record, text) and root == self.target:
|
|
330
|
+
changed = True
|
|
331
|
+
return changed
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _write(path, text):
|
|
335
|
+
try:
|
|
336
|
+
with open(path, encoding="utf-8", newline="") as f:
|
|
337
|
+
if f.read() == text:
|
|
338
|
+
return False
|
|
339
|
+
except (OSError, UnicodeDecodeError):
|
|
340
|
+
pass
|
|
258
341
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
259
|
-
|
|
260
|
-
|
|
342
|
+
# a new file moved into place: a link at the path is replaced, never written through
|
|
343
|
+
tmp = path + ".tmp"
|
|
344
|
+
with open(tmp, "w", encoding="utf-8", newline="\n") as f:
|
|
345
|
+
f.write(text)
|
|
346
|
+
os.replace(tmp, path)
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
def exclude_file(self):
|
|
350
|
+
"""The repo's .git/info/exclude (a worktree's shared one too), or None outside a git repo."""
|
|
351
|
+
try:
|
|
352
|
+
r = subprocess.run(["git", "-C", self.target, "rev-parse", "--git-path", "info/exclude"],
|
|
353
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=True)
|
|
354
|
+
except OSError:
|
|
355
|
+
return None
|
|
356
|
+
path = r.stdout.strip()
|
|
357
|
+
if r.returncode != 0 or not path:
|
|
358
|
+
return None
|
|
359
|
+
return os.path.normpath(os.path.join(self.target, path))
|
|
360
|
+
|
|
361
|
+
def excluded(self):
|
|
362
|
+
"""An earlier --private install left its block in .git/info/exclude: keep the install private."""
|
|
363
|
+
path = self.exclude_file()
|
|
364
|
+
return path is not None and EXCLUDE_BEGIN in _read_text(path).splitlines()
|
|
365
|
+
|
|
366
|
+
def write_exclude(self, path):
|
|
367
|
+
"""Put every in-repo file of the install between the markers in .git/info/exclude, keeping the
|
|
368
|
+
user's own lines. True when the file changed."""
|
|
369
|
+
try:
|
|
370
|
+
with open(path, encoding="utf-8") as f:
|
|
371
|
+
lines = f.read().splitlines()
|
|
372
|
+
except (OSError, UnicodeDecodeError):
|
|
373
|
+
lines = []
|
|
374
|
+
if EXCLUDE_BEGIN in lines and EXCLUDE_END in lines[lines.index(EXCLUDE_BEGIN):]:
|
|
375
|
+
start = lines.index(EXCLUDE_BEGIN)
|
|
376
|
+
end = lines.index(EXCLUDE_END, start)
|
|
377
|
+
del lines[start:end + 1]
|
|
378
|
+
names = [n.strip() for n in _read_text(self.target + "/" + INSTALLED).splitlines()]
|
|
379
|
+
block = ["/.feature-flow/"] + ["/" + n for n in names if n and not n.startswith(".feature-flow/")]
|
|
380
|
+
text = "\n".join(lines + [EXCLUDE_BEGIN] + block + [EXCLUDE_END]) + "\n"
|
|
381
|
+
return self._write(path, text)
|
|
382
|
+
|
|
383
|
+
def tracked_installed(self):
|
|
384
|
+
"""The installed files git already tracks: an exclude line does not untrack them."""
|
|
385
|
+
try:
|
|
386
|
+
r = subprocess.run(["git", "-C", self.target, "ls-files", "-z", "--full-name"],
|
|
387
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=True)
|
|
388
|
+
except OSError:
|
|
389
|
+
return []
|
|
390
|
+
names = set(_read_text(self.target + "/" + INSTALLED).splitlines())
|
|
391
|
+
return [n for n in r.stdout.split("\0") if n in names]
|
|
261
392
|
|
|
262
393
|
def report_leftovers(self, dirs):
|
|
263
394
|
"""A skill folder this installer does not own but that drives the flow (it runs the ticket
|
|
@@ -294,8 +425,7 @@ class Installer:
|
|
|
294
425
|
self.copy_tree(here + "/guides", self.target + "/.feature-flow/guides")
|
|
295
426
|
self.copy_tree(here + "/agents", self.target + "/.feature-flow/agents")
|
|
296
427
|
self.copy_tree(self.package, self.target + "/.feature-flow/feature_flow", skip=BUNDLE)
|
|
297
|
-
|
|
298
|
-
self.write_installed()
|
|
428
|
+
listed = not self.dry and self.write_installed()
|
|
299
429
|
|
|
300
430
|
self.out("")
|
|
301
431
|
verb = "would add" if self.dry else "added"
|
|
@@ -311,7 +441,17 @@ class Installer:
|
|
|
311
441
|
git = 127
|
|
312
442
|
if git != 0:
|
|
313
443
|
self.out("note: %s is not a git repository, and the flow needs one" % self.target)
|
|
314
|
-
elif self.
|
|
444
|
+
elif self.private or self.excluded():
|
|
445
|
+
self.private = True
|
|
446
|
+
exclude = self.exclude_file()
|
|
447
|
+
if exclude and self.write_exclude(exclude):
|
|
448
|
+
self.out("kept out of git: the install is listed in " + os.path.relpath(exclude, self.target)
|
|
449
|
+
.replace(os.sep, "/") + ", which is never committed")
|
|
450
|
+
if self.tracked_installed():
|
|
451
|
+
self.out("note: git still tracks the install from before. untrack it, keeping the files: "
|
|
452
|
+
"git rm -r -q --cached --ignore-unmatch --pathspec-from-file=%s"
|
|
453
|
+
' && git commit -m "chore: stop tracking feature-flow"' % INSTALLED)
|
|
454
|
+
elif self.added or self.updated or listed:
|
|
315
455
|
git_c = ""
|
|
316
456
|
if os.path.realpath(self.target) != os.path.realpath(os.getcwd()):
|
|
317
457
|
git_c = '-C "%s" ' % self.target if " " in self.target else "-C %s " % self.target
|
|
@@ -326,9 +466,17 @@ class Installer:
|
|
|
326
466
|
return 0
|
|
327
467
|
|
|
328
468
|
|
|
469
|
+
def _read_text(path):
|
|
470
|
+
data = _read(path)
|
|
471
|
+
try:
|
|
472
|
+
return data.decode("utf-8") if data is not None else ""
|
|
473
|
+
except UnicodeDecodeError:
|
|
474
|
+
return ""
|
|
475
|
+
|
|
476
|
+
|
|
329
477
|
def parse(argv):
|
|
330
478
|
"""The options, read in order like the bash case statement. Returns a dict, or None for --help."""
|
|
331
|
-
opts = {"target": "", "agent": "claude", "user": False, "force": False, "dry": False}
|
|
479
|
+
opts = {"target": "", "agent": "claude", "user": False, "private": False, "force": False, "dry": False}
|
|
332
480
|
i = 0
|
|
333
481
|
while i < len(argv):
|
|
334
482
|
a = argv[i]
|
|
@@ -341,6 +489,8 @@ def parse(argv):
|
|
|
341
489
|
opts["agent"] = a[len("--agent="):]
|
|
342
490
|
elif a == "--user":
|
|
343
491
|
opts["user"] = True
|
|
492
|
+
elif a == "--private":
|
|
493
|
+
opts["private"] = True
|
|
344
494
|
elif a == "--force":
|
|
345
495
|
opts["force"] = True
|
|
346
496
|
elif a == "--dry-run":
|
|
@@ -375,7 +525,7 @@ def run(argv, here, out, err):
|
|
|
375
525
|
return 2
|
|
376
526
|
target = os.path.normpath(os.path.join(_logical_cwd(), target))
|
|
377
527
|
installer = Installer(os.path.abspath(str(here)), target, opts["agent"], opts["user"],
|
|
378
|
-
opts["force"], opts["dry"], out)
|
|
528
|
+
opts["force"], opts["dry"], out, opts["private"])
|
|
379
529
|
return installer.run()
|
|
380
530
|
|
|
381
531
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The sha256 of every file that feature-flow-cli 0.1.0 to 0.1.4 installed (all three --agent
|
|
2
|
+
choices; .feature-flow/installed.txt aside). Those releases kept no record of what they wrote, so an
|
|
3
|
+
install over one of them replaces a file whose bytes are on this list: nobody edited it since. Later
|
|
4
|
+
installs record their own hashes in .feature-flow/installed.sha256, so this list never grows."""
|
|
5
|
+
|
|
6
|
+
RELEASED = frozenset((
|
|
7
|
+
"03111666084be6a375a0e7a42784ea8c5aed7a5a1e71e83cf2ad44fd656f758c",
|
|
8
|
+
"0385988f5d4a23daf13556a391b71bb387a25ab2b7fa8bba81b0276321028e07",
|
|
9
|
+
"0597a52bee02482ee472218b98ee6c507900df02c4f081ca795b0038eebc21bd",
|
|
10
|
+
"08419881ab53935e0a739d8ae7fc6e5cdfaeb597394bdd5c11d538a5a8086091",
|
|
11
|
+
"0b2544121889d10f5b3295700ed49d03940291243edbe6f69b5cc9eb5c2678db",
|
|
12
|
+
"0fbc776b32836e8050c07cd95d50265a415999f558c00bc7131a8b1c8f8dd893",
|
|
13
|
+
"116af2cb64dd22b061e7217a7aec96ecc2fa9193905f17154ce73542b8a3a049",
|
|
14
|
+
"158be4902bdd7a039c9fd844188545c148e289c8ca168a0a93890b9321342252",
|
|
15
|
+
"1598ed3851313756552c258447b74a7b59aca36365760ce66ecd0695a8614021",
|
|
16
|
+
"16e75f565e30ac0d4e418d813f9c399928607a26a7c84f4b983bb1e8fc4e30c1",
|
|
17
|
+
"1bdbe5d780580a9931d123decccdf5c06293b11e3481658dbfe50a8511326b85",
|
|
18
|
+
"1ee3b01e4c92e9fa9f8124a18fa89ea336562d2597325201ec0595ee431f0c09",
|
|
19
|
+
"2a3416153494015130e81876ee94f452a9c94848757cff488309ac056048887a",
|
|
20
|
+
"2a799ddadcfc4744c0837cb0a087f74556cdb88945be93ed8e508a5667c1f65a",
|
|
21
|
+
"2ae68f7df0e92d59a9e69dbd3da5103da9ab3de9b9a2ab7aec3054c410cb5ad1",
|
|
22
|
+
"2c6350ef4162ee3d208145821818c4540a99dcf4bb17915f0fdfca8a89e01bfb",
|
|
23
|
+
"2d2666ff07b3bafa9b257b013a1ef5f889b95c6657cc553f580bc74ef1232038",
|
|
24
|
+
"2f5b589e2acafc5bfefbcbf68d69bc57c028dc509d4766160762695be532aa9b",
|
|
25
|
+
"2f94fd1730077974774efa75d49741a6277e9a060041041c023ff9ff4d6129e7",
|
|
26
|
+
"38901e898c4f78b41527dede20bfca26080de99683b3e5bba0019d87046ae338",
|
|
27
|
+
"38b2d2b4c45e8c76ecd0381ef0820ca4cc516c4ac1f117b2ec99ac6ce3c2e300",
|
|
28
|
+
"39d97eb189726b3c92a399dc60e736d12b12a8252d637588c5b2a31fc2020b5f",
|
|
29
|
+
"3ff57472860da0076ac1d8a8a0537eddd29f3c63989cd5068906e5f31e7abebc",
|
|
30
|
+
"408779e5a420a8b90cf0422830660f1593efdd9c4999665a1beb0097219c2749",
|
|
31
|
+
"447f0c9f2eccb2f5313ca913d91d7fb9c336b30fa03b5d534609cd22de040703",
|
|
32
|
+
"45b62b9d60f88ef8ac8f22f585d662a3004b1dce3875f125b5a1649958511392",
|
|
33
|
+
"45cc664cb168ea82b3d04d16cd96c4392bcc21741f55749306e69fd8f2a0f68c",
|
|
34
|
+
"46ad183898dfa8d860480a123049a74c1bae56d364434ddcd51ed955fec02ca1",
|
|
35
|
+
"4bb17e6f9d9b3c77e858f061b888ab9a7bea9e3ef2cf269230c1c3d42d800a91",
|
|
36
|
+
"52c8362a637ea80835743fd568bce4c05c591d758608c5b89316391779b0166b",
|
|
37
|
+
"569951f6d447ae256d1802ab29ac287af6858e38a34879d3f4534d732097babc",
|
|
38
|
+
"5a2b9d23bbc192412b574d96cc26c09e486173c4ecf3b1d72e4f989f600bbaa9",
|
|
39
|
+
"5b4611c7d419c54d645189cf3e67630678b10d5300f7a6dbe32f75d87876dbf4",
|
|
40
|
+
"5e3895c34a7a0a95cd784c3c99beab4e102bc0a84edd76a9e7586007f55df16b",
|
|
41
|
+
"631fd0f9243caca088bf8ff37a08c1f5e75bcd42589b73a18137936259d211b7",
|
|
42
|
+
"6981a0e0de889d7296bc680f030adf3e354785424d589548257863ca6ae82835",
|
|
43
|
+
"69b13a131466af54a172e7255fade246aad706b177fa3601a843af7a873cb226",
|
|
44
|
+
"6b559ad3ea84b358417700469b7aa0aa48079fc6cddcc9fcbc2dbe312a6ccec8",
|
|
45
|
+
"71c83d16835bf50ee9e78a4a4183a101a1124d5059390e219b7509c96b70488c",
|
|
46
|
+
"71d26991a5263aade812bc9aaf90ddbee44b9c61263302fa5fbcf0093f74be6b",
|
|
47
|
+
"7530aa90149353b607560eb9c16a8ce9be75846f68fb3b71c5a2c9173d21a9b3",
|
|
48
|
+
"761710d8db4ffcb3c9c1a3d941210fa052569994bc2138e974ad44cc6ffe6ba3",
|
|
49
|
+
"774116e163377390b58dc779d008825013d4aff023a2c30332f46db53b473edd",
|
|
50
|
+
"7e42e219a8bb55cad10239ffa290a3d12912350635e8c846c105888dfada5670",
|
|
51
|
+
"800e3a2518755ca20f64ba8518943a11f60e14c8e0ec77bc7a631b7660968861",
|
|
52
|
+
"80535d95ae9fb56ddf746240e9ce6fcf6eaac401e0516ac37584b411113b7017",
|
|
53
|
+
"89522ebb5bf8891a5faab80c86715e3f610808ef588ce0bb261c5ba4bb83c115",
|
|
54
|
+
"8cfd3af473fa146477117cbc406841c542bc137516951fb801204395b444233b",
|
|
55
|
+
"9551f7f94fed0f7e1bb6f35dc6826e2eec37ae18f840ac741fbf369c0fb0bfe7",
|
|
56
|
+
"983395967e1e26bca59cf4da539481f426c3c3d02e67028076cd4d204527ac44",
|
|
57
|
+
"9892816825e0de4f5a40dbaf2506b30d1b3568d0aeadc6597c4a1fc0fd872225",
|
|
58
|
+
"9de945fd57f367ae4db739d8a63d45996912f3797450b53f3ad04f8c43f5cbd5",
|
|
59
|
+
"a0b250ed64f1d38dd9029507a8a34fdbd2bc3aa78a930c439029889f6acfd89f",
|
|
60
|
+
"a666d3eba10f164f50a5005ef70c19b1d5f33c3d43fdf926ac7a9a6815514366",
|
|
61
|
+
"a6d0a0b7ba5c5ccedf2a4d27e0b06e61b8e9563e81fdcd7617da9d4bc3506d71",
|
|
62
|
+
"a7871218beabd7928dc63729cd5141539dc291bb037baa1b9c82e8269c0e272a",
|
|
63
|
+
"aafd905d9d98879c7b6747aa544542df23f6d565cdcef47f7a207808983900d9",
|
|
64
|
+
"ae0790f7fca8c996c3844827e9904ee95b2ced3854277bd440faa59213603935",
|
|
65
|
+
"ae999c50a91a234b4943dff8731d40d30da7d04b2ba4cf13911a6ac6236d2c60",
|
|
66
|
+
"af9a25625f44a64770d6aebce5882c08dbda89b116a25d430a1b7855b8b0d9cf",
|
|
67
|
+
"b922951702a7837923fb52820930ca32fc96c13fc1f0a44650ee135828b4aa80",
|
|
68
|
+
"bc2f066334afbee46bd97168513c36ae129a098179c389c1986b0b7a47f98621",
|
|
69
|
+
"bd24f9b858bab7411493eb97a2c011d906ec56d7a7efb2c7e116cc308b0e4c55",
|
|
70
|
+
"bd7a2653a1015b9e42366c21a2dd60d1c4ea3ca07ca0afee9dddf3d36ec7741e",
|
|
71
|
+
"bda718088ea2b848b000dca2ff78e35795d4400b57ecbc0bfbccdab451245f45",
|
|
72
|
+
"c0a28ab04d02c5eff6f94ed3cdc543257919adcaaf30ec108cd6692a5319081c",
|
|
73
|
+
"c2dbc3162f8bd47a15cb0a10e6cade2cefd3f951ea8c75b175957be1cdceb174",
|
|
74
|
+
"c35e773600d4c3340b5e49eff9cc8eb2682adf7ab8a56599a2ce3fa709c10cbf",
|
|
75
|
+
"c36d2575c40b7f71abc45a58124e6a18b6f423919b9bc1ec69a1a1ff0095589d",
|
|
76
|
+
"c3a0196ebb59f110e968173ff40df3778e73e7eded2afe58deea9dc0734a9e20",
|
|
77
|
+
"cf78e2cfe6e224e3854017787634bffc3e048b48709b767b7ae953e20b707746",
|
|
78
|
+
"d3a2313826088d95dc0bc2625b0222f98223636eda3a59efeac5838f362e5b2b",
|
|
79
|
+
"e0a98bc037bfdef778a0d5d14a4d9abc2b23c4c0ea754491cba307e9c0e2169f",
|
|
80
|
+
"e8194c0aa46b169d8412837f9345c8f32378570b8474a2357c5570080093b7ce",
|
|
81
|
+
"eb346f05cd6359292749a0915fe8b3412b3f99a493a21b294e763a3da0a80852",
|
|
82
|
+
"ee7c8f100721d6ad36ad653a0b82c11eab02217f015344bc707c158f32a08c93",
|
|
83
|
+
"f05d890a79379ee8b89bcef86361acc93ca6b3f7b277500126d058243a60f9f2",
|
|
84
|
+
"f335488f1e89e63bf2f0588a71feccbfece080ef9dcb5fac15f89868576434f0",
|
|
85
|
+
"f34612dfc665fccc1a47f041d096877fa8a4d9c00aad832646d9ab6408305bba",
|
|
86
|
+
"f6352f7ca92571045aa0c7d8ffbe2708f8315c6bd852433e7f32fc8b4af65fdb",
|
|
87
|
+
"f714136a9c949bcb23b67755a3072d0946ca5a6c0a484f6a42f80c23964e5758",
|
|
88
|
+
"f799e5372b598cda13cf2b2d8b888c1703fed71a3af2ec2aa802b838a7de39ba",
|
|
89
|
+
"fded1be6a891c062ad84d14960b47daa0d2424d87ade33922d7f9acfe2b64600",
|
|
90
|
+
))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: feature-flow-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A ticket-graph workflow for coding agents: installs the feature-flow skill into a repo
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/hamilton-sky/feature-flow
|
|
@@ -53,7 +53,9 @@ uvx feature-flow-cli install /path/to/your/repo --agent all # or: pipx run fea
|
|
|
53
53
|
|
|
54
54
|
The package puts a `feature-flow` command on your PATH with `install`, `status <feature>` and `view <feature>`. It installs the same files, byte for byte, as `install.py` from a clone.
|
|
55
55
|
|
|
56
|
-
The installer lists what it wrote in `.feature-flow/installed.txt`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes.
|
|
56
|
+
The installer lists what it wrote in `.feature-flow/installed.txt`, with each file's hash in `.feature-flow/installed.sha256`. Commit those files (`git add --pathspec-from-file=.feature-flow/installed.txt`); until you do, the flow does not count them as uncommitted changes. Installing a newer version over an older one updates every file nobody edited since it was installed, and keeps (and names) the ones you changed; `--force` replaces those too.
|
|
57
|
+
|
|
58
|
+
To keep the install out of git, so only your plans and tickets get committed, install with `--private`. It lists the installed files in `.git/info/exclude`, which git reads like `.gitignore` but never commits, and later installs keep that list up to date. If you already committed the install, it prints the `git rm --cached` command that untracks it and keeps the files. Every clone then needs its own install.
|
|
57
59
|
|
|
58
60
|
Then, in your repo, in the agent:
|
|
59
61
|
|
|
@@ -12,7 +12,7 @@ Follow the repo's documented ticket location if it has one. Otherwise they are i
|
|
|
12
12
|
|
|
13
13
|
## Step 2: Pre-flight
|
|
14
14
|
|
|
15
|
-
Run `git status --porcelain`.
|
|
15
|
+
Run `git status --porcelain`. Files listed in `.feature-flow/installed.txt`, and that list itself, are feature-flow's own install and do not count, and neither does untracked Python bytecode (`__pycache__/`, `*.pyc`).
|
|
16
16
|
|
|
17
17
|
- Clean: continue.
|
|
18
18
|
- Dirty, manual: show the files and offer to commit, stash, or continue anyway.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""The feature-flow installer. usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]"""
|
|
2
|
+
"""The feature-flow installer. usage: python3 install.py [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]"""
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# usage: bash install.sh [target-repo] [--agent claude|codex|all] [--user] [--force] [--dry-run]
|
|
2
|
+
# usage: bash install.sh [target-repo] [--agent claude|codex|all] [--user] [--private] [--force] [--dry-run]
|
|
3
3
|
# runs the Python installer next to this file; python3 install.py --help says what it does.
|
|
4
4
|
exec python3 "$(dirname "$0")/install.py" "$@"
|
|
@@ -31,7 +31,7 @@ To add tickets to a plan that exists, edit it by hand following the ticket rules
|
|
|
31
31
|
|
|
32
32
|
With a plan present, check these before `start`, and stop at the first that fails:
|
|
33
33
|
|
|
34
|
-
- `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and
|
|
34
|
+
- `git status --porcelain` is empty, apart from untracked Python bytecode (`__pycache__/`, `*.pyc`) and files listed in `.feature-flow/installed.txt` and that list itself: those are feature-flow's own install or upgrade, so never stop for them (`next` still stops if one of them was edited). Suggest committing them (`git add --pathspec-from-file=.feature-flow/installed.txt && git commit -m "chore: install feature-flow"`) and carry on.
|
|
35
35
|
- `python3 scripts/flow-status.py <feature> --check` prints `OK`.
|
|
36
36
|
- `ticket-builder.md` and `ticket-reviewer.md` are in `.claude/agents/`, `~/.claude/agents/`, or `$CLAUDE_HOME/agents/` when `CLAUDE_HOME` is set. If not, say to run `uvx feature-flow-cli install .` in this repo (or `python3 install.py <repo>` from a feature-flow clone).
|
|
37
37
|
|
|
File without changes
|
|
File without changes
|
{feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/adapters/codex/feature-flow/agents/openai.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{feature_flow_cli-0.1.4 → feature_flow_cli-0.1.6}/feature_flow_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|