@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.
- package/dist/index.mjs +741 -159
- 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: "
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3931
|
-
sentinelEnd:
|
|
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:
|
|
3942
|
-
sentinelEnd:
|
|
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:
|
|
3953
|
-
sentinelEnd:
|
|
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:
|
|
3964
|
-
sentinelEnd:
|
|
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:
|
|
3975
|
-
sentinelEnd:
|
|
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-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
if (
|
|
3991
|
-
|
|
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 (
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
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
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
|
|
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
|
|
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/
|
|
4173
|
+
// src/lib/tool-switch.ts
|
|
4014
4174
|
import {
|
|
4015
|
-
existsSync as
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
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
|
|
4185
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
4022
4186
|
|
|
4023
|
-
// src/lib/
|
|
4024
|
-
import {
|
|
4025
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
4148
|
-
const
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4185
|
-
const action =
|
|
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.
|
|
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 (!
|
|
4385
|
+
if (!existsSync5(filePath))
|
|
4225
4386
|
return "created";
|
|
4226
4387
|
let current;
|
|
4227
4388
|
try {
|
|
4228
|
-
current =
|
|
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 (!
|
|
4396
|
+
if (!existsSync5(filePath))
|
|
4236
4397
|
return "";
|
|
4237
4398
|
try {
|
|
4238
|
-
return
|
|
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 = {
|
|
4409
|
+
const empty = {
|
|
4410
|
+
skillDirs: [],
|
|
4411
|
+
skillFiles: [],
|
|
4412
|
+
prompts: [],
|
|
4413
|
+
configs: []
|
|
4414
|
+
};
|
|
4249
4415
|
if (!prevManifest)
|
|
4250
4416
|
return empty;
|
|
4251
|
-
const currentSkills = new
|
|
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 = {
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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
|
|
4261
|
-
|
|
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:
|
|
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:
|
|
4465
|
+
path: join5(projectRoot, profile.configPath(configFile))
|
|
4282
4466
|
});
|
|
4283
4467
|
}
|
|
4284
4468
|
return removed;
|
|
4285
4469
|
}
|
|
4286
|
-
function
|
|
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
|
|
4291
|
-
if (
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
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
|
|
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
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
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
|
-
|
|
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
|
|
4441
|
-
if (
|
|
4442
|
-
|
|
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
|
|
4445
|
-
|
|
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
|
-
|
|
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}`);
|