@przeprogramowani/10x-cli 0.6.0 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.mjs +196 -38
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -2271,7 +2271,7 @@ var cac = (name = "") => new CAC(name);
2271
2271
  // package.json
2272
2272
  var package_default = {
2273
2273
  name: "@przeprogramowani/10x-cli",
2274
- version: "0.6.0",
2274
+ version: "1.1.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -3949,6 +3949,7 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as rea
3949
3949
  import { dirname as dirname2, join as join3 } from "node:path";
3950
3950
  var MANIFEST_FILENAME = ".10x-cli-manifest.json";
3951
3951
  var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
3952
+ var MANIFEST_VERSION = 2;
3952
3953
  function readManifest(dir) {
3953
3954
  const manifestPath = join3(dir, MANIFEST_FILENAME);
3954
3955
  if (!existsSync3(manifestPath))
@@ -3986,6 +3987,8 @@ function isManifest(value) {
3986
3987
  return false;
3987
3988
  if (typeof v2["version"] !== "string")
3988
3989
  return false;
3990
+ if (v2["manifestVersion"] !== MANIFEST_VERSION)
3991
+ return false;
3989
3992
  if (typeof v2["lastApplied"] !== "string")
3990
3993
  return false;
3991
3994
  if (typeof v2["lessonId"] !== "string")
@@ -3996,7 +3999,19 @@ function isManifest(value) {
3996
3999
  if (typeof files !== "object" || files === null)
3997
4000
  return false;
3998
4001
  const f2 = files;
3999
- return isStringArray(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4002
+ return isSkillsRecord(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4003
+ }
4004
+ function isSkillsRecord(value) {
4005
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4006
+ return false;
4007
+ for (const entry of Object.values(value)) {
4008
+ if (typeof entry !== "object" || entry === null)
4009
+ return false;
4010
+ const e2 = entry;
4011
+ if (!isStringArray(e2["files"]))
4012
+ return false;
4013
+ }
4014
+ return true;
4000
4015
  }
4001
4016
  function isStringArray(value) {
4002
4017
  return Array.isArray(value) && value.every((item) => typeof item === "string");
@@ -4010,6 +4025,7 @@ var PROFILES = {
4010
4025
  toolId: "claude-code",
4011
4026
  displayName: "Claude Code",
4012
4027
  skillPath: (n) => `.claude/skills/${n}/SKILL.md`,
4028
+ skillDir: (n) => `.claude/skills/${n}`,
4013
4029
  promptPath: (n) => `.claude/prompts/${n}.md`,
4014
4030
  configPath: (n) => `.claude/config-templates/${n}`,
4015
4031
  rulesFile: "CLAUDE.md",
@@ -4021,6 +4037,7 @@ var PROFILES = {
4021
4037
  toolId: "cursor",
4022
4038
  displayName: "Cursor",
4023
4039
  skillPath: (n) => `.cursor/skills/${n}/SKILL.md`,
4040
+ skillDir: (n) => `.cursor/skills/${n}`,
4024
4041
  promptPath: (n) => `.cursor/prompts/${n}.md`,
4025
4042
  configPath: (n) => `.cursor/config-templates/${n}`,
4026
4043
  rulesFile: ".cursor/rules/10x-course.mdc",
@@ -4032,6 +4049,7 @@ var PROFILES = {
4032
4049
  toolId: "copilot",
4033
4050
  displayName: "GitHub Copilot",
4034
4051
  skillPath: (n) => `.github/skills/${n}/SKILL.md`,
4052
+ skillDir: (n) => `.github/skills/${n}`,
4035
4053
  promptPath: (n) => `.github/prompts/${n}.md`,
4036
4054
  configPath: (n) => `.github/config-templates/${n}`,
4037
4055
  rulesFile: ".github/copilot-instructions.md",
@@ -4043,6 +4061,7 @@ var PROFILES = {
4043
4061
  toolId: "codex",
4044
4062
  displayName: "Codex CLI",
4045
4063
  skillPath: (n) => `.agents/skills/${n}/SKILL.md`,
4064
+ skillDir: (n) => `.agents/skills/${n}`,
4046
4065
  promptPath: (n) => `.agents/prompts/${n}.md`,
4047
4066
  configPath: (n) => `.agents/config-templates/${n}`,
4048
4067
  rulesFile: "AGENTS.md",
@@ -4054,6 +4073,7 @@ var PROFILES = {
4054
4073
  toolId: "generic",
4055
4074
  displayName: "Other / Generic",
4056
4075
  skillPath: (n) => `.ai/skills/${n}/SKILL.md`,
4076
+ skillDir: (n) => `.ai/skills/${n}`,
4057
4077
  promptPath: (n) => `.ai/prompts/${n}.md`,
4058
4078
  configPath: (n) => `.ai/config-templates/${n}`,
4059
4079
  rulesFile: "AGENTS.md",
@@ -4157,7 +4177,7 @@ import {
4157
4177
  mkdirSync as mkdirSync5,
4158
4178
  readFileSync as readFileSync5,
4159
4179
  renameSync as renameSync2,
4160
- rmdirSync,
4180
+ rmdirSync as rmdirSync2,
4161
4181
  rmSync as rmSync3,
4162
4182
  statSync,
4163
4183
  writeFileSync as writeFileSync4
@@ -4243,9 +4263,12 @@ function stripBlock(content, begin, end, label, warnings) {
4243
4263
 
4244
4264
  // src/lib/writer.ts
4245
4265
  import {
4266
+ chmodSync as chmodSync2,
4246
4267
  existsSync as existsSync5,
4247
4268
  mkdirSync as mkdirSync4,
4248
4269
  readFileSync as readFileSync4,
4270
+ readdirSync,
4271
+ rmdirSync,
4249
4272
  rmSync as rmSync2,
4250
4273
  writeFileSync as writeFileSync3
4251
4274
  } from "node:fs";
@@ -4259,19 +4282,30 @@ function applyBundle(bundle, projectRoot, options = {}) {
4259
4282
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4260
4283
  const manifestDir = join5(projectRoot, profile.manifestDir);
4261
4284
  const prevManifest = readManifest(manifestDir);
4262
- for (const skill of bundle.skills)
4285
+ for (const skill of bundle.skills) {
4263
4286
  assertSafeName(skill.name, "skill");
4287
+ for (const file of skill.files) {
4288
+ assertSafeSkillFilePath(file.path, skill.name);
4289
+ }
4290
+ }
4264
4291
  for (const prompt of bundle.prompts)
4265
4292
  assertSafeName(prompt.name, "prompt");
4266
4293
  for (const config of bundle.configs)
4267
4294
  assertSafeName(config.name, "config");
4268
4295
  const skills = bundle.skills.map((skill) => {
4269
- const target = join5(projectRoot, profile.skillPath(skill.name));
4270
- const action = computeFileAction(target, skill.content);
4271
- if (!dryRun && action !== "unchanged") {
4272
- writeFileAt(target, skill.content);
4273
- }
4274
- return { name: skill.name, path: target, action };
4296
+ const skillDir = join5(projectRoot, profile.skillDir(skill.name));
4297
+ const fileWrites = skill.files.map((file) => {
4298
+ const target = join5(skillDir, file.path);
4299
+ const action = computeFileAction(target, file.content);
4300
+ if (!dryRun && action !== "unchanged") {
4301
+ writeFileAt(target, file.content);
4302
+ }
4303
+ if (!dryRun && file.executable === true && action !== "unchanged") {
4304
+ chmodSync2(target, 493);
4305
+ }
4306
+ return { path: file.path, absolutePath: target, action };
4307
+ });
4308
+ return { name: skill.name, files: fileWrites };
4275
4309
  });
4276
4310
  const prompts = bundle.prompts.map((prompt) => {
4277
4311
  const target = join5(projectRoot, profile.promptPath(prompt.name));
@@ -4312,8 +4346,12 @@ function applyBundle(bundle, projectRoot, options = {}) {
4312
4346
  });
4313
4347
  if (!dryRun && !partial) {
4314
4348
  const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4315
- for (const entry of removed.skills)
4349
+ for (const entry of removed.skillDirs)
4316
4350
  rmSync2(entry.path, { recursive: true, force: true });
4351
+ for (const entry of removed.skillFiles) {
4352
+ rmSync2(entry.path, { force: true });
4353
+ removeEmptyParentDirs(entry.path, entry.skillDirAbs);
4354
+ }
4317
4355
  for (const entry of removed.prompts)
4318
4356
  rmSync2(entry.path, { force: true });
4319
4357
  for (const entry of removed.configs)
@@ -4323,12 +4361,13 @@ function applyBundle(bundle, projectRoot, options = {}) {
4323
4361
  const nextManifest = {
4324
4362
  package: CLI_PACKAGE_NAME,
4325
4363
  version: CLI_VERSION,
4364
+ manifestVersion: MANIFEST_VERSION,
4326
4365
  lastApplied: new Date().toISOString(),
4327
4366
  lessonId: bundle.lessonId,
4328
4367
  course,
4329
4368
  tool: profile.toolId,
4330
4369
  files: {
4331
- skills: bundle.skills.map((s) => s.name),
4370
+ skills: Object.fromEntries(bundle.skills.map((s) => [s.name, { files: s.files.map((f2) => f2.path) }])),
4332
4371
  prompts: bundle.prompts.map((p2) => `${p2.name}.md`),
4333
4372
  configs: bundle.configs.map((c2) => c2.name)
4334
4373
  }
@@ -4367,20 +4406,43 @@ function writeFileAt(filePath, content) {
4367
4406
  writeFileSync3(filePath, content);
4368
4407
  }
4369
4408
  function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4370
- const empty = { skills: [], prompts: [], configs: [] };
4409
+ const empty = {
4410
+ skillDirs: [],
4411
+ skillFiles: [],
4412
+ prompts: [],
4413
+ configs: []
4414
+ };
4371
4415
  if (!prevManifest)
4372
4416
  return empty;
4373
- const currentSkills = new Set(bundle.skills.map((s) => s.name));
4417
+ const currentSkills = new Map(bundle.skills.map((s) => [s.name, new Set(s.files.map((f2) => f2.path))]));
4374
4418
  const currentPrompts = new Set(bundle.prompts.map((p2) => `${p2.name}.md`));
4375
4419
  const currentConfigs = new Set(bundle.configs.map((c2) => c2.name));
4376
- const removed = { skills: [], prompts: [], configs: [] };
4377
- for (const skillName of prevManifest.files.skills) {
4378
- if (currentSkills.has(skillName))
4379
- continue;
4420
+ const removed = {
4421
+ skillDirs: [],
4422
+ skillFiles: [],
4423
+ prompts: [],
4424
+ configs: []
4425
+ };
4426
+ for (const [skillName, entry] of Object.entries(prevManifest.files.skills)) {
4380
4427
  if (!isSafeName(skillName))
4381
4428
  continue;
4382
- const skillTarget = join5(projectRoot, profile.skillPath(skillName));
4383
- removed.skills.push({ name: skillName, path: dirname3(skillTarget) });
4429
+ const skillDirAbs = join5(projectRoot, profile.skillDir(skillName));
4430
+ if (!currentSkills.has(skillName)) {
4431
+ removed.skillDirs.push({ name: skillName, path: skillDirAbs });
4432
+ continue;
4433
+ }
4434
+ const currentFiles = currentSkills.get(skillName);
4435
+ for (const relPath of entry.files) {
4436
+ if (currentFiles.has(relPath))
4437
+ continue;
4438
+ if (!isSafeSkillFilePath(relPath))
4439
+ continue;
4440
+ removed.skillFiles.push({
4441
+ name: `${skillName}/${relPath}`,
4442
+ path: join5(skillDirAbs, relPath),
4443
+ skillDirAbs
4444
+ });
4445
+ }
4384
4446
  }
4385
4447
  for (const promptFile of prevManifest.files.prompts) {
4386
4448
  if (currentPrompts.has(promptFile))
@@ -4405,6 +4467,19 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4405
4467
  }
4406
4468
  return removed;
4407
4469
  }
4470
+ function removeEmptyParentDirs(filePath, skillDirAbs) {
4471
+ let dir = dirname3(filePath);
4472
+ while (dir.startsWith(skillDirAbs) && dir !== skillDirAbs) {
4473
+ try {
4474
+ if (readdirSync(dir).length > 0)
4475
+ return;
4476
+ rmdirSync(dir);
4477
+ } catch {
4478
+ return;
4479
+ }
4480
+ dir = dirname3(dir);
4481
+ }
4482
+ }
4408
4483
  function findOrphanedManifests(projectRoot, currentProfile) {
4409
4484
  const out = [];
4410
4485
  for (const profile of Object.values(PROFILES)) {
@@ -4433,6 +4508,29 @@ function assertSafeName(name, kind) {
4433
4508
  throw new Error(`refused to write unsafe ${kind} name ${JSON.stringify(name)} — must not be empty, contain path separators, or start with '.'`);
4434
4509
  }
4435
4510
  }
4511
+ function assertSafeSkillFilePath(relPath, skillName) {
4512
+ if (!isSafeSkillFilePath(relPath)) {
4513
+ throw new Error(`refused to write unsafe file path ${JSON.stringify(relPath)} inside skill ${JSON.stringify(skillName)}`);
4514
+ }
4515
+ }
4516
+ function isSafeSkillFilePath(relPath) {
4517
+ if (typeof relPath !== "string" || relPath.length === 0)
4518
+ return false;
4519
+ if (relPath.startsWith("/") || relPath.startsWith("\\"))
4520
+ return false;
4521
+ if (/^[a-zA-Z]:/.test(relPath))
4522
+ return false;
4523
+ const segments = relPath.split(/[/\\]/);
4524
+ if (segments.length === 0)
4525
+ return false;
4526
+ for (const seg of segments) {
4527
+ if (seg === "" || seg === "." || seg === "..")
4528
+ return false;
4529
+ if (!isSafeName(seg))
4530
+ return false;
4531
+ }
4532
+ return true;
4533
+ }
4436
4534
  function isSafeName(name) {
4437
4535
  if (typeof name !== "string" || name.length === 0)
4438
4536
  return false;
@@ -4464,16 +4562,30 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
4464
4562
  skipped: []
4465
4563
  };
4466
4564
  const oldProfile = orphan.profile;
4467
- for (const skillName of orphan.manifest.files.skills) {
4565
+ for (const [skillName, entry] of Object.entries(orphan.manifest.files.skills)) {
4468
4566
  if (!isSafeName(skillName)) {
4469
4567
  summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
4470
4568
  continue;
4471
4569
  }
4472
- const from = join6(projectRoot, oldProfile.skillPath(skillName));
4473
- const to = join6(projectRoot, newProfile.skillPath(skillName));
4474
- if (moveIfSafe(from, to, summary.skipped)) {
4475
- summary.movedOrRemoved.skills.push(skillName);
4570
+ const fromSkillDir = join6(projectRoot, oldProfile.skillDir(skillName));
4571
+ const toSkillDir = join6(projectRoot, newProfile.skillDir(skillName));
4572
+ let anyMoved = false;
4573
+ for (const relPath of entry.files) {
4574
+ if (!isSafeSkillFilePath(relPath)) {
4575
+ summary.skipped.push({
4576
+ path: `${skillName}/${relPath}`,
4577
+ reason: "unsafe path in manifest"
4578
+ });
4579
+ continue;
4580
+ }
4581
+ const from = join6(fromSkillDir, relPath);
4582
+ const to = join6(toSkillDir, relPath);
4583
+ if (moveIfSafe(from, to, summary.skipped))
4584
+ anyMoved = true;
4476
4585
  }
4586
+ tryRemoveEmptyDir(fromSkillDir);
4587
+ if (anyMoved)
4588
+ summary.movedOrRemoved.skills.push(skillName);
4477
4589
  }
4478
4590
  for (const promptFile of orphan.manifest.files.prompts) {
4479
4591
  if (!isSafeName(promptFile)) {
@@ -4512,15 +4624,23 @@ function deleteArtifacts(projectRoot, orphan) {
4512
4624
  skipped: []
4513
4625
  };
4514
4626
  const oldProfile = orphan.profile;
4515
- for (const skillName of orphan.manifest.files.skills) {
4627
+ for (const [skillName, entry] of Object.entries(orphan.manifest.files.skills)) {
4516
4628
  if (!isSafeName(skillName)) {
4517
4629
  summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
4518
4630
  continue;
4519
4631
  }
4520
- const skillFile = join6(projectRoot, oldProfile.skillPath(skillName));
4521
- const skillDir = dirname4(skillFile);
4522
- rmSync3(skillFile, { force: true });
4523
- tryRemoveEmptyDir(skillDir);
4632
+ const skillDir = join6(projectRoot, oldProfile.skillDir(skillName));
4633
+ for (const relPath of entry.files) {
4634
+ if (!isSafeSkillFilePath(relPath)) {
4635
+ summary.skipped.push({
4636
+ path: `${skillName}/${relPath}`,
4637
+ reason: "unsafe path in manifest"
4638
+ });
4639
+ continue;
4640
+ }
4641
+ rmSync3(join6(skillDir, relPath), { force: true });
4642
+ }
4643
+ rmSync3(skillDir, { recursive: true, force: true });
4524
4644
  summary.movedOrRemoved.skills.push(skillName);
4525
4645
  }
4526
4646
  for (const promptFile of orphan.manifest.files.prompts) {
@@ -4623,7 +4743,7 @@ function tryRemoveEmptyDir(dir) {
4623
4743
  if (!existsSync6(dir))
4624
4744
  return;
4625
4745
  try {
4626
- rmdirSync(dir);
4746
+ rmdirSync2(dir);
4627
4747
  } catch {}
4628
4748
  }
4629
4749
 
@@ -4853,7 +4973,14 @@ async function runPrintMode(ctx, lessonId, course, profile, token, lang, options
4853
4973
  if (ctx.json) {
4854
4974
  output(ctx, "", result.data);
4855
4975
  } else {
4856
- process.stdout.write(result.data.content);
4976
+ const data = result.data;
4977
+ if (data.type === "skills") {
4978
+ const skillMd = data.files.find((f2) => f2.path === "SKILL.md");
4979
+ process.stdout.write(skillMd?.content ?? "");
4980
+ emitMultiFileSkillNotice(data.name, data.files);
4981
+ } else {
4982
+ process.stdout.write(data.content);
4983
+ }
4857
4984
  }
4858
4985
  } else {
4859
4986
  verbose(ctx, `fetching lesson ${course}/${lessonId} (filtering by ${options.type})`);
@@ -4861,17 +4988,40 @@ async function runPrintMode(ctx, lessonId, course, profile, token, lang, options
4861
4988
  if (!result.ok) {
4862
4989
  handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4863
4990
  }
4864
- const artifacts = result.data[options.type];
4865
- if (ctx.json) {
4866
- output(ctx, "", artifacts);
4991
+ const type = options.type;
4992
+ if (type === "skills") {
4993
+ const skills = result.data.skills;
4994
+ if (ctx.json) {
4995
+ output(ctx, "", skills);
4996
+ } else {
4997
+ const contents = skills.map((s) => s.files.find((f2) => f2.path === "SKILL.md")?.content ?? "");
4998
+ process.stdout.write(contents.join(`
4999
+ ---
5000
+ `));
5001
+ for (const skill of skills)
5002
+ emitMultiFileSkillNotice(skill.name, skill.files);
5003
+ }
4867
5004
  } else {
4868
- const contents = artifacts.map((a2) => a2.content);
4869
- process.stdout.write(contents.join(`
5005
+ const artifacts = result.data[type];
5006
+ if (ctx.json) {
5007
+ output(ctx, "", artifacts);
5008
+ } else {
5009
+ const contents = artifacts.map((a2) => a2.content);
5010
+ process.stdout.write(contents.join(`
4870
5011
  ---
4871
5012
  `));
5013
+ }
4872
5014
  }
4873
5015
  }
4874
5016
  }
5017
+ function emitMultiFileSkillNotice(skillName, files) {
5018
+ const extras = files.filter((f2) => f2.path !== "SKILL.md").map((f2) => f2.path);
5019
+ if (extras.length === 0)
5020
+ return;
5021
+ process.stderr.write(`Note: skill "${skillName}" has ${extras.length} additional file${extras.length === 1 ? "" : "s"} not shown in --print: ${extras.join(", ")}.
5022
+ ` + `Run without --print to materialize all files.
5023
+ `);
5024
+ }
4875
5025
  function handleLessonError(ctx, status, code, error, payload) {
4876
5026
  verbose(ctx, `lesson fetch failed: status=${status} code=${code}`);
4877
5027
  if (status === 403) {
@@ -4938,7 +5088,15 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
4938
5088
  lines.push("");
4939
5089
  lines.push(dryRun ? `Would write to ${targetDir}/:` : `Wrote to ${targetDir}/:`);
4940
5090
  for (const skill of writeResult.skills) {
4941
- lines.push(` [${skill.action}] skill ${skill.path}`);
5091
+ if (skill.files.length === 1) {
5092
+ const f2 = skill.files[0];
5093
+ lines.push(` [${f2.action}] skill ${f2.absolutePath}`);
5094
+ } else {
5095
+ lines.push(` skill ${skill.name} (${skill.files.length} files)`);
5096
+ for (const f2 of skill.files) {
5097
+ lines.push(` [${f2.action}] ${f2.path}`);
5098
+ }
5099
+ }
4942
5100
  }
4943
5101
  for (const prompt of writeResult.prompts) {
4944
5102
  lines.push(` [${prompt.action}] prompt ${prompt.path}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "0.6.0",
3
+ "version": "1.1.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",