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.
Files changed (73) hide show
  1. {codepraxis-0.3.0 → codepraxis-0.3.2}/PKG-INFO +19 -2
  2. {codepraxis-0.3.0 → codepraxis-0.3.2}/README.md +18 -1
  3. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/__init__.py +1 -1
  4. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/cli.py +57 -1
  5. codepraxis-0.3.2/src/codepraxis/commands/catalog.py +81 -0
  6. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/publish.py +6 -0
  7. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/client.py +10 -0
  8. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/installer.py +6 -2
  9. codepraxis-0.3.2/src/codepraxis/plugin/templates/commands/new.md +42 -0
  10. codepraxis-0.3.2/tests/test_catalog.py +67 -0
  11. {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_publish.py +24 -0
  12. {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_validation_rules.py +30 -0
  13. codepraxis-0.3.0/src/codepraxis/plugin/templates/commands/new.md +0 -32
  14. {codepraxis-0.3.0 → codepraxis-0.3.2}/.gitignore +0 -0
  15. {codepraxis-0.3.0 → codepraxis-0.3.2}/CONTRIBUTING.md +0 -0
  16. {codepraxis-0.3.0 → codepraxis-0.3.2}/RELEASING.md +0 -0
  17. {codepraxis-0.3.0 → codepraxis-0.3.2}/pyproject.toml +0 -0
  18. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/__main__.py +0 -0
  19. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/__init__.py +0 -0
  20. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/example.py +0 -0
  21. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/lint.py +0 -0
  22. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/login.py +0 -0
  23. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/commands/validate.py +0 -0
  24. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/__init__.py +0 -0
  25. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/contract.py +0 -0
  26. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/pack.py +0 -0
  27. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/domain/results.py +0 -0
  28. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/errors.py +0 -0
  29. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/__init__.py +0 -0
  30. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/executor.py +0 -0
  31. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/__init__.py +0 -0
  32. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/backends.py +0 -0
  33. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/executor.py +0 -0
  34. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/worker.py +0 -0
  35. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/local/workspace.py +0 -0
  36. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/__init__.py +0 -0
  37. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/config.py +0 -0
  38. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/execution/remote/executor.py +0 -0
  39. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/__init__.py +0 -0
  40. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/archive.py +0 -0
  41. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/discovery.py +0 -0
  42. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/loader.py +0 -0
  43. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/packio/toc.py +0 -0
  44. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/__init__.py +0 -0
  45. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/commands/validate.md +0 -0
  46. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/marketplace.json +0 -0
  47. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/plugin.json +0 -0
  48. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/plugin/templates/skills/pack-authoring/SKILL.md +0 -0
  49. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/__init__.py +0 -0
  50. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/human.py +0 -0
  51. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/json_reporter.py +0 -0
  52. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/reporting/reporter.py +0 -0
  53. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/__init__.py +0 -0
  54. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/generator.py +0 -0
  55. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/README.md +0 -0
  56. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/backend.conf +0 -0
  57. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/course_toc.json +0 -0
  58. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/feature.md +0 -0
  59. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/main.py +0 -0
  60. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/metadata.json +0 -0
  61. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/solution.py +0 -0
  62. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/scaffold/templates/test_1.py +0 -0
  63. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/__init__.py +0 -0
  64. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/registry.py +0 -0
  65. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rule.py +0 -0
  66. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/__init__.py +0 -0
  67. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/hygiene.py +0 -0
  68. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/instructions.py +0 -0
  69. {codepraxis-0.3.0 → codepraxis-0.3.2}/src/codepraxis/validation/rules/testcases.py +0 -0
  70. {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/conformance/test_corpus.py +0 -0
  71. {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/conftest.py +0 -0
  72. {codepraxis-0.3.0 → codepraxis-0.3.2}/tests/test_classification.py +0 -0
  73. {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.0
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`** — scaffold a pack from a description
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`** — scaffold a pack from a description
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
 
@@ -1,3 +1,3 @@
1
1
  """Author and validate CodePraxis challenge packs."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
@@ -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
- f" /plugin marketplace add {relative}",
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 scaffold a pack from a description",
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