@bakit-org/ai-sdlc-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -2
- package/bin/ai-sdlc.js +13 -0
- package/docs/cli.md +115 -0
- package/docs/tui.md +87 -0
- package/lib/apply-plan.js +61 -0
- package/lib/bundle.js +120 -0
- package/lib/cli-args.js +89 -0
- package/lib/cli-output.js +36 -0
- package/lib/command-plan.js +28 -0
- package/lib/commands.js +173 -0
- package/lib/config.js +42 -0
- package/lib/doctor.js +123 -0
- package/lib/errors.js +19 -0
- package/lib/fs-safe.js +133 -0
- package/lib/github-auth.js +115 -0
- package/lib/github-checks.js +43 -0
- package/lib/github-errors.js +39 -0
- package/lib/github-http.js +107 -0
- package/lib/github-release.js +156 -0
- package/lib/journal.js +117 -0
- package/lib/manifest.js +64 -0
- package/lib/merge-claude-md.js +63 -0
- package/lib/merge-settings.js +125 -0
- package/lib/path-rules.js +23 -0
- package/lib/plan-files.js +86 -0
- package/lib/plan.js +78 -0
- package/lib/project-picker.js +117 -0
- package/lib/release-source.js +30 -0
- package/lib/semver.js +27 -0
- package/lib/termination-cleanup.js +22 -0
- package/lib/tui/cancelled.js +16 -0
- package/lib/tui/colors.js +33 -0
- package/lib/tui/flows/init-flow.js +126 -0
- package/lib/tui/flows/intro.js +12 -0
- package/lib/tui/flows/menu-flow.js +51 -0
- package/lib/tui/flows/pick-project-step.js +42 -0
- package/lib/tui/flows/preflight.js +80 -0
- package/lib/tui/flows/project-choices.js +64 -0
- package/lib/tui/flows/recent-projects.js +76 -0
- package/lib/tui/flows/result-views.js +89 -0
- package/lib/tui/flows/review-view.js +45 -0
- package/lib/tui/flows/session-options.js +7 -0
- package/lib/tui/flows/terminal-views.js +17 -0
- package/lib/tui/keys.js +182 -0
- package/lib/tui/launch.js +19 -0
- package/lib/tui/logo-fonts.js +29 -0
- package/lib/tui/logo.js +98 -0
- package/lib/tui/sanitize.js +27 -0
- package/lib/tui/terminal-guards.js +58 -0
- package/lib/tui/terminal.js +196 -0
- package/lib/tui/text-width.js +122 -0
- package/lib/tui/theme.js +84 -0
- package/lib/tui/widgets/confirm.js +56 -0
- package/lib/tui/widgets/input-box.js +88 -0
- package/lib/tui/widgets/panel.js +31 -0
- package/lib/tui/widgets/path-input.js +110 -0
- package/lib/tui/widgets/progress-bar.js +17 -0
- package/lib/tui/widgets/select-list.js +89 -0
- package/lib/tui/widgets/spinner.js +41 -0
- package/lib/version-check.js +38 -0
- package/package.json +32 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ai-sdlc contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bakit-org/ai-sdlc-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line installer for the ai-sdlc Claude Code harness. It puts the BA / Dev / Test agents, skills and hooks into a project repository of your choice, keeps them up to date without overwriting your edits, and removes them cleanly.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g @bakit-org/ai-sdlc-cli # installs the `ai-sdlc` command
|
|
9
|
+
gh auth login # or export GH_TOKEN=... (needs read access to the kit repository)
|
|
10
|
+
cd ~/projects && ai-sdlc init # guided install: pick a project, review, apply
|
|
11
|
+
ai-sdlc doctor --project <path> # confirm the install is intact
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Scripted, or without network access:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
ai-sdlc install --project <path> --yes # latest kit release from GitHub
|
|
18
|
+
ai-sdlc install --project <path> --from-bundle ./ai-sdlc-0.1.0.bundle.json --yes
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add `--dry-run` first to see exactly what a run would do; a real run performs the same list.
|
|
22
|
+
|
|
23
|
+
## Requirements and access
|
|
24
|
+
|
|
25
|
+
- Prerequisites: Node 18 or newer, and read access to the GitHub repository that publishes the kit (`bakit-org/ai-sdlc-kit`; ask its owner if you do not have it). No runtime dependencies.
|
|
26
|
+
- GitHub credentials come from what you already use, first match wins: the `AI_SDLC_GITHUB_TOKEN`, `GH_TOKEN` or `GITHUB_TOKEN` environment variable, then `gh auth login` (the GitHub CLI), then your git credential helper for `github.com`. The token is only sent to GitHub's API and is never printed or stored. Tokens found automatically (`GH_TOKEN`, `GITHUB_TOKEN`, `gh`, git credential helper) are only used for the public API at `https://api.github.com`; if `AI_SDLC_GITHUB_API` points anywhere else (for example GitHub Enterprise) only an explicit `AI_SDLC_GITHUB_TOKEN` is used. The token needs read access to the kit repository's contents (for example a fine-grained token with `Contents: read` on it, or a classic token with the `repo` scope). For a repository behind SAML SSO, authorize the token for the organization.
|
|
27
|
+
- Downloads use Node's built-in `fetch`, which does not honour `HTTPS_PROXY` / `HTTP_PROXY`. Behind a proxy that GitHub is only reachable through, download the release bundle by other means and use `--from-bundle <file>`.
|
|
28
|
+
- Everything is scoped to the project you pick. Nothing is written to `~/.claude`.
|
|
29
|
+
- Works for both the Claude Code CLI and the VS Code extension (they share `.claude/`, `CLAUDE.md` and `.claude/settings.json`).
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
ai-sdlc init [--project <path>] [--root <dir>] [--from-bundle <file> | --version <tag>] [--force] [--no-tui]
|
|
35
|
+
ai-sdlc install [--project <path>] [--root <dir>] [--from-bundle <file> | --version <tag>] [--dry-run] [--force] [--yes] [--json]
|
|
36
|
+
ai-sdlc update [--project <path>] [--root <dir>] [--from-bundle <file> | --version <tag>] [--dry-run] [--force] [--yes] [--json]
|
|
37
|
+
ai-sdlc doctor [--project <path>] [--from-bundle <file>] [--json]
|
|
38
|
+
ai-sdlc uninstall [--project <path>] [--dry-run] [--yes] [--json]
|
|
39
|
+
ai-sdlc version [--check] [--project <path>] [--json]
|
|
40
|
+
ai-sdlc # in a terminal: menu (Install / Update / Doctor / Uninstall / Quit)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Exit codes: `0` success, `1` the operation failed or you cancelled (Esc / Ctrl-C), `2` wrong usage. If the process is stopped from outside, the terminal is restored first and the exit code is `130` (SIGINT) or `143` (SIGTERM).
|
|
44
|
+
|
|
45
|
+
Without `--from-bundle`, `install` and `update` download the latest release of the payload repository (or the one named by `--version <tag>`, for example `--version 1.2.0`), check its `.sha256` and the per-file hashes, and install it. With `--from-bundle <file>` they use a local release bundle instead (the `<file>.sha256` beside it is verified when present) and never touch the network. `ai-sdlc version --check` shows the latest release and, for the current or `--project` directory, whether the installed payload is behind it.
|
|
46
|
+
|
|
47
|
+
Failures are plain messages: an invalid or expired token, a repository you cannot read (the message names `https://github.com/<owner>/<repo>` and says to ask the repo owner to grant read access), a GitHub rate limit (with the reset time), or no network (use `--from-bundle`).
|
|
48
|
+
|
|
49
|
+
## Choosing the project
|
|
50
|
+
|
|
51
|
+
Without `--project` the CLI lists the git repositories directly under the current directory (and its parent), or under `--root`, plus an "enter a path" option. It shows the full plan and asks before writing. In scripts and CI pass `--project <path> --yes`; without a terminal the CLI refuses to guess and exits with `2`.
|
|
52
|
+
|
|
53
|
+
## Interactive mode
|
|
54
|
+
|
|
55
|
+
In a terminal, `ai-sdlc init` is a guided install: a gradient banner, an environment check, a filterable project list (git repositories next to you plus recent projects, with `installed` / `not installed` / `no .git` badges), a review of what will change, a progress bar and a result panel with next steps. Running `ai-sdlc` with no command opens a menu, and `update`, `doctor` and `uninstall` print coloured result views. Esc or Ctrl-C at any step writes nothing and restores the terminal.
|
|
56
|
+
|
|
57
|
+
It switches itself off for `--json`, `--yes`, `--no-tui`, `TERM=dumb` and whenever stdin or stdout is not a terminal; then the plain output and exit codes apply. Colours follow `NO_COLOR` and the terminal's capability, with an ASCII fallback. Details and key bindings: [docs/tui.md](docs/tui.md).
|
|
58
|
+
|
|
59
|
+
## What gets installed
|
|
60
|
+
|
|
61
|
+
| Path | Behaviour |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `.claude/agents`, `.claude/skills`, `.claude/hooks`, ... | Managed: replaced on `update` only while still identical to what was installed. A file you edited is kept and the new version is written next to it as `<file>.new`. |
|
|
64
|
+
| `CLAUDE.md` | Only the block between `<!-- ai-sdlc:begin -->` and `<!-- ai-sdlc:end -->` is managed. The rest of the file is never touched. |
|
|
65
|
+
| `.claude/settings.json` | Only the hook entries the CLI added are managed. Invalid JSON aborts the run untouched. |
|
|
66
|
+
| Starter files (project notes, design folder, ...) | Created if absent, never overwritten, never removed. |
|
|
67
|
+
| `.claude/ai-sdlc.manifest.json` | Records what was installed; written last. |
|
|
68
|
+
|
|
69
|
+
`uninstall` removes unmodified managed files, the block and the hooks it added, and gives `CLAUDE.md` and `settings.json` back byte-for-byte as they were before install. Files you modified and all starter files stay.
|
|
70
|
+
|
|
71
|
+
Note: `.claude/ai-sdlc.manifest.json` may hold a copy of your original `.claude/settings.json` text so uninstall can restore it exactly. Do not publish it.
|
|
72
|
+
|
|
73
|
+
See [docs/cli.md](docs/cli.md) for the details (update rules, doctor checks, bundle format, safety guarantees).
|
|
74
|
+
|
|
75
|
+
## Update, uninstall and offline use
|
|
76
|
+
|
|
77
|
+
- `ai-sdlc update` compares the installed files with the new release and your disk. Unmodified managed files are replaced; a file you edited is kept and the new version is written next to it as `<file>.new`; starter files are never touched. `--dry-run` lists the result first.
|
|
78
|
+
- `ai-sdlc uninstall` removes what the CLI installed and nothing else (see above for what stays).
|
|
79
|
+
- `--from-bundle <file>` installs or updates from a local `ai-sdlc-<version>.bundle.json` with no network access. The `<file>.sha256` beside it is verified when present. Use it behind a proxy, offline, or to try a release before publishing it.
|
|
80
|
+
- `ai-sdlc doctor` is read-only: Node version, a leftover run journal, manifest, managed files (edited = warning, missing = failure), hook registration and scripts, the `CLAUDE.md` block, starter files, installed version, and a note when optional Python 3 is missing. It exits `1` if a check fails. `--json` for machine output.
|
|
81
|
+
|
|
82
|
+
## Known limits
|
|
83
|
+
|
|
84
|
+
- Node's `fetch` ignores `HTTPS_PROXY` / `HTTP_PROXY` (use `--from-bundle`).
|
|
85
|
+
- Windows consoles have not been verified; the interactive screens fall back to ASCII on terminals without UTF-8.
|
|
86
|
+
- The bundle checksum comes from the same release as the bundle: it detects corruption, not tampering by someone who can edit the release.
|
|
87
|
+
- A symlinked `CLAUDE.md` or `settings.json` is refused.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
This installer is MIT licensed (see `LICENSE`). The kit it installs (`bakit-org/ai-sdlc-kit`) is a separate, commercially licensed product; access is granted by its owner.
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npm test # node:test, no dependencies
|
|
97
|
+
npm pack --dry-run # shows the published file list
|
|
98
|
+
```
|
package/bin/ai-sdlc.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { run } = require('../lib/commands');
|
|
4
|
+
|
|
5
|
+
run(process.argv.slice(2)).then(
|
|
6
|
+
(code) => {
|
|
7
|
+
process.exitCode = code;
|
|
8
|
+
},
|
|
9
|
+
(err) => {
|
|
10
|
+
process.stderr.write(`ai-sdlc: unexpected error: ${err && err.stack ? err.stack : err}\n`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
},
|
|
13
|
+
);
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
## Global behaviour
|
|
4
|
+
|
|
5
|
+
- Options: `--project <path>`, `--root <dir>`, `--from-bundle <file>`, `--version <tag>` (install/update), `--check` (version), `--dry-run`, `--force`, `--yes` (`-y`), `--json`, `--no-tui`.
|
|
6
|
+
- Exit codes: `0` ok, `1` failure or a cancelled interactive screen, `2` usage error (unknown option, missing value, invalid target, missing `--project` / `--yes` without a terminal, `--version` that is not a release version or is combined with `--from-bundle`). Access and network failures of a download (no credentials, rejected token, no read access, rate limit, offline) exit `1`.
|
|
7
|
+
- `--json` prints one JSON document on stdout and never prompts; it therefore needs `--project`, and `--yes` for runs that write.
|
|
8
|
+
- Target rules: the directory must exist, must not be your home directory or a filesystem root. A directory without `.git` is accepted with a warning.
|
|
9
|
+
|
|
10
|
+
- `init` is `install` with a guided wizard when stdin and stdout are terminals and none of `--yes`, `--json`, `--no-tui`, `--dry-run` or `TERM=dumb` applies; in every other case it behaves exactly like `install`. Without a command, a terminal gets a menu (usage text and exit `2` otherwise). `update`, `doctor` and `uninstall` print coloured result views on a terminal. `--no-tui` always gives the plain text. See [tui.md](tui.md).
|
|
11
|
+
- Cancelling an interactive screen with Esc or Ctrl-C changes nothing and exits `1`. A `SIGINT` or `SIGTERM` that arrives from outside restores the terminal and exits `130` or `143`.
|
|
12
|
+
|
|
13
|
+
## Choosing the project
|
|
14
|
+
|
|
15
|
+
1. `--project <path>` wins.
|
|
16
|
+
2. Otherwise, in a terminal: a numbered list of directories that contain `.git`, found one level under `--root` (default: the current directory, then its parent; the scanned directory itself is listed when it is a repository), plus `p` to type a path and `q` to cancel.
|
|
17
|
+
3. The plan is printed, then `Apply these changes? [y/N]` unless `--yes` or `--dry-run`.
|
|
18
|
+
|
|
19
|
+
## Where the bundle comes from
|
|
20
|
+
|
|
21
|
+
- `--from-bundle <file>`: a local bundle, no network.
|
|
22
|
+
- Otherwise `install` and `update` (and the `init` wizard) download it from the payload repository's GitHub release: `bakit-org/ai-sdlc-kit` by default; `AI_SDLC_PAYLOAD_REPO=<owner>/<repo>` points at another repository.
|
|
23
|
+
- `--version <tag>` pins a release (`1.2.0` and `v1.2.0` both work); without it the latest published release is used.
|
|
24
|
+
|
|
25
|
+
Steps: find credentials, check read access to the repository, look up the release, download `ai-sdlc-<version>.bundle.json` and its `.sha256` through the API (`Accept: application/octet-stream`) into a private temporary directory (mode `0600`), verify the checksum and the per-file hashes with the same checks as a local bundle, install, and delete the temporary files. A release without a checksum asset is refused.
|
|
26
|
+
|
|
27
|
+
Credentials, first match wins: environment `AI_SDLC_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`; the GitHub CLI (`gh auth token`, when `gh` is on the PATH); `git credential fill` for `github.com` (run with `core.askPass` and `credential.interactive` off and `GIT_ASKPASS`, `SSH_ASKPASS`, `GIT_TERMINAL_PROMPT` disabled, so no prompt or askpass dialog can appear; times out after 8 s). Only the name of the source is ever shown.
|
|
28
|
+
|
|
29
|
+
**Custom API base.** Tokens found on the machine (`GH_TOKEN`, `GITHUB_TOKEN`, `gh`, the git credential helper) belong to github.com and are used only when the API base is exactly `https://api.github.com`. With `AI_SDLC_GITHUB_API` set to anything else (GitHub Enterprise, a mirror, a local test server) only an explicit `AI_SDLC_GITHUB_TOKEN` is used, and neither `gh` nor `git` is started. The wizard shows the non-default base next to the kit source. The token is sent only to the API origin: when an asset download redirects to another host (GitHub's storage), the `Authorization` header is not forwarded. Only `https` is used (plain `http` only for a `localhost` API override). Requests time out after 20 s and are retried twice with backoff on network errors and 5xx.
|
|
30
|
+
|
|
31
|
+
| Situation | Message / exit |
|
|
32
|
+
|---|---|
|
|
33
|
+
| no credentials | lists the env vars, `gh auth login` and the git credential helper; exit `1` |
|
|
34
|
+
| 403 with an SSO header | the organization uses SAML SSO: authorize the token for it; exit `1` |
|
|
35
|
+
| 401 | token invalid or expired (names the source, not the value); exit `1` |
|
|
36
|
+
| 403 / 404 on the repository | `no read access to <owner>/<repo> — ask the repo owner to grant read access: https://github.com/<owner>/<repo>`, also pointing at `--from-bundle <file>`; exit `1` |
|
|
37
|
+
| rate limit (403/429) | shows the reset time (or the retry delay); exit `1` |
|
|
38
|
+
| offline, DNS, timeout | suggests `--from-bundle <file>`; exit `1` |
|
|
39
|
+
| release without bundle or checksum asset | names what is missing; exit `1` |
|
|
40
|
+
|
|
41
|
+
`ai-sdlc version --check` runs the access check and the release lookup (no download) and prints the latest payload version; with `--project <path>` (or an installed project in the current directory) it also compares the installed version. `--json` gives `{ version, repo, latest, installed, status }`.
|
|
42
|
+
|
|
43
|
+
Integrity: the `.sha256` asset comes from the same release as the bundle, so the check detects corruption or a partial download, not tampering by someone who can edit the release. The CLI also refuses a bundle whose payload version differs from its release tag. Redirect targets that carry a user name or password, or that are not https, are refused.
|
|
44
|
+
|
|
45
|
+
Proxies: Node's built-in `fetch` ignores `HTTPS_PROXY` / `HTTP_PROXY`. Where GitHub is reachable only through a proxy, get the bundle another way and use `--from-bundle <file>`.
|
|
46
|
+
|
|
47
|
+
Cancelling: in the guided install, Ctrl-C or Esc during the environment check or the download stops the in-flight request, removes the temporary files and ends with exit `1` (nothing written). In plain runs, `SIGINT` / `SIGTERM` during a download delete the temporary directory first and exit `130` / `143`.
|
|
48
|
+
|
|
49
|
+
Flag checks happen first, before any network access: `--version` only applies to `install`, `update` and `init`, cannot be combined with `--from-bundle` and must look like `1.2.0` or `v1.2.0`; `--check` only applies to `version`. Violations exit `2`.
|
|
50
|
+
|
|
51
|
+
Environment overrides meant for tests: `AI_SDLC_GITHUB_API` (API base URL).
|
|
52
|
+
|
|
53
|
+
## install
|
|
54
|
+
|
|
55
|
+
Verifies the bundle, then plans and applies. Refuses when a manifest already exists (use `update`, or `--force` to reinstall). A file already present at a managed path with different content is kept and the bundle's version is written beside it as `<file>.new`; `--force` overwrites instead.
|
|
56
|
+
|
|
57
|
+
## update
|
|
58
|
+
|
|
59
|
+
Three-way comparison per managed file: hash recorded at install, file on disk, file in the new bundle.
|
|
60
|
+
|
|
61
|
+
| Disk vs recorded | Bundle | Result |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| unchanged | changed | replaced |
|
|
64
|
+
| unchanged | same | nothing |
|
|
65
|
+
| edited | changed | kept, `<file>.new` written |
|
|
66
|
+
| edited | same | kept |
|
|
67
|
+
| missing | present | restored |
|
|
68
|
+
| unchanged | no longer shipped | deleted |
|
|
69
|
+
| edited | no longer shipped | kept, no longer tracked |
|
|
70
|
+
| not tracked | new in bundle | added (kept + `.new` if a different file is in the way) |
|
|
71
|
+
|
|
72
|
+
A renamed or moved folder (for example a skill folder that gains a prefix) is the last two "no longer shipped" rows plus the "new in bundle" row: the old files that you did not edit are deleted (empty folders are removed too), an old file you edited stays where it is and is no longer tracked, and the new files are added. A project that already has a different file at a new path keeps it and gets `<file>.new` beside it.
|
|
73
|
+
|
|
74
|
+
Starter files (`user-once`) are created only when they are new in the bundle. A starter file the previous install tracked and you have since deleted is not re-created by `update` (it is listed as `skip-deleted`; `doctor` still warns that it is missing). `install --force` does create it again. Existing starter files are never overwritten.
|
|
75
|
+
|
|
76
|
+
The `CLAUDE.md` block and the recorded hooks are re-rendered. A hook that is already registered identically stays where it is, so `settings.json` is not rewritten or reordered for it.
|
|
77
|
+
|
|
78
|
+
## doctor
|
|
79
|
+
|
|
80
|
+
Read-only. Reports Node version, a leftover run journal (even when the manifest is missing), manifest validity, managed files (modified = warning, missing = failure), registered hooks and hook scripts, the `CLAUDE.md` block, starter files, installed version (compared with `--from-bundle` when given), and a note about optional Python 3 only when installed scripts need it. A check that cannot run (symlink, permissions, bad encoding) is reported as a failed check rather than aborting the report, in `--json` too. Exit `1` if any check fails.
|
|
81
|
+
|
|
82
|
+
## uninstall
|
|
83
|
+
|
|
84
|
+
Deletes managed files that still match the manifest (never starter files), removes the exact hook entries and the block the CLI added, deletes files the CLI itself created if they are empty again (`CLAUDE.md`, `settings.json`), removes directories it emptied, and finally removes the manifest. Edited managed files, `<file>.new` leftovers and starter files stay.
|
|
85
|
+
|
|
86
|
+
## Bundle format
|
|
87
|
+
|
|
88
|
+
One JSON file: `{ manifest, contents }`.
|
|
89
|
+
|
|
90
|
+
- `manifest`: `version`, `payload_schema` (must be `1`), `min_cli_version`, `files[]` (`path`, `sha256`, `class`, `encoding`), optional `hooks[]` (`event`, optional `matcher`, `command`).
|
|
91
|
+
- `contents[path]`: file text (`utf8`) or base64 string; `sha256` is over the raw bytes.
|
|
92
|
+
- Classes: `managed` (under `.claude/`), `block` (`CLAUDE.md`, body without markers), `user-once`.
|
|
93
|
+
- Hook commands may contain `$CLAUDE_PROJECT_DIR`; it is written as is, not expanded.
|
|
94
|
+
- Optional `<bundle>.sha256` sidecar (`<hex> <name>`): verified when present; when absent a warning is printed and listed under `warnings` in `--json` output.
|
|
95
|
+
|
|
96
|
+
Rejected before any write: a block body that contains the begin/end markers, checksum mismatch, per-file hash mismatch, `payload_schema` other than `1`, `min_cli_version` above the CLI version, unsafe paths (absolute, `..`, backslash, empty segment), managed files outside `.claude/`, a `CLAUDE.md` that is not a block, entries in `contents` the manifest does not list.
|
|
97
|
+
|
|
98
|
+
## Safety guarantees
|
|
99
|
+
|
|
100
|
+
- Every write is a temporary file plus rename, and an existing file keeps its permission bits. The manifest is written last.
|
|
101
|
+
- Each real (non `--dry-run`) run takes a per-project lock by creating `.claude/ai-sdlc.journal.json` exclusively, before planning. A second run fails with "another ai-sdlc run is in progress". Just before applying, the journal records the previous content of every path the run will touch. A failure inside the process rolls all of them back.
|
|
102
|
+
- If the process is killed, the journal stays. `doctor` reports it, and the next `install` / `update` / `uninstall` refuses until you pass `--force`, which first rolls the interrupted run back from the journal (CLAUDE.md, settings.json, payload files and manifest return to their earlier state) and then proceeds. A journal owned by a still-running process is never recovered, not even with `--force`.
|
|
103
|
+
- Paths are resolved against the real project path; a symlink that leads outside it is refused. A symlinked `CLAUDE.md` or `settings.json` is refused too (replace it with a regular file, or run against the directory holding the real file). A `.claude` directory that is a symlink inside the project works and is never removed.
|
|
104
|
+
- The same path rules apply to the bundle and to the installed manifest before anything is deleted: no absolute paths, `..`, backslashes, nothing under `.git/`, managed files only under `.claude/`, and `.claude/settings.json`, `.claude/settings.local.json`, the manifest and the journal are reserved.
|
|
105
|
+
- `CLAUDE.md` and `settings.json` must be valid UTF-8; otherwise the run stops before changing anything.
|
|
106
|
+
- `settings.json` is parsed strictly; invalid JSON or an unexpected shape aborts before any file changes. Existing indentation and line endings are kept. The original text is recorded in the manifest so uninstall can restore it exactly (see the note below).
|
|
107
|
+
- `CLAUDE.md` insertion records the exact separator it added, so removal restores the original bytes (including a missing final newline or CRLF endings).
|
|
108
|
+
|
|
109
|
+
## Code map for the download
|
|
110
|
+
|
|
111
|
+
`lib/release-source.js` exports `resolveBundle(opts, { fetchLatestBundle })` (resolving to `{ bundlePath }` or `{ bundleObject }`, plus an optional `cleanup()`) and `loadReleaseBundle`, which loads, verifies and then cleans up. `lib/github-release.js` is the default `fetchLatestBundle`; `github-auth.js` finds the token, `github-http.js` does timeouts, retries and redirects, `github-errors.js` maps responses to messages, `config.js` holds the repository and API settings.
|
|
112
|
+
|
|
113
|
+
## Note on the manifest
|
|
114
|
+
|
|
115
|
+
`.claude/ai-sdlc.manifest.json` may contain a copy of the original `.claude/settings.json` text (base64) taken before hooks were merged in. Treat the manifest as local state: do not publish it or commit it to a public repository if your settings contain anything private.
|
package/docs/tui.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Interactive mode
|
|
2
|
+
|
|
3
|
+
When you run the CLI in a terminal it can guide you with interactive screens that redraw in place (no alternate screen): a gradient banner, a status line, a filterable project list, a review step, a progress bar and a result panel. There are no extra dependencies; everything is built on Node's raw terminal mode and ANSI sequences.
|
|
4
|
+
|
|
5
|
+
## When it starts
|
|
6
|
+
|
|
7
|
+
| You run | In a terminal | Otherwise |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `ai-sdlc init` | guided install (below) | same as `install` |
|
|
10
|
+
| `ai-sdlc` (no command) | menu: Install, Update, Doctor, Uninstall, Quit | usage text, exit `2` |
|
|
11
|
+
| `ai-sdlc update`, `doctor`, `uninstall` | coloured result views (checklist, panels); prompts as before | plain text |
|
|
12
|
+
| `ai-sdlc install` | unchanged (numbered picker, `[y/N]`) | unchanged |
|
|
13
|
+
|
|
14
|
+
"In a terminal" means stdin and stdout are both terminals. Interactive mode is never used when any of these is true, and the plain behaviour (output, prompts, exit codes `0`/`1`/`2`) is unchanged:
|
|
15
|
+
|
|
16
|
+
- `--json`, `--yes`/`-y` or `--no-tui` is given
|
|
17
|
+
- `TERM=dumb`
|
|
18
|
+
- `init --dry-run` (it prints the plan and exits, like `install --dry-run`)
|
|
19
|
+
|
|
20
|
+
`--project <path>` together with `init` skips the project list; the review step is still shown.
|
|
21
|
+
|
|
22
|
+
## The guided install (`ai-sdlc init`)
|
|
23
|
+
|
|
24
|
+
1. **Banner and checks.** A banner, a dim status line (`Using: node 24 | git ✔ | kit source: bundle file`) and one line per check: Node version, git, and the kit source. With `--from-bundle <file>` the source is that file. Without it the source is the GitHub release of the payload repository and two more checks run (with the spinner): `GitHub token` (shows only where it was found, for example `found via GH_TOKEN`) and `Access to <owner>/<repo>`. If either fails, the failure line explains it (owner contact for missing read access) and offers `--from-bundle <file>`; the run ends with exit `1` before the project list and nothing is written. The release is then downloaded behind a spinner, verified and read. Ctrl-C or Esc during either spinner cancels it immediately (the request and any helper process are stopped, temporary files removed; exit `1`, nothing written). Further checks plug in through the `extraChecks` list of `runPreflight` in `lib/tui/flows/preflight.js`.
|
|
25
|
+
2. **Project.** A list of git repositories found one level under `--root` (default: the current directory and its parent) plus recently used projects, newest first. Each row has a badge: `not installed`, `installed vX → update`, or `no .git ⚠`. Type to filter; `Enter a path…` opens a path field with Tab / Shift-Tab completion. `~`, files, missing paths and filesystem roots are refused with an inline message.
|
|
26
|
+
3. **Review.** What will happen by class (managed files, starter files, the `CLAUDE.md` block, hooks) with a note that only the marked block of `CLAUDE.md` is managed. `d` toggles the full file list. Nothing has been written at this point.
|
|
27
|
+
4. **Install.** The project lock is taken, the plan is computed again under the lock, and the changes are applied with a progress bar showing the current file.
|
|
28
|
+
5. **Result.** What was written and the next steps: open the folder in Claude Code, fill in `ai-sdlc/project.md`, run `ai-sdlc doctor`.
|
|
29
|
+
|
|
30
|
+
### Keys
|
|
31
|
+
|
|
32
|
+
| Key | Action |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Enter | choose / confirm / install |
|
|
35
|
+
| Esc | back one step; at the first step, cancel. In a filter or candidate list it first clears that |
|
|
36
|
+
| Ctrl-C | cancel immediately |
|
|
37
|
+
| Up / Down, Home / End, PgUp / PgDn | move in lists (lists wrap) |
|
|
38
|
+
| Tab / Shift-Tab | next / previous path completion |
|
|
39
|
+
| Left / Right, Home / End | move in text fields |
|
|
40
|
+
| Backspace, Delete | edit |
|
|
41
|
+
| Ctrl-A / Ctrl-E | start / end of line |
|
|
42
|
+
| Ctrl-U / Ctrl-W | delete to line start / previous word |
|
|
43
|
+
| `d` (review) | show or hide the full list |
|
|
44
|
+
| `1`-`5` (menu) | pick an entry |
|
|
45
|
+
|
|
46
|
+
Pasting works (bracketed paste); line breaks in pasted text are dropped. Enter sent as CRLF (Windows terminals) counts once. Resizing the window redraws the current screen.
|
|
47
|
+
|
|
48
|
+
## What the screens guard against
|
|
49
|
+
|
|
50
|
+
- **Typed-ahead keys.** Keys pressed before a screen is on display (for example Enter during the environment check) are dropped. The review ignores Enter and `y` for the first 150 ms after it appears. Only Ctrl-C survives typing ahead.
|
|
51
|
+
- **Closed input.** If stdin ends (pipe closed, terminal gone) between screens, the next screen cancels: exit `1`, nothing written.
|
|
52
|
+
- **Hostile text.** Folder names, versions, paths and messages come from disk, so control characters in them are shown as visible escapes (`^[`, `\n`, `\u009b`) and never sent to the terminal. Text typed or pasted into a field loses escape sequences and control characters. A manifest whose version is not a plain semantic version is treated as untrustworthy (a warning in the menu and the list).
|
|
53
|
+
- **Stale review.** The plan is computed again after the project lock is taken. If it differs from what was reviewed, the review is shown again with a note instead of installing something else.
|
|
54
|
+
- **Limits.** Fields hold at most 4096 characters; bracketed pastes are capped at 1 MB and abandoned after 2 s without an end marker; the project list shows at most 200 projects. A window smaller than 20x8 shows a single line asking for more room.
|
|
55
|
+
|
|
56
|
+
## Cancelling
|
|
57
|
+
|
|
58
|
+
Esc or Ctrl-C at any step writes nothing: the apply layer is only called after the final confirmation, and the project lock is not even taken before it. The command ends with `ai-sdlc: cancelled; nothing was changed` (or `interrupted; ...` for Ctrl-C) and exit code `1`, the same as declining the prompt in the plain flow. An external `SIGINT`/`SIGTERM` (for example `kill`) restores the terminal and exits `130`/`143`.
|
|
59
|
+
|
|
60
|
+
The terminal is always put back by one cleanup path (cursor shown, bracketed paste off, raw mode off), registered for normal exit, `SIGINT`, `SIGTERM` and uncaught exceptions, and run again from a `finally` when a screen ends or throws.
|
|
61
|
+
|
|
62
|
+
## Looks
|
|
63
|
+
|
|
64
|
+
- **Colour:** truecolor when `COLORTERM` says so (also iTerm, Windows Terminal); 16 colours otherwise on a colour terminal; none for `NO_COLOR`, `FORCE_COLOR=0`, `TERM=dumb` or when output is not a terminal. `FORCE_COLOR=1` forces colour on.
|
|
65
|
+
- **Banner** (chosen by terminal width): 120+ columns thick 2-pixel letters, 100+ the 1-pixel letters, 50+ a compact half-block version, below 50 columns, without colour, or without unicode a single line `> AI-SDLC`. `node lib/tui/logo.js` previews the banner for the current terminal.
|
|
66
|
+
- **Unicode:** box drawing, `❯ ✔ ⚠ ✖ →` need a UTF-8 locale. With `TERM=linux`, `TERM=dumb` or a `LANG`/`LC_ALL` that is not UTF-8 the screens use ASCII (`+--+`, `|`, `>`, `+ ! x`).
|
|
67
|
+
- Long lines are cut with an ellipsis, never wrapped; long paths keep their start and end.
|
|
68
|
+
|
|
69
|
+
## Recent projects
|
|
70
|
+
|
|
71
|
+
Project paths (nothing else) are remembered in `recent.json` under `$XDG_CONFIG_HOME/ai-sdlc/`, `%APPDATA%\ai-sdlc\` on Windows, or `~/.config/ai-sdlc/`. The file is replaced atomically, only paths of existing folders are shown, and any problem reading or writing it is ignored. Delete the file to forget everything.
|
|
72
|
+
|
|
73
|
+
## Layout of the code
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
lib/tui/
|
|
77
|
+
keys.js raw input -> key events (arrows, Home/End, Enter/CRLF, Esc, Tab, Ctrl-keys, paste)
|
|
78
|
+
terminal.js session: raw mode, live frame redraw, resize, single cleanup path
|
|
79
|
+
theme.js colour level, attribute support, unicode/ASCII symbols, gradient
|
|
80
|
+
text-width.js ANSI-aware width, truncation (end and middle)
|
|
81
|
+
logo.js banner variants; logo-fonts.js has the bitmap fonts
|
|
82
|
+
launch.js decides whether interactive mode may start
|
|
83
|
+
widgets/ input-box select-list path-input confirm spinner progress-bar panel
|
|
84
|
+
flows/ init-flow menu-flow result-views plus their helpers
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Frames are pure functions of `(state, theme, width)` returning lines; the session draws them by moving the cursor up over the previous frame and clearing it (no alternate screen). Tests drive widgets and flows with scripted key bytes over in-memory streams (`tests/tui-*.test.js`).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { resolveInside, writeAtomic, pruneEmptyDirs } = require('./fs-safe');
|
|
5
|
+
const { MANIFEST_REL } = require('./path-rules');
|
|
6
|
+
const { rollbackEntries } = require('./journal');
|
|
7
|
+
const { CliError } = require('./errors');
|
|
8
|
+
|
|
9
|
+
// Disk changes in execution order, the manifest always last.
|
|
10
|
+
function orderedChanges(plan) {
|
|
11
|
+
const changes = plan.actions.filter((a) => a.write || a.remove);
|
|
12
|
+
return [...changes.filter((a) => a.path !== MANIFEST_REL), ...changes.filter((a) => a.path === MANIFEST_REL)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Previous content of every path about to change (base64, or null when absent).
|
|
16
|
+
function beforeImages(project, changes) {
|
|
17
|
+
return changes.map((a) => {
|
|
18
|
+
const abs = resolveInside(project, a.path);
|
|
19
|
+
return { path: a.path, before: fs.existsSync(abs) ? fs.readFileSync(abs).toString('base64') : null };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Executes the planned writes/removals while holding `lock`. The before-images
|
|
24
|
+
// go into the journal first, so a failure here (or a crash, recovered later with
|
|
25
|
+
// --force) can put every path back exactly as it was.
|
|
26
|
+
// `onProgress({ done, total, path })` (optional) is called before the first
|
|
27
|
+
// change and after each one; it has no influence on what is written.
|
|
28
|
+
function applyPlan(project, plan, op, lock, { onProgress } = {}) {
|
|
29
|
+
const changes = orderedChanges(plan);
|
|
30
|
+
const entries = beforeImages(project, changes);
|
|
31
|
+
const report = (done, rel) => {
|
|
32
|
+
if (!onProgress) return;
|
|
33
|
+
try {
|
|
34
|
+
onProgress({ done, total: changes.length, path: rel });
|
|
35
|
+
} catch (_) {
|
|
36
|
+
/* a display problem must not undo or abort the install */
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
lock.record(entries);
|
|
41
|
+
report(0, null);
|
|
42
|
+
changes.forEach((a, i) => {
|
|
43
|
+
const abs = resolveInside(project, a.path);
|
|
44
|
+
if (a.write) writeAtomic(abs, a.write);
|
|
45
|
+
else {
|
|
46
|
+
fs.rmSync(abs, { force: true });
|
|
47
|
+
pruneEmptyDirs(project, path.dirname(abs));
|
|
48
|
+
}
|
|
49
|
+
report(i + 1, a.path);
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const problems = rollbackEntries(project, entries);
|
|
53
|
+
if (problems.length) {
|
|
54
|
+
lock.keep();
|
|
55
|
+
throw new CliError(`${op} failed: ${err.message} (rollback also failed: ${problems.join('; ')}; re-run with --force to retry the rollback)`);
|
|
56
|
+
}
|
|
57
|
+
throw new CliError(`${op} failed: ${err.message} (rolled back)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { applyPlan, orderedChanges, beforeImages };
|
package/lib/bundle.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { CliError } = require('./errors');
|
|
5
|
+
const { sha256, assertRelPath } = require('./fs-safe');
|
|
6
|
+
const semver = require('./semver');
|
|
7
|
+
const { installPathProblem } = require('./path-rules');
|
|
8
|
+
const { BEGIN, END } = require('./merge-claude-md');
|
|
9
|
+
|
|
10
|
+
const SUPPORTED_SCHEMA = 1;
|
|
11
|
+
const CLASSES = new Set(['managed', 'block', 'user-once']);
|
|
12
|
+
const ENCODINGS = new Set(['utf8', 'base64']);
|
|
13
|
+
|
|
14
|
+
function fail(msg) {
|
|
15
|
+
throw new CliError(`invalid bundle: ${msg}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Compares the bundle file against the "<hex> <name>" sidecar next to it.
|
|
19
|
+
function verifySidecar(file, bytes, warnings) {
|
|
20
|
+
const sidecar = `${file}.sha256`;
|
|
21
|
+
if (!fs.existsSync(sidecar)) {
|
|
22
|
+
warnings.push(`no checksum file beside the bundle (${path.basename(sidecar)}); integrity not verified`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const expected = fs.readFileSync(sidecar, 'utf8').trim().split(/\s+/)[0] || '';
|
|
26
|
+
if (!/^[0-9a-f]{64}$/i.test(expected)) fail(`checksum file ${path.basename(sidecar)} is malformed`);
|
|
27
|
+
if (expected.toLowerCase() !== sha256(bytes)) {
|
|
28
|
+
throw new CliError(`bundle checksum mismatch for ${path.basename(file)}: the file or its .sha256 was altered`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readSource(source, warnings) {
|
|
33
|
+
if (source.bundleObject) return source.bundleObject;
|
|
34
|
+
let bytes;
|
|
35
|
+
try {
|
|
36
|
+
bytes = fs.readFileSync(source.bundlePath);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new CliError(`cannot read bundle ${source.bundlePath}: ${err.code || err.message}`);
|
|
39
|
+
}
|
|
40
|
+
verifySidecar(source.bundlePath, bytes, warnings);
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return fail(`not valid JSON (${err.message})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkHeader(manifest, cliVersion) {
|
|
49
|
+
if (!manifest || typeof manifest !== 'object') fail('missing manifest');
|
|
50
|
+
if (manifest.payload_schema !== SUPPORTED_SCHEMA) {
|
|
51
|
+
fail(`payload_schema ${manifest.payload_schema} is not supported (this CLI reads ${SUPPORTED_SCHEMA})`);
|
|
52
|
+
}
|
|
53
|
+
if (!semver.isStrict(manifest.version)) fail('manifest.version is not a version');
|
|
54
|
+
const cmp = semver.compare(cliVersion, manifest.min_cli_version);
|
|
55
|
+
if (cmp === null) fail('manifest.min_cli_version is not a version');
|
|
56
|
+
if (cmp < 0) {
|
|
57
|
+
throw new CliError(`this payload needs ai-sdlc-cli >= ${manifest.min_cli_version}, but this is ${cliVersion}; upgrade the CLI first`);
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(manifest.files) || manifest.files.length === 0) fail('manifest.files is empty');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decodeFiles(manifest, contents) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const files = manifest.files.map((f) => {
|
|
65
|
+
if (!f || typeof f !== 'object') fail('manifest.files has a non-object entry');
|
|
66
|
+
assertRelPath(f.path, 'bundle path');
|
|
67
|
+
if (seen.has(f.path)) fail(`duplicate path ${f.path}`);
|
|
68
|
+
seen.add(f.path);
|
|
69
|
+
if (!CLASSES.has(f.class)) fail(`${f.path}: unknown class "${f.class}"`);
|
|
70
|
+
if (!ENCODINGS.has(f.encoding)) fail(`${f.path}: unknown encoding "${f.encoding}"`);
|
|
71
|
+
if (typeof f.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(f.sha256)) fail(`${f.path}: bad sha256`);
|
|
72
|
+
if (typeof contents[f.path] !== 'string') fail(`${f.path}: no content in bundle`);
|
|
73
|
+
const why = installPathProblem(f.path, f.class);
|
|
74
|
+
if (why) fail(`${f.path}: ${why}`);
|
|
75
|
+
const bytes = Buffer.from(contents[f.path], f.encoding);
|
|
76
|
+
if (sha256(bytes) !== f.sha256) throw new CliError(`bundle file hash mismatch: ${f.path}`);
|
|
77
|
+
if (f.class === 'block' && f.encoding !== 'utf8') fail('the CLAUDE.md block must be utf8');
|
|
78
|
+
return { path: f.path, class: f.class, sha256: f.sha256, encoding: f.encoding, bytes };
|
|
79
|
+
});
|
|
80
|
+
const extra = Object.keys(contents).filter((p) => !seen.has(p));
|
|
81
|
+
if (extra.length) fail(`contents has files not listed in the manifest: ${extra[0]}`);
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function checkHooks(hooks) {
|
|
86
|
+
if (hooks === undefined) return [];
|
|
87
|
+
if (!Array.isArray(hooks)) fail('manifest.hooks must be an array');
|
|
88
|
+
return hooks.map((h) => {
|
|
89
|
+
if (!h || typeof h.event !== 'string' || !h.event || typeof h.command !== 'string' || !h.command) {
|
|
90
|
+
fail('each hook needs a non-empty event and command');
|
|
91
|
+
}
|
|
92
|
+
if (h.matcher !== undefined && typeof h.matcher !== 'string') fail('hook matcher must be a string');
|
|
93
|
+
return h.matcher === undefined ? { event: h.event, command: h.command } : { event: h.event, matcher: h.matcher, command: h.command };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// source: { bundlePath } or { bundleObject }. Returns a verified, decoded bundle.
|
|
98
|
+
function loadBundle(source, { cliVersion }) {
|
|
99
|
+
const warnings = [];
|
|
100
|
+
const raw = readSource(source, warnings);
|
|
101
|
+
if (!raw || typeof raw !== 'object' || !raw.manifest || !raw.contents || typeof raw.contents !== 'object') {
|
|
102
|
+
fail('expected { manifest, contents }');
|
|
103
|
+
}
|
|
104
|
+
checkHeader(raw.manifest, cliVersion);
|
|
105
|
+
const files = decodeFiles(raw.manifest, raw.contents);
|
|
106
|
+
const hooks = checkHooks(raw.manifest.hooks);
|
|
107
|
+
const blockFile = files.find((f) => f.class === 'block');
|
|
108
|
+
const body = blockFile ? blockFile.bytes.toString('utf8') : '';
|
|
109
|
+
if (body.includes(BEGIN) || body.includes(END)) fail('the CLAUDE.md block body must not contain the ai-sdlc begin/end markers');
|
|
110
|
+
return {
|
|
111
|
+
version: raw.manifest.version,
|
|
112
|
+
payloadSchema: raw.manifest.payload_schema,
|
|
113
|
+
files: files.filter((f) => f.class !== 'block'),
|
|
114
|
+
block: blockFile ? { sha256: blockFile.sha256, body } : null,
|
|
115
|
+
hooks,
|
|
116
|
+
warnings,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { loadBundle, SUPPORTED_SCHEMA };
|
package/lib/cli-args.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { UsageError } = require('./errors');
|
|
3
|
+
const semver = require('./semver');
|
|
4
|
+
|
|
5
|
+
const COMMANDS = ['init', 'install', 'update', 'doctor', 'uninstall', 'version', 'help'];
|
|
6
|
+
const VALUE_FLAGS = { '--project': 'project', '--root': 'root', '--from-bundle': 'fromBundle' };
|
|
7
|
+
const BOOL_FLAGS = { '--dry-run': 'dryRun', '--check': 'check', '--force': 'force', '--yes': 'yes', '-y': 'yes', '--json': 'json', '--no-tui': 'noTui', '--help': 'help', '-h': 'help' };
|
|
8
|
+
|
|
9
|
+
// Returns { command, flags }. Anything unrecognised is a usage error (exit 2).
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const flags = {};
|
|
12
|
+
let command = null;
|
|
13
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
|
|
16
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
17
|
+
if (VALUE_FLAGS[name]) {
|
|
18
|
+
const value = eq === -1 ? argv[(i += 1)] : arg.slice(eq + 1);
|
|
19
|
+
if (value === undefined || value === '' || (eq === -1 && value.startsWith('--'))) throw new UsageError(`${name} needs a value`);
|
|
20
|
+
flags[VALUE_FLAGS[name]] = value;
|
|
21
|
+
} else if (BOOL_FLAGS[name] && eq === -1) {
|
|
22
|
+
flags[BOOL_FLAGS[name]] = true;
|
|
23
|
+
} else if (name === '--version' && eq !== -1) {
|
|
24
|
+
flags.version = arg.slice(eq + 1);
|
|
25
|
+
if (!flags.version) throw new UsageError('--version needs a value');
|
|
26
|
+
} else if (arg === '--version' && command && command !== 'version') {
|
|
27
|
+
// After a command, --version <tag> pins the release to install.
|
|
28
|
+
const next = argv[i + 1];
|
|
29
|
+
if (next === undefined || next.startsWith('-')) throw new UsageError('--version needs a value');
|
|
30
|
+
flags.version = next;
|
|
31
|
+
i += 1;
|
|
32
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
33
|
+
command = command || 'version';
|
|
34
|
+
} else if (arg.startsWith('-')) {
|
|
35
|
+
throw new UsageError(`unknown option ${arg}`);
|
|
36
|
+
} else if (command === null) {
|
|
37
|
+
if (!COMMANDS.includes(arg)) throw new UsageError(`unknown command "${arg}"`);
|
|
38
|
+
// init is install with a guided wizard when a terminal is available.
|
|
39
|
+
command = arg === 'init' ? 'install' : arg;
|
|
40
|
+
if (arg === 'init') flags.init = true;
|
|
41
|
+
} else {
|
|
42
|
+
throw new UsageError(`unexpected argument "${arg}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (flags.help) command = 'help';
|
|
46
|
+
return { command, flags };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Rejects flag combinations that make no sense, before anything touches the network or the disk.
|
|
50
|
+
function validateFlags(command, flags) {
|
|
51
|
+
if (command === 'help') return;
|
|
52
|
+
if (flags.check && command !== 'version') throw new UsageError('--check only applies to the version command');
|
|
53
|
+
if (flags.version === undefined) return;
|
|
54
|
+
if (command !== 'install' && command !== 'update') throw new UsageError('--version only applies to install, update and init');
|
|
55
|
+
if (flags.fromBundle) throw new UsageError('--version cannot be combined with --from-bundle');
|
|
56
|
+
if (!semver.isReleaseTag(flags.version)) throw new UsageError('--version must be a release version such as 1.2.0 or v1.2.0');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const HELP = `ai-sdlc - install the ai-sdlc Claude Code harness into a project
|
|
60
|
+
|
|
61
|
+
Usage: ai-sdlc <command> [options]
|
|
62
|
+
|
|
63
|
+
Commands:
|
|
64
|
+
init guided install in a terminal; otherwise the same as install
|
|
65
|
+
install install into a project (refuses if already installed; see --force)
|
|
66
|
+
update bring an installed project up to the given bundle
|
|
67
|
+
doctor read-only health check of an installed project
|
|
68
|
+
uninstall remove what install added, keep anything you changed
|
|
69
|
+
version print the CLI version (--check: compare with the latest release)
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--project <path> target project directory (skips the picker)
|
|
73
|
+
--root <dir> where the picker looks for git repositories
|
|
74
|
+
--from-bundle <file> use a local release bundle (<file>.sha256 is verified when present)
|
|
75
|
+
without it, install/update download the latest release from GitHub
|
|
76
|
+
--version <tag> with install/update: download this release instead of the latest
|
|
77
|
+
--check with version: also show the latest release and the installed version
|
|
78
|
+
--dry-run show exactly what would change, write nothing
|
|
79
|
+
--force reinstall over an existing install / overwrite modified managed files
|
|
80
|
+
--yes, -y do not ask for confirmation
|
|
81
|
+
--json machine-readable output (needs --project; mutating runs also need --yes)
|
|
82
|
+
--no-tui plain text output and prompts, no interactive screens
|
|
83
|
+
|
|
84
|
+
Run without a command in a terminal to open a menu.
|
|
85
|
+
|
|
86
|
+
Exit codes: 0 ok, 1 failure, 2 usage error.
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
module.exports = { parseArgs, validateFlags, HELP };
|