@follenfang/fupload 0.0.0-bootstrap.0 → 0.0.2

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 (47) hide show
  1. package/README.md +236 -3
  2. package/fupload/SKILL.md +142 -0
  3. package/fupload/agents/openai.yaml +4 -0
  4. package/fupload/examples/curseforge-plugin-upload.json +21 -0
  5. package/fupload/examples/dd-config-delete.json +5 -0
  6. package/fupload/examples/dd-config-update.json +25 -0
  7. package/fupload/examples/dd-plugin-delete.json +5 -0
  8. package/fupload/examples/dd-plugin-update.json +9 -0
  9. package/fupload/examples/dd-wa-delete.json +5 -0
  10. package/fupload/examples/dd-wa-edit.json +9 -0
  11. package/fupload/examples/newbee-config-delete.json +5 -0
  12. package/fupload/examples/newbee-config-update.json +10 -0
  13. package/fupload/examples/newbee-plugin-create.json +14 -0
  14. package/fupload/examples/newbee-plugin-delete.json +5 -0
  15. package/fupload/examples/newbee-wa-delete.json +5 -0
  16. package/fupload/examples/newbee-wa-update.json +8 -0
  17. package/fupload/references/curseforge.md +233 -0
  18. package/fupload/references/dd.md +105 -0
  19. package/fupload/references/newbee-official-cli.md +288 -0
  20. package/fupload/references/newbee.md +80 -0
  21. package/fupload/references/workflow.md +67 -0
  22. package/fupload/scripts/fupload.py +17 -0
  23. package/fupload/scripts/fupload_cli/__init__.py +3 -0
  24. package/fupload/scripts/fupload_cli/cli.py +281 -0
  25. package/fupload/scripts/fupload_cli/curseforge.py +186 -0
  26. package/fupload/scripts/fupload_cli/dd.py +2406 -0
  27. package/fupload/scripts/fupload_cli/dd_broker.py +634 -0
  28. package/fupload/scripts/fupload_cli/dd_sidecar.py +860 -0
  29. package/fupload/scripts/fupload_cli/errors.py +94 -0
  30. package/fupload/scripts/fupload_cli/io.py +125 -0
  31. package/fupload/scripts/fupload_cli/newbee.py +1412 -0
  32. package/fupload/scripts/fupload_cli/newbee_auth.py +135 -0
  33. package/fupload/scripts/fupload_cli/schema.py +587 -0
  34. package/fupload/scripts/fupload_cli/transport.py +125 -0
  35. package/fupload/scripts/fupload_cli/trust.py +207 -0
  36. package/npm/bin/fupload.mjs +92 -0
  37. package/npm/lib/curseforge-config.mjs +36 -0
  38. package/npm/lib/managed-install.mjs +86 -0
  39. package/npm/lib/options.mjs +38 -0
  40. package/npm/lib/python.mjs +45 -0
  41. package/npm/lib/skill-installer.mjs +228 -0
  42. package/npm/lib/uninstall.mjs +211 -0
  43. package/npm/lib/update.mjs +102 -0
  44. package/npm/lib/versions.mjs +63 -0
  45. package/npm/postinstall.mjs +21 -0
  46. package/npm/skill-manifest.json +179 -0
  47. package/package.json +50 -6
@@ -0,0 +1,228 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export const INSTALL_STAMP = ".fupload-npm-install.json";
6
+ export const INSTALL_STAMP_SCHEMA = "fupload.npm-skill-install.v1";
7
+ const LOCK_STALE_MS = 5 * 60 * 1000;
8
+ const LOCK_WAIT_MS = 15 * 1000;
9
+ const TEXT_EXTENSIONS = new Set([".json", ".md", ".py", ".txt", ".yaml", ".yml"]);
10
+
11
+ function sha256(buffer) {
12
+ return crypto.createHash("sha256").update(buffer).digest("hex");
13
+ }
14
+
15
+ function canonicalContent(filename, content) {
16
+ if (!TEXT_EXTENSIONS.has(path.extname(filename).toLowerCase())) {
17
+ return content;
18
+ }
19
+ return Buffer.from(content.toString("utf8").replaceAll("\r\n", "\n"), "utf8");
20
+ }
21
+
22
+ function readJson(filename) {
23
+ return JSON.parse(fs.readFileSync(filename, "utf8"));
24
+ }
25
+
26
+ function writeJson(filename, value) {
27
+ fs.writeFileSync(filename, `${JSON.stringify(value, null, 2)}\n`, {
28
+ encoding: "utf8",
29
+ mode: 0o600,
30
+ });
31
+ }
32
+
33
+ function normalizedFiles(root, current = root) {
34
+ if (!fs.existsSync(current)) {
35
+ return [];
36
+ }
37
+ const result = [];
38
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
39
+ if (
40
+ entry.name === INSTALL_STAMP ||
41
+ entry.name === "tests" ||
42
+ entry.name === "__pycache__" ||
43
+ entry.name === ".pytest_cache" ||
44
+ entry.name.endsWith(".pyc")
45
+ ) {
46
+ continue;
47
+ }
48
+ const absolute = path.join(current, entry.name);
49
+ if (entry.isDirectory()) {
50
+ result.push(...normalizedFiles(root, absolute));
51
+ } else if (entry.isFile()) {
52
+ result.push(path.relative(root, absolute).split(path.sep).join("/"));
53
+ }
54
+ }
55
+ return result.sort((left, right) => left.localeCompare(right));
56
+ }
57
+
58
+ export function loadDistribution(packageRoot) {
59
+ const packageRecord = readJson(path.join(packageRoot, "package.json"));
60
+ const manifest = readJson(path.join(packageRoot, "npm", "skill-manifest.json"));
61
+ if (
62
+ manifest.schema !== "fupload.npm-skill-manifest.v1" ||
63
+ manifest.package_name !== packageRecord.name ||
64
+ manifest.package_version !== packageRecord.version ||
65
+ manifest.skill_version !== packageRecord.version
66
+ ) {
67
+ throw new Error("The packaged Skill manifest does not match package.json.");
68
+ }
69
+ return { packageRecord, manifest, sourceSkill: path.join(packageRoot, "fupload") };
70
+ }
71
+
72
+ export function verifySkill(root, manifest) {
73
+ const expected = manifest.files.map((entry) => entry.path);
74
+ const actual = normalizedFiles(root);
75
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
76
+ return { valid: false, reason: "file_inventory_mismatch" };
77
+ }
78
+ for (const entry of manifest.files) {
79
+ const absolute = path.join(root, ...entry.path.split("/"));
80
+ try {
81
+ const content = canonicalContent(absolute, fs.readFileSync(absolute));
82
+ if (content.length !== entry.bytes || sha256(content) !== entry.sha256) {
83
+ return { valid: false, reason: "file_hash_mismatch", path: entry.path };
84
+ }
85
+ } catch {
86
+ return { valid: false, reason: "file_missing", path: entry.path };
87
+ }
88
+ }
89
+ return { valid: true, reason: "matched" };
90
+ }
91
+
92
+ function copyManifestFiles(source, destination, manifest) {
93
+ fs.mkdirSync(destination, { recursive: true });
94
+ for (const entry of manifest.files) {
95
+ const from = path.join(source, ...entry.path.split("/"));
96
+ const to = path.join(destination, ...entry.path.split("/"));
97
+ fs.mkdirSync(path.dirname(to), { recursive: true });
98
+ fs.copyFileSync(from, to);
99
+ }
100
+ }
101
+
102
+ function sleep(milliseconds) {
103
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
104
+ }
105
+
106
+ async function acquireLock(target) {
107
+ const lock = `${target}.npm-install.lock`;
108
+ fs.mkdirSync(path.dirname(target), { recursive: true });
109
+ const deadline = Date.now() + LOCK_WAIT_MS;
110
+ while (Date.now() < deadline) {
111
+ try {
112
+ const descriptor = fs.openSync(lock, "wx", 0o600);
113
+ fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid })}\n`);
114
+ return {
115
+ release() {
116
+ fs.closeSync(descriptor);
117
+ fs.rmSync(lock, { force: true });
118
+ },
119
+ };
120
+ } catch (error) {
121
+ if (error.code !== "EEXIST") {
122
+ throw error;
123
+ }
124
+ try {
125
+ if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
126
+ fs.rmSync(lock, { force: true });
127
+ continue;
128
+ }
129
+ } catch (statError) {
130
+ if (statError.code !== "ENOENT") {
131
+ throw statError;
132
+ }
133
+ }
134
+ await sleep(50);
135
+ }
136
+ }
137
+ throw new Error(`Timed out waiting for the Skill install lock: ${lock}`);
138
+ }
139
+
140
+ export function readInstallStamp(target) {
141
+ try {
142
+ return readJson(path.join(target, INSTALL_STAMP));
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ function installStamp(distribution, target) {
149
+ return {
150
+ schema: INSTALL_STAMP_SCHEMA,
151
+ package_name: distribution.packageRecord.name,
152
+ package_version: distribution.packageRecord.version,
153
+ skill_version: distribution.manifest.skill_version,
154
+ tree_sha256: distribution.manifest.tree_sha256,
155
+ target: path.resolve(target),
156
+ installed_at: new Date().toISOString(),
157
+ };
158
+ }
159
+
160
+ export async function ensureSkill({ packageRoot, target }) {
161
+ const distribution = loadDistribution(packageRoot);
162
+ const sourceCheck = verifySkill(distribution.sourceSkill, distribution.manifest);
163
+ if (!sourceCheck.valid) {
164
+ throw new Error(`The packaged Skill failed validation: ${sourceCheck.reason}.`);
165
+ }
166
+ const lock = await acquireLock(target);
167
+ try {
168
+ const exists = fs.existsSync(target);
169
+ const targetCheck = exists
170
+ ? verifySkill(target, distribution.manifest)
171
+ : { valid: false, reason: "missing" };
172
+ const stamp = exists ? readInstallStamp(target) : null;
173
+ if (targetCheck.valid) {
174
+ if (!stamp) {
175
+ writeJson(path.join(target, INSTALL_STAMP), installStamp(distribution, target));
176
+ return { status: "adopted", target, distribution };
177
+ }
178
+ if (stamp.package_name !== distribution.packageRecord.name) {
179
+ throw new Error(`The Skill target is managed by ${stamp.package_name}.`);
180
+ }
181
+ if (stamp.target !== path.resolve(target)) {
182
+ writeJson(path.join(target, INSTALL_STAMP), installStamp(distribution, target));
183
+ }
184
+ return { status: "current", target, distribution };
185
+ }
186
+ if (exists && (!stamp || stamp.package_name !== distribution.packageRecord.name)) {
187
+ throw new Error(`The Skill target already exists and is not a matching managed Fuploader Skill: ${target}`);
188
+ }
189
+
190
+ const parent = path.dirname(target);
191
+ const nonce = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
192
+ const staging = path.join(parent, `.fupload-stage-${nonce}`);
193
+ const backup = path.join(parent, `.fupload-backup-${nonce}`);
194
+ let movedOld = false;
195
+ try {
196
+ copyManifestFiles(distribution.sourceSkill, staging, distribution.manifest);
197
+ writeJson(path.join(staging, INSTALL_STAMP), installStamp(distribution, target));
198
+ const stagingCheck = verifySkill(staging, distribution.manifest);
199
+ if (!stagingCheck.valid) {
200
+ throw new Error(`The staged Skill failed validation: ${stagingCheck.reason}.`);
201
+ }
202
+ if (exists) {
203
+ fs.renameSync(target, backup);
204
+ movedOld = true;
205
+ }
206
+ try {
207
+ fs.renameSync(staging, target);
208
+ } catch (error) {
209
+ if (movedOld && !fs.existsSync(target)) {
210
+ fs.renameSync(backup, target);
211
+ movedOld = false;
212
+ }
213
+ throw error;
214
+ }
215
+ if (movedOld) {
216
+ fs.rmSync(backup, { recursive: true, force: true });
217
+ }
218
+ return { status: exists ? "upgraded" : "installed", target, distribution };
219
+ } finally {
220
+ fs.rmSync(staging, { recursive: true, force: true });
221
+ if (movedOld && fs.existsSync(backup) && !fs.existsSync(target)) {
222
+ fs.renameSync(backup, target);
223
+ }
224
+ }
225
+ } finally {
226
+ lock.release();
227
+ }
228
+ }
@@ -0,0 +1,211 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { clearManagedInstall, readManagedInstall } from "./managed-install.mjs";
7
+ import { resolveSkillDirectory } from "./options.mjs";
8
+ import { INSTALL_STAMP, INSTALL_STAMP_SCHEMA, readInstallStamp } from "./skill-installer.mjs";
9
+
10
+ export const PACKAGE_NAME = "@follenfang/fupload";
11
+
12
+ export class ManagedUninstallError extends Error {
13
+ constructor(message, details) {
14
+ super(message);
15
+ this.name = "ManagedUninstallError";
16
+ this.code = "FUPLOAD_UNINSTALL_FAILED";
17
+ this.details = details;
18
+ }
19
+ }
20
+
21
+ export function inspectManagedSkill(target) {
22
+ const resolved = path.resolve(target);
23
+ if (resolved === path.parse(resolved).root) {
24
+ return { target: resolved, status: "preserved_unmanaged", reason: "filesystem_root" };
25
+ }
26
+ if (!fs.existsSync(resolved)) {
27
+ return { target: resolved, status: "missing" };
28
+ }
29
+ const stamp = readInstallStamp(resolved);
30
+ if (
31
+ stamp?.schema !== INSTALL_STAMP_SCHEMA ||
32
+ stamp?.package_name !== PACKAGE_NAME ||
33
+ path.resolve(stamp?.target || "") !== resolved
34
+ ) {
35
+ return { target: resolved, status: "preserved_unmanaged", reason: "ownership_unknown" };
36
+ }
37
+ return { target: resolved, status: "managed", marker: path.join(resolved, INSTALL_STAMP) };
38
+ }
39
+
40
+ export async function cleanupManagedSkills({
41
+ platform = process.platform,
42
+ env = process.env,
43
+ home = os.homedir(),
44
+ extraTargets = [],
45
+ removePath = (target) => fs.rmSync(target, { recursive: true, force: true }),
46
+ } = {}) {
47
+ const registry = readManagedInstall({ platform, env, home });
48
+ const targets = [
49
+ ...new Set([
50
+ ...registry.targets
51
+ .filter((entry) => typeof entry?.path === "string")
52
+ .map((entry) => path.resolve(entry.path)),
53
+ path.resolve(resolveSkillDirectory({ env, home })),
54
+ ...extraTargets.map((target) => path.resolve(target)),
55
+ ]),
56
+ ];
57
+ const result = {
58
+ schema: "fupload.npm-cleanup-result.v1",
59
+ skills: [],
60
+ project_data: "preserved",
61
+ platform_credentials: "preserved",
62
+ platform_logs: "preserved",
63
+ };
64
+ for (const target of targets) {
65
+ const inspection = inspectManagedSkill(target);
66
+ if (inspection.status !== "managed") {
67
+ result.skills.push(inspection);
68
+ continue;
69
+ }
70
+ try {
71
+ removePath(inspection.target);
72
+ if (fs.existsSync(inspection.target)) {
73
+ throw new Error("path still exists after deletion");
74
+ }
75
+ result.skills.push({ target: inspection.target, status: "removed" });
76
+ } catch (error) {
77
+ result.skills.push({ target: inspection.target, status: "failed" });
78
+ throw new ManagedUninstallError("A managed Fuploader Skill could not be removed.", {
79
+ result,
80
+ object: inspection.target,
81
+ reason: error.message,
82
+ });
83
+ }
84
+ }
85
+ clearManagedInstall({ platform, env, home });
86
+ return result;
87
+ }
88
+
89
+ export function resolveGlobalInstall(packageRoot, platform = process.platform) {
90
+ let current = path.resolve(packageRoot);
91
+ let nodeModules;
92
+ while (true) {
93
+ if (path.basename(current).toLowerCase() === "node_modules") {
94
+ nodeModules = current;
95
+ break;
96
+ }
97
+ const parent = path.dirname(current);
98
+ if (parent === current) {
99
+ break;
100
+ }
101
+ current = parent;
102
+ }
103
+ if (!nodeModules) {
104
+ throw new ManagedUninstallError("The npm installation prefix could not be resolved.", {
105
+ package_root: path.resolve(packageRoot),
106
+ });
107
+ }
108
+ let prefix = path.dirname(nodeModules);
109
+ if (platform !== "win32" && path.basename(prefix) === "lib") {
110
+ prefix = path.dirname(prefix);
111
+ }
112
+ const launcher = platform === "win32"
113
+ ? path.join(prefix, "fupload.cmd")
114
+ : path.join(prefix, "bin", "fupload");
115
+ if (!fs.existsSync(launcher)) {
116
+ throw new ManagedUninstallError("fupload uninstall requires a global npm installation.", {
117
+ package_root: path.resolve(packageRoot),
118
+ prefix,
119
+ launcher,
120
+ });
121
+ }
122
+ return { prefix, launcher, packageRoot: path.resolve(packageRoot) };
123
+ }
124
+
125
+ export function resolveNpmCli({
126
+ platform = process.platform,
127
+ env = process.env,
128
+ run = spawnSync,
129
+ } = {}) {
130
+ if (env.npm_execpath && fs.existsSync(env.npm_execpath)) {
131
+ return path.resolve(env.npm_execpath);
132
+ }
133
+ if (platform === "win32") {
134
+ const located = run("where.exe", ["npm.cmd"], { encoding: "utf8", windowsHide: true });
135
+ if (located.status === 0) {
136
+ for (const shim of located.stdout.split(/\r?\n/).filter(Boolean)) {
137
+ const candidate = path.join(path.dirname(shim.trim()), "node_modules", "npm", "bin", "npm-cli.js");
138
+ if (fs.existsSync(candidate)) {
139
+ return candidate;
140
+ }
141
+ }
142
+ }
143
+ } else {
144
+ const located = run("which", ["npm"], { encoding: "utf8" });
145
+ if (located.status === 0) {
146
+ try {
147
+ const candidate = fs.realpathSync(located.stdout.trim());
148
+ if (fs.existsSync(candidate)) {
149
+ return candidate;
150
+ }
151
+ } catch {
152
+ // Fall through to the stable error below.
153
+ }
154
+ }
155
+ }
156
+ throw new ManagedUninstallError("npm-cli.js could not be located for self-removal.");
157
+ }
158
+
159
+ export function runNpmCli(args, { platform = process.platform, env = process.env } = {}) {
160
+ return spawnSync(process.execPath, [resolveNpmCli({ platform, env }), ...args], {
161
+ encoding: "utf8", env, shell: false, windowsHide: true,
162
+ });
163
+ }
164
+
165
+ function bounded(value) {
166
+ return String(value || "").trim().slice(0, 4000);
167
+ }
168
+
169
+ export async function uninstallSelf({
170
+ packageRoot,
171
+ target,
172
+ platform = process.platform,
173
+ env = process.env,
174
+ home = os.homedir(),
175
+ runNpm = runNpmCli,
176
+ } = {}) {
177
+ const installation = resolveGlobalInstall(packageRoot, platform);
178
+ const cleanup = await cleanupManagedSkills({
179
+ platform,
180
+ env,
181
+ home,
182
+ extraTargets: target ? [target] : [],
183
+ });
184
+ const args = ["uninstall", "-g", "--ignore-scripts", "--prefix", installation.prefix, PACKAGE_NAME];
185
+ const npmResult = runNpm(args, { platform, env });
186
+ if (npmResult.error || npmResult.status !== 0) {
187
+ throw new ManagedUninstallError("npm could not remove the Fuploader package and CLI.", {
188
+ cleanup,
189
+ prefix: installation.prefix,
190
+ exit_status: npmResult.status,
191
+ stdout: bounded(npmResult.stdout),
192
+ stderr: bounded(npmResult.stderr || npmResult.error?.message),
193
+ });
194
+ }
195
+ const residuals = [installation.launcher, installation.packageRoot].filter((candidate) => fs.existsSync(candidate));
196
+ if (residuals.length) {
197
+ throw new ManagedUninstallError("npm reported success but Fuploader installation files remain.", {
198
+ cleanup,
199
+ prefix: installation.prefix,
200
+ residuals,
201
+ });
202
+ }
203
+ return {
204
+ schema: "fupload.npm-uninstall-result.v1",
205
+ success: true,
206
+ package: PACKAGE_NAME,
207
+ prefix: installation.prefix,
208
+ cleanup,
209
+ npm_exit_status: npmResult.status,
210
+ };
211
+ }
@@ -0,0 +1,102 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ import { ensureCurseForgeEnv } from "./curseforge-config.mjs";
5
+ import { readManagedInstall, recordManagedSkill } from "./managed-install.mjs";
6
+ import { resolveSkillDirectory } from "./options.mjs";
7
+ import { ensureSkill, loadDistribution } from "./skill-installer.mjs";
8
+ import { PACKAGE_NAME, inspectManagedSkill, resolveGlobalInstall, runNpmCli } from "./uninstall.mjs";
9
+
10
+ export class ManagedUpdateError extends Error {
11
+ constructor(message, details) {
12
+ super(message);
13
+ this.name = "ManagedUpdateError";
14
+ this.code = "FUPLOAD_UPDATE_FAILED";
15
+ this.details = details;
16
+ }
17
+ }
18
+
19
+ function bounded(value) {
20
+ return String(value || "").trim().slice(0, 4000);
21
+ }
22
+
23
+ export async function updateSelf({
24
+ packageRoot,
25
+ target,
26
+ platform = process.platform,
27
+ env = process.env,
28
+ home = os.homedir(),
29
+ runNpm = runNpmCli,
30
+ } = {}) {
31
+ const curseforgeConfig = ensureCurseForgeEnv({ home, platform });
32
+ const installation = resolveGlobalInstall(packageRoot, platform);
33
+ const current = loadDistribution(packageRoot);
34
+ const primary = target || resolveSkillDirectory({ env, home });
35
+ await ensureSkill({ packageRoot, target: primary });
36
+ recordManagedSkill(primary, { platform, env, home });
37
+
38
+ const args = ["install", "-g", "--ignore-scripts", "--prefix", installation.prefix, `${PACKAGE_NAME}@latest`];
39
+ const npmResult = runNpm(args, { platform, env });
40
+ if (npmResult.error || npmResult.status !== 0) {
41
+ throw new ManagedUpdateError("npm could not update the Fuploader package and CLI.", {
42
+ from_version: current.packageRecord.version,
43
+ prefix: installation.prefix,
44
+ exit_status: npmResult.status,
45
+ stdout: bounded(npmResult.stdout),
46
+ stderr: bounded(npmResult.stderr || npmResult.error?.message),
47
+ });
48
+ }
49
+
50
+ let updated;
51
+ try {
52
+ updated = loadDistribution(packageRoot);
53
+ } catch (error) {
54
+ throw new ManagedUpdateError("The updated Fuploader package failed distribution validation.", {
55
+ from_version: current.packageRecord.version,
56
+ prefix: installation.prefix,
57
+ reason: error.message,
58
+ });
59
+ }
60
+ const registry = readManagedInstall({ platform, env, home });
61
+ const targets = [
62
+ ...new Set([
63
+ ...registry.targets
64
+ .filter((entry) => typeof entry?.path === "string")
65
+ .map((entry) => path.resolve(entry.path)),
66
+ path.resolve(resolveSkillDirectory({ env, home })),
67
+ path.resolve(primary),
68
+ ]),
69
+ ];
70
+ const skills = [];
71
+ for (const candidate of targets) {
72
+ const inspection = inspectManagedSkill(candidate);
73
+ if (inspection.status === "preserved_unmanaged") {
74
+ skills.push(inspection);
75
+ continue;
76
+ }
77
+ try {
78
+ const ensured = await ensureSkill({ packageRoot, target: candidate });
79
+ recordManagedSkill(candidate, { platform, env, home });
80
+ skills.push({ target: candidate, status: ensured.status, version: updated.packageRecord.version });
81
+ } catch (error) {
82
+ throw new ManagedUpdateError("The npm CLI updated but a managed Fuploader Skill could not be synchronized.", {
83
+ from_version: current.packageRecord.version,
84
+ to_version: updated.packageRecord.version,
85
+ target: candidate,
86
+ skills,
87
+ reason: error.message,
88
+ });
89
+ }
90
+ }
91
+ return {
92
+ schema: "fupload.npm-update-result.v1",
93
+ success: true,
94
+ package: PACKAGE_NAME,
95
+ prefix: installation.prefix,
96
+ from_version: current.packageRecord.version,
97
+ to_version: updated.packageRecord.version,
98
+ npm_exit_status: npmResult.status,
99
+ curseforge_config: curseforgeConfig,
100
+ skills,
101
+ };
102
+ }
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function readJson(filename) {
5
+ return JSON.parse(fs.readFileSync(filename, "utf8"));
6
+ }
7
+
8
+ function matchedVersion(filename, pattern, label) {
9
+ const source = fs.readFileSync(filename, "utf8");
10
+ const match = source.match(pattern);
11
+ if (!match) {
12
+ throw new Error(`${label} version could not be read from ${filename}.`);
13
+ }
14
+ return match[1];
15
+ }
16
+
17
+ export function readVersions(packageRoot, { includeManifest = true } = {}) {
18
+ const packageRecord = readJson(path.join(packageRoot, "package.json"));
19
+ const skillFile = path.join(packageRoot, "fupload", "SKILL.md");
20
+ const frontmatter = fs.readFileSync(skillFile, "utf8").split(/^---\s*$/m)[1] || "";
21
+ const skillMatch = frontmatter.match(/^\s*version:\s*["']?([^\s"']+)["']?\s*$/m);
22
+ if (!skillMatch) {
23
+ throw new Error(`Skill metadata version could not be read from ${skillFile}.`);
24
+ }
25
+ const versions = {
26
+ package: packageRecord.version,
27
+ skill: skillMatch[1],
28
+ python: matchedVersion(
29
+ path.join(packageRoot, "fupload", "scripts", "fupload_cli", "__init__.py"),
30
+ /__version__\s*=\s*["']([^"']+)["']/,
31
+ "Python CLI",
32
+ ),
33
+ };
34
+ const lockFile = path.join(packageRoot, "package-lock.json");
35
+ if (fs.existsSync(lockFile)) {
36
+ const lock = readJson(lockFile);
37
+ versions.lock = lock.version;
38
+ versions.lockRoot = lock.packages?.[""]?.version;
39
+ }
40
+ const manifestFile = path.join(packageRoot, "npm", "skill-manifest.json");
41
+ if (includeManifest && fs.existsSync(manifestFile)) {
42
+ const manifest = readJson(manifestFile);
43
+ versions.manifestPackage = manifest.package_version;
44
+ versions.manifestSkill = manifest.skill_version;
45
+ }
46
+ return versions;
47
+ }
48
+
49
+ export function assertUnifiedVersions(versions) {
50
+ const expected = versions.package;
51
+ const mismatches = Object.entries(versions).filter(([, value]) => value !== expected);
52
+ if (mismatches.length) {
53
+ throw new Error(
54
+ `Version mismatch: expected ${expected}; ${mismatches.map(([name, value]) => `${name}=${value}`).join(", ")}.`,
55
+ );
56
+ }
57
+ return expected;
58
+ }
59
+
60
+ export function selectedReleaseTag({ env = process.env, argv = process.argv.slice(2) } = {}) {
61
+ const candidates = [env.GITHUB_REF_NAME, ...argv];
62
+ return candidates.find((value) => /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value || "")) || "";
63
+ }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { ensureCurseForgeEnv } from "./lib/curseforge-config.mjs";
5
+ import { recordManagedSkill } from "./lib/managed-install.mjs";
6
+ import { resolveSkillDirectory } from "./lib/options.mjs";
7
+ import { ensureSkill } from "./lib/skill-installer.mjs";
8
+
9
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const target = resolveSkillDirectory();
11
+
12
+ try {
13
+ const config = ensureCurseForgeEnv();
14
+ const result = await ensureSkill({ packageRoot, target });
15
+ recordManagedSkill(target);
16
+ process.stdout.write(`Fuploader Skill ${result.status}: ${target}\n`);
17
+ process.stdout.write(`CurseForge configuration ${config.status}: ${config.path}\n`);
18
+ } catch (error) {
19
+ process.stderr.write(`Fuploader Skill installation failed: ${error.message}\n`);
20
+ process.exitCode = 1;
21
+ }