codepraxis 0.3.1__tar.gz → 0.3.3__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.1 → codepraxis-0.3.3}/PKG-INFO +45 -2
- {codepraxis-0.3.1 → codepraxis-0.3.3}/README.md +44 -1
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/__init__.py +1 -1
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/cli.py +74 -1
- codepraxis-0.3.3/src/codepraxis/commands/catalog.py +114 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/commands/publish.py +29 -14
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/remote/client.py +13 -0
- codepraxis-0.3.3/src/codepraxis/plugin/templates/commands/new.md +42 -0
- codepraxis-0.3.3/tests/test_catalog.py +67 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/tests/test_publish.py +114 -0
- codepraxis-0.3.1/src/codepraxis/plugin/templates/commands/new.md +0 -32
- {codepraxis-0.3.1 → codepraxis-0.3.3}/.gitignore +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/CONTRIBUTING.md +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/RELEASING.md +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/pyproject.toml +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/__main__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/commands/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/commands/example.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/commands/lint.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/commands/login.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/commands/validate.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/domain/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/domain/contract.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/domain/pack.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/domain/results.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/errors.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/executor.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/local/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/local/backends.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/local/executor.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/local/worker.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/local/workspace.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/remote/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/remote/config.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/execution/remote/executor.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/packio/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/packio/archive.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/packio/discovery.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/packio/loader.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/packio/toc.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/plugin/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/plugin/installer.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/plugin/templates/commands/validate.md +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/plugin/templates/marketplace.json +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/plugin/templates/plugin.json +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/plugin/templates/skills/pack-authoring/SKILL.md +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/reporting/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/reporting/human.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/reporting/json_reporter.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/reporting/reporter.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/generator.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/README.md +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/backend.conf +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/course_toc.json +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/feature.md +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/main.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/metadata.json +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/solution.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/scaffold/templates/test_1.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/registry.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/rule.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/rules/__init__.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/rules/hygiene.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/rules/instructions.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/src/codepraxis/validation/rules/testcases.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/tests/conformance/test_corpus.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/tests/conftest.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/tests/test_classification.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/tests/test_toc.py +0 -0
- {codepraxis-0.3.1 → codepraxis-0.3.3}/tests/test_validation_rules.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.3
|
|
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,9 @@ 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
|
|
40
|
+
codepraxis --publish my-challenge --challenge-id 123 # publish an edit, not a duplicate
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
## What this is
|
|
@@ -153,7 +156,7 @@ codepraxis --install claude-plugin
|
|
|
153
156
|
Writes a local plugin into `.codepraxis/claude-plugin/`, then tells you the two
|
|
154
157
|
commands to enable it. You get:
|
|
155
158
|
|
|
156
|
-
- **`/codepraxis:new`** —
|
|
159
|
+
- **`/codepraxis:new`** — design, build, validate, publish and return a URL
|
|
157
160
|
- **`/codepraxis:validate`** — validate and fix what fails, in a loop
|
|
158
161
|
- **`pack-authoring` skill** — loads automatically when Claude touches a pack,
|
|
159
162
|
so it already knows the `testCases` contract and the two-fixture rule
|
|
@@ -199,6 +202,46 @@ assigned to candidates immediately:
|
|
|
199
202
|
publish into someone else's catalog.
|
|
200
203
|
|
|
201
204
|
You'll be shown the company and asked to confirm before anything is created.
|
|
205
|
+
When the platform can start a preview container, `--publish` prints that URL so
|
|
206
|
+
you can inspect the question immediately.
|
|
207
|
+
|
|
208
|
+
## Company catalog
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
codepraxis --list
|
|
212
|
+
codepraxis --edit 123 --title "Debug webhook delivery" --status draft
|
|
213
|
+
codepraxis --edit 123 --open
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`--list` shows the questions owned by your API key's company, including drafts.
|
|
217
|
+
`--edit` updates question metadata and returns a container URL for inspection.
|
|
218
|
+
|
|
219
|
+
### Publishing an edit
|
|
220
|
+
|
|
221
|
+
Changing the *content* of a question means re-publishing the pack. Pass the
|
|
222
|
+
question's id, or you get a second copy:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
codepraxis --publish my-challenge --challenge-id 123
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
With `--challenge-id` the platform adds a new version to that question and
|
|
229
|
+
keeps its id, assignments and history. Without it, publish always creates a
|
|
230
|
+
new question — which is the right behaviour the first time and a duplicate
|
|
231
|
+
every time after.
|
|
232
|
+
|
|
233
|
+
### Deleting
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
codepraxis --delete 123
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Asks first, and the platform refuses outright once the question has been
|
|
240
|
+
assigned to anyone — deleting it then would orphan attempts and reports that
|
|
241
|
+
candidates and reviewers still depend on. To take an assigned question out of
|
|
242
|
+
circulation, set it back to draft with `--edit 123 --status draft`.
|
|
243
|
+
Code changes still go through the pack workflow: edit locally, validate, and
|
|
244
|
+
publish a new version.
|
|
202
245
|
|
|
203
246
|
## Development
|
|
204
247
|
|
|
@@ -10,6 +10,9 @@ 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
|
|
15
|
+
codepraxis --publish my-challenge --challenge-id 123 # publish an edit, not a duplicate
|
|
13
16
|
```
|
|
14
17
|
|
|
15
18
|
## What this is
|
|
@@ -128,7 +131,7 @@ codepraxis --install claude-plugin
|
|
|
128
131
|
Writes a local plugin into `.codepraxis/claude-plugin/`, then tells you the two
|
|
129
132
|
commands to enable it. You get:
|
|
130
133
|
|
|
131
|
-
- **`/codepraxis:new`** —
|
|
134
|
+
- **`/codepraxis:new`** — design, build, validate, publish and return a URL
|
|
132
135
|
- **`/codepraxis:validate`** — validate and fix what fails, in a loop
|
|
133
136
|
- **`pack-authoring` skill** — loads automatically when Claude touches a pack,
|
|
134
137
|
so it already knows the `testCases` contract and the two-fixture rule
|
|
@@ -174,6 +177,46 @@ assigned to candidates immediately:
|
|
|
174
177
|
publish into someone else's catalog.
|
|
175
178
|
|
|
176
179
|
You'll be shown the company and asked to confirm before anything is created.
|
|
180
|
+
When the platform can start a preview container, `--publish` prints that URL so
|
|
181
|
+
you can inspect the question immediately.
|
|
182
|
+
|
|
183
|
+
## Company catalog
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
codepraxis --list
|
|
187
|
+
codepraxis --edit 123 --title "Debug webhook delivery" --status draft
|
|
188
|
+
codepraxis --edit 123 --open
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`--list` shows the questions owned by your API key's company, including drafts.
|
|
192
|
+
`--edit` updates question metadata and returns a container URL for inspection.
|
|
193
|
+
|
|
194
|
+
### Publishing an edit
|
|
195
|
+
|
|
196
|
+
Changing the *content* of a question means re-publishing the pack. Pass the
|
|
197
|
+
question's id, or you get a second copy:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
codepraxis --publish my-challenge --challenge-id 123
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
With `--challenge-id` the platform adds a new version to that question and
|
|
204
|
+
keeps its id, assignments and history. Without it, publish always creates a
|
|
205
|
+
new question — which is the right behaviour the first time and a duplicate
|
|
206
|
+
every time after.
|
|
207
|
+
|
|
208
|
+
### Deleting
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
codepraxis --delete 123
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Asks first, and the platform refuses outright once the question has been
|
|
215
|
+
assigned to anyone — deleting it then would orphan attempts and reports that
|
|
216
|
+
candidates and reviewers still depend on. To take an assigned question out of
|
|
217
|
+
circulation, set it back to draft with `--edit 123 --status draft`.
|
|
218
|
+
Code changes still go through the pack workflow: edit locally, validate, and
|
|
219
|
+
publish a new version.
|
|
177
220
|
|
|
178
221
|
## Development
|
|
179
222
|
|
|
@@ -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,23 @@ 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
|
+
)
|
|
74
|
+
actions.add_argument(
|
|
75
|
+
"--delete",
|
|
76
|
+
type=int,
|
|
77
|
+
metavar="CHALLENGE_ID",
|
|
78
|
+
help="Delete a question your company owns. Refused once it has been assigned.",
|
|
79
|
+
)
|
|
62
80
|
actions.add_argument(
|
|
63
81
|
"--install",
|
|
64
82
|
choices=INSTALLABLES,
|
|
@@ -84,6 +102,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
84
102
|
action="store_true",
|
|
85
103
|
help="With --publish, publish straight to candidates instead of as a draft.",
|
|
86
104
|
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--challenge-id",
|
|
107
|
+
type=int,
|
|
108
|
+
help=(
|
|
109
|
+
"With --publish, publish a new version of this existing question "
|
|
110
|
+
"instead of creating another one. Get the id from --list."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
87
113
|
parser.add_argument(
|
|
88
114
|
"--validation-run-id",
|
|
89
115
|
help="With --publish, reuse a passing validation run instead of running a new one.",
|
|
@@ -99,7 +125,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
99
125
|
"--open",
|
|
100
126
|
action="store_true",
|
|
101
127
|
dest="open_browser",
|
|
102
|
-
help="With --example, open the container in your browser.",
|
|
128
|
+
help="With --example or --edit, open the container in your browser.",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument("--title", help="With --edit, update the question title.")
|
|
131
|
+
parser.add_argument("--description", help="With --edit, update the question description.")
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--status",
|
|
134
|
+
choices=("draft", "published"),
|
|
135
|
+
help="With --edit, update whether the question is draft or published.",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument("--difficulty", type=int, choices=(1, 2, 3), help="With --edit, set difficulty.")
|
|
138
|
+
parser.add_argument("--max-time", type=int, help="With --edit, set time limit in minutes.")
|
|
139
|
+
parser.add_argument("--max-attempt", type=int, help="With --edit, set maximum attempts.")
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
"--tech-stack",
|
|
142
|
+
help="With --edit, comma-separated technologies, for example 'Python,FastAPI'.",
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--ai-enabled",
|
|
146
|
+
action=argparse.BooleanOptionalAction,
|
|
147
|
+
default=None,
|
|
148
|
+
help="With --edit, toggle AI assistance.",
|
|
103
149
|
)
|
|
104
150
|
|
|
105
151
|
subparsers = parser.add_subparsers(dest="command")
|
|
@@ -245,6 +291,22 @@ def _handle_new(args: argparse.Namespace) -> int:
|
|
|
245
291
|
return 0
|
|
246
292
|
|
|
247
293
|
|
|
294
|
+
def _edit_updates(args: argparse.Namespace) -> dict:
|
|
295
|
+
tech_stack = None
|
|
296
|
+
if args.tech_stack is not None:
|
|
297
|
+
tech_stack = [item.strip() for item in args.tech_stack.split(",") if item.strip()]
|
|
298
|
+
return {
|
|
299
|
+
"challenge_name": args.title,
|
|
300
|
+
"description": args.description,
|
|
301
|
+
"status": args.status,
|
|
302
|
+
"difficulty": args.difficulty,
|
|
303
|
+
"max_time": args.max_time,
|
|
304
|
+
"max_attempt": args.max_attempt,
|
|
305
|
+
"tech_stack": tech_stack,
|
|
306
|
+
"ai_enabled": args.ai_enabled,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
248
310
|
def _handle_install(target: str, force: bool) -> int:
|
|
249
311
|
if target == "claude-plugin":
|
|
250
312
|
result = installer.install(Path.cwd(), force=force)
|
|
@@ -268,7 +330,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
268
330
|
assume_yes=args.yes,
|
|
269
331
|
live=args.live,
|
|
270
332
|
validation_run_id=args.validation_run_id,
|
|
333
|
+
challenge_id=args.challenge_id,
|
|
334
|
+
)
|
|
335
|
+
if args.list:
|
|
336
|
+
return catalog_command.list_questions(as_json=args.json)
|
|
337
|
+
if args.edit is not None:
|
|
338
|
+
return catalog_command.edit_question(
|
|
339
|
+
challenge_id=args.edit,
|
|
340
|
+
updates=_edit_updates(args),
|
|
341
|
+
open_browser=args.open_browser,
|
|
271
342
|
)
|
|
343
|
+
if args.delete is not None:
|
|
344
|
+
return catalog_command.delete_question(args.delete, assume_yes=args.yes)
|
|
272
345
|
if args.install:
|
|
273
346
|
return _handle_install(args.install, args.force)
|
|
274
347
|
if args.example:
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
EXIT_ABORTED = 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_questions(client: ApiClient | None = None, as_json: bool = False) -> int:
|
|
18
|
+
client = client or ApiClient(RemoteConfig.resolve())
|
|
19
|
+
payload = client.get("/challenges")
|
|
20
|
+
challenges = payload.get("challenges") or []
|
|
21
|
+
|
|
22
|
+
if as_json:
|
|
23
|
+
print(json.dumps({"challenges": challenges}, indent=2))
|
|
24
|
+
return EXIT_OK
|
|
25
|
+
|
|
26
|
+
if not challenges:
|
|
27
|
+
print("No questions found for this company.")
|
|
28
|
+
return EXIT_OK
|
|
29
|
+
|
|
30
|
+
for challenge in challenges:
|
|
31
|
+
_print_challenge(challenge)
|
|
32
|
+
return EXIT_OK
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def edit_question(
|
|
36
|
+
challenge_id: int,
|
|
37
|
+
updates: dict[str, Any] | None = None,
|
|
38
|
+
client: ApiClient | None = None,
|
|
39
|
+
open_browser: bool = False,
|
|
40
|
+
) -> int:
|
|
41
|
+
client = client or ApiClient(RemoteConfig.resolve())
|
|
42
|
+
updates = {key: value for key, value in (updates or {}).items() if value is not None}
|
|
43
|
+
|
|
44
|
+
if updates:
|
|
45
|
+
payload = client.patch_json(f"/challenges/{challenge_id}", updates)
|
|
46
|
+
challenge = payload.get("challenge") or {}
|
|
47
|
+
updated_id = challenge.get("challenge_id") or challenge_id
|
|
48
|
+
updated_name = challenge.get("challenge_name") or ""
|
|
49
|
+
print(f"Updated question {updated_id}: {updated_name}")
|
|
50
|
+
else:
|
|
51
|
+
print(f"Opening question {challenge_id} in a container...")
|
|
52
|
+
|
|
53
|
+
session = client.post_json(f"/challenges/{challenge_id}/setup-codebase", {})
|
|
54
|
+
url = session.get("container_url")
|
|
55
|
+
if not url:
|
|
56
|
+
raise PraxisError("The platform prepared the question but returned no container URL.")
|
|
57
|
+
|
|
58
|
+
print(f"\n {url}\n")
|
|
59
|
+
print(
|
|
60
|
+
"Use the container to inspect or adjust the question. "
|
|
61
|
+
"Publish a new pack version when code changes are ready."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if open_browser:
|
|
65
|
+
webbrowser.open(url)
|
|
66
|
+
|
|
67
|
+
return EXIT_OK
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _print_challenge(challenge: dict[str, Any]) -> None:
|
|
71
|
+
challenge_id = challenge.get("challenge_id")
|
|
72
|
+
name = challenge.get("challenge_name") or "(untitled)"
|
|
73
|
+
status = challenge.get("status") or "unknown"
|
|
74
|
+
version = challenge.get("challenge_version_id")
|
|
75
|
+
updated = challenge.get("updated_at") or challenge.get("created_at")
|
|
76
|
+
|
|
77
|
+
line = f"{challenge_id}\t{name}\t{status}"
|
|
78
|
+
if version:
|
|
79
|
+
line += f"\tv{version}"
|
|
80
|
+
if updated:
|
|
81
|
+
line += f"\t{updated}"
|
|
82
|
+
print(line)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def delete_question(
|
|
86
|
+
challenge_id: int,
|
|
87
|
+
client: ApiClient | None = None,
|
|
88
|
+
assume_yes: bool = False,
|
|
89
|
+
) -> int:
|
|
90
|
+
"""Delete a question the company owns.
|
|
91
|
+
|
|
92
|
+
Confirmed by default: unlike a draft, this is not recoverable from the
|
|
93
|
+
dashboard. The platform refuses outright once the question has been assigned
|
|
94
|
+
to anyone, so the destructive case this guards is an unassigned question the
|
|
95
|
+
author still wanted.
|
|
96
|
+
"""
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
client = client or ApiClient(RemoteConfig.resolve())
|
|
100
|
+
|
|
101
|
+
if not assume_yes:
|
|
102
|
+
if not sys.stdin.isatty():
|
|
103
|
+
raise PraxisError(
|
|
104
|
+
"Deleting needs confirmation, but stdin is not a terminal. Pass --yes."
|
|
105
|
+
)
|
|
106
|
+
print(f"Permanently delete question {challenge_id}? This cannot be undone.")
|
|
107
|
+
if input("Continue? [y/N] ").strip().lower() not in {"y", "yes"}:
|
|
108
|
+
print("Aborted.", file=sys.stderr)
|
|
109
|
+
return EXIT_ABORTED
|
|
110
|
+
|
|
111
|
+
payload = client.delete(f"/challenges/{challenge_id}")
|
|
112
|
+
removed = payload.get("versions_removed", 0)
|
|
113
|
+
print(f"Deleted question {challenge_id} ({removed} version(s) removed).")
|
|
114
|
+
return EXIT_OK
|
|
@@ -40,6 +40,7 @@ def run(
|
|
|
40
40
|
assume_yes: bool = False,
|
|
41
41
|
live: bool = False,
|
|
42
42
|
validation_run_id: str | None = None,
|
|
43
|
+
challenge_id: int | None = None,
|
|
43
44
|
client: ApiClient | None = None,
|
|
44
45
|
) -> int:
|
|
45
46
|
if not selector:
|
|
@@ -68,27 +69,40 @@ def run(
|
|
|
68
69
|
run_id = _validate(pack, client, reporter)
|
|
69
70
|
|
|
70
71
|
company = _company_name(client, config)
|
|
71
|
-
if not _confirm(pack.name, company, live, assume_yes):
|
|
72
|
+
if not _confirm(pack.name, company, live, assume_yes, challenge_id):
|
|
72
73
|
print("Aborted.", file=sys.stderr)
|
|
73
74
|
return EXIT_ABORTED
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
body = {
|
|
77
|
+
# No company_id: the server derives ownership from the API key.
|
|
78
|
+
"name": pack.name,
|
|
79
|
+
"validation_run_id": run_id,
|
|
80
|
+
"status": "published" if live else "draft",
|
|
81
|
+
}
|
|
82
|
+
# With an id, this publishes a new version of an existing question. Without
|
|
83
|
+
# one the platform creates a new question — which is why editing a pack and
|
|
84
|
+
# re-publishing used to leave a duplicate behind.
|
|
85
|
+
if challenge_id is not None:
|
|
86
|
+
body["challenge_id"] = challenge_id
|
|
87
|
+
|
|
88
|
+
payload = client.post_json("/challenges", body)
|
|
84
89
|
|
|
85
90
|
challenge_id = payload.get("challenge_id")
|
|
86
91
|
version_id = payload.get("challenge_version_id")
|
|
92
|
+
container_url = payload.get("container_url")
|
|
93
|
+
preview_error = payload.get("preview_error")
|
|
87
94
|
state = "published" if live else "draft"
|
|
95
|
+
created = payload.get("created", True)
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
verb = "published" if created else "updated"
|
|
98
|
+
print(f"{pack.name} {verb} for {company} ({state})")
|
|
90
99
|
if challenge_id:
|
|
91
|
-
|
|
100
|
+
label = "challenge" if created else "challenge (new version of)"
|
|
101
|
+
print(f" {label} {challenge_id}, version {version_id}")
|
|
102
|
+
if container_url:
|
|
103
|
+
print(f" {container_url}")
|
|
104
|
+
elif preview_error:
|
|
105
|
+
print(f" Preview container unavailable: {preview_error}")
|
|
92
106
|
if not live:
|
|
93
107
|
print(" Publish it to candidates from the dashboard when you are ready.")
|
|
94
108
|
return EXIT_OK
|
|
@@ -123,7 +137,7 @@ def _company_name(client: ApiClient, config: RemoteConfig | None) -> str:
|
|
|
123
137
|
return company.get("name") or cached or "your company"
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def _confirm(pack_name: str, company: str, live: bool, assume_yes: bool) -> bool:
|
|
140
|
+
def _confirm(pack_name: str, company: str, live: bool, assume_yes: bool, challenge_id: int | None = None) -> bool:
|
|
127
141
|
if assume_yes:
|
|
128
142
|
return True
|
|
129
143
|
if not sys.stdin.isatty():
|
|
@@ -132,5 +146,6 @@ def _confirm(pack_name: str, company: str, live: bool, assume_yes: bool) -> bool
|
|
|
132
146
|
"Pass --yes to publish non-interactively (CI)."
|
|
133
147
|
)
|
|
134
148
|
visibility = "LIVE to candidates" if live else "as a draft"
|
|
135
|
-
|
|
149
|
+
target = f"as a new version of question {challenge_id}" if challenge_id else "as a new question"
|
|
150
|
+
print(f"\nAbout to publish {pack_name} to {company} {target}, {visibility}.")
|
|
136
151
|
return input("Continue? [y/N] ").strip().lower() in {"y", "yes"}
|
|
@@ -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,17 @@ 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
|
+
|
|
128
|
+
def delete(self, path: str) -> dict[str, Any]:
|
|
129
|
+
return self.request("DELETE", path)
|
|
130
|
+
|
|
118
131
|
def post_bytes(self, path: str, blob: bytes) -> dict[str, Any]:
|
|
119
132
|
return self.request("POST", path, body=blob, content_type="application/zip")
|
|
120
133
|
|
|
@@ -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,117 @@ 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
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestRepublishingAnEdit:
|
|
233
|
+
"""Editing a pack and publishing again must update, not duplicate.
|
|
234
|
+
|
|
235
|
+
Without a challenge_id the platform creates a new question every time, which
|
|
236
|
+
is how a typo fix used to leave two copies in the catalog.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def test_challenge_id_is_sent_when_given(self, tmp_path):
|
|
240
|
+
build_pack(tmp_path)
|
|
241
|
+
client = FakeClient()
|
|
242
|
+
|
|
243
|
+
publish.run(
|
|
244
|
+
root=tmp_path,
|
|
245
|
+
selector="demo",
|
|
246
|
+
reporter=NullReporter(),
|
|
247
|
+
assume_yes=True,
|
|
248
|
+
validation_run_id="vr_1",
|
|
249
|
+
challenge_id=64,
|
|
250
|
+
client=client,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assert client.posted[0][1]["challenge_id"] == 64
|
|
254
|
+
|
|
255
|
+
def test_no_challenge_id_means_create(self, tmp_path):
|
|
256
|
+
build_pack(tmp_path)
|
|
257
|
+
client = FakeClient()
|
|
258
|
+
|
|
259
|
+
publish.run(
|
|
260
|
+
root=tmp_path,
|
|
261
|
+
selector="demo",
|
|
262
|
+
reporter=NullReporter(),
|
|
263
|
+
assume_yes=True,
|
|
264
|
+
validation_run_id="vr_1",
|
|
265
|
+
client=client,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert "challenge_id" not in client.posted[0][1]
|
|
269
|
+
|
|
270
|
+
def test_output_says_updated_when_the_platform_reused_a_question(self, tmp_path, capsys):
|
|
271
|
+
build_pack(tmp_path)
|
|
272
|
+
client = FakeClient()
|
|
273
|
+
client.post_json = lambda path, payload: {
|
|
274
|
+
"challenge_id": 64,
|
|
275
|
+
"challenge_version_id": 91,
|
|
276
|
+
"created": False,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
publish.run(
|
|
280
|
+
root=tmp_path,
|
|
281
|
+
selector="demo",
|
|
282
|
+
reporter=NullReporter(),
|
|
283
|
+
assume_yes=True,
|
|
284
|
+
validation_run_id="vr_1",
|
|
285
|
+
challenge_id=64,
|
|
286
|
+
client=client,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
output = capsys.readouterr().out
|
|
290
|
+
assert "updated" in output
|
|
291
|
+
assert "published for" not in output
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TestDelete:
|
|
295
|
+
class Client:
|
|
296
|
+
def __init__(self, payload=None):
|
|
297
|
+
self.deleted = []
|
|
298
|
+
self.payload = payload or {"challenge_id": 7, "versions_removed": 2}
|
|
299
|
+
|
|
300
|
+
def delete(self, path):
|
|
301
|
+
self.deleted.append(path)
|
|
302
|
+
return self.payload
|
|
303
|
+
|
|
304
|
+
def test_deletes_after_confirmation_is_waived(self, capsys):
|
|
305
|
+
from codepraxis.commands import catalog
|
|
306
|
+
|
|
307
|
+
client = self.Client()
|
|
308
|
+
assert catalog.delete_question(7, client=client, assume_yes=True) == 0
|
|
309
|
+
assert client.deleted == ["/challenges/7"]
|
|
310
|
+
assert "Deleted question 7" in capsys.readouterr().out
|
|
311
|
+
|
|
312
|
+
def test_refuses_without_a_terminal_and_without_yes(self):
|
|
313
|
+
from codepraxis.commands import catalog
|
|
314
|
+
from codepraxis.errors import PraxisError
|
|
315
|
+
|
|
316
|
+
client = self.Client()
|
|
317
|
+
with pytest.raises(PraxisError, match="not a terminal"):
|
|
318
|
+
catalog.delete_question(7, client=client)
|
|
319
|
+
assert client.deleted == []
|
|
@@ -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
|
|
File without changes
|
{codepraxis-0.3.1 → codepraxis-0.3.3}/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
|
|
File without changes
|