@przeprogramowani/10x-cli 0.4.0 → 0.6.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 CHANGED
@@ -13,6 +13,26 @@ npm install -g @przeprogramowani/10x-cli
13
13
  # https://github.com/przeprogramowani/10x-cli/releases
14
14
  ```
15
15
 
16
+ ## Agentic Installation
17
+
18
+ Let your AI coding agent handle the setup. This repo ships a [`10x-cli-setup`](skills/10x-cli-setup/SKILL.md) skill that walks your agent through installing, authenticating, and configuring the CLI — all driven by the latest README.
19
+
20
+ Install the skill with [skills.sh](https://skills.sh):
21
+
22
+ ```bash
23
+ # Add the skill to your current project (symlinked)
24
+ npx skills add przeprogramowani/10x-cli
25
+
26
+ # Or install globally so it's available in every project
27
+ npx skills add przeprogramowani/10x-cli -g
28
+
29
+ # Target a specific agent
30
+ npx skills add przeprogramowani/10x-cli -a claude-code
31
+ npx skills add przeprogramowani/10x-cli -a cursor
32
+ ```
33
+
34
+ Once installed, just tell your agent to **set up 10x-cli** and it will pick up the skill automatically.
35
+
16
36
  ## Quick Start
17
37
 
18
38
  ```bash
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.4.0",
2274
+ version: "0.6.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -2287,6 +2287,7 @@ var package_default = {
2287
2287
  },
2288
2288
  files: [
2289
2289
  "dist",
2290
+ "skills",
2290
2291
  "README.md",
2291
2292
  "LICENSE"
2292
2293
  ],
@@ -2323,6 +2324,9 @@ var package_default = {
2323
2324
  }
2324
2325
  };
2325
2326
 
2327
+ // node_modules/@clack/prompts/dist/index.mjs
2328
+ import { stripVTControlCharacters as T2 } from "node:util";
2329
+
2326
2330
  // node_modules/@clack/core/dist/index.mjs
2327
2331
  var import_sisteransi = __toESM(require_src(), 1);
2328
2332
  var import_picocolors = __toESM(require_picocolors(), 1);
@@ -2832,6 +2836,21 @@ ${import_picocolors2.default.cyan(m2)}
2832
2836
  }
2833
2837
  } }).prompt();
2834
2838
  };
2839
+ var ye = (s = "", n = "") => {
2840
+ const t = `
2841
+ ${s}
2842
+ `.split(`
2843
+ `), i = T2(n).length, r2 = Math.max(t.reduce((o, l2) => {
2844
+ const $2 = T2(l2);
2845
+ return $2.length > o ? $2.length : o;
2846
+ }, 0), i) + 2, c2 = t.map((o) => `${import_picocolors2.default.gray(a)} ${import_picocolors2.default.dim(o)}${" ".repeat(r2 - T2(o).length)}${import_picocolors2.default.gray(a)}`).join(`
2847
+ `);
2848
+ process.stdout.write(`${import_picocolors2.default.gray(a)}
2849
+ ${import_picocolors2.default.green(S2)} ${import_picocolors2.default.reset(n)} ${import_picocolors2.default.gray(N2.repeat(Math.max(r2 - i - 1, 1)) + re)}
2850
+ ${c2}
2851
+ ${import_picocolors2.default.gray(ie + N2.repeat(r2 + 2) + ne)}
2852
+ `);
2853
+ };
2835
2854
  var ve = (s = "") => {
2836
2855
  process.stdout.write(`${import_picocolors2.default.gray(m2)} ${import_picocolors2.default.red(s)}
2837
2856
 
@@ -2926,6 +2945,20 @@ function readAuth() {
2926
2945
  return null;
2927
2946
  }
2928
2947
  }
2948
+ function writeJsonAtomic(path, data, opts = {}) {
2949
+ const tmp = `${path}.tmp`;
2950
+ rmSync(tmp, { force: true });
2951
+ const body = `${JSON.stringify(data, null, 2)}
2952
+ `;
2953
+ const writeOpts = opts.mode !== undefined ? { mode: opts.mode } : undefined;
2954
+ writeFileSync(tmp, body, writeOpts);
2955
+ if (opts.mode !== undefined && process.platform !== "win32") {
2956
+ try {
2957
+ chmodSync(tmp, opts.mode);
2958
+ } catch {}
2959
+ }
2960
+ renameSync(tmp, path);
2961
+ }
2929
2962
  function saveAuth(data) {
2930
2963
  const dir = configDir();
2931
2964
  if (!existsSync(dir)) {
@@ -2936,17 +2969,7 @@ function saveAuth(data) {
2936
2969
  chmodSync(dir, 448);
2937
2970
  } catch {}
2938
2971
  }
2939
- const file = authFilePath();
2940
- const tmp = `${file}.tmp`;
2941
- rmSync(tmp, { force: true });
2942
- writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
2943
- `, { mode: 384 });
2944
- if (process.platform !== "win32") {
2945
- try {
2946
- chmodSync(tmp, 384);
2947
- } catch {}
2948
- }
2949
- renameSync(tmp, file);
2972
+ writeJsonAtomic(authFilePath(), data, { mode: 384 });
2950
2973
  }
2951
2974
  function deleteAuth() {
2952
2975
  const file = authFilePath();
@@ -2966,6 +2989,9 @@ function readToolConfig() {
2966
2989
  const parsed = JSON.parse(raw);
2967
2990
  if (typeof parsed.tool !== "string")
2968
2991
  return null;
2992
+ if (parsed.acknowledgedOrphans !== undefined && (!Array.isArray(parsed.acknowledgedOrphans) || !parsed.acknowledgedOrphans.every((x2) => typeof x2 === "string"))) {
2993
+ delete parsed.acknowledgedOrphans;
2994
+ }
2969
2995
  return parsed;
2970
2996
  } catch {
2971
2997
  return null;
@@ -2976,9 +3002,7 @@ function saveToolConfig(config) {
2976
3002
  if (!existsSync(dir)) {
2977
3003
  mkdirSync(dir, { recursive: true, mode: 448 });
2978
3004
  }
2979
- const file = toolConfigPath();
2980
- writeFileSync(file, `${JSON.stringify(config, null, 2)}
2981
- `);
3005
+ writeJsonAtomic(toolConfigPath(), config);
2982
3006
  }
2983
3007
 
2984
3008
  // src/lib/api-client.ts
@@ -3916,7 +3940,71 @@ function parseModuleRef(raw) {
3916
3940
  return module;
3917
3941
  }
3918
3942
 
3943
+ // src/lib/tool-detect.ts
3944
+ import { existsSync as existsSync4 } from "node:fs";
3945
+ import { join as join4 } from "node:path";
3946
+
3947
+ // src/lib/manifest.ts
3948
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
3949
+ import { dirname as dirname2, join as join3 } from "node:path";
3950
+ var MANIFEST_FILENAME = ".10x-cli-manifest.json";
3951
+ var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
3952
+ function readManifest(dir) {
3953
+ const manifestPath = join3(dir, MANIFEST_FILENAME);
3954
+ if (!existsSync3(manifestPath))
3955
+ return null;
3956
+ let raw;
3957
+ try {
3958
+ raw = readFileSync2(manifestPath, "utf8");
3959
+ } catch {
3960
+ return null;
3961
+ }
3962
+ let parsed;
3963
+ try {
3964
+ parsed = JSON.parse(raw);
3965
+ } catch {
3966
+ return null;
3967
+ }
3968
+ if (!isManifest(parsed))
3969
+ return null;
3970
+ return parsed;
3971
+ }
3972
+ function writeManifest(dir, manifest) {
3973
+ if (!existsSync3(dir)) {
3974
+ mkdirSync3(dir, { recursive: true });
3975
+ }
3976
+ const manifestPath = join3(dir, MANIFEST_FILENAME);
3977
+ mkdirSync3(dirname2(manifestPath), { recursive: true });
3978
+ writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
3979
+ `);
3980
+ }
3981
+ function isManifest(value) {
3982
+ if (typeof value !== "object" || value === null)
3983
+ return false;
3984
+ const v2 = value;
3985
+ if (v2["package"] !== CLI_PACKAGE_NAME)
3986
+ return false;
3987
+ if (typeof v2["version"] !== "string")
3988
+ return false;
3989
+ if (typeof v2["lastApplied"] !== "string")
3990
+ return false;
3991
+ if (typeof v2["lessonId"] !== "string")
3992
+ return false;
3993
+ if (typeof v2["course"] !== "string")
3994
+ return false;
3995
+ const files = v2["files"];
3996
+ if (typeof files !== "object" || files === null)
3997
+ return false;
3998
+ const f2 = files;
3999
+ return isStringArray(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4000
+ }
4001
+ function isStringArray(value) {
4002
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
4003
+ }
4004
+
3919
4005
  // src/lib/tool-profile.ts
4006
+ var SENTINEL_BEGIN = "<!-- BEGIN @przeprogramowani/10x-cli -->";
4007
+ var SENTINEL_END = "<!-- END @przeprogramowani/10x-cli -->";
3920
4008
  var PROFILES = {
3921
4009
  "claude-code": {
3922
4010
  toolId: "claude-code",
@@ -3926,8 +4014,8 @@ var PROFILES = {
3926
4014
  configPath: (n) => `.claude/config-templates/${n}`,
3927
4015
  rulesFile: "CLAUDE.md",
3928
4016
  manifestDir: ".claude",
3929
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3930
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4017
+ sentinelBegin: SENTINEL_BEGIN,
4018
+ sentinelEnd: SENTINEL_END
3931
4019
  },
3932
4020
  cursor: {
3933
4021
  toolId: "cursor",
@@ -3937,8 +4025,8 @@ var PROFILES = {
3937
4025
  configPath: (n) => `.cursor/config-templates/${n}`,
3938
4026
  rulesFile: ".cursor/rules/10x-course.mdc",
3939
4027
  manifestDir: ".cursor",
3940
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3941
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4028
+ sentinelBegin: SENTINEL_BEGIN,
4029
+ sentinelEnd: SENTINEL_END
3942
4030
  },
3943
4031
  copilot: {
3944
4032
  toolId: "copilot",
@@ -3948,8 +4036,8 @@ var PROFILES = {
3948
4036
  configPath: (n) => `.github/config-templates/${n}`,
3949
4037
  rulesFile: ".github/copilot-instructions.md",
3950
4038
  manifestDir: ".github",
3951
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3952
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4039
+ sentinelBegin: SENTINEL_BEGIN,
4040
+ sentinelEnd: SENTINEL_END
3953
4041
  },
3954
4042
  codex: {
3955
4043
  toolId: "codex",
@@ -3959,8 +4047,8 @@ var PROFILES = {
3959
4047
  configPath: (n) => `.agents/config-templates/${n}`,
3960
4048
  rulesFile: "AGENTS.md",
3961
4049
  manifestDir: ".agents",
3962
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3963
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4050
+ sentinelBegin: SENTINEL_BEGIN,
4051
+ sentinelEnd: SENTINEL_END
3964
4052
  },
3965
4053
  generic: {
3966
4054
  toolId: "generic",
@@ -3970,116 +4058,143 @@ var PROFILES = {
3970
4058
  configPath: (n) => `.ai/config-templates/${n}`,
3971
4059
  rulesFile: "AGENTS.md",
3972
4060
  manifestDir: ".ai",
3973
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3974
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4061
+ sentinelBegin: SENTINEL_BEGIN,
4062
+ sentinelEnd: SENTINEL_END
3975
4063
  }
3976
4064
  };
3977
4065
  var DEFAULT_TOOL = "claude-code";
3978
4066
 
3979
- // src/lib/tool-prompt.ts
3980
- async function resolveToolProfile(flagOverride) {
3981
- if (flagOverride) {
3982
- const profile = PROFILES[flagOverride];
3983
- if (!profile) {
3984
- throw new Error(`Unknown tool '${flagOverride}'. Supported: ${Object.keys(PROFILES).join(", ")}`);
3985
- }
3986
- return profile;
3987
- }
3988
- const config = readToolConfig();
3989
- if (config?.tool && PROFILES[config.tool]) {
3990
- return PROFILES[config.tool];
4067
+ // src/lib/tool-detect.ts
4068
+ function detectTools(projectRoot) {
4069
+ const hit = (rel) => existsSync4(join4(projectRoot, rel));
4070
+ const signals = [];
4071
+ if (hit(".claude/" + MANIFEST_FILENAME)) {
4072
+ signals.push({
4073
+ profileId: "claude-code",
4074
+ confidence: "strong",
4075
+ reason: ".claude/.10x-cli-manifest.json"
4076
+ });
4077
+ } else if (hit(".claude")) {
4078
+ signals.push({ profileId: "claude-code", confidence: "medium", reason: ".claude/ directory" });
4079
+ } else if (hit("CLAUDE.md")) {
4080
+ signals.push({ profileId: "claude-code", confidence: "weak", reason: "CLAUDE.md" });
4081
+ }
4082
+ if (hit(".cursor/" + MANIFEST_FILENAME)) {
4083
+ signals.push({
4084
+ profileId: "cursor",
4085
+ confidence: "strong",
4086
+ reason: ".cursor/.10x-cli-manifest.json"
4087
+ });
4088
+ } else if (hit(".cursor/rules")) {
4089
+ signals.push({ profileId: "cursor", confidence: "strong", reason: ".cursor/rules/" });
4090
+ } else if (hit(".cursor")) {
4091
+ signals.push({ profileId: "cursor", confidence: "medium", reason: ".cursor/ directory" });
4092
+ }
4093
+ if (hit(".github/" + MANIFEST_FILENAME)) {
4094
+ signals.push({
4095
+ profileId: "copilot",
4096
+ confidence: "strong",
4097
+ reason: ".github/.10x-cli-manifest.json"
4098
+ });
4099
+ } else if (hit(".github/copilot-instructions.md")) {
4100
+ signals.push({
4101
+ profileId: "copilot",
4102
+ confidence: "strong",
4103
+ reason: ".github/copilot-instructions.md"
4104
+ });
4105
+ } else if (hit(".github/skills") || hit(".github/prompts")) {
4106
+ signals.push({
4107
+ profileId: "copilot",
4108
+ confidence: "medium",
4109
+ reason: ".github/skills/ or .github/prompts/"
4110
+ });
3991
4111
  }
3992
- if (process.stdout.isTTY) {
3993
- const choice = await de({
3994
- message: "Which AI coding tool do you use?",
3995
- options: Object.values(PROFILES).map((profile) => ({
3996
- value: profile.toolId,
3997
- label: profile.displayName,
3998
- hint: profile.toolId === DEFAULT_TOOL ? "default" : undefined
3999
- })),
4000
- initialValue: DEFAULT_TOOL
4112
+ if (hit(".agents/" + MANIFEST_FILENAME)) {
4113
+ signals.push({
4114
+ profileId: "codex",
4115
+ confidence: "strong",
4116
+ reason: ".agents/.10x-cli-manifest.json"
4001
4117
  });
4002
- if (BD(choice)) {
4003
- ve("Using default (Claude Code).");
4004
- return PROFILES[DEFAULT_TOOL];
4005
- }
4006
- saveToolConfig({ tool: choice });
4007
- return PROFILES[choice];
4118
+ } else if (hit(".agents")) {
4119
+ signals.push({ profileId: "codex", confidence: "strong", reason: ".agents/ directory" });
4120
+ } else if (hit("AGENTS.md") && !hit(".ai")) {
4121
+ signals.push({ profileId: "codex", confidence: "medium", reason: "AGENTS.md" });
4122
+ }
4123
+ if (hit(".ai/" + MANIFEST_FILENAME)) {
4124
+ signals.push({
4125
+ profileId: "generic",
4126
+ confidence: "strong",
4127
+ reason: ".ai/.10x-cli-manifest.json"
4128
+ });
4129
+ } else if (hit(".ai")) {
4130
+ signals.push({ profileId: "generic", confidence: "strong", reason: ".ai/ directory" });
4131
+ } else if (hit("AGENTS.md") && !hit(".agents")) {
4132
+ signals.push({ profileId: "generic", confidence: "weak", reason: "AGENTS.md (generic fallback)" });
4008
4133
  }
4009
- return PROFILES[DEFAULT_TOOL];
4134
+ return rankSignals(signals);
4135
+ }
4136
+ var CONFIDENCE_ORDER = { strong: 3, medium: 2, weak: 1 };
4137
+ var PROFILE_ORDER = ["claude-code", "cursor", "copilot", "codex", "generic"];
4138
+ function rankSignals(signals) {
4139
+ return [...signals].sort((a2, b2) => {
4140
+ const c2 = CONFIDENCE_ORDER[b2.confidence] - CONFIDENCE_ORDER[a2.confidence];
4141
+ if (c2 !== 0)
4142
+ return c2;
4143
+ return PROFILE_ORDER.indexOf(a2.profileId) - PROFILE_ORDER.indexOf(b2.profileId);
4144
+ });
4145
+ }
4146
+ function topDetectedProfile(signals) {
4147
+ const top = signals[0];
4148
+ if (!top)
4149
+ return null;
4150
+ return PROFILES[top.profileId] ?? null;
4010
4151
  }
4011
4152
 
4012
- // src/lib/writer.ts
4153
+ // src/lib/tool-switch.ts
4013
4154
  import {
4014
- existsSync as existsSync4,
4015
- mkdirSync as mkdirSync4,
4016
- readFileSync as readFileSync3,
4017
- rmSync as rmSync2,
4018
- writeFileSync as writeFileSync3
4155
+ existsSync as existsSync6,
4156
+ lstatSync,
4157
+ mkdirSync as mkdirSync5,
4158
+ readFileSync as readFileSync5,
4159
+ renameSync as renameSync2,
4160
+ rmdirSync,
4161
+ rmSync as rmSync3,
4162
+ statSync,
4163
+ writeFileSync as writeFileSync4
4019
4164
  } from "node:fs";
4020
- import { dirname as dirname3, join as join4 } from "node:path";
4165
+ import { dirname as dirname4, join as join6 } from "node:path";
4021
4166
 
4022
- // src/lib/manifest.ts
4023
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
4024
- import { dirname as dirname2, join as join3 } from "node:path";
4025
- var MANIFEST_FILENAME = ".10x-cli-manifest.json";
4026
- var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
4027
- function readManifest(dir) {
4028
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4029
- if (!existsSync3(manifestPath))
4030
- return null;
4031
- let raw;
4167
+ // src/lib/fs-utils.ts
4168
+ import { readFileSync as readFileSync3 } from "node:fs";
4169
+ function readFileOrNull(path) {
4032
4170
  try {
4033
- raw = readFileSync2(manifestPath, "utf8");
4171
+ return readFileSync3(path, "utf8");
4034
4172
  } catch {
4035
4173
  return null;
4036
4174
  }
4037
- let parsed;
4038
- try {
4039
- parsed = JSON.parse(raw);
4040
- } catch {
4041
- return null;
4042
- }
4043
- if (!isManifest(parsed))
4044
- return null;
4045
- return parsed;
4046
- }
4047
- function writeManifest(dir, manifest) {
4048
- if (!existsSync3(dir)) {
4049
- mkdirSync3(dir, { recursive: true });
4050
- }
4051
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4052
- mkdirSync3(dirname2(manifestPath), { recursive: true });
4053
- writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
4054
- `);
4055
- }
4056
- function isManifest(value) {
4057
- if (typeof value !== "object" || value === null)
4058
- return false;
4059
- const v2 = value;
4060
- if (v2["package"] !== CLI_PACKAGE_NAME)
4061
- return false;
4062
- if (typeof v2["version"] !== "string")
4063
- return false;
4064
- if (typeof v2["lastApplied"] !== "string")
4065
- return false;
4066
- if (typeof v2["lessonId"] !== "string")
4067
- return false;
4068
- if (typeof v2["course"] !== "string")
4069
- return false;
4070
- const files = v2["files"];
4071
- if (typeof files !== "object" || files === null)
4072
- return false;
4073
- const f2 = files;
4074
- return isStringArray(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4075
- }
4076
- function isStringArray(value) {
4077
- return Array.isArray(value) && value.every((item) => typeof item === "string");
4078
4175
  }
4079
4176
 
4080
4177
  // src/lib/sentinel-migration.ts
4081
4178
  var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
4082
4179
  var OLD_END = "<!-- END @przeprogramowani/10x-toolkit -->";
4180
+ function removeRulesBlockWithMarkers(existing, begin, end) {
4181
+ const beginIdx = existing.indexOf(begin);
4182
+ const endIdx = existing.indexOf(end);
4183
+ if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
4184
+ return { content: existing, removed: false };
4185
+ }
4186
+ const beforeRaw = existing.slice(0, beginIdx);
4187
+ const afterRaw = existing.slice(endIdx + end.length);
4188
+ const before = beforeRaw.replace(/[\r\n]*$/, "");
4189
+ const after = afterRaw.replace(/^[\r\n]*/, "");
4190
+ const joiner = before.length > 0 && after.length > 0 ? `
4191
+
4192
+ ` : "";
4193
+ const combined = before + joiner + after;
4194
+ const content = combined.length === 0 ? "" : combined.replace(/[\r\n]*$/, `
4195
+ `);
4196
+ return { content, removed: true };
4197
+ }
4083
4198
  function applyRulesBlockWithMarkers(existingContent, rulesBody, beginMarker, endMarker) {
4084
4199
  for (const marker of [OLD_BEGIN, OLD_END, beginMarker, endMarker]) {
4085
4200
  if (rulesBody.includes(marker)) {
@@ -4127,6 +4242,14 @@ function stripBlock(content, begin, end, label, warnings) {
4127
4242
  }
4128
4243
 
4129
4244
  // src/lib/writer.ts
4245
+ import {
4246
+ existsSync as existsSync5,
4247
+ mkdirSync as mkdirSync4,
4248
+ readFileSync as readFileSync4,
4249
+ rmSync as rmSync2,
4250
+ writeFileSync as writeFileSync3
4251
+ } from "node:fs";
4252
+ import { dirname as dirname3, join as join5 } from "node:path";
4130
4253
  var CLI_VERSION = package_default.version;
4131
4254
  var DEFAULT_COURSE = "10xdevs3";
4132
4255
  function applyBundle(bundle, projectRoot, options = {}) {
@@ -4134,7 +4257,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
4134
4257
  const partial = options.partial === true;
4135
4258
  const course = options.course ?? DEFAULT_COURSE;
4136
4259
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4137
- const manifestDir = join4(projectRoot, profile.manifestDir);
4260
+ const manifestDir = join5(projectRoot, profile.manifestDir);
4138
4261
  const prevManifest = readManifest(manifestDir);
4139
4262
  for (const skill of bundle.skills)
4140
4263
  assertSafeName(skill.name, "skill");
@@ -4143,7 +4266,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
4143
4266
  for (const config of bundle.configs)
4144
4267
  assertSafeName(config.name, "config");
4145
4268
  const skills = bundle.skills.map((skill) => {
4146
- const target = join4(projectRoot, profile.skillPath(skill.name));
4269
+ const target = join5(projectRoot, profile.skillPath(skill.name));
4147
4270
  const action = computeFileAction(target, skill.content);
4148
4271
  if (!dryRun && action !== "unchanged") {
4149
4272
  writeFileAt(target, skill.content);
@@ -4151,14 +4274,14 @@ function applyBundle(bundle, projectRoot, options = {}) {
4151
4274
  return { name: skill.name, path: target, action };
4152
4275
  });
4153
4276
  const prompts = bundle.prompts.map((prompt) => {
4154
- const target = join4(projectRoot, profile.promptPath(prompt.name));
4277
+ const target = join5(projectRoot, profile.promptPath(prompt.name));
4155
4278
  const action = computeFileAction(target, prompt.content);
4156
4279
  if (!dryRun && action !== "unchanged") {
4157
4280
  writeFileAt(target, prompt.content);
4158
4281
  }
4159
4282
  return { name: prompt.name, path: target, action };
4160
4283
  });
4161
- const rulesFilePath = join4(projectRoot, profile.rulesFile);
4284
+ const rulesFilePath = join5(projectRoot, profile.rulesFile);
4162
4285
  const existingRules = readFileOrEmpty(rulesFilePath);
4163
4286
  let rulesAction;
4164
4287
  if (bundle.rules.length === 0) {
@@ -4180,8 +4303,8 @@ function applyBundle(bundle, projectRoot, options = {}) {
4180
4303
  }
4181
4304
  }
4182
4305
  const configs = bundle.configs.map((config) => {
4183
- const target = join4(projectRoot, profile.configPath(config.name));
4184
- const action = existsSync4(target) ? "skipped" : "created";
4306
+ const target = join5(projectRoot, profile.configPath(config.name));
4307
+ const action = existsSync5(target) ? "skipped" : "created";
4185
4308
  if (!dryRun && action === "created") {
4186
4309
  writeFileAt(target, config.content);
4187
4310
  }
@@ -4220,21 +4343,21 @@ function applyBundle(bundle, projectRoot, options = {}) {
4220
4343
  };
4221
4344
  }
4222
4345
  function computeFileAction(filePath, newContent) {
4223
- if (!existsSync4(filePath))
4346
+ if (!existsSync5(filePath))
4224
4347
  return "created";
4225
4348
  let current;
4226
4349
  try {
4227
- current = readFileSync3(filePath, "utf8");
4350
+ current = readFileSync4(filePath, "utf8");
4228
4351
  } catch {
4229
4352
  return "updated";
4230
4353
  }
4231
4354
  return current === newContent ? "unchanged" : "updated";
4232
4355
  }
4233
4356
  function readFileOrEmpty(filePath) {
4234
- if (!existsSync4(filePath))
4357
+ if (!existsSync5(filePath))
4235
4358
  return "";
4236
4359
  try {
4237
- return readFileSync3(filePath, "utf8");
4360
+ return readFileSync4(filePath, "utf8");
4238
4361
  } catch {
4239
4362
  return "";
4240
4363
  }
@@ -4256,7 +4379,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4256
4379
  continue;
4257
4380
  if (!isSafeName(skillName))
4258
4381
  continue;
4259
- const skillTarget = join4(projectRoot, profile.skillPath(skillName));
4382
+ const skillTarget = join5(projectRoot, profile.skillPath(skillName));
4260
4383
  removed.skills.push({ name: skillName, path: dirname3(skillTarget) });
4261
4384
  }
4262
4385
  for (const promptFile of prevManifest.files.prompts) {
@@ -4267,7 +4390,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4267
4390
  const promptName = promptFile.replace(/\.md$/, "");
4268
4391
  removed.prompts.push({
4269
4392
  name: promptFile,
4270
- path: join4(projectRoot, profile.promptPath(promptName))
4393
+ path: join5(projectRoot, profile.promptPath(promptName))
4271
4394
  });
4272
4395
  }
4273
4396
  for (const configFile of prevManifest.files.configs) {
@@ -4277,23 +4400,33 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4277
4400
  continue;
4278
4401
  removed.configs.push({
4279
4402
  name: configFile,
4280
- path: join4(projectRoot, profile.configPath(configFile))
4403
+ path: join5(projectRoot, profile.configPath(configFile))
4281
4404
  });
4282
4405
  }
4283
4406
  return removed;
4284
4407
  }
4285
- function detectOrphanedArtifacts(projectRoot, currentProfile) {
4408
+ function findOrphanedManifests(projectRoot, currentProfile) {
4409
+ const out = [];
4286
4410
  for (const profile of Object.values(PROFILES)) {
4287
4411
  if (profile.toolId === currentProfile.toolId)
4288
4412
  continue;
4289
- const otherManifest = join4(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
4290
- if (existsSync4(otherManifest)) {
4291
- return `Found existing 10x artifacts in ${profile.manifestDir}/ from ${profile.displayName}.
4292
- Manually remove ${profile.manifestDir}/ if you no longer need it.
4293
- Your new artifacts will be written to ${currentProfile.manifestDir}/`;
4294
- }
4413
+ const manifestPath = join5(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
4414
+ if (!existsSync5(manifestPath))
4415
+ continue;
4416
+ const manifest = readManifest(join5(projectRoot, profile.manifestDir));
4417
+ if (!manifest)
4418
+ continue;
4419
+ out.push({ profile, manifestPath, manifest });
4295
4420
  }
4296
- return null;
4421
+ return out;
4422
+ }
4423
+ function detectOrphanedArtifacts(projectRoot, currentProfile) {
4424
+ const first = findOrphanedManifests(projectRoot, currentProfile)[0];
4425
+ if (!first)
4426
+ return null;
4427
+ return `Found existing 10x artifacts in ${first.profile.manifestDir}/ from ${first.profile.displayName}.
4428
+ Manually remove ${first.profile.manifestDir}/ if you no longer need it.
4429
+ Your new artifacts will be written to ${currentProfile.manifestDir}/`;
4297
4430
  }
4298
4431
  function assertSafeName(name, kind) {
4299
4432
  if (!isSafeName(name)) {
@@ -4321,6 +4454,296 @@ function isSafeName(name) {
4321
4454
  return true;
4322
4455
  }
4323
4456
 
4457
+ // src/lib/tool-switch.ts
4458
+ function migrateArtifacts(projectRoot, orphan, newProfile) {
4459
+ const summary = {
4460
+ action: "migrated",
4461
+ oldToolId: orphan.profile.toolId,
4462
+ movedOrRemoved: { skills: [], prompts: [], configs: [] },
4463
+ sentinelStripped: false,
4464
+ skipped: []
4465
+ };
4466
+ const oldProfile = orphan.profile;
4467
+ for (const skillName of orphan.manifest.files.skills) {
4468
+ if (!isSafeName(skillName)) {
4469
+ summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
4470
+ continue;
4471
+ }
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);
4476
+ }
4477
+ }
4478
+ for (const promptFile of orphan.manifest.files.prompts) {
4479
+ if (!isSafeName(promptFile)) {
4480
+ summary.skipped.push({ path: promptFile, reason: "unsafe name in manifest" });
4481
+ continue;
4482
+ }
4483
+ const promptName = promptFile.replace(/\.md$/, "");
4484
+ const from = join6(projectRoot, oldProfile.promptPath(promptName));
4485
+ const to = join6(projectRoot, newProfile.promptPath(promptName));
4486
+ if (moveIfSafe(from, to, summary.skipped)) {
4487
+ summary.movedOrRemoved.prompts.push(promptFile);
4488
+ }
4489
+ }
4490
+ for (const configFile of orphan.manifest.files.configs) {
4491
+ if (!isSafeName(configFile)) {
4492
+ summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
4493
+ continue;
4494
+ }
4495
+ const from = join6(projectRoot, oldProfile.configPath(configFile));
4496
+ const to = join6(projectRoot, newProfile.configPath(configFile));
4497
+ if (moveIfSafe(from, to, summary.skipped)) {
4498
+ summary.movedOrRemoved.configs.push(configFile);
4499
+ }
4500
+ }
4501
+ summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
4502
+ rmSync3(orphan.manifestPath, { force: true });
4503
+ tryRemoveEmptyDir(join6(projectRoot, oldProfile.manifestDir));
4504
+ return summary;
4505
+ }
4506
+ function deleteArtifacts(projectRoot, orphan) {
4507
+ const summary = {
4508
+ action: "deleted",
4509
+ oldToolId: orphan.profile.toolId,
4510
+ movedOrRemoved: { skills: [], prompts: [], configs: [] },
4511
+ sentinelStripped: false,
4512
+ skipped: []
4513
+ };
4514
+ const oldProfile = orphan.profile;
4515
+ for (const skillName of orphan.manifest.files.skills) {
4516
+ if (!isSafeName(skillName)) {
4517
+ summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
4518
+ continue;
4519
+ }
4520
+ const skillFile = join6(projectRoot, oldProfile.skillPath(skillName));
4521
+ const skillDir = dirname4(skillFile);
4522
+ rmSync3(skillFile, { force: true });
4523
+ tryRemoveEmptyDir(skillDir);
4524
+ summary.movedOrRemoved.skills.push(skillName);
4525
+ }
4526
+ for (const promptFile of orphan.manifest.files.prompts) {
4527
+ if (!isSafeName(promptFile)) {
4528
+ summary.skipped.push({ path: promptFile, reason: "unsafe name in manifest" });
4529
+ continue;
4530
+ }
4531
+ const promptName = promptFile.replace(/\.md$/, "");
4532
+ rmSync3(join6(projectRoot, oldProfile.promptPath(promptName)), { force: true });
4533
+ summary.movedOrRemoved.prompts.push(promptFile);
4534
+ }
4535
+ for (const configFile of orphan.manifest.files.configs) {
4536
+ if (!isSafeName(configFile)) {
4537
+ summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
4538
+ continue;
4539
+ }
4540
+ rmSync3(join6(projectRoot, oldProfile.configPath(configFile)), { force: true });
4541
+ summary.movedOrRemoved.configs.push(configFile);
4542
+ }
4543
+ summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
4544
+ rmSync3(orphan.manifestPath, { force: true });
4545
+ const manifestDirAbs = join6(projectRoot, oldProfile.manifestDir);
4546
+ tryRemoveEmptyDir(join6(manifestDirAbs, "skills"));
4547
+ tryRemoveEmptyDir(join6(manifestDirAbs, "prompts"));
4548
+ tryRemoveEmptyDir(join6(manifestDirAbs, "config-templates"));
4549
+ tryRemoveEmptyDir(manifestDirAbs);
4550
+ return summary;
4551
+ }
4552
+ function moveIfSafe(from, to, skipped) {
4553
+ if (!existsSync6(from)) {
4554
+ return false;
4555
+ }
4556
+ if (lstatSync(from).isSymbolicLink()) {
4557
+ skipped.push({ path: from, reason: "source is a symlink" });
4558
+ return false;
4559
+ }
4560
+ if (existsSync6(to)) {
4561
+ if (!filesAreIdentical(from, to)) {
4562
+ skipped.push({
4563
+ path: to,
4564
+ reason: "destination already exists with different content"
4565
+ });
4566
+ return false;
4567
+ }
4568
+ rmSync3(from, { force: true });
4569
+ return true;
4570
+ }
4571
+ mkdirSync5(dirname4(to), { recursive: true });
4572
+ try {
4573
+ renameSync2(from, to);
4574
+ } catch (err) {
4575
+ const code = err?.code;
4576
+ if (code !== "EXDEV")
4577
+ throw err;
4578
+ const bytes = readFileSync5(from);
4579
+ const tmp = `${to}.tmp`;
4580
+ writeFileSync4(tmp, bytes);
4581
+ renameSync2(tmp, to);
4582
+ try {
4583
+ rmSync3(from, { force: true });
4584
+ } catch {
4585
+ skipped.push({
4586
+ path: from,
4587
+ reason: "copied to destination but could not remove source"
4588
+ });
4589
+ return true;
4590
+ }
4591
+ }
4592
+ return true;
4593
+ }
4594
+ function stripSentinelFromRulesFile(projectRoot, oldProfile) {
4595
+ const rulesPath = join6(projectRoot, oldProfile.rulesFile);
4596
+ if (!existsSync6(rulesPath))
4597
+ return false;
4598
+ const existing = readFileOrNull(rulesPath);
4599
+ if (existing === null)
4600
+ return false;
4601
+ const { content, removed } = removeRulesBlockWithMarkers(existing, oldProfile.sentinelBegin, oldProfile.sentinelEnd);
4602
+ if (!removed)
4603
+ return false;
4604
+ if (content.trim().length === 0) {
4605
+ rmSync3(rulesPath, { force: true });
4606
+ } else {
4607
+ writeFileSync4(rulesPath, content);
4608
+ }
4609
+ return true;
4610
+ }
4611
+ function filesAreIdentical(a2, b2) {
4612
+ try {
4613
+ const aStat = statSync(a2);
4614
+ const bStat = statSync(b2);
4615
+ if (aStat.size !== bStat.size)
4616
+ return false;
4617
+ return readFileSync5(a2).equals(readFileSync5(b2));
4618
+ } catch {
4619
+ return false;
4620
+ }
4621
+ }
4622
+ function tryRemoveEmptyDir(dir) {
4623
+ if (!existsSync6(dir))
4624
+ return;
4625
+ try {
4626
+ rmdirSync(dir);
4627
+ } catch {}
4628
+ }
4629
+
4630
+ // src/lib/tool-prompt.ts
4631
+ async function resolveToolProfile(flagOverride, projectRoot = process.cwd()) {
4632
+ const profile = await resolveProfileOnly(flagOverride, projectRoot);
4633
+ await handleToolSwitch(projectRoot, profile);
4634
+ return profile;
4635
+ }
4636
+ async function resolveProfileOnly(flagOverride, projectRoot) {
4637
+ if (flagOverride) {
4638
+ const profile = PROFILES[flagOverride];
4639
+ if (!profile) {
4640
+ throw new Error(`Unknown tool '${flagOverride}'. Supported: ${Object.keys(PROFILES).join(", ")}`);
4641
+ }
4642
+ return profile;
4643
+ }
4644
+ const config = readToolConfig();
4645
+ if (config?.tool && PROFILES[config.tool]) {
4646
+ return PROFILES[config.tool];
4647
+ }
4648
+ if (process.stdout.isTTY) {
4649
+ const signals = detectTools(projectRoot);
4650
+ const detected = topDetectedProfile(signals);
4651
+ const top = signals[0];
4652
+ if (top && detected) {
4653
+ ye(`Detected: ${detected.displayName} (${top.reason})`);
4654
+ }
4655
+ const initial = detected?.toolId ?? DEFAULT_TOOL;
4656
+ const choice = await de({
4657
+ message: "Which AI coding tool do you use?",
4658
+ options: Object.values(PROFILES).map((profile) => ({
4659
+ value: profile.toolId,
4660
+ label: profile.displayName,
4661
+ hint: profile.toolId === initial ? "default" : undefined
4662
+ })),
4663
+ initialValue: initial
4664
+ });
4665
+ if (BD(choice)) {
4666
+ ve("Using default (Claude Code).");
4667
+ return PROFILES[DEFAULT_TOOL];
4668
+ }
4669
+ saveToolConfig({ ...config ?? {}, tool: choice });
4670
+ return PROFILES[choice];
4671
+ }
4672
+ return PROFILES[DEFAULT_TOOL];
4673
+ }
4674
+ async function handleToolSwitch(projectRoot, newProfile) {
4675
+ if (!process.stdout.isTTY)
4676
+ return;
4677
+ const cfg = readToolConfig();
4678
+ const acknowledged = new Set(cfg?.acknowledgedOrphans ?? []);
4679
+ const orphans = findOrphanedManifests(projectRoot, newProfile).filter((o) => !acknowledged.has(o.profile.toolId));
4680
+ if (orphans.length === 0)
4681
+ return;
4682
+ for (const orphan of orphans) {
4683
+ const action = await de({
4684
+ message: `Found 10x artifacts from ${orphan.profile.displayName} in ${orphan.profile.manifestDir}/. What should we do?`,
4685
+ options: [
4686
+ {
4687
+ value: "migrate",
4688
+ label: `Migrate to ${newProfile.manifestDir}/`,
4689
+ hint: "recommended"
4690
+ },
4691
+ {
4692
+ value: "delete",
4693
+ label: `Remove 10x artifacts from ${orphan.profile.manifestDir}/`
4694
+ },
4695
+ { value: "keep", label: "Keep both (don't ask again)" }
4696
+ ],
4697
+ initialValue: "migrate"
4698
+ });
4699
+ if (BD(action))
4700
+ continue;
4701
+ if (action === "migrate") {
4702
+ const summary = migrateArtifacts(projectRoot, orphan, newProfile);
4703
+ printMigrationSummary(summary, newProfile);
4704
+ } else if (action === "delete") {
4705
+ const summary = deleteArtifacts(projectRoot, orphan);
4706
+ printMigrationSummary(summary, newProfile);
4707
+ } else {
4708
+ const existing = readToolConfig();
4709
+ const nextAcks = [
4710
+ ...existing?.acknowledgedOrphans ?? [],
4711
+ orphan.profile.toolId
4712
+ ];
4713
+ saveToolConfig({
4714
+ ...existing ?? {},
4715
+ tool: existing?.tool ?? newProfile.toolId,
4716
+ acknowledgedOrphans: nextAcks
4717
+ });
4718
+ }
4719
+ }
4720
+ }
4721
+ function printMigrationSummary(summary, newProfile) {
4722
+ const { skills, prompts, configs } = summary.movedOrRemoved;
4723
+ const verb = summary.action === "migrated" ? "Moved" : "Removed";
4724
+ const target = summary.action === "migrated" ? `to ${newProfile.manifestDir}/` : "";
4725
+ const parts = [];
4726
+ if (skills.length)
4727
+ parts.push(`${skills.length} skill${skills.length === 1 ? "" : "s"}`);
4728
+ if (prompts.length)
4729
+ parts.push(`${prompts.length} prompt${prompts.length === 1 ? "" : "s"}`);
4730
+ if (configs.length)
4731
+ parts.push(`${configs.length} config${configs.length === 1 ? "" : "s"}`);
4732
+ const lines = [];
4733
+ if (parts.length > 0) {
4734
+ lines.push(`${verb} ${parts.join(", ")}${target ? " " + target : ""}.`);
4735
+ }
4736
+ if (summary.sentinelStripped) {
4737
+ lines.push(`Removed 10x block from the old rules file.`);
4738
+ }
4739
+ for (const s of summary.skipped) {
4740
+ lines.push(`Skipped ${s.path} (${s.reason}).`);
4741
+ }
4742
+ if (lines.length > 0)
4743
+ ye(lines.join(`
4744
+ `));
4745
+ }
4746
+
4324
4747
  // src/commands/get.ts
4325
4748
  var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
4326
4749
  function isArtifactType(s) {
@@ -4350,7 +4773,7 @@ async function runGet(ctx, ref, options) {
4350
4773
  }
4351
4774
  const auth = await requireAuth(ctx);
4352
4775
  const course = options.course ?? DEFAULT_COURSE2;
4353
- const profile = await resolveToolProfile(options.tool);
4776
+ const profile = await resolveToolProfile(options.tool, process.cwd());
4354
4777
  const lang = options.lang ?? readToolConfig()?.lang ?? "en";
4355
4778
  if (options.lang) {
4356
4779
  const existing = readToolConfig();
@@ -4370,9 +4793,11 @@ async function runGet(ctx, ref, options) {
4370
4793
  if (isFallback && contentLang) {
4371
4794
  verbose(ctx, `${lang.toUpperCase()} not available for ${parsed.lessonId}, showing ${contentLang.toUpperCase()}.`);
4372
4795
  }
4373
- const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4374
- if (orphanWarning)
4375
- verbose(ctx, orphanWarning);
4796
+ if (!process.stdout.isTTY) {
4797
+ const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4798
+ if (orphanWarning)
4799
+ verbose(ctx, orphanWarning);
4800
+ }
4376
4801
  const isFiltered = options.type !== undefined;
4377
4802
  const bundle = filterBundle(ctx, result.data, options);
4378
4803
  const writeResult = applyBundle(bundle, process.cwd(), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
+ "skills",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: 10x-cli-setup
3
+ description: ALWAYS invoke this skill when the user mentions 10x-cli, @przeprogramowani/10x-cli, the 10xDevs CLI, or the 10xDevs course environment in a setup context. This skill fetches the live README — Claude does not know 10x-cli's current install steps without it. Applies to: installing, updating, reconfiguring for different AI tools (Cursor, Copilot, Claude Code), permission/npm errors, authentication, and onboarding after 10xDevs enrollment. Excludes: developing 10x-cli source code, contributing to the repo, building similar CLIs, or general project setup.
4
+ ---
5
+
6
+ # 10x-cli Setup
7
+
8
+ This skill sets up the `@przeprogramowani/10x-cli` on the user's machine. The core principle is simple: **the README is the single source of truth**. The CLI evolves — version requirements change, new install methods appear, commands get updated. Rather than hardcoding any of that here, this skill tells you *how to work*, and the README tells you *what to do*.
9
+
10
+ ## Step 1: Check if the CLI is already installed
11
+
12
+ Before anything else, check the current state:
13
+
14
+ ```bash
15
+ 10x --version 2>/dev/null || echo "NOT_INSTALLED"
16
+ ```
17
+
18
+ - If a version is printed, the CLI is already installed. Tell the user and ask if they want to update, reconfigure, or troubleshoot.
19
+ - If not installed, proceed to Step 2.
20
+
21
+ This avoids wasting time on prerequisites when the user might just need a config change or re-auth.
22
+
23
+ ## Step 2: Fetch the latest README
24
+
25
+ Retrieve the current README from GitHub — this is the authoritative source for all install steps, prerequisites, commands, and tool configurations:
26
+
27
+ ```
28
+ URL: https://raw.githubusercontent.com/przeprogramowani/10x-cli/refs/heads/master/README.md
29
+ ```
30
+
31
+ Use WebFetch or `curl -sL` to get it. If the fetch fails, tell the user and stop — don't guess at install steps from memory, because they may be outdated.
32
+
33
+ ## Step 3: Build a plan from the README and execute it
34
+
35
+ Read the fetched README and construct a step-by-step setup plan from it. The README contains everything needed: prerequisites, install commands, auth flow, available commands, and tool-specific configuration. Your job is to translate the README into actionable steps for the user's specific situation.
36
+
37
+ The general flow from the README is:
38
+ 1. **Prerequisites** — whatever the README says is required (runtime version, package manager, etc.). Check each one and stop if something is missing.
39
+ 2. **Install** — use the install method described in the README. Verify it worked.
40
+ 3. **Authenticate** — the README describes the auth command and flow. Note: auth is interactive (magic-link email), so the user may need to run it themselves via `! 10x auth` if the shell doesn't support input.
41
+ 4. **Verify** — the README lists a diagnostic command. Run it and review the output with the user.
42
+ 5. **Explore** — show the user how to browse and fetch content using the commands from the README.
43
+ 6. **Tool configuration** — if the user mentioned a specific AI tool (Claude Code, Cursor, etc.), use the README's multi-tool support section to configure it. If not, explain the options and let them choose.
44
+
45
+ Do not hardcode specific version numbers, command flags, or directory paths — read them from the README. This way the skill stays correct even when the CLI changes.
46
+
47
+ ## Important principles
48
+
49
+ - **README over memory.** If you think you know a command or requirement, but the fetched README says something different, follow the README. Always.
50
+ - **Check before installing.** Step 1 exists for a reason — don't reinstall what's already there.
51
+ - **Be interactive.** Confirm with the user before installing global packages or modifying their system. Ask before running `sudo`.
52
+ - **Diagnose before fixing.** If something fails, read the error and the README's guidance before suggesting a fix. Don't just retry blindly.
53
+ - **Stay focused on end-user setup.** This skill is about installing and configuring the published CLI package, not about development/contributing workflows.