@przeprogramowani/10x-cli 0.5.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 +741 -159
  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: "1.1.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,170 +3940,281 @@ 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
+ var MANIFEST_VERSION = 2;
3953
+ function readManifest(dir) {
3954
+ const manifestPath = join3(dir, MANIFEST_FILENAME);
3955
+ if (!existsSync3(manifestPath))
3956
+ return null;
3957
+ let raw;
3958
+ try {
3959
+ raw = readFileSync2(manifestPath, "utf8");
3960
+ } catch {
3961
+ return null;
3962
+ }
3963
+ let parsed;
3964
+ try {
3965
+ parsed = JSON.parse(raw);
3966
+ } catch {
3967
+ return null;
3968
+ }
3969
+ if (!isManifest(parsed))
3970
+ return null;
3971
+ return parsed;
3972
+ }
3973
+ function writeManifest(dir, manifest) {
3974
+ if (!existsSync3(dir)) {
3975
+ mkdirSync3(dir, { recursive: true });
3976
+ }
3977
+ const manifestPath = join3(dir, MANIFEST_FILENAME);
3978
+ mkdirSync3(dirname2(manifestPath), { recursive: true });
3979
+ writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
3980
+ `);
3981
+ }
3982
+ function isManifest(value) {
3983
+ if (typeof value !== "object" || value === null)
3984
+ return false;
3985
+ const v2 = value;
3986
+ if (v2["package"] !== CLI_PACKAGE_NAME)
3987
+ return false;
3988
+ if (typeof v2["version"] !== "string")
3989
+ return false;
3990
+ if (v2["manifestVersion"] !== MANIFEST_VERSION)
3991
+ return false;
3992
+ if (typeof v2["lastApplied"] !== "string")
3993
+ return false;
3994
+ if (typeof v2["lessonId"] !== "string")
3995
+ return false;
3996
+ if (typeof v2["course"] !== "string")
3997
+ return false;
3998
+ const files = v2["files"];
3999
+ if (typeof files !== "object" || files === null)
4000
+ return false;
4001
+ const f2 = files;
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;
4015
+ }
4016
+ function isStringArray(value) {
4017
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
4018
+ }
4019
+
3920
4020
  // src/lib/tool-profile.ts
4021
+ var SENTINEL_BEGIN = "<!-- BEGIN @przeprogramowani/10x-cli -->";
4022
+ var SENTINEL_END = "<!-- END @przeprogramowani/10x-cli -->";
3921
4023
  var PROFILES = {
3922
4024
  "claude-code": {
3923
4025
  toolId: "claude-code",
3924
4026
  displayName: "Claude Code",
3925
4027
  skillPath: (n) => `.claude/skills/${n}/SKILL.md`,
4028
+ skillDir: (n) => `.claude/skills/${n}`,
3926
4029
  promptPath: (n) => `.claude/prompts/${n}.md`,
3927
4030
  configPath: (n) => `.claude/config-templates/${n}`,
3928
4031
  rulesFile: "CLAUDE.md",
3929
4032
  manifestDir: ".claude",
3930
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3931
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4033
+ sentinelBegin: SENTINEL_BEGIN,
4034
+ sentinelEnd: SENTINEL_END
3932
4035
  },
3933
4036
  cursor: {
3934
4037
  toolId: "cursor",
3935
4038
  displayName: "Cursor",
3936
4039
  skillPath: (n) => `.cursor/skills/${n}/SKILL.md`,
4040
+ skillDir: (n) => `.cursor/skills/${n}`,
3937
4041
  promptPath: (n) => `.cursor/prompts/${n}.md`,
3938
4042
  configPath: (n) => `.cursor/config-templates/${n}`,
3939
4043
  rulesFile: ".cursor/rules/10x-course.mdc",
3940
4044
  manifestDir: ".cursor",
3941
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3942
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4045
+ sentinelBegin: SENTINEL_BEGIN,
4046
+ sentinelEnd: SENTINEL_END
3943
4047
  },
3944
4048
  copilot: {
3945
4049
  toolId: "copilot",
3946
4050
  displayName: "GitHub Copilot",
3947
4051
  skillPath: (n) => `.github/skills/${n}/SKILL.md`,
4052
+ skillDir: (n) => `.github/skills/${n}`,
3948
4053
  promptPath: (n) => `.github/prompts/${n}.md`,
3949
4054
  configPath: (n) => `.github/config-templates/${n}`,
3950
4055
  rulesFile: ".github/copilot-instructions.md",
3951
4056
  manifestDir: ".github",
3952
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3953
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4057
+ sentinelBegin: SENTINEL_BEGIN,
4058
+ sentinelEnd: SENTINEL_END
3954
4059
  },
3955
4060
  codex: {
3956
4061
  toolId: "codex",
3957
4062
  displayName: "Codex CLI",
3958
4063
  skillPath: (n) => `.agents/skills/${n}/SKILL.md`,
4064
+ skillDir: (n) => `.agents/skills/${n}`,
3959
4065
  promptPath: (n) => `.agents/prompts/${n}.md`,
3960
4066
  configPath: (n) => `.agents/config-templates/${n}`,
3961
4067
  rulesFile: "AGENTS.md",
3962
4068
  manifestDir: ".agents",
3963
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3964
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4069
+ sentinelBegin: SENTINEL_BEGIN,
4070
+ sentinelEnd: SENTINEL_END
3965
4071
  },
3966
4072
  generic: {
3967
4073
  toolId: "generic",
3968
4074
  displayName: "Other / Generic",
3969
4075
  skillPath: (n) => `.ai/skills/${n}/SKILL.md`,
4076
+ skillDir: (n) => `.ai/skills/${n}`,
3970
4077
  promptPath: (n) => `.ai/prompts/${n}.md`,
3971
4078
  configPath: (n) => `.ai/config-templates/${n}`,
3972
4079
  rulesFile: "AGENTS.md",
3973
4080
  manifestDir: ".ai",
3974
- sentinelBegin: "<!-- BEGIN @przeprogramowani/10x-cli -->",
3975
- sentinelEnd: "<!-- END @przeprogramowani/10x-cli -->"
4081
+ sentinelBegin: SENTINEL_BEGIN,
4082
+ sentinelEnd: SENTINEL_END
3976
4083
  }
3977
4084
  };
3978
4085
  var DEFAULT_TOOL = "claude-code";
3979
4086
 
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];
4087
+ // src/lib/tool-detect.ts
4088
+ function detectTools(projectRoot) {
4089
+ const hit = (rel) => existsSync4(join4(projectRoot, rel));
4090
+ const signals = [];
4091
+ if (hit(".claude/" + MANIFEST_FILENAME)) {
4092
+ signals.push({
4093
+ profileId: "claude-code",
4094
+ confidence: "strong",
4095
+ reason: ".claude/.10x-cli-manifest.json"
4096
+ });
4097
+ } else if (hit(".claude")) {
4098
+ signals.push({ profileId: "claude-code", confidence: "medium", reason: ".claude/ directory" });
4099
+ } else if (hit("CLAUDE.md")) {
4100
+ signals.push({ profileId: "claude-code", confidence: "weak", reason: "CLAUDE.md" });
4101
+ }
4102
+ if (hit(".cursor/" + MANIFEST_FILENAME)) {
4103
+ signals.push({
4104
+ profileId: "cursor",
4105
+ confidence: "strong",
4106
+ reason: ".cursor/.10x-cli-manifest.json"
4107
+ });
4108
+ } else if (hit(".cursor/rules")) {
4109
+ signals.push({ profileId: "cursor", confidence: "strong", reason: ".cursor/rules/" });
4110
+ } else if (hit(".cursor")) {
4111
+ signals.push({ profileId: "cursor", confidence: "medium", reason: ".cursor/ directory" });
4112
+ }
4113
+ if (hit(".github/" + MANIFEST_FILENAME)) {
4114
+ signals.push({
4115
+ profileId: "copilot",
4116
+ confidence: "strong",
4117
+ reason: ".github/.10x-cli-manifest.json"
4118
+ });
4119
+ } else if (hit(".github/copilot-instructions.md")) {
4120
+ signals.push({
4121
+ profileId: "copilot",
4122
+ confidence: "strong",
4123
+ reason: ".github/copilot-instructions.md"
4124
+ });
4125
+ } else if (hit(".github/skills") || hit(".github/prompts")) {
4126
+ signals.push({
4127
+ profileId: "copilot",
4128
+ confidence: "medium",
4129
+ reason: ".github/skills/ or .github/prompts/"
4130
+ });
3992
4131
  }
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
4132
+ if (hit(".agents/" + MANIFEST_FILENAME)) {
4133
+ signals.push({
4134
+ profileId: "codex",
4135
+ confidence: "strong",
4136
+ reason: ".agents/.10x-cli-manifest.json"
4002
4137
  });
4003
- if (BD(choice)) {
4004
- ve("Using default (Claude Code).");
4005
- return PROFILES[DEFAULT_TOOL];
4006
- }
4007
- saveToolConfig({ tool: choice });
4008
- return PROFILES[choice];
4138
+ } else if (hit(".agents")) {
4139
+ signals.push({ profileId: "codex", confidence: "strong", reason: ".agents/ directory" });
4140
+ } else if (hit("AGENTS.md") && !hit(".ai")) {
4141
+ signals.push({ profileId: "codex", confidence: "medium", reason: "AGENTS.md" });
4142
+ }
4143
+ if (hit(".ai/" + MANIFEST_FILENAME)) {
4144
+ signals.push({
4145
+ profileId: "generic",
4146
+ confidence: "strong",
4147
+ reason: ".ai/.10x-cli-manifest.json"
4148
+ });
4149
+ } else if (hit(".ai")) {
4150
+ signals.push({ profileId: "generic", confidence: "strong", reason: ".ai/ directory" });
4151
+ } else if (hit("AGENTS.md") && !hit(".agents")) {
4152
+ signals.push({ profileId: "generic", confidence: "weak", reason: "AGENTS.md (generic fallback)" });
4009
4153
  }
4010
- return PROFILES[DEFAULT_TOOL];
4154
+ return rankSignals(signals);
4155
+ }
4156
+ var CONFIDENCE_ORDER = { strong: 3, medium: 2, weak: 1 };
4157
+ var PROFILE_ORDER = ["claude-code", "cursor", "copilot", "codex", "generic"];
4158
+ function rankSignals(signals) {
4159
+ return [...signals].sort((a2, b2) => {
4160
+ const c2 = CONFIDENCE_ORDER[b2.confidence] - CONFIDENCE_ORDER[a2.confidence];
4161
+ if (c2 !== 0)
4162
+ return c2;
4163
+ return PROFILE_ORDER.indexOf(a2.profileId) - PROFILE_ORDER.indexOf(b2.profileId);
4164
+ });
4165
+ }
4166
+ function topDetectedProfile(signals) {
4167
+ const top = signals[0];
4168
+ if (!top)
4169
+ return null;
4170
+ return PROFILES[top.profileId] ?? null;
4011
4171
  }
4012
4172
 
4013
- // src/lib/writer.ts
4173
+ // src/lib/tool-switch.ts
4014
4174
  import {
4015
- existsSync as existsSync4,
4016
- mkdirSync as mkdirSync4,
4017
- readFileSync as readFileSync3,
4018
- rmSync as rmSync2,
4019
- writeFileSync as writeFileSync3
4175
+ existsSync as existsSync6,
4176
+ lstatSync,
4177
+ mkdirSync as mkdirSync5,
4178
+ readFileSync as readFileSync5,
4179
+ renameSync as renameSync2,
4180
+ rmdirSync as rmdirSync2,
4181
+ rmSync as rmSync3,
4182
+ statSync,
4183
+ writeFileSync as writeFileSync4
4020
4184
  } from "node:fs";
4021
- import { dirname as dirname3, join as join4 } from "node:path";
4185
+ import { dirname as dirname4, join as join6 } from "node:path";
4022
4186
 
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;
4033
- try {
4034
- raw = readFileSync2(manifestPath, "utf8");
4035
- } catch {
4036
- return null;
4037
- }
4038
- let parsed;
4187
+ // src/lib/fs-utils.ts
4188
+ import { readFileSync as readFileSync3 } from "node:fs";
4189
+ function readFileOrNull(path) {
4039
4190
  try {
4040
- parsed = JSON.parse(raw);
4191
+ return readFileSync3(path, "utf8");
4041
4192
  } catch {
4042
4193
  return null;
4043
4194
  }
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
4195
  }
4080
4196
 
4081
4197
  // src/lib/sentinel-migration.ts
4082
4198
  var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
4083
4199
  var OLD_END = "<!-- END @przeprogramowani/10x-toolkit -->";
4200
+ function removeRulesBlockWithMarkers(existing, begin, end) {
4201
+ const beginIdx = existing.indexOf(begin);
4202
+ const endIdx = existing.indexOf(end);
4203
+ if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
4204
+ return { content: existing, removed: false };
4205
+ }
4206
+ const beforeRaw = existing.slice(0, beginIdx);
4207
+ const afterRaw = existing.slice(endIdx + end.length);
4208
+ const before = beforeRaw.replace(/[\r\n]*$/, "");
4209
+ const after = afterRaw.replace(/^[\r\n]*/, "");
4210
+ const joiner = before.length > 0 && after.length > 0 ? `
4211
+
4212
+ ` : "";
4213
+ const combined = before + joiner + after;
4214
+ const content = combined.length === 0 ? "" : combined.replace(/[\r\n]*$/, `
4215
+ `);
4216
+ return { content, removed: true };
4217
+ }
4084
4218
  function applyRulesBlockWithMarkers(existingContent, rulesBody, beginMarker, endMarker) {
4085
4219
  for (const marker of [OLD_BEGIN, OLD_END, beginMarker, endMarker]) {
4086
4220
  if (rulesBody.includes(marker)) {
@@ -4128,6 +4262,17 @@ function stripBlock(content, begin, end, label, warnings) {
4128
4262
  }
4129
4263
 
4130
4264
  // src/lib/writer.ts
4265
+ import {
4266
+ chmodSync as chmodSync2,
4267
+ existsSync as existsSync5,
4268
+ mkdirSync as mkdirSync4,
4269
+ readFileSync as readFileSync4,
4270
+ readdirSync,
4271
+ rmdirSync,
4272
+ rmSync as rmSync2,
4273
+ writeFileSync as writeFileSync3
4274
+ } from "node:fs";
4275
+ import { dirname as dirname3, join as join5 } from "node:path";
4131
4276
  var CLI_VERSION = package_default.version;
4132
4277
  var DEFAULT_COURSE = "10xdevs3";
4133
4278
  function applyBundle(bundle, projectRoot, options = {}) {
@@ -4135,31 +4280,42 @@ function applyBundle(bundle, projectRoot, options = {}) {
4135
4280
  const partial = options.partial === true;
4136
4281
  const course = options.course ?? DEFAULT_COURSE;
4137
4282
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4138
- const manifestDir = join4(projectRoot, profile.manifestDir);
4283
+ const manifestDir = join5(projectRoot, profile.manifestDir);
4139
4284
  const prevManifest = readManifest(manifestDir);
4140
- for (const skill of bundle.skills)
4285
+ for (const skill of bundle.skills) {
4141
4286
  assertSafeName(skill.name, "skill");
4287
+ for (const file of skill.files) {
4288
+ assertSafeSkillFilePath(file.path, skill.name);
4289
+ }
4290
+ }
4142
4291
  for (const prompt of bundle.prompts)
4143
4292
  assertSafeName(prompt.name, "prompt");
4144
4293
  for (const config of bundle.configs)
4145
4294
  assertSafeName(config.name, "config");
4146
4295
  const skills = bundle.skills.map((skill) => {
4147
- const target = join4(projectRoot, profile.skillPath(skill.name));
4148
- const action = computeFileAction(target, skill.content);
4149
- if (!dryRun && action !== "unchanged") {
4150
- writeFileAt(target, skill.content);
4151
- }
4152
- 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 };
4153
4309
  });
4154
4310
  const prompts = bundle.prompts.map((prompt) => {
4155
- const target = join4(projectRoot, profile.promptPath(prompt.name));
4311
+ const target = join5(projectRoot, profile.promptPath(prompt.name));
4156
4312
  const action = computeFileAction(target, prompt.content);
4157
4313
  if (!dryRun && action !== "unchanged") {
4158
4314
  writeFileAt(target, prompt.content);
4159
4315
  }
4160
4316
  return { name: prompt.name, path: target, action };
4161
4317
  });
4162
- const rulesFilePath = join4(projectRoot, profile.rulesFile);
4318
+ const rulesFilePath = join5(projectRoot, profile.rulesFile);
4163
4319
  const existingRules = readFileOrEmpty(rulesFilePath);
4164
4320
  let rulesAction;
4165
4321
  if (bundle.rules.length === 0) {
@@ -4181,8 +4337,8 @@ function applyBundle(bundle, projectRoot, options = {}) {
4181
4337
  }
4182
4338
  }
4183
4339
  const configs = bundle.configs.map((config) => {
4184
- const target = join4(projectRoot, profile.configPath(config.name));
4185
- const action = existsSync4(target) ? "skipped" : "created";
4340
+ const target = join5(projectRoot, profile.configPath(config.name));
4341
+ const action = existsSync5(target) ? "skipped" : "created";
4186
4342
  if (!dryRun && action === "created") {
4187
4343
  writeFileAt(target, config.content);
4188
4344
  }
@@ -4190,8 +4346,12 @@ function applyBundle(bundle, projectRoot, options = {}) {
4190
4346
  });
4191
4347
  if (!dryRun && !partial) {
4192
4348
  const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4193
- for (const entry of removed.skills)
4349
+ for (const entry of removed.skillDirs)
4194
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
+ }
4195
4355
  for (const entry of removed.prompts)
4196
4356
  rmSync2(entry.path, { force: true });
4197
4357
  for (const entry of removed.configs)
@@ -4201,12 +4361,13 @@ function applyBundle(bundle, projectRoot, options = {}) {
4201
4361
  const nextManifest = {
4202
4362
  package: CLI_PACKAGE_NAME,
4203
4363
  version: CLI_VERSION,
4364
+ manifestVersion: MANIFEST_VERSION,
4204
4365
  lastApplied: new Date().toISOString(),
4205
4366
  lessonId: bundle.lessonId,
4206
4367
  course,
4207
4368
  tool: profile.toolId,
4208
4369
  files: {
4209
- skills: bundle.skills.map((s) => s.name),
4370
+ skills: Object.fromEntries(bundle.skills.map((s) => [s.name, { files: s.files.map((f2) => f2.path) }])),
4210
4371
  prompts: bundle.prompts.map((p2) => `${p2.name}.md`),
4211
4372
  configs: bundle.configs.map((c2) => c2.name)
4212
4373
  }
@@ -4221,21 +4382,21 @@ function applyBundle(bundle, projectRoot, options = {}) {
4221
4382
  };
4222
4383
  }
4223
4384
  function computeFileAction(filePath, newContent) {
4224
- if (!existsSync4(filePath))
4385
+ if (!existsSync5(filePath))
4225
4386
  return "created";
4226
4387
  let current;
4227
4388
  try {
4228
- current = readFileSync3(filePath, "utf8");
4389
+ current = readFileSync4(filePath, "utf8");
4229
4390
  } catch {
4230
4391
  return "updated";
4231
4392
  }
4232
4393
  return current === newContent ? "unchanged" : "updated";
4233
4394
  }
4234
4395
  function readFileOrEmpty(filePath) {
4235
- if (!existsSync4(filePath))
4396
+ if (!existsSync5(filePath))
4236
4397
  return "";
4237
4398
  try {
4238
- return readFileSync3(filePath, "utf8");
4399
+ return readFileSync4(filePath, "utf8");
4239
4400
  } catch {
4240
4401
  return "";
4241
4402
  }
@@ -4245,20 +4406,43 @@ function writeFileAt(filePath, content) {
4245
4406
  writeFileSync3(filePath, content);
4246
4407
  }
4247
4408
  function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4248
- const empty = { skills: [], prompts: [], configs: [] };
4409
+ const empty = {
4410
+ skillDirs: [],
4411
+ skillFiles: [],
4412
+ prompts: [],
4413
+ configs: []
4414
+ };
4249
4415
  if (!prevManifest)
4250
4416
  return empty;
4251
- 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))]));
4252
4418
  const currentPrompts = new Set(bundle.prompts.map((p2) => `${p2.name}.md`));
4253
4419
  const currentConfigs = new Set(bundle.configs.map((c2) => c2.name));
4254
- const removed = { skills: [], prompts: [], configs: [] };
4255
- for (const skillName of prevManifest.files.skills) {
4256
- if (currentSkills.has(skillName))
4257
- continue;
4420
+ const removed = {
4421
+ skillDirs: [],
4422
+ skillFiles: [],
4423
+ prompts: [],
4424
+ configs: []
4425
+ };
4426
+ for (const [skillName, entry] of Object.entries(prevManifest.files.skills)) {
4258
4427
  if (!isSafeName(skillName))
4259
4428
  continue;
4260
- const skillTarget = join4(projectRoot, profile.skillPath(skillName));
4261
- 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
+ }
4262
4446
  }
4263
4447
  for (const promptFile of prevManifest.files.prompts) {
4264
4448
  if (currentPrompts.has(promptFile))
@@ -4268,7 +4452,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4268
4452
  const promptName = promptFile.replace(/\.md$/, "");
4269
4453
  removed.prompts.push({
4270
4454
  name: promptFile,
4271
- path: join4(projectRoot, profile.promptPath(promptName))
4455
+ path: join5(projectRoot, profile.promptPath(promptName))
4272
4456
  });
4273
4457
  }
4274
4458
  for (const configFile of prevManifest.files.configs) {
@@ -4278,29 +4462,75 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4278
4462
  continue;
4279
4463
  removed.configs.push({
4280
4464
  name: configFile,
4281
- path: join4(projectRoot, profile.configPath(configFile))
4465
+ path: join5(projectRoot, profile.configPath(configFile))
4282
4466
  });
4283
4467
  }
4284
4468
  return removed;
4285
4469
  }
4286
- function detectOrphanedArtifacts(projectRoot, currentProfile) {
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
+ }
4483
+ function findOrphanedManifests(projectRoot, currentProfile) {
4484
+ const out = [];
4287
4485
  for (const profile of Object.values(PROFILES)) {
4288
4486
  if (profile.toolId === currentProfile.toolId)
4289
4487
  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
- }
4488
+ const manifestPath = join5(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
4489
+ if (!existsSync5(manifestPath))
4490
+ continue;
4491
+ const manifest = readManifest(join5(projectRoot, profile.manifestDir));
4492
+ if (!manifest)
4493
+ continue;
4494
+ out.push({ profile, manifestPath, manifest });
4296
4495
  }
4297
- return null;
4496
+ return out;
4497
+ }
4498
+ function detectOrphanedArtifacts(projectRoot, currentProfile) {
4499
+ const first = findOrphanedManifests(projectRoot, currentProfile)[0];
4500
+ if (!first)
4501
+ return null;
4502
+ return `Found existing 10x artifacts in ${first.profile.manifestDir}/ from ${first.profile.displayName}.
4503
+ Manually remove ${first.profile.manifestDir}/ if you no longer need it.
4504
+ Your new artifacts will be written to ${currentProfile.manifestDir}/`;
4298
4505
  }
4299
4506
  function assertSafeName(name, kind) {
4300
4507
  if (!isSafeName(name)) {
4301
4508
  throw new Error(`refused to write unsafe ${kind} name ${JSON.stringify(name)} — must not be empty, contain path separators, or start with '.'`);
4302
4509
  }
4303
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
+ }
4304
4534
  function isSafeName(name) {
4305
4535
  if (typeof name !== "string" || name.length === 0)
4306
4536
  return false;
@@ -4322,6 +4552,318 @@ function isSafeName(name) {
4322
4552
  return true;
4323
4553
  }
4324
4554
 
4555
+ // src/lib/tool-switch.ts
4556
+ function migrateArtifacts(projectRoot, orphan, newProfile) {
4557
+ const summary = {
4558
+ action: "migrated",
4559
+ oldToolId: orphan.profile.toolId,
4560
+ movedOrRemoved: { skills: [], prompts: [], configs: [] },
4561
+ sentinelStripped: false,
4562
+ skipped: []
4563
+ };
4564
+ const oldProfile = orphan.profile;
4565
+ for (const [skillName, entry] of Object.entries(orphan.manifest.files.skills)) {
4566
+ if (!isSafeName(skillName)) {
4567
+ summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
4568
+ continue;
4569
+ }
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;
4585
+ }
4586
+ tryRemoveEmptyDir(fromSkillDir);
4587
+ if (anyMoved)
4588
+ summary.movedOrRemoved.skills.push(skillName);
4589
+ }
4590
+ for (const promptFile of orphan.manifest.files.prompts) {
4591
+ if (!isSafeName(promptFile)) {
4592
+ summary.skipped.push({ path: promptFile, reason: "unsafe name in manifest" });
4593
+ continue;
4594
+ }
4595
+ const promptName = promptFile.replace(/\.md$/, "");
4596
+ const from = join6(projectRoot, oldProfile.promptPath(promptName));
4597
+ const to = join6(projectRoot, newProfile.promptPath(promptName));
4598
+ if (moveIfSafe(from, to, summary.skipped)) {
4599
+ summary.movedOrRemoved.prompts.push(promptFile);
4600
+ }
4601
+ }
4602
+ for (const configFile of orphan.manifest.files.configs) {
4603
+ if (!isSafeName(configFile)) {
4604
+ summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
4605
+ continue;
4606
+ }
4607
+ const from = join6(projectRoot, oldProfile.configPath(configFile));
4608
+ const to = join6(projectRoot, newProfile.configPath(configFile));
4609
+ if (moveIfSafe(from, to, summary.skipped)) {
4610
+ summary.movedOrRemoved.configs.push(configFile);
4611
+ }
4612
+ }
4613
+ summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
4614
+ rmSync3(orphan.manifestPath, { force: true });
4615
+ tryRemoveEmptyDir(join6(projectRoot, oldProfile.manifestDir));
4616
+ return summary;
4617
+ }
4618
+ function deleteArtifacts(projectRoot, orphan) {
4619
+ const summary = {
4620
+ action: "deleted",
4621
+ oldToolId: orphan.profile.toolId,
4622
+ movedOrRemoved: { skills: [], prompts: [], configs: [] },
4623
+ sentinelStripped: false,
4624
+ skipped: []
4625
+ };
4626
+ const oldProfile = orphan.profile;
4627
+ for (const [skillName, entry] of Object.entries(orphan.manifest.files.skills)) {
4628
+ if (!isSafeName(skillName)) {
4629
+ summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
4630
+ continue;
4631
+ }
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 });
4644
+ summary.movedOrRemoved.skills.push(skillName);
4645
+ }
4646
+ for (const promptFile of orphan.manifest.files.prompts) {
4647
+ if (!isSafeName(promptFile)) {
4648
+ summary.skipped.push({ path: promptFile, reason: "unsafe name in manifest" });
4649
+ continue;
4650
+ }
4651
+ const promptName = promptFile.replace(/\.md$/, "");
4652
+ rmSync3(join6(projectRoot, oldProfile.promptPath(promptName)), { force: true });
4653
+ summary.movedOrRemoved.prompts.push(promptFile);
4654
+ }
4655
+ for (const configFile of orphan.manifest.files.configs) {
4656
+ if (!isSafeName(configFile)) {
4657
+ summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
4658
+ continue;
4659
+ }
4660
+ rmSync3(join6(projectRoot, oldProfile.configPath(configFile)), { force: true });
4661
+ summary.movedOrRemoved.configs.push(configFile);
4662
+ }
4663
+ summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
4664
+ rmSync3(orphan.manifestPath, { force: true });
4665
+ const manifestDirAbs = join6(projectRoot, oldProfile.manifestDir);
4666
+ tryRemoveEmptyDir(join6(manifestDirAbs, "skills"));
4667
+ tryRemoveEmptyDir(join6(manifestDirAbs, "prompts"));
4668
+ tryRemoveEmptyDir(join6(manifestDirAbs, "config-templates"));
4669
+ tryRemoveEmptyDir(manifestDirAbs);
4670
+ return summary;
4671
+ }
4672
+ function moveIfSafe(from, to, skipped) {
4673
+ if (!existsSync6(from)) {
4674
+ return false;
4675
+ }
4676
+ if (lstatSync(from).isSymbolicLink()) {
4677
+ skipped.push({ path: from, reason: "source is a symlink" });
4678
+ return false;
4679
+ }
4680
+ if (existsSync6(to)) {
4681
+ if (!filesAreIdentical(from, to)) {
4682
+ skipped.push({
4683
+ path: to,
4684
+ reason: "destination already exists with different content"
4685
+ });
4686
+ return false;
4687
+ }
4688
+ rmSync3(from, { force: true });
4689
+ return true;
4690
+ }
4691
+ mkdirSync5(dirname4(to), { recursive: true });
4692
+ try {
4693
+ renameSync2(from, to);
4694
+ } catch (err) {
4695
+ const code = err?.code;
4696
+ if (code !== "EXDEV")
4697
+ throw err;
4698
+ const bytes = readFileSync5(from);
4699
+ const tmp = `${to}.tmp`;
4700
+ writeFileSync4(tmp, bytes);
4701
+ renameSync2(tmp, to);
4702
+ try {
4703
+ rmSync3(from, { force: true });
4704
+ } catch {
4705
+ skipped.push({
4706
+ path: from,
4707
+ reason: "copied to destination but could not remove source"
4708
+ });
4709
+ return true;
4710
+ }
4711
+ }
4712
+ return true;
4713
+ }
4714
+ function stripSentinelFromRulesFile(projectRoot, oldProfile) {
4715
+ const rulesPath = join6(projectRoot, oldProfile.rulesFile);
4716
+ if (!existsSync6(rulesPath))
4717
+ return false;
4718
+ const existing = readFileOrNull(rulesPath);
4719
+ if (existing === null)
4720
+ return false;
4721
+ const { content, removed } = removeRulesBlockWithMarkers(existing, oldProfile.sentinelBegin, oldProfile.sentinelEnd);
4722
+ if (!removed)
4723
+ return false;
4724
+ if (content.trim().length === 0) {
4725
+ rmSync3(rulesPath, { force: true });
4726
+ } else {
4727
+ writeFileSync4(rulesPath, content);
4728
+ }
4729
+ return true;
4730
+ }
4731
+ function filesAreIdentical(a2, b2) {
4732
+ try {
4733
+ const aStat = statSync(a2);
4734
+ const bStat = statSync(b2);
4735
+ if (aStat.size !== bStat.size)
4736
+ return false;
4737
+ return readFileSync5(a2).equals(readFileSync5(b2));
4738
+ } catch {
4739
+ return false;
4740
+ }
4741
+ }
4742
+ function tryRemoveEmptyDir(dir) {
4743
+ if (!existsSync6(dir))
4744
+ return;
4745
+ try {
4746
+ rmdirSync2(dir);
4747
+ } catch {}
4748
+ }
4749
+
4750
+ // src/lib/tool-prompt.ts
4751
+ async function resolveToolProfile(flagOverride, projectRoot = process.cwd()) {
4752
+ const profile = await resolveProfileOnly(flagOverride, projectRoot);
4753
+ await handleToolSwitch(projectRoot, profile);
4754
+ return profile;
4755
+ }
4756
+ async function resolveProfileOnly(flagOverride, projectRoot) {
4757
+ if (flagOverride) {
4758
+ const profile = PROFILES[flagOverride];
4759
+ if (!profile) {
4760
+ throw new Error(`Unknown tool '${flagOverride}'. Supported: ${Object.keys(PROFILES).join(", ")}`);
4761
+ }
4762
+ return profile;
4763
+ }
4764
+ const config = readToolConfig();
4765
+ if (config?.tool && PROFILES[config.tool]) {
4766
+ return PROFILES[config.tool];
4767
+ }
4768
+ if (process.stdout.isTTY) {
4769
+ const signals = detectTools(projectRoot);
4770
+ const detected = topDetectedProfile(signals);
4771
+ const top = signals[0];
4772
+ if (top && detected) {
4773
+ ye(`Detected: ${detected.displayName} (${top.reason})`);
4774
+ }
4775
+ const initial = detected?.toolId ?? DEFAULT_TOOL;
4776
+ const choice = await de({
4777
+ message: "Which AI coding tool do you use?",
4778
+ options: Object.values(PROFILES).map((profile) => ({
4779
+ value: profile.toolId,
4780
+ label: profile.displayName,
4781
+ hint: profile.toolId === initial ? "default" : undefined
4782
+ })),
4783
+ initialValue: initial
4784
+ });
4785
+ if (BD(choice)) {
4786
+ ve("Using default (Claude Code).");
4787
+ return PROFILES[DEFAULT_TOOL];
4788
+ }
4789
+ saveToolConfig({ ...config ?? {}, tool: choice });
4790
+ return PROFILES[choice];
4791
+ }
4792
+ return PROFILES[DEFAULT_TOOL];
4793
+ }
4794
+ async function handleToolSwitch(projectRoot, newProfile) {
4795
+ if (!process.stdout.isTTY)
4796
+ return;
4797
+ const cfg = readToolConfig();
4798
+ const acknowledged = new Set(cfg?.acknowledgedOrphans ?? []);
4799
+ const orphans = findOrphanedManifests(projectRoot, newProfile).filter((o) => !acknowledged.has(o.profile.toolId));
4800
+ if (orphans.length === 0)
4801
+ return;
4802
+ for (const orphan of orphans) {
4803
+ const action = await de({
4804
+ message: `Found 10x artifacts from ${orphan.profile.displayName} in ${orphan.profile.manifestDir}/. What should we do?`,
4805
+ options: [
4806
+ {
4807
+ value: "migrate",
4808
+ label: `Migrate to ${newProfile.manifestDir}/`,
4809
+ hint: "recommended"
4810
+ },
4811
+ {
4812
+ value: "delete",
4813
+ label: `Remove 10x artifacts from ${orphan.profile.manifestDir}/`
4814
+ },
4815
+ { value: "keep", label: "Keep both (don't ask again)" }
4816
+ ],
4817
+ initialValue: "migrate"
4818
+ });
4819
+ if (BD(action))
4820
+ continue;
4821
+ if (action === "migrate") {
4822
+ const summary = migrateArtifacts(projectRoot, orphan, newProfile);
4823
+ printMigrationSummary(summary, newProfile);
4824
+ } else if (action === "delete") {
4825
+ const summary = deleteArtifacts(projectRoot, orphan);
4826
+ printMigrationSummary(summary, newProfile);
4827
+ } else {
4828
+ const existing = readToolConfig();
4829
+ const nextAcks = [
4830
+ ...existing?.acknowledgedOrphans ?? [],
4831
+ orphan.profile.toolId
4832
+ ];
4833
+ saveToolConfig({
4834
+ ...existing ?? {},
4835
+ tool: existing?.tool ?? newProfile.toolId,
4836
+ acknowledgedOrphans: nextAcks
4837
+ });
4838
+ }
4839
+ }
4840
+ }
4841
+ function printMigrationSummary(summary, newProfile) {
4842
+ const { skills, prompts, configs } = summary.movedOrRemoved;
4843
+ const verb = summary.action === "migrated" ? "Moved" : "Removed";
4844
+ const target = summary.action === "migrated" ? `to ${newProfile.manifestDir}/` : "";
4845
+ const parts = [];
4846
+ if (skills.length)
4847
+ parts.push(`${skills.length} skill${skills.length === 1 ? "" : "s"}`);
4848
+ if (prompts.length)
4849
+ parts.push(`${prompts.length} prompt${prompts.length === 1 ? "" : "s"}`);
4850
+ if (configs.length)
4851
+ parts.push(`${configs.length} config${configs.length === 1 ? "" : "s"}`);
4852
+ const lines = [];
4853
+ if (parts.length > 0) {
4854
+ lines.push(`${verb} ${parts.join(", ")}${target ? " " + target : ""}.`);
4855
+ }
4856
+ if (summary.sentinelStripped) {
4857
+ lines.push(`Removed 10x block from the old rules file.`);
4858
+ }
4859
+ for (const s of summary.skipped) {
4860
+ lines.push(`Skipped ${s.path} (${s.reason}).`);
4861
+ }
4862
+ if (lines.length > 0)
4863
+ ye(lines.join(`
4864
+ `));
4865
+ }
4866
+
4325
4867
  // src/commands/get.ts
4326
4868
  var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
4327
4869
  function isArtifactType(s) {
@@ -4351,7 +4893,7 @@ async function runGet(ctx, ref, options) {
4351
4893
  }
4352
4894
  const auth = await requireAuth(ctx);
4353
4895
  const course = options.course ?? DEFAULT_COURSE2;
4354
- const profile = await resolveToolProfile(options.tool);
4896
+ const profile = await resolveToolProfile(options.tool, process.cwd());
4355
4897
  const lang = options.lang ?? readToolConfig()?.lang ?? "en";
4356
4898
  if (options.lang) {
4357
4899
  const existing = readToolConfig();
@@ -4371,9 +4913,11 @@ async function runGet(ctx, ref, options) {
4371
4913
  if (isFallback && contentLang) {
4372
4914
  verbose(ctx, `${lang.toUpperCase()} not available for ${parsed.lessonId}, showing ${contentLang.toUpperCase()}.`);
4373
4915
  }
4374
- const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4375
- if (orphanWarning)
4376
- verbose(ctx, orphanWarning);
4916
+ if (!process.stdout.isTTY) {
4917
+ const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4918
+ if (orphanWarning)
4919
+ verbose(ctx, orphanWarning);
4920
+ }
4377
4921
  const isFiltered = options.type !== undefined;
4378
4922
  const bundle = filterBundle(ctx, result.data, options);
4379
4923
  const writeResult = applyBundle(bundle, process.cwd(), {
@@ -4429,7 +4973,14 @@ async function runPrintMode(ctx, lessonId, course, profile, token, lang, options
4429
4973
  if (ctx.json) {
4430
4974
  output(ctx, "", result.data);
4431
4975
  } else {
4432
- 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
+ }
4433
4984
  }
4434
4985
  } else {
4435
4986
  verbose(ctx, `fetching lesson ${course}/${lessonId} (filtering by ${options.type})`);
@@ -4437,17 +4988,40 @@ async function runPrintMode(ctx, lessonId, course, profile, token, lang, options
4437
4988
  if (!result.ok) {
4438
4989
  handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4439
4990
  }
4440
- const artifacts = result.data[options.type];
4441
- if (ctx.json) {
4442
- 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
+ }
4443
5004
  } else {
4444
- const contents = artifacts.map((a2) => a2.content);
4445
- 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(`
4446
5011
  ---
4447
5012
  `));
5013
+ }
4448
5014
  }
4449
5015
  }
4450
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
+ }
4451
5025
  function handleLessonError(ctx, status, code, error, payload) {
4452
5026
  verbose(ctx, `lesson fetch failed: status=${status} code=${code}`);
4453
5027
  if (status === 403) {
@@ -4514,7 +5088,15 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
4514
5088
  lines.push("");
4515
5089
  lines.push(dryRun ? `Would write to ${targetDir}/:` : `Wrote to ${targetDir}/:`);
4516
5090
  for (const skill of writeResult.skills) {
4517
- 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
+ }
4518
5100
  }
4519
5101
  for (const prompt of writeResult.prompts) {
4520
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.5.0",
3
+ "version": "1.1.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",