@przeprogramowani/10x-cli 0.5.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.
Files changed (2) hide show
  1. package/dist/index.mjs +561 -137
  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.5.0",
2274
+ version: "0.6.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -2324,6 +2324,9 @@ var package_default = {
2324
2324
  }
2325
2325
  };
2326
2326
 
2327
+ // node_modules/@clack/prompts/dist/index.mjs
2328
+ import { stripVTControlCharacters as T2 } from "node:util";
2329
+
2327
2330
  // node_modules/@clack/core/dist/index.mjs
2328
2331
  var import_sisteransi = __toESM(require_src(), 1);
2329
2332
  var import_picocolors = __toESM(require_picocolors(), 1);
@@ -2833,6 +2836,21 @@ ${import_picocolors2.default.cyan(m2)}
2833
2836
  }
2834
2837
  } }).prompt();
2835
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
+ };
2836
2854
  var ve = (s = "") => {
2837
2855
  process.stdout.write(`${import_picocolors2.default.gray(m2)} ${import_picocolors2.default.red(s)}
2838
2856
 
@@ -2927,6 +2945,20 @@ function readAuth() {
2927
2945
  return null;
2928
2946
  }
2929
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
+ }
2930
2962
  function saveAuth(data) {
2931
2963
  const dir = configDir();
2932
2964
  if (!existsSync(dir)) {
@@ -2937,17 +2969,7 @@ function saveAuth(data) {
2937
2969
  chmodSync(dir, 448);
2938
2970
  } catch {}
2939
2971
  }
2940
- const file = authFilePath();
2941
- const tmp = `${file}.tmp`;
2942
- rmSync(tmp, { force: true });
2943
- writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
2944
- `, { mode: 384 });
2945
- if (process.platform !== "win32") {
2946
- try {
2947
- chmodSync(tmp, 384);
2948
- } catch {}
2949
- }
2950
- renameSync(tmp, file);
2972
+ writeJsonAtomic(authFilePath(), data, { mode: 384 });
2951
2973
  }
2952
2974
  function deleteAuth() {
2953
2975
  const file = authFilePath();
@@ -2967,6 +2989,9 @@ function readToolConfig() {
2967
2989
  const parsed = JSON.parse(raw);
2968
2990
  if (typeof parsed.tool !== "string")
2969
2991
  return null;
2992
+ if (parsed.acknowledgedOrphans !== undefined && (!Array.isArray(parsed.acknowledgedOrphans) || !parsed.acknowledgedOrphans.every((x2) => typeof x2 === "string"))) {
2993
+ delete parsed.acknowledgedOrphans;
2994
+ }
2970
2995
  return parsed;
2971
2996
  } catch {
2972
2997
  return null;
@@ -2977,9 +3002,7 @@ function saveToolConfig(config) {
2977
3002
  if (!existsSync(dir)) {
2978
3003
  mkdirSync(dir, { recursive: true, mode: 448 });
2979
3004
  }
2980
- const file = toolConfigPath();
2981
- writeFileSync(file, `${JSON.stringify(config, null, 2)}
2982
- `);
3005
+ writeJsonAtomic(toolConfigPath(), config);
2983
3006
  }
2984
3007
 
2985
3008
  // src/lib/api-client.ts
@@ -3917,7 +3940,71 @@ function parseModuleRef(raw) {
3917
3940
  return module;
3918
3941
  }
3919
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
+
3920
4005
  // src/lib/tool-profile.ts
4006
+ var SENTINEL_BEGIN = "<!-- BEGIN @przeprogramowani/10x-cli -->";
4007
+ var SENTINEL_END = "<!-- END @przeprogramowani/10x-cli -->";
3921
4008
  var PROFILES = {
3922
4009
  "claude-code": {
3923
4010
  toolId: "claude-code",
@@ -3927,8 +4014,8 @@ var PROFILES = {
3927
4014
  configPath: (n) => `.claude/config-templates/${n}`,
3928
4015
  rulesFile: "CLAUDE.md",
3929
4016
  manifestDir: ".claude",
3930
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3931
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4017
+ sentinelBegin: SENTINEL_BEGIN,
4018
+ sentinelEnd: SENTINEL_END
3932
4019
  },
3933
4020
  cursor: {
3934
4021
  toolId: "cursor",
@@ -3938,8 +4025,8 @@ var PROFILES = {
3938
4025
  configPath: (n) => `.cursor/config-templates/${n}`,
3939
4026
  rulesFile: ".cursor/rules/10x-course.mdc",
3940
4027
  manifestDir: ".cursor",
3941
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3942
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4028
+ sentinelBegin: SENTINEL_BEGIN,
4029
+ sentinelEnd: SENTINEL_END
3943
4030
  },
3944
4031
  copilot: {
3945
4032
  toolId: "copilot",
@@ -3949,8 +4036,8 @@ var PROFILES = {
3949
4036
  configPath: (n) => `.github/config-templates/${n}`,
3950
4037
  rulesFile: ".github/copilot-instructions.md",
3951
4038
  manifestDir: ".github",
3952
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3953
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4039
+ sentinelBegin: SENTINEL_BEGIN,
4040
+ sentinelEnd: SENTINEL_END
3954
4041
  },
3955
4042
  codex: {
3956
4043
  toolId: "codex",
@@ -3960,8 +4047,8 @@ var PROFILES = {
3960
4047
  configPath: (n) => `.agents/config-templates/${n}`,
3961
4048
  rulesFile: "AGENTS.md",
3962
4049
  manifestDir: ".agents",
3963
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3964
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4050
+ sentinelBegin: SENTINEL_BEGIN,
4051
+ sentinelEnd: SENTINEL_END
3965
4052
  },
3966
4053
  generic: {
3967
4054
  toolId: "generic",
@@ -3971,116 +4058,143 @@ var PROFILES = {
3971
4058
  configPath: (n) => `.ai/config-templates/${n}`,
3972
4059
  rulesFile: "AGENTS.md",
3973
4060
  manifestDir: ".ai",
3974
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3975
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4061
+ sentinelBegin: SENTINEL_BEGIN,
4062
+ sentinelEnd: SENTINEL_END
3976
4063
  }
3977
4064
  };
3978
4065
  var DEFAULT_TOOL = "claude-code";
3979
4066
 
3980
- // src/lib/tool-prompt.ts
3981
- async function resolveToolProfile(flagOverride) {
3982
- if (flagOverride) {
3983
- const profile = PROFILES[flagOverride];
3984
- if (!profile) {
3985
- throw new Error(`Unknown tool '${flagOverride}'. Supported: ${Object.keys(PROFILES).join(", ")}`);
3986
- }
3987
- return profile;
3988
- }
3989
- const config = readToolConfig();
3990
- if (config?.tool && PROFILES[config.tool]) {
3991
- 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
+ });
3992
4111
  }
3993
- if (process.stdout.isTTY) {
3994
- const choice = await de({
3995
- message: "Which AI coding tool do you use?",
3996
- options: Object.values(PROFILES).map((profile) => ({
3997
- value: profile.toolId,
3998
- label: profile.displayName,
3999
- hint: profile.toolId === DEFAULT_TOOL ? "default" : undefined
4000
- })),
4001
- 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"
4002
4117
  });
4003
- if (BD(choice)) {
4004
- ve("Using default (Claude Code).");
4005
- return PROFILES[DEFAULT_TOOL];
4006
- }
4007
- saveToolConfig({ tool: choice });
4008
- 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)" });
4009
4133
  }
4010
- 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;
4011
4151
  }
4012
4152
 
4013
- // src/lib/writer.ts
4153
+ // src/lib/tool-switch.ts
4014
4154
  import {
4015
- existsSync as existsSync4,
4016
- mkdirSync as mkdirSync4,
4017
- readFileSync as readFileSync3,
4018
- rmSync as rmSync2,
4019
- 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
4020
4164
  } from "node:fs";
4021
- import { dirname as dirname3, join as join4 } from "node:path";
4165
+ import { dirname as dirname4, join as join6 } from "node:path";
4022
4166
 
4023
- // src/lib/manifest.ts
4024
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
4025
- import { dirname as dirname2, join as join3 } from "node:path";
4026
- var MANIFEST_FILENAME = ".10x-cli-manifest.json";
4027
- var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
4028
- function readManifest(dir) {
4029
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4030
- if (!existsSync3(manifestPath))
4031
- return null;
4032
- let raw;
4167
+ // src/lib/fs-utils.ts
4168
+ import { readFileSync as readFileSync3 } from "node:fs";
4169
+ function readFileOrNull(path) {
4033
4170
  try {
4034
- raw = readFileSync2(manifestPath, "utf8");
4171
+ return readFileSync3(path, "utf8");
4035
4172
  } catch {
4036
4173
  return null;
4037
4174
  }
4038
- let parsed;
4039
- try {
4040
- parsed = JSON.parse(raw);
4041
- } catch {
4042
- return null;
4043
- }
4044
- if (!isManifest(parsed))
4045
- return null;
4046
- return parsed;
4047
- }
4048
- function writeManifest(dir, manifest) {
4049
- if (!existsSync3(dir)) {
4050
- mkdirSync3(dir, { recursive: true });
4051
- }
4052
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4053
- mkdirSync3(dirname2(manifestPath), { recursive: true });
4054
- writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
4055
- `);
4056
- }
4057
- function isManifest(value) {
4058
- if (typeof value !== "object" || value === null)
4059
- return false;
4060
- const v2 = value;
4061
- if (v2["package"] !== CLI_PACKAGE_NAME)
4062
- return false;
4063
- if (typeof v2["version"] !== "string")
4064
- return false;
4065
- if (typeof v2["lastApplied"] !== "string")
4066
- return false;
4067
- if (typeof v2["lessonId"] !== "string")
4068
- return false;
4069
- if (typeof v2["course"] !== "string")
4070
- return false;
4071
- const files = v2["files"];
4072
- if (typeof files !== "object" || files === null)
4073
- return false;
4074
- const f2 = files;
4075
- return isStringArray(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4076
- }
4077
- function isStringArray(value) {
4078
- return Array.isArray(value) && value.every((item) => typeof item === "string");
4079
4175
  }
4080
4176
 
4081
4177
  // src/lib/sentinel-migration.ts
4082
4178
  var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
4083
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
+ }
4084
4198
  function applyRulesBlockWithMarkers(existingContent, rulesBody, beginMarker, endMarker) {
4085
4199
  for (const marker of [OLD_BEGIN, OLD_END, beginMarker, endMarker]) {
4086
4200
  if (rulesBody.includes(marker)) {
@@ -4128,6 +4242,14 @@ function stripBlock(content, begin, end, label, warnings) {
4128
4242
  }
4129
4243
 
4130
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";
4131
4253
  var CLI_VERSION = package_default.version;
4132
4254
  var DEFAULT_COURSE = "10xdevs3";
4133
4255
  function applyBundle(bundle, projectRoot, options = {}) {
@@ -4135,7 +4257,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
4135
4257
  const partial = options.partial === true;
4136
4258
  const course = options.course ?? DEFAULT_COURSE;
4137
4259
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4138
- const manifestDir = join4(projectRoot, profile.manifestDir);
4260
+ const manifestDir = join5(projectRoot, profile.manifestDir);
4139
4261
  const prevManifest = readManifest(manifestDir);
4140
4262
  for (const skill of bundle.skills)
4141
4263
  assertSafeName(skill.name, "skill");
@@ -4144,7 +4266,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
4144
4266
  for (const config of bundle.configs)
4145
4267
  assertSafeName(config.name, "config");
4146
4268
  const skills = bundle.skills.map((skill) => {
4147
- const target = join4(projectRoot, profile.skillPath(skill.name));
4269
+ const target = join5(projectRoot, profile.skillPath(skill.name));
4148
4270
  const action = computeFileAction(target, skill.content);
4149
4271
  if (!dryRun && action !== "unchanged") {
4150
4272
  writeFileAt(target, skill.content);
@@ -4152,14 +4274,14 @@ function applyBundle(bundle, projectRoot, options = {}) {
4152
4274
  return { name: skill.name, path: target, action };
4153
4275
  });
4154
4276
  const prompts = bundle.prompts.map((prompt) => {
4155
- const target = join4(projectRoot, profile.promptPath(prompt.name));
4277
+ const target = join5(projectRoot, profile.promptPath(prompt.name));
4156
4278
  const action = computeFileAction(target, prompt.content);
4157
4279
  if (!dryRun && action !== "unchanged") {
4158
4280
  writeFileAt(target, prompt.content);
4159
4281
  }
4160
4282
  return { name: prompt.name, path: target, action };
4161
4283
  });
4162
- const rulesFilePath = join4(projectRoot, profile.rulesFile);
4284
+ const rulesFilePath = join5(projectRoot, profile.rulesFile);
4163
4285
  const existingRules = readFileOrEmpty(rulesFilePath);
4164
4286
  let rulesAction;
4165
4287
  if (bundle.rules.length === 0) {
@@ -4181,8 +4303,8 @@ function applyBundle(bundle, projectRoot, options = {}) {
4181
4303
  }
4182
4304
  }
4183
4305
  const configs = bundle.configs.map((config) => {
4184
- const target = join4(projectRoot, profile.configPath(config.name));
4185
- const action = existsSync4(target) ? "skipped" : "created";
4306
+ const target = join5(projectRoot, profile.configPath(config.name));
4307
+ const action = existsSync5(target) ? "skipped" : "created";
4186
4308
  if (!dryRun && action === "created") {
4187
4309
  writeFileAt(target, config.content);
4188
4310
  }
@@ -4221,21 +4343,21 @@ function applyBundle(bundle, projectRoot, options = {}) {
4221
4343
  };
4222
4344
  }
4223
4345
  function computeFileAction(filePath, newContent) {
4224
- if (!existsSync4(filePath))
4346
+ if (!existsSync5(filePath))
4225
4347
  return "created";
4226
4348
  let current;
4227
4349
  try {
4228
- current = readFileSync3(filePath, "utf8");
4350
+ current = readFileSync4(filePath, "utf8");
4229
4351
  } catch {
4230
4352
  return "updated";
4231
4353
  }
4232
4354
  return current === newContent ? "unchanged" : "updated";
4233
4355
  }
4234
4356
  function readFileOrEmpty(filePath) {
4235
- if (!existsSync4(filePath))
4357
+ if (!existsSync5(filePath))
4236
4358
  return "";
4237
4359
  try {
4238
- return readFileSync3(filePath, "utf8");
4360
+ return readFileSync4(filePath, "utf8");
4239
4361
  } catch {
4240
4362
  return "";
4241
4363
  }
@@ -4257,7 +4379,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4257
4379
  continue;
4258
4380
  if (!isSafeName(skillName))
4259
4381
  continue;
4260
- const skillTarget = join4(projectRoot, profile.skillPath(skillName));
4382
+ const skillTarget = join5(projectRoot, profile.skillPath(skillName));
4261
4383
  removed.skills.push({ name: skillName, path: dirname3(skillTarget) });
4262
4384
  }
4263
4385
  for (const promptFile of prevManifest.files.prompts) {
@@ -4268,7 +4390,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4268
4390
  const promptName = promptFile.replace(/\.md$/, "");
4269
4391
  removed.prompts.push({
4270
4392
  name: promptFile,
4271
- path: join4(projectRoot, profile.promptPath(promptName))
4393
+ path: join5(projectRoot, profile.promptPath(promptName))
4272
4394
  });
4273
4395
  }
4274
4396
  for (const configFile of prevManifest.files.configs) {
@@ -4278,23 +4400,33 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4278
4400
  continue;
4279
4401
  removed.configs.push({
4280
4402
  name: configFile,
4281
- path: join4(projectRoot, profile.configPath(configFile))
4403
+ path: join5(projectRoot, profile.configPath(configFile))
4282
4404
  });
4283
4405
  }
4284
4406
  return removed;
4285
4407
  }
4286
- function detectOrphanedArtifacts(projectRoot, currentProfile) {
4408
+ function findOrphanedManifests(projectRoot, currentProfile) {
4409
+ const out = [];
4287
4410
  for (const profile of Object.values(PROFILES)) {
4288
4411
  if (profile.toolId === currentProfile.toolId)
4289
4412
  continue;
4290
- const otherManifest = join4(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
4291
- if (existsSync4(otherManifest)) {
4292
- return `Found existing 10x artifacts in ${profile.manifestDir}/ from ${profile.displayName}.
4293
- Manually remove ${profile.manifestDir}/ if you no longer need it.
4294
- Your new artifacts will be written to ${currentProfile.manifestDir}/`;
4295
- }
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 });
4296
4420
  }
4297
- 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}/`;
4298
4430
  }
4299
4431
  function assertSafeName(name, kind) {
4300
4432
  if (!isSafeName(name)) {
@@ -4322,6 +4454,296 @@ function isSafeName(name) {
4322
4454
  return true;
4323
4455
  }
4324
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
+
4325
4747
  // src/commands/get.ts
4326
4748
  var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
4327
4749
  function isArtifactType(s) {
@@ -4351,7 +4773,7 @@ async function runGet(ctx, ref, options) {
4351
4773
  }
4352
4774
  const auth = await requireAuth(ctx);
4353
4775
  const course = options.course ?? DEFAULT_COURSE2;
4354
- const profile = await resolveToolProfile(options.tool);
4776
+ const profile = await resolveToolProfile(options.tool, process.cwd());
4355
4777
  const lang = options.lang ?? readToolConfig()?.lang ?? "en";
4356
4778
  if (options.lang) {
4357
4779
  const existing = readToolConfig();
@@ -4371,9 +4793,11 @@ async function runGet(ctx, ref, options) {
4371
4793
  if (isFallback && contentLang) {
4372
4794
  verbose(ctx, `${lang.toUpperCase()} not available for ${parsed.lessonId}, showing ${contentLang.toUpperCase()}.`);
4373
4795
  }
4374
- const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4375
- if (orphanWarning)
4376
- verbose(ctx, orphanWarning);
4796
+ if (!process.stdout.isTTY) {
4797
+ const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4798
+ if (orphanWarning)
4799
+ verbose(ctx, orphanWarning);
4800
+ }
4377
4801
  const isFiltered = options.type !== undefined;
4378
4802
  const bundle = filterBundle(ctx, result.data, options);
4379
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.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",