@przeprogramowani/10x-cli 0.2.0 → 0.3.0
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.
- package/README.md +46 -1
- package/dist/index.mjs +128 -6
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -28,9 +28,40 @@ npm install -g @przeprogramowani/10x-cli
|
|
|
28
28
|
|---------|-------------|
|
|
29
29
|
| `10x auth` | Magic-link login with your Circle-registered email |
|
|
30
30
|
| `10x list` | Browse modules and lessons in your course |
|
|
31
|
-
| `10x get <ref>` | Fetch a lesson and apply artifacts to
|
|
31
|
+
| `10x get <ref>` | Fetch a lesson and apply artifacts to your workspace |
|
|
32
32
|
| `10x doctor` | Diagnose auth, API connectivity, and local config |
|
|
33
33
|
|
|
34
|
+
### `10x get` Flags
|
|
35
|
+
|
|
36
|
+
| Flag | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `--tool <tool>` | AI coding tool: `claude-code`, `cursor`, `copilot`, `codex`, `generic` |
|
|
39
|
+
| `--print` | Output artifact content to stdout instead of writing files |
|
|
40
|
+
| `--type <type>` | Filter by artifact type: `skills`, `prompts`, `rules`, `configs` |
|
|
41
|
+
| `--name <name>` | Filter by artifact name (requires `--type`) |
|
|
42
|
+
| `--dry-run` | Show what would be written without touching the filesystem |
|
|
43
|
+
| `--course <slug>` | Override the course slug (default: `10xdevs3`) |
|
|
44
|
+
|
|
45
|
+
#### Examples
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Fetch full lesson — writes skills, prompts, rules, configs
|
|
49
|
+
10x get m1l1
|
|
50
|
+
|
|
51
|
+
# Write only skills (skip prompts, rules, configs)
|
|
52
|
+
10x get m1l1 --type skills
|
|
53
|
+
|
|
54
|
+
# Write a single artifact
|
|
55
|
+
10x get m1l1 --type skills --name code-review
|
|
56
|
+
|
|
57
|
+
# Print to stdout (pipe-friendly)
|
|
58
|
+
10x get m1l1 --print --type skills --name code-review
|
|
59
|
+
10x get m1l1 --print --type skills --name code-review | pbcopy
|
|
60
|
+
|
|
61
|
+
# Use with a different AI coding tool
|
|
62
|
+
10x get m1l1 --tool cursor
|
|
63
|
+
```
|
|
64
|
+
|
|
34
65
|
### Global Flags
|
|
35
66
|
|
|
36
67
|
- `--json` — Machine-readable JSON output (auto-detected when piped)
|
|
@@ -45,6 +76,20 @@ Lessons are referenced by module and lesson number:
|
|
|
45
76
|
- `m1l1` — Module 1, Lesson 1
|
|
46
77
|
- `m2l3` — Module 2, Lesson 3
|
|
47
78
|
|
|
79
|
+
### Multi-Tool Support
|
|
80
|
+
|
|
81
|
+
On first run, the CLI prompts you to choose your AI coding tool. Artifacts are written to the correct directory for your tool:
|
|
82
|
+
|
|
83
|
+
| Tool | Directory | Rules file |
|
|
84
|
+
|------|-----------|------------|
|
|
85
|
+
| Claude Code | `.claude/` | `CLAUDE.md` |
|
|
86
|
+
| Cursor | `.cursor/` | `.cursor/rules/10x-course.mdc` |
|
|
87
|
+
| GitHub Copilot | `.github/` | `.github/copilot-instructions.md` |
|
|
88
|
+
| Codex CLI | `.agents/` | `AGENTS.md` |
|
|
89
|
+
| Generic | `.ai/` | `AGENTS.md` |
|
|
90
|
+
|
|
91
|
+
Override anytime with `--tool <name>`. Your choice is saved in `~/.config/10x-cli/config.json`.
|
|
92
|
+
|
|
48
93
|
## Development
|
|
49
94
|
|
|
50
95
|
```bash
|
package/dist/index.mjs
CHANGED
|
@@ -2271,8 +2271,16 @@ var cac = (name = "") => new CAC(name);
|
|
|
2271
2271
|
// package.json
|
|
2272
2272
|
var package_default = {
|
|
2273
2273
|
name: "@przeprogramowani/10x-cli",
|
|
2274
|
-
version: "0.
|
|
2274
|
+
version: "0.3.0",
|
|
2275
2275
|
description: "Open-source CLI for 10xDevs course content",
|
|
2276
|
+
repository: {
|
|
2277
|
+
type: "git",
|
|
2278
|
+
url: "https://github.com/przeprogramowani/10x-cli.git"
|
|
2279
|
+
},
|
|
2280
|
+
homepage: "https://github.com/przeprogramowani/10x-cli",
|
|
2281
|
+
bugs: {
|
|
2282
|
+
url: "https://github.com/przeprogramowani/10x-cli/issues"
|
|
2283
|
+
},
|
|
2276
2284
|
type: "module",
|
|
2277
2285
|
bin: {
|
|
2278
2286
|
"10x": "./dist/index.mjs"
|
|
@@ -3518,6 +3526,41 @@ async function fetchLesson(course, lessonId, token, options = {}) {
|
|
|
3518
3526
|
}
|
|
3519
3527
|
return result;
|
|
3520
3528
|
}
|
|
3529
|
+
async function fetchArtifact(course, lessonId, type, name, tool, token, options = {}) {
|
|
3530
|
+
const params = new URLSearchParams({ tool });
|
|
3531
|
+
const path = `/api/artifacts/${encodeURIComponent(course)}/${encodeURIComponent(lessonId)}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?${params}`;
|
|
3532
|
+
const result = await apiGet(path, { token, signal: options.signal });
|
|
3533
|
+
if (!result.ok)
|
|
3534
|
+
return result;
|
|
3535
|
+
const signature = result.responseHeaders.get("X-Bundle-Signature");
|
|
3536
|
+
const keyIdRaw = result.responseHeaders.get("X-Bundle-Key-Id");
|
|
3537
|
+
if (signature && keyIdRaw) {
|
|
3538
|
+
const keyId = Number(keyIdRaw);
|
|
3539
|
+
const responseBody = JSON.stringify(result.data);
|
|
3540
|
+
try {
|
|
3541
|
+
verifyBundleSignature(responseBody, signature, keyId);
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3544
|
+
return {
|
|
3545
|
+
ok: false,
|
|
3546
|
+
status: 0,
|
|
3547
|
+
code: err instanceof SignatureError ? "signature_error" : "signature_internal_error",
|
|
3548
|
+
error: message
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
} else if (REQUIRE_SIGNATURES) {
|
|
3552
|
+
return {
|
|
3553
|
+
ok: false,
|
|
3554
|
+
status: 0,
|
|
3555
|
+
code: "signature_missing",
|
|
3556
|
+
error: "Artifact is missing a signature. The API may be misconfigured or compromised. " + "Do NOT use the content. Report this to the course team."
|
|
3557
|
+
};
|
|
3558
|
+
} else {
|
|
3559
|
+
process.stderr.write(`Warning: artifact is not signed. Signature verification skipped.
|
|
3560
|
+
`);
|
|
3561
|
+
}
|
|
3562
|
+
return result;
|
|
3563
|
+
}
|
|
3521
3564
|
async function fetchHealth(options = {}) {
|
|
3522
3565
|
const timeoutMs = options.timeoutMs ?? 5000;
|
|
3523
3566
|
const controller = new AbortController;
|
|
@@ -4023,6 +4066,7 @@ var CLI_VERSION = package_default.version;
|
|
|
4023
4066
|
var DEFAULT_COURSE = "10xdevs3";
|
|
4024
4067
|
function applyBundle(bundle, projectRoot, options = {}) {
|
|
4025
4068
|
const dryRun = options.dryRun === true;
|
|
4069
|
+
const partial = options.partial === true;
|
|
4026
4070
|
const course = options.course ?? DEFAULT_COURSE;
|
|
4027
4071
|
const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
|
|
4028
4072
|
const manifestDir = join4(projectRoot, profile.manifestDir);
|
|
@@ -4078,7 +4122,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
|
|
|
4078
4122
|
}
|
|
4079
4123
|
return { name: config.name, path: target, action };
|
|
4080
4124
|
});
|
|
4081
|
-
if (!dryRun) {
|
|
4125
|
+
if (!dryRun && !partial) {
|
|
4082
4126
|
const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
|
|
4083
4127
|
for (const entry of removed.skills)
|
|
4084
4128
|
rmSync2(entry.path, { recursive: true, force: true });
|
|
@@ -4087,7 +4131,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
|
|
|
4087
4131
|
for (const entry of removed.configs)
|
|
4088
4132
|
rmSync2(entry.path, { force: true });
|
|
4089
4133
|
}
|
|
4090
|
-
if (!dryRun) {
|
|
4134
|
+
if (!dryRun && !partial) {
|
|
4091
4135
|
const nextManifest = {
|
|
4092
4136
|
package: CLI_PACKAGE_NAME,
|
|
4093
4137
|
version: CLI_VERSION,
|
|
@@ -4213,9 +4257,13 @@ function isSafeName(name) {
|
|
|
4213
4257
|
}
|
|
4214
4258
|
|
|
4215
4259
|
// src/commands/get.ts
|
|
4260
|
+
var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
|
|
4261
|
+
function isArtifactType(s) {
|
|
4262
|
+
return ARTIFACT_TYPES.includes(s);
|
|
4263
|
+
}
|
|
4216
4264
|
var DEFAULT_COURSE2 = "10xdevs3";
|
|
4217
4265
|
function registerGetCommand(cli) {
|
|
4218
|
-
cli.command("get <ref>", "Fetch and apply a lesson pack").option("--dry-run", "Show what would be written without touching the filesystem").option("--course <course>", "Override the course slug (default: 10xdevs3)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, generic)").action(async (ref, options) => {
|
|
4266
|
+
cli.command("get <ref>", "Fetch and apply a lesson pack").option("--dry-run", "Show what would be written without touching the filesystem").option("--course <course>", "Override the course slug (default: 10xdevs3)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, generic)").option("--print", "Print artifact content to stdout instead of writing to files").option("--type <type>", "Artifact type filter: skills, prompts, rules, configs").option("--name <name>", "Artifact name filter (requires --type)").action(async (ref, options) => {
|
|
4219
4267
|
const ctx = resolveContext(options);
|
|
4220
4268
|
await runGet(ctx, ref, options);
|
|
4221
4269
|
});
|
|
@@ -4225,9 +4273,19 @@ async function runGet(ctx, ref, options) {
|
|
|
4225
4273
|
if (!parsed) {
|
|
4226
4274
|
outputError(ctx, "invalid_lesson_ref", `'${ref}' is not a valid lesson reference.`, ExitCodes.USAGE, "Use the form 'm<module>l<lesson>', for example 'm1l1' or 'm2l3'.");
|
|
4227
4275
|
}
|
|
4276
|
+
if (options.name && !options.type) {
|
|
4277
|
+
outputError(ctx, "missing_type", "Specify --type when using --name.", ExitCodes.USAGE, "Usage: 10x get m1l1 --type skills --name code-review");
|
|
4278
|
+
}
|
|
4279
|
+
if (options.type && !isArtifactType(options.type)) {
|
|
4280
|
+
outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
|
|
4281
|
+
}
|
|
4228
4282
|
const auth = await requireAuth(ctx);
|
|
4229
4283
|
const course = options.course ?? DEFAULT_COURSE2;
|
|
4230
4284
|
const profile = await resolveToolProfile(options.tool);
|
|
4285
|
+
if (options.print) {
|
|
4286
|
+
await runPrintMode(ctx, parsed.lessonId, course, profile, auth.access_token, options);
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4231
4289
|
verbose(ctx, `fetching lesson ${course}/${parsed.lessonId}`);
|
|
4232
4290
|
const result = await fetchLesson(course, parsed.lessonId, auth.access_token);
|
|
4233
4291
|
if (!result.ok) {
|
|
@@ -4236,13 +4294,77 @@ async function runGet(ctx, ref, options) {
|
|
|
4236
4294
|
const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
|
|
4237
4295
|
if (orphanWarning)
|
|
4238
4296
|
verbose(ctx, orphanWarning);
|
|
4239
|
-
const
|
|
4297
|
+
const isFiltered = options.type !== undefined;
|
|
4298
|
+
const bundle = filterBundle(ctx, result.data, options);
|
|
4240
4299
|
const writeResult = applyBundle(bundle, process.cwd(), {
|
|
4241
4300
|
dryRun: options.dryRun === true,
|
|
4242
|
-
profile
|
|
4301
|
+
profile,
|
|
4302
|
+
partial: isFiltered
|
|
4243
4303
|
});
|
|
4244
4304
|
renderGetResult(ctx, bundle, writeResult, options.dryRun === true, profile);
|
|
4245
4305
|
}
|
|
4306
|
+
function filterBundle(ctx, bundle, options) {
|
|
4307
|
+
if (!options.type) {
|
|
4308
|
+
if (options.name) {
|
|
4309
|
+
outputError(ctx, "missing_type", "Specify --type when using --name.", ExitCodes.USAGE, "Usage: 10x get m1l1 --type skills --name code-review");
|
|
4310
|
+
}
|
|
4311
|
+
return bundle;
|
|
4312
|
+
}
|
|
4313
|
+
if (!isArtifactType(options.type)) {
|
|
4314
|
+
outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
|
|
4315
|
+
}
|
|
4316
|
+
const type = options.type;
|
|
4317
|
+
const empty = {
|
|
4318
|
+
...bundle,
|
|
4319
|
+
skills: [],
|
|
4320
|
+
prompts: [],
|
|
4321
|
+
rules: [],
|
|
4322
|
+
configs: []
|
|
4323
|
+
};
|
|
4324
|
+
if (options.name) {
|
|
4325
|
+
const match = bundle[type].find((a2) => a2.name === options.name);
|
|
4326
|
+
if (!match) {
|
|
4327
|
+
outputError(ctx, "artifact_not_found", `No ${type} artifact named '${options.name}' in ${bundle.lessonId}.`, ExitCodes.NOT_FOUND, `Available ${type}: ${bundle[type].map((a2) => a2.name).join(", ") || "(none)"}`);
|
|
4328
|
+
}
|
|
4329
|
+
return { ...empty, [type]: [match] };
|
|
4330
|
+
}
|
|
4331
|
+
return { ...empty, [type]: bundle[type] };
|
|
4332
|
+
}
|
|
4333
|
+
async function runPrintMode(ctx, lessonId, course, profile, token, options) {
|
|
4334
|
+
if (!options.type) {
|
|
4335
|
+
outputError(ctx, "missing_type", "Specify --type for --print mode.", ExitCodes.USAGE, "Usage: 10x get m1l1 --print --type skills [--name code-review]");
|
|
4336
|
+
}
|
|
4337
|
+
if (!isArtifactType(options.type)) {
|
|
4338
|
+
outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
|
|
4339
|
+
}
|
|
4340
|
+
if (options.name) {
|
|
4341
|
+
verbose(ctx, `fetching artifact ${course}/${lessonId}/${options.type}/${options.name}`);
|
|
4342
|
+
const result = await fetchArtifact(course, lessonId, options.type, options.name, profile.toolId, token);
|
|
4343
|
+
if (!result.ok) {
|
|
4344
|
+
handleLessonError(ctx, result.status, result.code, result.error, result.payload);
|
|
4345
|
+
}
|
|
4346
|
+
if (ctx.json) {
|
|
4347
|
+
output(ctx, "", result.data);
|
|
4348
|
+
} else {
|
|
4349
|
+
process.stdout.write(result.data.content);
|
|
4350
|
+
}
|
|
4351
|
+
} else {
|
|
4352
|
+
verbose(ctx, `fetching lesson ${course}/${lessonId} (filtering by ${options.type})`);
|
|
4353
|
+
const result = await fetchLesson(course, lessonId, token);
|
|
4354
|
+
if (!result.ok) {
|
|
4355
|
+
handleLessonError(ctx, result.status, result.code, result.error, result.payload);
|
|
4356
|
+
}
|
|
4357
|
+
const artifacts = result.data[options.type];
|
|
4358
|
+
if (ctx.json) {
|
|
4359
|
+
output(ctx, "", artifacts);
|
|
4360
|
+
} else {
|
|
4361
|
+
const contents = artifacts.map((a2) => a2.content);
|
|
4362
|
+
process.stdout.write(contents.join(`
|
|
4363
|
+
---
|
|
4364
|
+
`));
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4246
4368
|
function handleLessonError(ctx, status, code, error, payload) {
|
|
4247
4369
|
verbose(ctx, `lesson fetch failed: status=${status} code=${code}`);
|
|
4248
4370
|
if (status === 403) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@przeprogramowani/10x-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Open-source CLI for 10xDevs course content",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/przeprogramowani/10x-cli.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/przeprogramowani/10x-cli",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/przeprogramowani/10x-cli/issues"
|
|
12
|
+
},
|
|
5
13
|
"type": "module",
|
|
6
14
|
"bin": {
|
|
7
15
|
"10x": "./dist/index.mjs"
|