dna-cli 0.1.0__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.
- dna_cli-0.1.0/.gitignore +23 -0
- dna_cli-0.1.0/LICENSE +21 -0
- dna_cli-0.1.0/PKG-INFO +57 -0
- dna_cli-0.1.0/README.md +31 -0
- dna_cli-0.1.0/dna_cli/__init__.py +87 -0
- dna_cli-0.1.0/dna_cli/_active_story.py +134 -0
- dna_cli-0.1.0/dna_cli/_banner.py +61 -0
- dna_cli-0.1.0/dna_cli/_ctx.py +433 -0
- dna_cli-0.1.0/dna_cli/_git_symbiosis.py +167 -0
- dna_cli-0.1.0/dna_cli/_github_bridge.py +294 -0
- dna_cli-0.1.0/dna_cli/_methodology_gates.py +164 -0
- dna_cli-0.1.0/dna_cli/data/git-hooks/prepare-commit-msg +132 -0
- dna_cli-0.1.0/dna_cli/doc_cmd.py +842 -0
- dna_cli-0.1.0/dna_cli/docs_cmd.py +79 -0
- dna_cli-0.1.0/dna_cli/eval_cmd.py +274 -0
- dna_cli-0.1.0/dna_cli/hooks_cmd.py +146 -0
- dna_cli-0.1.0/dna_cli/install_cmd.py +496 -0
- dna_cli-0.1.0/dna_cli/issue_bridge_cmd.py +305 -0
- dna_cli-0.1.0/dna_cli/kind_cmd.py +84 -0
- dna_cli-0.1.0/dna_cli/memory_cmd.py +298 -0
- dna_cli-0.1.0/dna_cli/pr_cmd.py +251 -0
- dna_cli-0.1.0/dna_cli/recall_cmd.py +161 -0
- dna_cli-0.1.0/dna_cli/research_cmd.py +284 -0
- dna_cli-0.1.0/dna_cli/scope_cmd.py +129 -0
- dna_cli-0.1.0/dna_cli/sdlc_cmd.py +6549 -0
- dna_cli-0.1.0/dna_cli/source_cmd.py +437 -0
- dna_cli-0.1.0/dna_cli/testkit_cmd.py +352 -0
- dna_cli-0.1.0/pyproject.toml +68 -0
- dna_cli-0.1.0/tests/conftest.py +98 -0
- dna_cli-0.1.0/tests/test_changelog_cmd.py +81 -0
- dna_cli-0.1.0/tests/test_doc_apply_bundle_entries.py +179 -0
- dna_cli-0.1.0/tests/test_doc_apply_prompt_budget.py +196 -0
- dna_cli-0.1.0/tests/test_docs_cmd.py +130 -0
- dna_cli-0.1.0/tests/test_eval_cmd.py +226 -0
- dna_cli-0.1.0/tests/test_focus_gates_cli.py +218 -0
- dna_cli-0.1.0/tests/test_focus_gates_guards.py +118 -0
- dna_cli-0.1.0/tests/test_git_symbiosis_hook.py +211 -0
- dna_cli-0.1.0/tests/test_hooks_cli.py +109 -0
- dna_cli-0.1.0/tests/test_install_cmd.py +279 -0
- dna_cli-0.1.0/tests/test_install_github_real.py +53 -0
- dna_cli-0.1.0/tests/test_issue_bridge_cli.py +473 -0
- dna_cli-0.1.0/tests/test_issue_comment_cli.py +104 -0
- dna_cli-0.1.0/tests/test_kaizen_event.py +317 -0
- dna_cli-0.1.0/tests/test_kaizen_transitions.py +183 -0
- dna_cli-0.1.0/tests/test_memory_cmd.py +91 -0
- dna_cli-0.1.0/tests/test_plan_from_file.py +89 -0
- dna_cli-0.1.0/tests/test_produces_cmd.py +52 -0
- dna_cli-0.1.0/tests/test_recall_cmd.py +112 -0
- dna_cli-0.1.0/tests/test_research_cmd.py +95 -0
- dna_cli-0.1.0/tests/test_scope_detect.py +70 -0
- dna_cli-0.1.0/tests/test_sdlc_brief.py +149 -0
- dna_cli-0.1.0/tests/test_sdlc_correctness.py +189 -0
- dna_cli-0.1.0/tests/test_sdlc_ergonomics.py +21 -0
- dna_cli-0.1.0/tests/test_sdlc_scope_default.py +178 -0
- dna_cli-0.1.0/tests/test_sdlc_workitem_cli.py +376 -0
- dna_cli-0.1.0/tests/test_source_url_resolution.py +43 -0
- dna_cli-0.1.0/tests/test_spike_cli.py +196 -0
- dna_cli-0.1.0/tests/test_story_check.py +176 -0
- dna_cli-0.1.0/tests/test_story_commits_backref.py +150 -0
- dna_cli-0.1.0/tests/test_story_done_guard.py +61 -0
- dna_cli-0.1.0/tests/test_story_pr_cli.py +297 -0
- dna_cli-0.1.0/tests/test_story_review_pr_guard.py +98 -0
- dna_cli-0.1.0/tests/test_story_show.py +78 -0
- dna_cli-0.1.0/tests/test_story_start_gate.py +40 -0
- dna_cli-0.1.0/tests/test_tenant_write_note.py +59 -0
- dna_cli-0.1.0/tests/test_testkit_cmd.py +196 -0
dna_cli-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
node_modules/
|
|
7
|
+
*.tsbuildinfo
|
|
8
|
+
dist/
|
|
9
|
+
coverage/
|
|
10
|
+
.DS_Store
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
!.env.example
|
|
14
|
+
# resolver cache — created when a demo/example resolves github: deps
|
|
15
|
+
.dna-cache/
|
|
16
|
+
# MkDocs build output — the docs site is built in CI / on Pages, never committed
|
|
17
|
+
site/
|
|
18
|
+
# per-workstation pointer written by `dna sdlc story start` (read by the
|
|
19
|
+
# prepare-commit-msg hook) — never source-of-truth, never committed
|
|
20
|
+
.dna/active-story.txt
|
|
21
|
+
|
|
22
|
+
# Local hybrid-search index store (dna recall/search) — s-record-search-sqlite
|
|
23
|
+
.dna-search/
|
dna_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DNA 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.
|
dna_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dna-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DNA command-line interface — the `dna` binary for the declarative SDLC + document CRUD (kernel-local, no service required)
|
|
5
|
+
Project-URL: Homepage, https://ruinosus.github.io/dna/
|
|
6
|
+
Project-URL: Repository, https://github.com/ruinosus/dna
|
|
7
|
+
Project-URL: Documentation, https://ruinosus.github.io/dna/reference/cli/
|
|
8
|
+
Project-URL: Changelog, https://github.com/ruinosus/dna/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/ruinosus/dna/issues
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,cli,declarative,dna,manifests,sdlc
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: <3.14,>=3.12
|
|
18
|
+
Requires-Dist: click>=8.1.0
|
|
19
|
+
Requires-Dist: dna-sdk<0.2,>=0.1
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: dna-sdk[search-sqlite]; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# dna-cli
|
|
28
|
+
|
|
29
|
+
The `dna` command-line interface for **DNA — Domain Notation of Anything**:
|
|
30
|
+
document CRUD (`dna doc`, `dna kind`, `dna scope`, `dna source`), semantic
|
|
31
|
+
recall and memory (`dna recall`, `dna memory`), research syntheses
|
|
32
|
+
(`dna research`), and a declarative, story-first SDLC (`dna sdlc`) — all
|
|
33
|
+
kernel-local against your filesystem or SQL source, no service required.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install dna-cli # or: uv tool install dna-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Pre-release / exact-pin alternative — from the repo:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd packages/sdk-py && uv venv && uv pip install -e ".[dev]" -e ../cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick taste
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ dna scope list
|
|
51
|
+
$ dna doc show Agent greeter --scope hello-genome
|
|
52
|
+
$ dna sdlc story create s-my-story --title "..." --ac "..." --dod "..."
|
|
53
|
+
$ dna recall "reciprocal rank fusion" --kind Story -k 1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Full CLI reference: <https://ruinosus.github.io/dna/reference/cli/> ·
|
|
57
|
+
Repository: <https://github.com/ruinosus/dna>
|
dna_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# dna-cli
|
|
2
|
+
|
|
3
|
+
The `dna` command-line interface for **DNA — Domain Notation of Anything**:
|
|
4
|
+
document CRUD (`dna doc`, `dna kind`, `dna scope`, `dna source`), semantic
|
|
5
|
+
recall and memory (`dna recall`, `dna memory`), research syntheses
|
|
6
|
+
(`dna research`), and a declarative, story-first SDLC (`dna sdlc`) — all
|
|
7
|
+
kernel-local against your filesystem or SQL source, no service required.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install dna-cli # or: uv tool install dna-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Pre-release / exact-pin alternative — from the repo:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd packages/sdk-py && uv venv && uv pip install -e ".[dev]" -e ../cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick taste
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
$ dna scope list
|
|
25
|
+
$ dna doc show Agent greeter --scope hello-genome
|
|
26
|
+
$ dna sdlc story create s-my-story --title "..." --ac "..." --dod "..."
|
|
27
|
+
$ dna recall "reciprocal rank fusion" --kind Story -k 1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Full CLI reference: <https://ruinosus.github.io/dna/reference/cli/> ·
|
|
31
|
+
Repository: <https://github.com/ruinosus/dna>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""``dna`` — user-facing CLI for the DNA kernel.
|
|
2
|
+
|
|
3
|
+
Boots a local Kernel against ``DNA_SOURCE_URL`` / ``DNA_BASE_DIR``
|
|
4
|
+
(filesystem source) and runs one command per invocation. No service
|
|
5
|
+
required — the kernel IS the backend.
|
|
6
|
+
|
|
7
|
+
Output formatting:
|
|
8
|
+
- ``--json`` for machine-readable (default for CRUD writes).
|
|
9
|
+
- rich tables for ``list`` / ``show`` (default).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from dna_cli import (
|
|
19
|
+
doc_cmd,
|
|
20
|
+
docs_cmd,
|
|
21
|
+
eval_cmd,
|
|
22
|
+
install_cmd,
|
|
23
|
+
kind_cmd,
|
|
24
|
+
memory_cmd,
|
|
25
|
+
recall_cmd,
|
|
26
|
+
research_cmd,
|
|
27
|
+
scope_cmd,
|
|
28
|
+
sdlc_cmd,
|
|
29
|
+
source_cmd,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _BannerGroup(click.Group):
|
|
34
|
+
"""Click Group that prints the gradient banner before ``--help`` output."""
|
|
35
|
+
|
|
36
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
37
|
+
from dna_cli._banner import render_banner
|
|
38
|
+
banner = render_banner()
|
|
39
|
+
if banner:
|
|
40
|
+
formatter.write(banner)
|
|
41
|
+
formatter.write_paragraph()
|
|
42
|
+
super().format_help(ctx, formatter)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.group(
|
|
46
|
+
cls=_BannerGroup,
|
|
47
|
+
help=(
|
|
48
|
+
"DNA — declarative lifecycle + document CLI.\n\n"
|
|
49
|
+
"Boots a local kernel via DNA_SOURCE_URL / DNA_BASE_DIR "
|
|
50
|
+
"(filesystem source). Run `dna kind list` to start exploring, "
|
|
51
|
+
"`dna sdlc --help` for the lifecycle verbs."
|
|
52
|
+
),
|
|
53
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
54
|
+
)
|
|
55
|
+
@click.version_option(version="0.1.0", prog_name="dna")
|
|
56
|
+
def main() -> None:
|
|
57
|
+
"""Top-level group — subcommands attached below."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
main.add_command(kind_cmd.kind)
|
|
61
|
+
main.add_command(doc_cmd.doc)
|
|
62
|
+
main.add_command(scope_cmd.scope)
|
|
63
|
+
main.add_command(docs_cmd.docs_)
|
|
64
|
+
main.add_command(sdlc_cmd.sdlc)
|
|
65
|
+
main.add_command(research_cmd.research)
|
|
66
|
+
main.add_command(recall_cmd.recall)
|
|
67
|
+
main.add_command(recall_cmd.search)
|
|
68
|
+
main.add_command(memory_cmd.memory)
|
|
69
|
+
# Importing testkit_cmd registers `sdlc test-guide` + `sdlc test-run` on the
|
|
70
|
+
# sdlc group via its decorators (TESTS as first-class SDLC).
|
|
71
|
+
from dna_cli import testkit_cmd as _testkit_cmd # noqa: E402,F401
|
|
72
|
+
# Importing hooks_cmd registers `sdlc hooks install|uninstall|status` — the
|
|
73
|
+
# git↔SDLC symbiosis wiring (Work-Item trailers via prepare-commit-msg).
|
|
74
|
+
from dna_cli import hooks_cmd as _hooks_cmd # noqa: E402,F401
|
|
75
|
+
# Importing pr_cmd registers `sdlc story pr` + `sdlc pr-footer` — the PR
|
|
76
|
+
# half of the symbiosis (attribution footer, PR born from the Story).
|
|
77
|
+
from dna_cli import pr_cmd as _pr_cmd # noqa: E402,F401
|
|
78
|
+
# Importing issue_bridge_cmd registers `sdlc issue publish|import|sync` —
|
|
79
|
+
# the GitHub Issues side of the symbiosis (bridge with provenance).
|
|
80
|
+
from dna_cli import issue_bridge_cmd as _issue_bridge_cmd # noqa: E402,F401
|
|
81
|
+
main.add_command(source_cmd.source)
|
|
82
|
+
main.add_command(install_cmd.install)
|
|
83
|
+
main.add_command(eval_cmd.eval_)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Active-Story pointer — `.dna/active-story.txt` at the repo root.
|
|
2
|
+
|
|
3
|
+
Written by ``dna sdlc story start <name>`` so that out-of-band tools
|
|
4
|
+
(Claude Code hooks, IDE plugins, dashboards) can attribute their
|
|
5
|
+
events to the Story the human-or-agent is currently working on.
|
|
6
|
+
|
|
7
|
+
File format (single line, plain text)::
|
|
8
|
+
|
|
9
|
+
<scope>:<story-name>
|
|
10
|
+
|
|
11
|
+
Examples::
|
|
12
|
+
|
|
13
|
+
dna-development:s-foo
|
|
14
|
+
hr-screening:s-bar
|
|
15
|
+
|
|
16
|
+
A trailing newline is optional. The file is gitignored — it is per-
|
|
17
|
+
workstation state, not source-of-truth, so a fresh clone has no
|
|
18
|
+
active Story until ``story start`` is invoked.
|
|
19
|
+
|
|
20
|
+
Discovery walks up from ``cwd`` looking for ``.git`` to anchor the
|
|
21
|
+
``.dna/`` directory; this matches the convention used by source
|
|
22
|
+
replicas (``.dna-replicas.yaml`` discovery in ``source_replicas.py``).
|
|
23
|
+
Override via ``DNA_ACTIVE_STORY_PATH`` for tests.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
_ENV = "DNA_ACTIVE_STORY_PATH"
|
|
31
|
+
_FILENAME = ".dna/active-story.txt"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_repo_root(start: Path | None = None) -> Path | None:
|
|
35
|
+
"""Walk up from ``start`` looking for ``.git`` (file or dir).
|
|
36
|
+
|
|
37
|
+
Returns ``None`` when no repo root is found. Symmetric with the
|
|
38
|
+
discovery used in ``source_replicas.py``.
|
|
39
|
+
"""
|
|
40
|
+
cur = (start or Path.cwd()).resolve()
|
|
41
|
+
for parent in (cur, *cur.parents):
|
|
42
|
+
if (parent / ".git").exists():
|
|
43
|
+
return parent
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_active_story_path(*, start: Path | None = None) -> Path:
|
|
48
|
+
"""Resolve the canonical pointer path. Order of precedence:
|
|
49
|
+
|
|
50
|
+
1. ``DNA_ACTIVE_STORY_PATH`` env var (tests + advanced setups)
|
|
51
|
+
2. ``<repo-root>/.dna/active-story.txt`` if walking up finds ``.git``
|
|
52
|
+
3. ``<cwd>/.dna/active-story.txt`` as fallback (loose-tree mode)
|
|
53
|
+
"""
|
|
54
|
+
env = os.environ.get(_ENV)
|
|
55
|
+
if env:
|
|
56
|
+
return Path(env)
|
|
57
|
+
root = _find_repo_root(start)
|
|
58
|
+
if root is not None:
|
|
59
|
+
return root / _FILENAME
|
|
60
|
+
return (start or Path.cwd()).resolve() / _FILENAME
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_active_story(scope: str, name: str, *, start: Path | None = None) -> Path:
|
|
64
|
+
"""Stamp ``<scope>:<name>`` to the pointer file. Creates the
|
|
65
|
+
parent directory if missing. Returns the resolved path. Atomic
|
|
66
|
+
via tmpfile + rename.
|
|
67
|
+
"""
|
|
68
|
+
if not scope or not name:
|
|
69
|
+
raise ValueError("scope and name must both be non-empty")
|
|
70
|
+
if ":" in scope or ":" in name:
|
|
71
|
+
# Reserve `:` as the field separator. Story slugs use `s-` /
|
|
72
|
+
# `f-` / `e-` prefixes by convention so this never collides
|
|
73
|
+
# in practice; defensive guard just in case.
|
|
74
|
+
raise ValueError("scope/name must not contain ':'")
|
|
75
|
+
path = get_active_story_path(start=start)
|
|
76
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
78
|
+
tmp.write_text(f"{scope}:{name}\n", encoding="utf-8")
|
|
79
|
+
tmp.replace(path)
|
|
80
|
+
return path
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def read_active_story(
|
|
84
|
+
*, start: Path | None = None
|
|
85
|
+
) -> tuple[str, str] | None:
|
|
86
|
+
"""Read the pointer; returns ``(scope, name)`` or ``None`` when
|
|
87
|
+
absent / empty / malformed. Non-fatal — callers (hooks) treat
|
|
88
|
+
``None`` as "no active story; broadcast as ephemeral".
|
|
89
|
+
"""
|
|
90
|
+
path = get_active_story_path(start=start)
|
|
91
|
+
try:
|
|
92
|
+
raw = path.read_text(encoding="utf-8").strip()
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
return None
|
|
95
|
+
except OSError:
|
|
96
|
+
return None
|
|
97
|
+
if not raw:
|
|
98
|
+
return None
|
|
99
|
+
if ":" not in raw:
|
|
100
|
+
return None
|
|
101
|
+
scope, _, name = raw.partition(":")
|
|
102
|
+
scope = scope.strip()
|
|
103
|
+
name = name.strip()
|
|
104
|
+
if not scope or not name:
|
|
105
|
+
return None
|
|
106
|
+
return scope, name
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def clear_active_story(*, start: Path | None = None) -> bool:
|
|
110
|
+
"""Remove the pointer file. Returns True if a file was removed,
|
|
111
|
+
False if it was already absent. Idempotent.
|
|
112
|
+
"""
|
|
113
|
+
path = get_active_story_path(start=start)
|
|
114
|
+
try:
|
|
115
|
+
path.unlink()
|
|
116
|
+
return True
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def clear_if_matches(
|
|
122
|
+
scope: str, name: str, *, start: Path | None = None
|
|
123
|
+
) -> bool:
|
|
124
|
+
"""Clear the pointer ONLY when it currently points at the given
|
|
125
|
+
Story. Used by ``dna sdlc story done|block|cancel`` so that
|
|
126
|
+
closing some other Story (not the active one) doesn't blank the
|
|
127
|
+
pointer. Returns True when cleared.
|
|
128
|
+
"""
|
|
129
|
+
cur = read_active_story(start=start)
|
|
130
|
+
if cur is None:
|
|
131
|
+
return False
|
|
132
|
+
if cur != (scope, name):
|
|
133
|
+
return False
|
|
134
|
+
return clear_active_story(start=start)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""ASCII banner with cobre/orange gradient, shown on ``dna --help``.
|
|
2
|
+
|
|
3
|
+
Inspired by ``claude-code-templates`` ``cct`` banner. Renders only when
|
|
4
|
+
stdout is a TTY (skip in pipes / CI). Set ``DNA_NO_BANNER=1`` to disable.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
# Cobre → laranja → pêssego gradient (same palette aitmpl.com uses).
|
|
16
|
+
_GRADIENT = ["#EA580C", "#F97316", "#FB923C", "#FDBA74", "#FED7AA", "#FFE7CC"]
|
|
17
|
+
|
|
18
|
+
# Compact 5-line block. Stencil-ish (block characters) for a "Claude Code SDK"
|
|
19
|
+
# feel without being noisy. Width ~50 cols → fits any terminal.
|
|
20
|
+
_LOGO = r"""
|
|
21
|
+
█████ █████ ██████
|
|
22
|
+
██ ██ ██ ██ ██ ██
|
|
23
|
+
███████ ███████ ██████
|
|
24
|
+
██ ██ ██ ██ ██
|
|
25
|
+
██ ██ ██ ██ ██ declarative agent platform
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _gradient_text(line: str) -> Text:
|
|
30
|
+
"""Apply per-character color gradient across visible glyphs."""
|
|
31
|
+
t = Text()
|
|
32
|
+
n = len(_GRADIENT)
|
|
33
|
+
for i, ch in enumerate(line):
|
|
34
|
+
if ch in (" ", "\n", "\t"):
|
|
35
|
+
t.append(ch)
|
|
36
|
+
else:
|
|
37
|
+
t.append(ch, style=_GRADIENT[i % n])
|
|
38
|
+
return t
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render_banner() -> str:
|
|
42
|
+
"""Return colorized banner string, or empty if banner suppressed."""
|
|
43
|
+
if os.environ.get("DNA_NO_BANNER") == "1":
|
|
44
|
+
return ""
|
|
45
|
+
if not sys.stdout.isatty():
|
|
46
|
+
return ""
|
|
47
|
+
lines = _LOGO.strip("\n").splitlines()
|
|
48
|
+
console = Console(file=sys.stdout, force_terminal=True)
|
|
49
|
+
# Build the colored block via Rich's capture so we can return it as a string
|
|
50
|
+
# (Click's HelpFormatter writes strings, not Rich objects).
|
|
51
|
+
with console.capture() as cap:
|
|
52
|
+
for line in lines:
|
|
53
|
+
console.print(_gradient_text(line))
|
|
54
|
+
return cap.get()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_banner() -> None:
|
|
58
|
+
"""Print banner to stdout when appropriate."""
|
|
59
|
+
out = render_banner()
|
|
60
|
+
if out:
|
|
61
|
+
click.echo(out)
|