codepraxis 0.3.0__tar.gz → 0.3.2__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.
- {codepraxis-0.3.0 → codepraxis-0.3.2}/PKG-INFO +19 -2
- {codepraxis-0.3.0 → codepraxis-0.3.2}/README.md +18 -1
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/__init__.py +1 -1
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/cli.py +57 -1
- codepraxis-0.3.2/src/codepraxis/commands/catalog.py +81 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/publish.py +6 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/client.py +10 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/installer.py +6 -2
- codepraxis-0.3.2/src/codepraxis/plugin/templates/commands/new.md +42 -0
- codepraxis-0.3.2/tests/test_catalog.py +67 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_publish.py +24 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_validation_rules.py +30 -0
- codepraxis-0.3.0/src/codepraxis/plugin/templates/commands/new.md +0 -32
- {codepraxis-0.3.0 → codepraxis-0.3.2}/.gitignore +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/CONTRIBUTING.md +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/RELEASING.md +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/pyproject.toml +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/__main__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/example.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/lint.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/login.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/validate.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/contract.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/pack.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/results.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/errors.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/executor.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/backends.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/executor.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/worker.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/workspace.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/config.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/executor.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/archive.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/discovery.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/loader.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/toc.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/commands/validate.md +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/marketplace.json +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/plugin.json +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/skills/pack-authoring/SKILL.md +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/human.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/json_reporter.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/reporter.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/generator.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/README.md +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/backend.conf +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/course_toc.json +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/feature.md +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/main.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/metadata.json +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/solution.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/test_1.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/registry.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rule.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/__init__.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/hygiene.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/instructions.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/testcases.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/conformance/test_corpus.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/conftest.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_classification.py +0 -0
- {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_toc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codepraxis
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Author and validate CodePraxis challenge packs.
|
|
5
5
|
Project-URL: Documentation, https://docs.codepraxis.com/authoring
|
|
6
6
|
Author: CodePraxis
|
|
@@ -35,6 +35,8 @@ codepraxis lint my-challenge # static checks, no execution
|
|
|
35
35
|
codepraxis validate --local my-challenge # run it, fast and advisory
|
|
36
36
|
codepraxis --login
|
|
37
37
|
codepraxis --publish my-challenge # validates in the runner, then publishes
|
|
38
|
+
codepraxis --list # list company questions
|
|
39
|
+
codepraxis --edit 123 --open # update metadata/open a preview container
|
|
38
40
|
```
|
|
39
41
|
|
|
40
42
|
## What this is
|
|
@@ -153,7 +155,7 @@ codepraxis --install claude-plugin
|
|
|
153
155
|
Writes a local plugin into `.codepraxis/claude-plugin/`, then tells you the two
|
|
154
156
|
commands to enable it. You get:
|
|
155
157
|
|
|
156
|
-
- **`/codepraxis:new`** —
|
|
158
|
+
- **`/codepraxis:new`** — design, build, validate, publish and return a URL
|
|
157
159
|
- **`/codepraxis:validate`** — validate and fix what fails, in a loop
|
|
158
160
|
- **`pack-authoring` skill** — loads automatically when Claude touches a pack,
|
|
159
161
|
so it already knows the `testCases` contract and the two-fixture rule
|
|
@@ -199,6 +201,21 @@ assigned to candidates immediately:
|
|
|
199
201
|
publish into someone else's catalog.
|
|
200
202
|
|
|
201
203
|
You'll be shown the company and asked to confirm before anything is created.
|
|
204
|
+
When the platform can start a preview container, `--publish` prints that URL so
|
|
205
|
+
you can inspect the question immediately.
|
|
206
|
+
|
|
207
|
+
## Company catalog
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
codepraxis --list
|
|
211
|
+
codepraxis --edit 123 --title "Debug webhook delivery" --status draft
|
|
212
|
+
codepraxis --edit 123 --open
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`--list` shows the questions owned by your API key's company, including drafts.
|
|
216
|
+
`--edit` updates question metadata and returns a container URL for inspection.
|
|
217
|
+
Code changes still go through the pack workflow: edit locally, validate, and
|
|
218
|
+
publish a new version.
|
|
202
219
|
|
|
203
220
|
## Development
|
|
204
221
|
|
|
@@ -10,6 +10,8 @@ codepraxis lint my-challenge # static checks, no execution
|
|
|
10
10
|
codepraxis validate --local my-challenge # run it, fast and advisory
|
|
11
11
|
codepraxis --login
|
|
12
12
|
codepraxis --publish my-challenge # validates in the runner, then publishes
|
|
13
|
+
codepraxis --list # list company questions
|
|
14
|
+
codepraxis --edit 123 --open # update metadata/open a preview container
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
## What this is
|
|
@@ -128,7 +130,7 @@ codepraxis --install claude-plugin
|
|
|
128
130
|
Writes a local plugin into `.codepraxis/claude-plugin/`, then tells you the two
|
|
129
131
|
commands to enable it. You get:
|
|
130
132
|
|
|
131
|
-
- **`/codepraxis:new`** —
|
|
133
|
+
- **`/codepraxis:new`** — design, build, validate, publish and return a URL
|
|
132
134
|
- **`/codepraxis:validate`** — validate and fix what fails, in a loop
|
|
133
135
|
- **`pack-authoring` skill** — loads automatically when Claude touches a pack,
|
|
134
136
|
so it already knows the `testCases` contract and the two-fixture rule
|
|
@@ -174,6 +176,21 @@ assigned to candidates immediately:
|
|
|
174
176
|
publish into someone else's catalog.
|
|
175
177
|
|
|
176
178
|
You'll be shown the company and asked to confirm before anything is created.
|
|
179
|
+
When the platform can start a preview container, `--publish` prints that URL so
|
|
180
|
+
you can inspect the question immediately.
|
|
181
|
+
|
|
182
|
+
## Company catalog
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
codepraxis --list
|
|
186
|
+
codepraxis --edit 123 --title "Debug webhook delivery" --status draft
|
|
187
|
+
codepraxis --edit 123 --open
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`--list` shows the questions owned by your API key's company, including drafts.
|
|
191
|
+
`--edit` updates question metadata and returns a container URL for inspection.
|
|
192
|
+
Code changes still go through the pack workflow: edit locally, validate, and
|
|
193
|
+
publish a new version.
|
|
177
194
|
|
|
178
195
|
## Development
|
|
179
196
|
|
|
@@ -12,6 +12,7 @@ import sys
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from . import __version__
|
|
15
|
+
from .commands import catalog as catalog_command
|
|
15
16
|
from .commands import example as example_command
|
|
16
17
|
from .commands import lint as lint_command
|
|
17
18
|
from .commands import login as login_command
|
|
@@ -59,6 +60,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
59
60
|
metavar="PACK",
|
|
60
61
|
help="Validate a pack in the runner, then publish it to your company.",
|
|
61
62
|
)
|
|
63
|
+
actions.add_argument(
|
|
64
|
+
"--list",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="List questions that exist for your company.",
|
|
67
|
+
)
|
|
68
|
+
actions.add_argument(
|
|
69
|
+
"--edit",
|
|
70
|
+
type=int,
|
|
71
|
+
metavar="CHALLENGE_ID",
|
|
72
|
+
help="Edit question metadata and open its container preview.",
|
|
73
|
+
)
|
|
62
74
|
actions.add_argument(
|
|
63
75
|
"--install",
|
|
64
76
|
choices=INSTALLABLES,
|
|
@@ -99,7 +111,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
99
111
|
"--open",
|
|
100
112
|
action="store_true",
|
|
101
113
|
dest="open_browser",
|
|
102
|
-
help="With --example, open the container in your browser.",
|
|
114
|
+
help="With --example or --edit, open the container in your browser.",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument("--title", help="With --edit, update the question title.")
|
|
117
|
+
parser.add_argument("--description", help="With --edit, update the question description.")
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--status",
|
|
120
|
+
choices=("draft", "published"),
|
|
121
|
+
help="With --edit, update whether the question is draft or published.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument("--difficulty", type=int, choices=(1, 2, 3), help="With --edit, set difficulty.")
|
|
124
|
+
parser.add_argument("--max-time", type=int, help="With --edit, set time limit in minutes.")
|
|
125
|
+
parser.add_argument("--max-attempt", type=int, help="With --edit, set maximum attempts.")
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--tech-stack",
|
|
128
|
+
help="With --edit, comma-separated technologies, for example 'Python,FastAPI'.",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--ai-enabled",
|
|
132
|
+
action=argparse.BooleanOptionalAction,
|
|
133
|
+
default=None,
|
|
134
|
+
help="With --edit, toggle AI assistance.",
|
|
103
135
|
)
|
|
104
136
|
|
|
105
137
|
subparsers = parser.add_subparsers(dest="command")
|
|
@@ -245,6 +277,22 @@ def _handle_new(args: argparse.Namespace) -> int:
|
|
|
245
277
|
return 0
|
|
246
278
|
|
|
247
279
|
|
|
280
|
+
def _edit_updates(args: argparse.Namespace) -> dict:
|
|
281
|
+
tech_stack = None
|
|
282
|
+
if args.tech_stack is not None:
|
|
283
|
+
tech_stack = [item.strip() for item in args.tech_stack.split(",") if item.strip()]
|
|
284
|
+
return {
|
|
285
|
+
"challenge_name": args.title,
|
|
286
|
+
"description": args.description,
|
|
287
|
+
"status": args.status,
|
|
288
|
+
"difficulty": args.difficulty,
|
|
289
|
+
"max_time": args.max_time,
|
|
290
|
+
"max_attempt": args.max_attempt,
|
|
291
|
+
"tech_stack": tech_stack,
|
|
292
|
+
"ai_enabled": args.ai_enabled,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
248
296
|
def _handle_install(target: str, force: bool) -> int:
|
|
249
297
|
if target == "claude-plugin":
|
|
250
298
|
result = installer.install(Path.cwd(), force=force)
|
|
@@ -269,6 +317,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
269
317
|
live=args.live,
|
|
270
318
|
validation_run_id=args.validation_run_id,
|
|
271
319
|
)
|
|
320
|
+
if args.list:
|
|
321
|
+
return catalog_command.list_questions(as_json=args.json)
|
|
322
|
+
if args.edit is not None:
|
|
323
|
+
return catalog_command.edit_question(
|
|
324
|
+
challenge_id=args.edit,
|
|
325
|
+
updates=_edit_updates(args),
|
|
326
|
+
open_browser=args.open_browser,
|
|
327
|
+
)
|
|
272
328
|
if args.install:
|
|
273
329
|
return _handle_install(args.install, args.force)
|
|
274
330
|
if args.example:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Company challenge catalog commands for the public authoring API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import webbrowser
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..errors import PraxisError
|
|
10
|
+
from ..execution.remote.client import ApiClient
|
|
11
|
+
from ..execution.remote.config import RemoteConfig
|
|
12
|
+
|
|
13
|
+
EXIT_OK = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_questions(client: ApiClient | None = None, as_json: bool = False) -> int:
|
|
17
|
+
client = client or ApiClient(RemoteConfig.resolve())
|
|
18
|
+
payload = client.get("/challenges")
|
|
19
|
+
challenges = payload.get("challenges") or []
|
|
20
|
+
|
|
21
|
+
if as_json:
|
|
22
|
+
print(json.dumps({"challenges": challenges}, indent=2))
|
|
23
|
+
return EXIT_OK
|
|
24
|
+
|
|
25
|
+
if not challenges:
|
|
26
|
+
print("No questions found for this company.")
|
|
27
|
+
return EXIT_OK
|
|
28
|
+
|
|
29
|
+
for challenge in challenges:
|
|
30
|
+
_print_challenge(challenge)
|
|
31
|
+
return EXIT_OK
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def edit_question(
|
|
35
|
+
challenge_id: int,
|
|
36
|
+
updates: dict[str, Any] | None = None,
|
|
37
|
+
client: ApiClient | None = None,
|
|
38
|
+
open_browser: bool = False,
|
|
39
|
+
) -> int:
|
|
40
|
+
client = client or ApiClient(RemoteConfig.resolve())
|
|
41
|
+
updates = {key: value for key, value in (updates or {}).items() if value is not None}
|
|
42
|
+
|
|
43
|
+
if updates:
|
|
44
|
+
payload = client.patch_json(f"/challenges/{challenge_id}", updates)
|
|
45
|
+
challenge = payload.get("challenge") or {}
|
|
46
|
+
updated_id = challenge.get("challenge_id") or challenge_id
|
|
47
|
+
updated_name = challenge.get("challenge_name") or ""
|
|
48
|
+
print(f"Updated question {updated_id}: {updated_name}")
|
|
49
|
+
else:
|
|
50
|
+
print(f"Opening question {challenge_id} in a container...")
|
|
51
|
+
|
|
52
|
+
session = client.post_json(f"/challenges/{challenge_id}/setup-codebase", {})
|
|
53
|
+
url = session.get("container_url")
|
|
54
|
+
if not url:
|
|
55
|
+
raise PraxisError("The platform prepared the question but returned no container URL.")
|
|
56
|
+
|
|
57
|
+
print(f"\n {url}\n")
|
|
58
|
+
print(
|
|
59
|
+
"Use the container to inspect or adjust the question. "
|
|
60
|
+
"Publish a new pack version when code changes are ready."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if open_browser:
|
|
64
|
+
webbrowser.open(url)
|
|
65
|
+
|
|
66
|
+
return EXIT_OK
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _print_challenge(challenge: dict[str, Any]) -> None:
|
|
70
|
+
challenge_id = challenge.get("challenge_id")
|
|
71
|
+
name = challenge.get("challenge_name") or "(untitled)"
|
|
72
|
+
status = challenge.get("status") or "unknown"
|
|
73
|
+
version = challenge.get("challenge_version_id")
|
|
74
|
+
updated = challenge.get("updated_at") or challenge.get("created_at")
|
|
75
|
+
|
|
76
|
+
line = f"{challenge_id}\t{name}\t{status}"
|
|
77
|
+
if version:
|
|
78
|
+
line += f"\tv{version}"
|
|
79
|
+
if updated:
|
|
80
|
+
line += f"\t{updated}"
|
|
81
|
+
print(line)
|
|
@@ -84,11 +84,17 @@ def run(
|
|
|
84
84
|
|
|
85
85
|
challenge_id = payload.get("challenge_id")
|
|
86
86
|
version_id = payload.get("challenge_version_id")
|
|
87
|
+
container_url = payload.get("container_url")
|
|
88
|
+
preview_error = payload.get("preview_error")
|
|
87
89
|
state = "published" if live else "draft"
|
|
88
90
|
|
|
89
91
|
print(f"{pack.name} {state} for {company}")
|
|
90
92
|
if challenge_id:
|
|
91
93
|
print(f" challenge {challenge_id}, version {version_id}")
|
|
94
|
+
if container_url:
|
|
95
|
+
print(f" {container_url}")
|
|
96
|
+
elif preview_error:
|
|
97
|
+
print(f" Preview container unavailable: {preview_error}")
|
|
92
98
|
if not live:
|
|
93
99
|
print(" Publish it to candidates from the dashboard when you are ready.")
|
|
94
100
|
return EXIT_OK
|
|
@@ -11,6 +11,8 @@ Endpoints used, served by ``api_authoring_controller`` under ``/api/public``
|
|
|
11
11
|
``POST /validation-runs`` -> submit a bundle, returns a run id
|
|
12
12
|
``GET /validation-runs/{id}`` -> poll status + result
|
|
13
13
|
``POST /challenges`` -> publish a validated pack
|
|
14
|
+
``GET /challenges`` -> list company challenges
|
|
15
|
+
``PATCH /challenges/{id}`` -> edit challenge metadata
|
|
14
16
|
|
|
15
17
|
All four require an API key holding the ``challenges:write`` scope, except
|
|
16
18
|
``/me`` which only requires a valid key.
|
|
@@ -115,6 +117,14 @@ class ApiClient:
|
|
|
115
117
|
content_type="application/json",
|
|
116
118
|
)
|
|
117
119
|
|
|
120
|
+
def patch_json(self, path: str, payload: dict) -> dict[str, Any]:
|
|
121
|
+
return self.request(
|
|
122
|
+
"PATCH",
|
|
123
|
+
path,
|
|
124
|
+
body=json.dumps(payload).encode("utf-8"),
|
|
125
|
+
content_type="application/json",
|
|
126
|
+
)
|
|
127
|
+
|
|
118
128
|
def post_bytes(self, path: str, blob: bytes) -> dict[str, Any]:
|
|
119
129
|
return self.request("POST", path, body=blob, content_type="application/zip")
|
|
120
130
|
|
|
@@ -108,12 +108,16 @@ def describe(result: InstallResult) -> str:
|
|
|
108
108
|
"",
|
|
109
109
|
"Enable it in Claude Code:",
|
|
110
110
|
"",
|
|
111
|
-
|
|
111
|
+
# The absolute path is deliberate. `/plugin marketplace add` reads a
|
|
112
|
+
# bare `foo/bar` as a GitHub owner/repo shorthand and tries to clone
|
|
113
|
+
# it, so a relative path here fails with "Repository not found". A
|
|
114
|
+
# leading slash cannot be parsed that way.
|
|
115
|
+
f" /plugin marketplace add {result.root}",
|
|
112
116
|
f" /plugin install {PLUGIN_NAME}@{MARKETPLACE_NAME}",
|
|
113
117
|
"",
|
|
114
118
|
"Then:",
|
|
115
119
|
"",
|
|
116
|
-
" /codepraxis:new
|
|
120
|
+
" /codepraxis:new scaffold a pack from a description",
|
|
117
121
|
" /codepraxis:validate validate a pack and fix what fails",
|
|
118
122
|
"",
|
|
119
123
|
"The pack-authoring skill loads automatically when Claude touches a pack.",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Design, build, validate, publish, and preview a CodePraxis question
|
|
3
|
+
argument-hint: "<what the challenge should test>"
|
|
4
|
+
allowed-tools: Bash(codepraxis validate:*), Bash(codepraxis --publish:*), Read, Write, Edit, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Create and publish a new CodePraxis question for: **$ARGUMENTS**
|
|
8
|
+
|
|
9
|
+
Build it in this order. Do not skip the design step, and do not claim the
|
|
10
|
+
question is done until publish returns a URL.
|
|
11
|
+
|
|
12
|
+
1. **Decide the question architecture.** Write down what skill this tests, the
|
|
13
|
+
expected task shape, the test cases you will need, and roughly how much code
|
|
14
|
+
the candidate should write. Prefer a realistic engineering task with one
|
|
15
|
+
obvious entry point, concrete sample input, and an output contract a test can
|
|
16
|
+
check automatically. Not a toy exercise, not a puzzle.
|
|
17
|
+
|
|
18
|
+
2. **Implement the pack** under `challenges/<slug>/`:
|
|
19
|
+
- `metadata.json` — a unique snake_case `name`
|
|
20
|
+
- `backend.conf` — `BACKEND` and `LANGUAGE`
|
|
21
|
+
- `source/` — the minimal stub the grader calls, plus a `README.md` saying
|
|
22
|
+
what is broken and how to run it. No working implementation.
|
|
23
|
+
- `._tests/test_1.py` — the `testCases` class
|
|
24
|
+
- `._course_data/course_toc.json` and `feature.md` — the candidate brief
|
|
25
|
+
- `setup.sh` at the pack root if dependencies are needed
|
|
26
|
+
|
|
27
|
+
3. **Write the reference solution** in `challenges/<slug>/../solution/` — a
|
|
28
|
+
sibling of the pack, never inside it.
|
|
29
|
+
|
|
30
|
+
4. **Validate without asking the user to choose tiers.** Run
|
|
31
|
+
`codepraxis validate --local <slug>` and iterate until the solution passes
|
|
32
|
+
everything and the starter passes nothing. Then publish with
|
|
33
|
+
`codepraxis --publish <slug>`; the CLI runs the authoritative remote
|
|
34
|
+
validation before it publishes.
|
|
35
|
+
|
|
36
|
+
5. **Publish and return the URL.** When `codepraxis --publish <slug>` succeeds,
|
|
37
|
+
copy the container URL from its output into your final answer so the user can
|
|
38
|
+
open the question and inspect it.
|
|
39
|
+
|
|
40
|
+
Consult the `pack-authoring` skill for the `testCases` contract. The two
|
|
41
|
+
mistakes worth checking before you finish: a starter that already passes, and a
|
|
42
|
+
two-argument `__init__`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from codepraxis.commands import catalog
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeClient:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self.calls = []
|
|
9
|
+
|
|
10
|
+
def get(self, path: str) -> dict:
|
|
11
|
+
self.calls.append(("GET", path))
|
|
12
|
+
return {
|
|
13
|
+
"challenges": [
|
|
14
|
+
{
|
|
15
|
+
"challenge_id": 7,
|
|
16
|
+
"challenge_name": "Debug webhooks",
|
|
17
|
+
"status": "draft",
|
|
18
|
+
"challenge_version_id": 11,
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def patch_json(self, path: str, payload: dict) -> dict:
|
|
24
|
+
self.calls.append(("PATCH", path, payload))
|
|
25
|
+
return {"challenge": {"challenge_id": 7, "challenge_name": payload["challenge_name"]}}
|
|
26
|
+
|
|
27
|
+
def post_json(self, path: str, payload: dict) -> dict:
|
|
28
|
+
self.calls.append(("POST", path, payload))
|
|
29
|
+
return {"container_url": "https://container.example/?folder=/home/praxis/debug_webhooks"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_lists_company_questions(capsys):
|
|
33
|
+
client = FakeClient()
|
|
34
|
+
|
|
35
|
+
assert catalog.list_questions(client=client) == 0
|
|
36
|
+
|
|
37
|
+
assert client.calls == [("GET", "/challenges")]
|
|
38
|
+
output = capsys.readouterr().out
|
|
39
|
+
assert "Debug webhooks" in output
|
|
40
|
+
assert "draft" in output
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_edit_updates_metadata_and_prints_container_url(capsys):
|
|
44
|
+
client = FakeClient()
|
|
45
|
+
|
|
46
|
+
assert catalog.edit_question(
|
|
47
|
+
challenge_id=7,
|
|
48
|
+
updates={"challenge_name": "Debug queues", "description": None},
|
|
49
|
+
client=client,
|
|
50
|
+
) == 0
|
|
51
|
+
|
|
52
|
+
assert client.calls == [
|
|
53
|
+
("PATCH", "/challenges/7", {"challenge_name": "Debug queues"}),
|
|
54
|
+
("POST", "/challenges/7/setup-codebase", {}),
|
|
55
|
+
]
|
|
56
|
+
output = capsys.readouterr().out
|
|
57
|
+
assert "Updated question 7" in output
|
|
58
|
+
assert "https://container.example" in output
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_edit_can_open_without_metadata_changes(capsys):
|
|
62
|
+
client = FakeClient()
|
|
63
|
+
|
|
64
|
+
assert catalog.edit_question(challenge_id=7, updates={}, client=client) == 0
|
|
65
|
+
|
|
66
|
+
assert client.calls == [("POST", "/challenges/7/setup-codebase", {})]
|
|
67
|
+
assert "Opening question 7" in capsys.readouterr().out
|
|
@@ -203,3 +203,27 @@ def test_live_publishes_immediately(tmp_path: Path):
|
|
|
203
203
|
)
|
|
204
204
|
|
|
205
205
|
assert client.posted[0][1]["status"] == "published"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_prints_container_url_when_platform_returns_one(tmp_path: Path, capsys):
|
|
209
|
+
build_pack(tmp_path)
|
|
210
|
+
|
|
211
|
+
class PreviewClient(FakeClient):
|
|
212
|
+
def post_json(self, path: str, payload: dict) -> dict:
|
|
213
|
+
self.posted.append((path, payload))
|
|
214
|
+
return {
|
|
215
|
+
"challenge_id": 7,
|
|
216
|
+
"challenge_version_id": 9,
|
|
217
|
+
"container_url": "https://container.example/?folder=/home/praxis/demo",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
publish.run(
|
|
221
|
+
root=tmp_path,
|
|
222
|
+
selector="demo",
|
|
223
|
+
reporter=NullReporter(),
|
|
224
|
+
assume_yes=True,
|
|
225
|
+
validation_run_id="vr_1",
|
|
226
|
+
client=PreviewClient(),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
assert "https://container.example" in capsys.readouterr().out
|
|
@@ -219,3 +219,33 @@ class TestExampleCommand:
|
|
|
219
219
|
monkeypatch.delenv("CODEPRAXIS_TOKEN", raising=False)
|
|
220
220
|
|
|
221
221
|
assert RemoteConfig.resolve_public().token == ""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TestPluginInstructions:
|
|
225
|
+
"""`/plugin marketplace add` reads a bare `foo/bar` as a GitHub repo.
|
|
226
|
+
|
|
227
|
+
A relative path there fails with "Repository not found", so the printed
|
|
228
|
+
command must be one that cannot be parsed as owner/repo.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def test_marketplace_command_uses_an_absolute_path(self, tmp_path, monkeypatch):
|
|
232
|
+
from codepraxis.plugin import installer
|
|
233
|
+
|
|
234
|
+
monkeypatch.chdir(tmp_path)
|
|
235
|
+
result = installer.install(tmp_path)
|
|
236
|
+
line = next(
|
|
237
|
+
text for text in installer.describe(result).splitlines() if "marketplace add" in text
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
path = line.split("marketplace add", 1)[1].strip()
|
|
241
|
+
assert path.startswith("/"), f"{path!r} would be read as a GitHub owner/repo"
|
|
242
|
+
assert Path(path).is_dir()
|
|
243
|
+
|
|
244
|
+
def test_the_marketplace_manifest_is_where_the_command_points(self, tmp_path, monkeypatch):
|
|
245
|
+
from codepraxis.plugin import installer
|
|
246
|
+
|
|
247
|
+
monkeypatch.chdir(tmp_path)
|
|
248
|
+
result = installer.install(tmp_path)
|
|
249
|
+
|
|
250
|
+
assert (result.root / ".claude-plugin" / "marketplace.json").is_file()
|
|
251
|
+
assert (result.root / "codepraxis" / ".claude-plugin" / "plugin.json").is_file()
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Scaffold a new CodePraxis challenge pack from a description
|
|
3
|
-
argument-hint: "<what the challenge should test>"
|
|
4
|
-
allowed-tools: Bash(codepraxis validate:*), Read, Write, Edit, Glob
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Create a new CodePraxis challenge pack for: **$ARGUMENTS**
|
|
8
|
-
|
|
9
|
-
Build it in this order, and validate before you claim it is done.
|
|
10
|
-
|
|
11
|
-
1. **Decide the shape.** A realistic engineering task with one obvious entry
|
|
12
|
-
point, concrete sample input, and an output contract a test can check
|
|
13
|
-
automatically. Not a toy exercise, not a puzzle.
|
|
14
|
-
|
|
15
|
-
2. **Write the pack** under `challenges/<slug>/`:
|
|
16
|
-
- `metadata.json` — a unique snake_case `name`
|
|
17
|
-
- `backend.conf` — `BACKEND` and `LANGUAGE`
|
|
18
|
-
- `source/` — the minimal stub the grader calls, plus a `README.md` saying
|
|
19
|
-
what is broken and how to run it. No working implementation.
|
|
20
|
-
- `._tests/test_1.py` — the `testCases` class
|
|
21
|
-
- `._course_data/course_toc.json` and `feature.md` — the candidate brief
|
|
22
|
-
- `setup.sh` at the pack root if dependencies are needed
|
|
23
|
-
|
|
24
|
-
3. **Write the reference solution** in `challenges/<slug>/../solution/` — a
|
|
25
|
-
sibling of the pack, never inside it.
|
|
26
|
-
|
|
27
|
-
4. **Validate**: `codepraxis validate --local <slug>`. Iterate until the
|
|
28
|
-
solution passes everything and the starter passes nothing.
|
|
29
|
-
|
|
30
|
-
Consult the `pack-authoring` skill for the `testCases` contract. The two
|
|
31
|
-
mistakes worth checking before you finish: a starter that already passes, and a
|
|
32
|
-
two-argument `__init__`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/skills/pack-authoring/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|