blog-cli 0.3.0__tar.gz → 0.4.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.
- {blog_cli-0.3.0 → blog_cli-0.4.0}/PKG-INFO +54 -3
- {blog_cli-0.3.0 → blog_cli-0.4.0}/README.md +51 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/pyproject.toml +7 -3
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/blog_cli.egg-info/PKG-INFO +54 -3
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/blog_cli.egg-info/SOURCES.txt +8 -1
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/cli.py +351 -1
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/client.py +45 -0
- blog_cli-0.4.0/src/personal_cli/skill.py +34 -0
- blog_cli-0.4.0/src/personal_cli/skill_data/SKILL.md +30 -0
- blog_cli-0.4.0/src/personal_cli/skill_data/agents/openai.yaml +7 -0
- blog_cli-0.4.0/src/personal_cli/skill_data/references/articles.md +46 -0
- blog_cli-0.4.0/src/personal_cli/skill_data/references/pages.md +132 -0
- blog_cli-0.4.0/src/personal_cli/skill_data/references/projects.md +50 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/tests/test_cli.py +122 -0
- blog_cli-0.4.0/tests/test_pages_categories.py +254 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/setup.cfg +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/blog_cli.egg-info/dependency_links.txt +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/blog_cli.egg-info/entry_points.txt +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/blog_cli.egg-info/requires.txt +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/blog_cli.egg-info/top_level.txt +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/__init__.py +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/__main__.py +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/credentials.py +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/formatting.py +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/src/personal_cli/setup_server.py +0 -0
- {blog_cli-0.3.0 → blog_cli-0.4.0}/tests/test_client.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blog-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Agent-friendly CLI for managing a
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Agent-friendly CLI for managing a personal site. Articles, projects, categories, private pages, media, and a bundled ChatGPT/Codex skill.
|
|
5
5
|
Author: tanaka-mambinge
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Repository, https://github.com/tanaka-mambinge/personal-cli
|
|
8
8
|
Project-URL: Issues, https://github.com/tanaka-mambinge/personal-cli/issues
|
|
9
|
-
Keywords: blog,cli,agent,markdown,writing
|
|
9
|
+
Keywords: blog,cli,agent,markdown,writing,mdx,cms,dashboard
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -194,6 +194,57 @@ blog-cli article tag-add my-project --tag python --tag agents
|
|
|
194
194
|
blog-cli article tag-remove my-project --tag agents
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
## Categories
|
|
198
|
+
|
|
199
|
+
Categories are first-class models that group private content pages. The slug is auto-derived from the name. A category cannot be deleted while pages still belong to it.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Create (slug auto from name, e.g. "YouTube Notes" -> "youtube-notes")
|
|
203
|
+
blog-cli category create --name "Ideas" --icon bulb --description "Captured ideas"
|
|
204
|
+
|
|
205
|
+
# List / show / update / delete
|
|
206
|
+
blog-cli category list
|
|
207
|
+
blog-cli category show ideas
|
|
208
|
+
blog-cli category update ideas --name "Idea Box" --icon lightbulb
|
|
209
|
+
blog-cli category delete ideas
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Pages
|
|
213
|
+
|
|
214
|
+
Pages are private content shown only on the dashboard at `/d/<slug>`. They are for your eyes only. Each page belongs to a category and supports MDX with prebuilt components (`<Callout>`, `<Steps>`, `<ImageGrid>`, `<Video>`, `<Figure>`).
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Create a page
|
|
218
|
+
blog-cli page create \
|
|
219
|
+
--title "An idea" \
|
|
220
|
+
--description "Short summary" \
|
|
221
|
+
--category ideas \
|
|
222
|
+
--tag web --tag ai \
|
|
223
|
+
--markdown-file idea.mdx
|
|
224
|
+
|
|
225
|
+
# List (optionally filter by category)
|
|
226
|
+
blog-cli page list
|
|
227
|
+
blog-cli page list --category ideas
|
|
228
|
+
|
|
229
|
+
# Show / update / delete
|
|
230
|
+
blog-cli page show an-idea
|
|
231
|
+
blog-cli page update an-idea --title "A better title"
|
|
232
|
+
blog-cli page update an-idea --markdown-file updated.mdx
|
|
233
|
+
blog-cli page delete an-idea
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Page bodies are MDX. Referenced media uses names uploaded via `blog-cli media upload --name <name> <file>`; the site resolves names to media URLs at render time.
|
|
237
|
+
|
|
238
|
+
## ChatGPT / Codex skill
|
|
239
|
+
|
|
240
|
+
The CLI ships a bundled `content-pipeline` skill that routes blog, project, and page tasks to the right reference doc. Install it into the user-level skills directory:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
blog-cli skill install
|
|
244
|
+
blog-cli skill uninstall
|
|
245
|
+
blog-cli skill path
|
|
246
|
+
```
|
|
247
|
+
|
|
197
248
|
## Media
|
|
198
249
|
|
|
199
250
|
Upload media using a stable name, then reference that name from article Markdown:
|
|
@@ -164,6 +164,57 @@ blog-cli article tag-add my-project --tag python --tag agents
|
|
|
164
164
|
blog-cli article tag-remove my-project --tag agents
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
## Categories
|
|
168
|
+
|
|
169
|
+
Categories are first-class models that group private content pages. The slug is auto-derived from the name. A category cannot be deleted while pages still belong to it.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Create (slug auto from name, e.g. "YouTube Notes" -> "youtube-notes")
|
|
173
|
+
blog-cli category create --name "Ideas" --icon bulb --description "Captured ideas"
|
|
174
|
+
|
|
175
|
+
# List / show / update / delete
|
|
176
|
+
blog-cli category list
|
|
177
|
+
blog-cli category show ideas
|
|
178
|
+
blog-cli category update ideas --name "Idea Box" --icon lightbulb
|
|
179
|
+
blog-cli category delete ideas
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Pages
|
|
183
|
+
|
|
184
|
+
Pages are private content shown only on the dashboard at `/d/<slug>`. They are for your eyes only. Each page belongs to a category and supports MDX with prebuilt components (`<Callout>`, `<Steps>`, `<ImageGrid>`, `<Video>`, `<Figure>`).
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Create a page
|
|
188
|
+
blog-cli page create \
|
|
189
|
+
--title "An idea" \
|
|
190
|
+
--description "Short summary" \
|
|
191
|
+
--category ideas \
|
|
192
|
+
--tag web --tag ai \
|
|
193
|
+
--markdown-file idea.mdx
|
|
194
|
+
|
|
195
|
+
# List (optionally filter by category)
|
|
196
|
+
blog-cli page list
|
|
197
|
+
blog-cli page list --category ideas
|
|
198
|
+
|
|
199
|
+
# Show / update / delete
|
|
200
|
+
blog-cli page show an-idea
|
|
201
|
+
blog-cli page update an-idea --title "A better title"
|
|
202
|
+
blog-cli page update an-idea --markdown-file updated.mdx
|
|
203
|
+
blog-cli page delete an-idea
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Page bodies are MDX. Referenced media uses names uploaded via `blog-cli media upload --name <name> <file>`; the site resolves names to media URLs at render time.
|
|
207
|
+
|
|
208
|
+
## ChatGPT / Codex skill
|
|
209
|
+
|
|
210
|
+
The CLI ships a bundled `content-pipeline` skill that routes blog, project, and page tasks to the right reference doc. Install it into the user-level skills directory:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
blog-cli skill install
|
|
214
|
+
blog-cli skill uninstall
|
|
215
|
+
blog-cli skill path
|
|
216
|
+
```
|
|
217
|
+
|
|
167
218
|
## Media
|
|
168
219
|
|
|
169
220
|
Upload media using a stable name, then reference that name from article Markdown:
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "blog-cli"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Agent-friendly CLI for managing a
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Agent-friendly CLI for managing a personal site. Articles, projects, categories, private pages, media, and a bundled ChatGPT/Codex skill."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [{name = "tanaka-mambinge"}]
|
|
13
|
-
keywords = ["blog", "cli", "agent", "markdown", "writing"]
|
|
13
|
+
keywords = ["blog", "cli", "agent", "markdown", "writing", "mdx", "cms", "dashboard"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"License :: OSI Approved :: MIT License",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
@@ -54,6 +54,10 @@ package = true
|
|
|
54
54
|
|
|
55
55
|
[tool.setuptools]
|
|
56
56
|
package-dir = {"" = "src"}
|
|
57
|
+
include-package-data = true
|
|
57
58
|
|
|
58
59
|
[tool.setuptools.packages.find]
|
|
59
60
|
where = ["src"]
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.package-data]
|
|
63
|
+
personal_cli = ["skill_data/**/*"]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blog-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Agent-friendly CLI for managing a
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Agent-friendly CLI for managing a personal site. Articles, projects, categories, private pages, media, and a bundled ChatGPT/Codex skill.
|
|
5
5
|
Author: tanaka-mambinge
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Repository, https://github.com/tanaka-mambinge/personal-cli
|
|
8
8
|
Project-URL: Issues, https://github.com/tanaka-mambinge/personal-cli/issues
|
|
9
|
-
Keywords: blog,cli,agent,markdown,writing
|
|
9
|
+
Keywords: blog,cli,agent,markdown,writing,mdx,cms,dashboard
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -194,6 +194,57 @@ blog-cli article tag-add my-project --tag python --tag agents
|
|
|
194
194
|
blog-cli article tag-remove my-project --tag agents
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
## Categories
|
|
198
|
+
|
|
199
|
+
Categories are first-class models that group private content pages. The slug is auto-derived from the name. A category cannot be deleted while pages still belong to it.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Create (slug auto from name, e.g. "YouTube Notes" -> "youtube-notes")
|
|
203
|
+
blog-cli category create --name "Ideas" --icon bulb --description "Captured ideas"
|
|
204
|
+
|
|
205
|
+
# List / show / update / delete
|
|
206
|
+
blog-cli category list
|
|
207
|
+
blog-cli category show ideas
|
|
208
|
+
blog-cli category update ideas --name "Idea Box" --icon lightbulb
|
|
209
|
+
blog-cli category delete ideas
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Pages
|
|
213
|
+
|
|
214
|
+
Pages are private content shown only on the dashboard at `/d/<slug>`. They are for your eyes only. Each page belongs to a category and supports MDX with prebuilt components (`<Callout>`, `<Steps>`, `<ImageGrid>`, `<Video>`, `<Figure>`).
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Create a page
|
|
218
|
+
blog-cli page create \
|
|
219
|
+
--title "An idea" \
|
|
220
|
+
--description "Short summary" \
|
|
221
|
+
--category ideas \
|
|
222
|
+
--tag web --tag ai \
|
|
223
|
+
--markdown-file idea.mdx
|
|
224
|
+
|
|
225
|
+
# List (optionally filter by category)
|
|
226
|
+
blog-cli page list
|
|
227
|
+
blog-cli page list --category ideas
|
|
228
|
+
|
|
229
|
+
# Show / update / delete
|
|
230
|
+
blog-cli page show an-idea
|
|
231
|
+
blog-cli page update an-idea --title "A better title"
|
|
232
|
+
blog-cli page update an-idea --markdown-file updated.mdx
|
|
233
|
+
blog-cli page delete an-idea
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Page bodies are MDX. Referenced media uses names uploaded via `blog-cli media upload --name <name> <file>`; the site resolves names to media URLs at render time.
|
|
237
|
+
|
|
238
|
+
## ChatGPT / Codex skill
|
|
239
|
+
|
|
240
|
+
The CLI ships a bundled `content-pipeline` skill that routes blog, project, and page tasks to the right reference doc. Install it into the user-level skills directory:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
blog-cli skill install
|
|
244
|
+
blog-cli skill uninstall
|
|
245
|
+
blog-cli skill path
|
|
246
|
+
```
|
|
247
|
+
|
|
197
248
|
## Media
|
|
198
249
|
|
|
199
250
|
Upload media using a stable name, then reference that name from article Markdown:
|
|
@@ -13,5 +13,12 @@ src/personal_cli/client.py
|
|
|
13
13
|
src/personal_cli/credentials.py
|
|
14
14
|
src/personal_cli/formatting.py
|
|
15
15
|
src/personal_cli/setup_server.py
|
|
16
|
+
src/personal_cli/skill.py
|
|
17
|
+
src/personal_cli/skill_data/SKILL.md
|
|
18
|
+
src/personal_cli/skill_data/agents/openai.yaml
|
|
19
|
+
src/personal_cli/skill_data/references/articles.md
|
|
20
|
+
src/personal_cli/skill_data/references/pages.md
|
|
21
|
+
src/personal_cli/skill_data/references/projects.md
|
|
16
22
|
tests/test_cli.py
|
|
17
|
-
tests/test_client.py
|
|
23
|
+
tests/test_client.py
|
|
24
|
+
tests/test_pages_categories.py
|
|
@@ -22,12 +22,18 @@ blog_app = typer.Typer(help="Manage blog posts.")
|
|
|
22
22
|
project_app = typer.Typer(help="Manage projects.")
|
|
23
23
|
media_app = typer.Typer(help="Manage media uploads.")
|
|
24
24
|
keys_app = typer.Typer(help="Manage stored credentials.")
|
|
25
|
+
category_app = typer.Typer(help="Manage content categories.")
|
|
26
|
+
page_app = typer.Typer(help="Manage private content pages.")
|
|
27
|
+
skill_app = typer.Typer(help="Manage the bundled ChatGPT/Codex skill.")
|
|
25
28
|
|
|
26
29
|
app.add_typer(article_app, name="article")
|
|
27
30
|
article_app.add_typer(blog_app, name="blog")
|
|
28
31
|
article_app.add_typer(project_app, name="project")
|
|
29
32
|
app.add_typer(media_app, name="media")
|
|
30
33
|
app.add_typer(keys_app, name="keys")
|
|
34
|
+
app.add_typer(category_app, name="category")
|
|
35
|
+
app.add_typer(page_app, name="page")
|
|
36
|
+
app.add_typer(skill_app, name="skill")
|
|
31
37
|
|
|
32
38
|
Result = TypeVar("Result")
|
|
33
39
|
|
|
@@ -55,6 +61,19 @@ def build_client(
|
|
|
55
61
|
return ArticleApiClient(server_url or url, api_key=api_key, verify=not insecure)
|
|
56
62
|
|
|
57
63
|
|
|
64
|
+
def _site_url() -> str:
|
|
65
|
+
_, _, site_url = get_config()
|
|
66
|
+
return site_url.rstrip("/")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _dashboard_url(slug: str) -> str:
|
|
70
|
+
return f"{_site_url()}/d/{slug}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _category_dashboard_url(slug: str) -> str:
|
|
74
|
+
return f"{_site_url()}/d?category={slug}"
|
|
75
|
+
|
|
76
|
+
|
|
58
77
|
def run(coro):
|
|
59
78
|
return asyncio.run(coro)
|
|
60
79
|
|
|
@@ -164,7 +183,7 @@ def project_create(
|
|
|
164
183
|
description: str = typer.Option(..., "--description", help="Short summary."),
|
|
165
184
|
slug: str | None = typer.Option(None, "--slug", help="Optional slug override."),
|
|
166
185
|
tag: list[str] = typer.Option([], "--tag", help="Repeat for each tag."),
|
|
167
|
-
cover_image: str
|
|
186
|
+
cover_image: str = typer.Option(..., "--cover-image", help="Uploaded media name for the project banner image."),
|
|
168
187
|
status: str = typer.Option("draft", "--status", help="draft or published."),
|
|
169
188
|
pinned: bool = typer.Option(False, "--pinned/--not-pinned", help="Pin to the home page."),
|
|
170
189
|
sort_order: int = typer.Option(0, "--sort-order", help="Order among pinned projects (lower first)."),
|
|
@@ -221,6 +240,16 @@ def article_update(
|
|
|
221
240
|
if cover_image is not None and clear_cover_image:
|
|
222
241
|
raise CLIError("Use either --cover-image or --clear-cover-image, not both.")
|
|
223
242
|
client = build_client(server_url, insecure=insecure)
|
|
243
|
+
current = next(
|
|
244
|
+
(article for article in run(client.list_articles(type_filter="all")) if article.get("slug") == slug),
|
|
245
|
+
None,
|
|
246
|
+
)
|
|
247
|
+
if current is None:
|
|
248
|
+
raise CLIError(f"Article not found: {slug}")
|
|
249
|
+
target_type = article_type or current.get("type")
|
|
250
|
+
resulting_cover = cover_image if cover_image is not None else current.get("cover_image")
|
|
251
|
+
if target_type == "project" and (clear_cover_image or not resulting_cover):
|
|
252
|
+
raise CLIError("Projects must have a cover image. Use --cover-image or keep the existing cover image.")
|
|
224
253
|
payload: dict[str, object] = {}
|
|
225
254
|
if title is not None:
|
|
226
255
|
payload["title"] = title
|
|
@@ -511,6 +540,327 @@ def keys_show(
|
|
|
511
540
|
raise typer.Exit(code=1) from exc
|
|
512
541
|
|
|
513
542
|
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
# Categories
|
|
545
|
+
# ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@category_app.command("create")
|
|
549
|
+
def category_create(
|
|
550
|
+
name: str = typer.Option(..., "--name", help="Display name. Slug is derived from this."),
|
|
551
|
+
icon: str | None = typer.Option(None, "--icon", help="Tabler icon name, e.g. bulb."),
|
|
552
|
+
description: str | None = typer.Option(None, "--description", help="Short description."),
|
|
553
|
+
sort_order: int = typer.Option(0, "--sort-order", help="Lower sorts first."),
|
|
554
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
555
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
556
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
557
|
+
) -> None:
|
|
558
|
+
def _op() -> None:
|
|
559
|
+
client = build_client(server_url, insecure=insecure)
|
|
560
|
+
payload: dict[str, object] = {"name": name, "sort_order": sort_order}
|
|
561
|
+
if icon is not None:
|
|
562
|
+
payload["icon"] = icon
|
|
563
|
+
if description is not None:
|
|
564
|
+
payload["description"] = description
|
|
565
|
+
result = run(client.create_category(payload))
|
|
566
|
+
result["dashboard_url"] = _category_dashboard_url(result["slug"])
|
|
567
|
+
emit_result(result, json_output=json_output)
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
_run(_op)
|
|
571
|
+
except CLIError as exc:
|
|
572
|
+
typer.echo(str(exc), err=True)
|
|
573
|
+
raise typer.Exit(code=1) from exc
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@category_app.command("list")
|
|
577
|
+
def category_list(
|
|
578
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
579
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
580
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
581
|
+
) -> None:
|
|
582
|
+
def _op() -> None:
|
|
583
|
+
client = build_client(server_url, insecure=insecure)
|
|
584
|
+
result = run(client.list_categories())
|
|
585
|
+
emit_result(result, json_output=json_output)
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
_run(_op)
|
|
589
|
+
except CLIError as exc:
|
|
590
|
+
typer.echo(str(exc), err=True)
|
|
591
|
+
raise typer.Exit(code=1) from exc
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@category_app.command("show")
|
|
595
|
+
def category_show(
|
|
596
|
+
slug: str,
|
|
597
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
598
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
599
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
600
|
+
) -> None:
|
|
601
|
+
def _op() -> None:
|
|
602
|
+
client = build_client(server_url, insecure=insecure)
|
|
603
|
+
result = run(client.get_category(slug))
|
|
604
|
+
result["dashboard_url"] = _category_dashboard_url(result["slug"])
|
|
605
|
+
emit_result(result, json_output=json_output)
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
_run(_op)
|
|
609
|
+
except CLIError as exc:
|
|
610
|
+
typer.echo(str(exc), err=True)
|
|
611
|
+
raise typer.Exit(code=1) from exc
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@category_app.command("update")
|
|
615
|
+
def category_update(
|
|
616
|
+
slug: str,
|
|
617
|
+
name: str | None = typer.Option(None, "--name", help="New display name."),
|
|
618
|
+
icon: str | None = typer.Option(None, "--icon", help="New tabler icon name."),
|
|
619
|
+
description: str | None = typer.Option(None, "--description", help="New description."),
|
|
620
|
+
sort_order: int | None = typer.Option(None, "--sort-order", help="Lower sorts first."),
|
|
621
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
622
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
623
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
624
|
+
) -> None:
|
|
625
|
+
def _op() -> None:
|
|
626
|
+
client = build_client(server_url, insecure=insecure)
|
|
627
|
+
payload: dict[str, object] = {}
|
|
628
|
+
if name is not None:
|
|
629
|
+
payload["name"] = name
|
|
630
|
+
if icon is not None:
|
|
631
|
+
payload["icon"] = icon
|
|
632
|
+
if description is not None:
|
|
633
|
+
payload["description"] = description
|
|
634
|
+
if sort_order is not None:
|
|
635
|
+
payload["sort_order"] = sort_order
|
|
636
|
+
if not payload:
|
|
637
|
+
raise CLIError("No update fields provided.")
|
|
638
|
+
result = run(client.update_category(slug, payload))
|
|
639
|
+
emit_result(result, json_output=json_output)
|
|
640
|
+
|
|
641
|
+
try:
|
|
642
|
+
_run(_op)
|
|
643
|
+
except CLIError as exc:
|
|
644
|
+
typer.echo(str(exc), err=True)
|
|
645
|
+
raise typer.Exit(code=1) from exc
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@category_app.command("delete")
|
|
649
|
+
def category_delete(
|
|
650
|
+
slug: str,
|
|
651
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
652
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
653
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
654
|
+
) -> None:
|
|
655
|
+
def _op() -> None:
|
|
656
|
+
client = build_client(server_url, insecure=insecure)
|
|
657
|
+
result = run(client.delete_category(slug))
|
|
658
|
+
emit_result(result, json_output=json_output)
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
_run(_op)
|
|
662
|
+
except CLIError as exc:
|
|
663
|
+
typer.echo(str(exc), err=True)
|
|
664
|
+
raise typer.Exit(code=1) from exc
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
# ---------------------------------------------------------------------------
|
|
668
|
+
# Pages
|
|
669
|
+
# ---------------------------------------------------------------------------
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@page_app.command("create")
|
|
673
|
+
def page_create(
|
|
674
|
+
title: str = typer.Option(..., "--title", help="Page title."),
|
|
675
|
+
description: str = typer.Option(..., "--description", help="Short summary."),
|
|
676
|
+
category: str = typer.Option(..., "--category", help="Category slug the page belongs to."),
|
|
677
|
+
slug: str | None = typer.Option(None, "--slug", help="Optional slug override."),
|
|
678
|
+
sort_order: int = typer.Option(0, "--sort-order", help="Lower sorts first."),
|
|
679
|
+
markdown: str | None = typer.Option(None, "--markdown", help="Inline MDX body."),
|
|
680
|
+
markdown_file: Path | None = typer.Option(None, "--markdown-file", exists=True, readable=True, dir_okay=False),
|
|
681
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
682
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
683
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
684
|
+
) -> None:
|
|
685
|
+
def _op() -> None:
|
|
686
|
+
body = read_markdown_from_source(markdown=markdown, markdown_file=markdown_file)
|
|
687
|
+
client = build_client(server_url, insecure=insecure)
|
|
688
|
+
payload = {
|
|
689
|
+
"title": title,
|
|
690
|
+
"description": description,
|
|
691
|
+
"category_slug": category,
|
|
692
|
+
"slug": slug,
|
|
693
|
+
"sort_order": sort_order,
|
|
694
|
+
"markdown": body,
|
|
695
|
+
}
|
|
696
|
+
result = run(client.create_page(payload))
|
|
697
|
+
result["dashboard_url"] = _dashboard_url(result["slug"])
|
|
698
|
+
emit_result(result, json_output=json_output)
|
|
699
|
+
|
|
700
|
+
try:
|
|
701
|
+
_run(_op)
|
|
702
|
+
except CLIError as exc:
|
|
703
|
+
typer.echo(str(exc), err=True)
|
|
704
|
+
raise typer.Exit(code=1) from exc
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@page_app.command("list")
|
|
708
|
+
def page_list(
|
|
709
|
+
category: str | None = typer.Option(None, "--category", help="Filter by category slug."),
|
|
710
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
711
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
712
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
713
|
+
) -> None:
|
|
714
|
+
def _op() -> None:
|
|
715
|
+
client = build_client(server_url, insecure=insecure)
|
|
716
|
+
result = run(client.list_pages(category=category))
|
|
717
|
+
emit_result(result, json_output=json_output)
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
_run(_op)
|
|
721
|
+
except CLIError as exc:
|
|
722
|
+
typer.echo(str(exc), err=True)
|
|
723
|
+
raise typer.Exit(code=1) from exc
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@page_app.command("show")
|
|
727
|
+
def page_show(
|
|
728
|
+
slug: str,
|
|
729
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
730
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
731
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
732
|
+
) -> None:
|
|
733
|
+
def _op() -> None:
|
|
734
|
+
client = build_client(server_url, insecure=insecure)
|
|
735
|
+
result = run(client.get_page(slug))
|
|
736
|
+
result["dashboard_url"] = _dashboard_url(result["slug"])
|
|
737
|
+
emit_result(result, json_output=json_output)
|
|
738
|
+
|
|
739
|
+
try:
|
|
740
|
+
_run(_op)
|
|
741
|
+
except CLIError as exc:
|
|
742
|
+
typer.echo(str(exc), err=True)
|
|
743
|
+
raise typer.Exit(code=1) from exc
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@page_app.command("update")
|
|
747
|
+
def page_update(
|
|
748
|
+
slug: str,
|
|
749
|
+
title: str | None = typer.Option(None, "--title", help="New title."),
|
|
750
|
+
description: str | None = typer.Option(None, "--description", help="New summary."),
|
|
751
|
+
category: str | None = typer.Option(None, "--category", help="New category slug."),
|
|
752
|
+
sort_order: int | None = typer.Option(None, "--sort-order", help="Lower sorts first."),
|
|
753
|
+
markdown: str | None = typer.Option(None, "--markdown", help="Inline MDX body."),
|
|
754
|
+
markdown_file: Path | None = typer.Option(None, "--markdown-file", exists=True, readable=True, dir_okay=False),
|
|
755
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
756
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
757
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
758
|
+
) -> None:
|
|
759
|
+
def _op() -> None:
|
|
760
|
+
client = build_client(server_url, insecure=insecure)
|
|
761
|
+
payload: dict[str, object] = {}
|
|
762
|
+
if title is not None:
|
|
763
|
+
payload["title"] = title
|
|
764
|
+
if description is not None:
|
|
765
|
+
payload["description"] = description
|
|
766
|
+
if category is not None:
|
|
767
|
+
payload["category_slug"] = category
|
|
768
|
+
if sort_order is not None:
|
|
769
|
+
payload["sort_order"] = sort_order
|
|
770
|
+
if markdown is not None or markdown_file is not None:
|
|
771
|
+
payload["markdown"] = read_markdown_from_source(markdown=markdown, markdown_file=markdown_file)
|
|
772
|
+
if not payload:
|
|
773
|
+
raise CLIError("No update fields provided.")
|
|
774
|
+
result = run(client.update_page(slug, payload))
|
|
775
|
+
result["dashboard_url"] = _dashboard_url(result["slug"])
|
|
776
|
+
emit_result(result, json_output=json_output)
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
_run(_op)
|
|
780
|
+
except CLIError as exc:
|
|
781
|
+
typer.echo(str(exc), err=True)
|
|
782
|
+
raise typer.Exit(code=1) from exc
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@page_app.command("delete")
|
|
786
|
+
def page_delete(
|
|
787
|
+
slug: str,
|
|
788
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
789
|
+
insecure: bool = typer.Option(False, "--insecure", help="Skip SSL verification."),
|
|
790
|
+
server_url: str | None = typer.Option(None, "--server-url", help="FastAPI base URL."),
|
|
791
|
+
) -> None:
|
|
792
|
+
def _op() -> None:
|
|
793
|
+
client = build_client(server_url, insecure=insecure)
|
|
794
|
+
result = run(client.delete_page(slug))
|
|
795
|
+
emit_result(result, json_output=json_output)
|
|
796
|
+
|
|
797
|
+
try:
|
|
798
|
+
_run(_op)
|
|
799
|
+
except CLIError as exc:
|
|
800
|
+
typer.echo(str(exc), err=True)
|
|
801
|
+
raise typer.Exit(code=1) from exc
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
# ---------------------------------------------------------------------------
|
|
805
|
+
# Skill installation
|
|
806
|
+
# ---------------------------------------------------------------------------
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@skill_app.command("install")
|
|
810
|
+
def skill_install(
|
|
811
|
+
target_dir: Path | None = typer.Option(
|
|
812
|
+
None,
|
|
813
|
+
"--dir",
|
|
814
|
+
help="Destination directory. Defaults to ~/.agents/skills/content-pipeline.",
|
|
815
|
+
),
|
|
816
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
817
|
+
) -> None:
|
|
818
|
+
"""Install the bundled ChatGPT/Codex skill into the agents skills directory."""
|
|
819
|
+
from personal_cli.skill import install_skill
|
|
820
|
+
|
|
821
|
+
try:
|
|
822
|
+
destination = install_skill(target_dir)
|
|
823
|
+
emit_result(
|
|
824
|
+
{"installed": True, "path": str(destination)},
|
|
825
|
+
json_output=json_output,
|
|
826
|
+
)
|
|
827
|
+
except Exception as exc:
|
|
828
|
+
typer.echo(str(exc), err=True)
|
|
829
|
+
raise typer.Exit(code=1) from exc
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
@skill_app.command("uninstall")
|
|
833
|
+
def skill_uninstall(
|
|
834
|
+
target_dir: Path | None = typer.Option(
|
|
835
|
+
None,
|
|
836
|
+
"--dir",
|
|
837
|
+
help="Installed location. Defaults to ~/.agents/skills/content-pipeline.",
|
|
838
|
+
),
|
|
839
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
840
|
+
) -> None:
|
|
841
|
+
"""Remove the installed skill."""
|
|
842
|
+
from personal_cli.skill import uninstall_skill
|
|
843
|
+
|
|
844
|
+
try:
|
|
845
|
+
removed = uninstall_skill(target_dir)
|
|
846
|
+
emit_result({"uninstalled": removed}, json_output=json_output)
|
|
847
|
+
except Exception as exc:
|
|
848
|
+
typer.echo(str(exc), err=True)
|
|
849
|
+
raise typer.Exit(code=1) from exc
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
@skill_app.command("path")
|
|
853
|
+
def skill_path(
|
|
854
|
+
target_dir: Path | None = typer.Option(None, "--dir", help="Custom install location."),
|
|
855
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
856
|
+
) -> None:
|
|
857
|
+
"""Print where the skill would be installed."""
|
|
858
|
+
from personal_cli.skill import skill_install_path
|
|
859
|
+
|
|
860
|
+
destination = skill_install_path(target_dir)
|
|
861
|
+
emit_result({"path": str(destination)}, json_output=json_output)
|
|
862
|
+
|
|
863
|
+
|
|
514
864
|
def main() -> None:
|
|
515
865
|
app()
|
|
516
866
|
|