@triflare/fx 1.0.3 → 2.0.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 (50) hide show
  1. package/README.md +32 -0
  2. package/bin/fx.js +4 -1162
  3. package/package.json +5 -3
  4. package/product.yml +1 -1
  5. package/src/brand.js +9 -3
  6. package/src/cli/commands/build.js +98 -0
  7. package/src/cli/commands/catalog.js +42 -0
  8. package/src/cli/commands/check.js +44 -0
  9. package/src/cli/commands/completion.js +54 -0
  10. package/src/cli/commands/decompile.js +266 -0
  11. package/src/cli/commands/dev.js +179 -0
  12. package/src/cli/commands/extension.js +110 -0
  13. package/src/cli/commands/index.js +39 -0
  14. package/src/cli/commands/init.js +169 -0
  15. package/src/cli/commands/package.js +137 -0
  16. package/src/cli/commands/sprite/add.js +85 -0
  17. package/src/cli/commands/sprite/build.js +62 -0
  18. package/src/cli/commands/sprite/helpers.js +66 -0
  19. package/src/cli/commands/sprite/index.js +29 -0
  20. package/src/cli/commands/sprite/list.js +31 -0
  21. package/src/cli/commands/sprite/remove.js +51 -0
  22. package/src/cli/commands/vendor/add.js +98 -0
  23. package/src/cli/commands/vendor/helpers.js +50 -0
  24. package/src/cli/commands/vendor/index.js +39 -0
  25. package/src/cli/commands/vendor/install.js +64 -0
  26. package/src/cli/commands/vendor/list.js +45 -0
  27. package/src/cli/commands/vendor/migrate.js +47 -0
  28. package/src/cli/commands/vendor/outdated.js +51 -0
  29. package/src/cli/commands/vendor/remove.js +34 -0
  30. package/src/cli/commands/vendor/update.js +49 -0
  31. package/src/cli/commands/vendor/verify.js +44 -0
  32. package/src/cli/completion/bash.js +10 -0
  33. package/src/cli/completion/fish.js +9 -0
  34. package/src/cli/completion/index.js +97 -0
  35. package/src/cli/completion/zsh.js +11 -0
  36. package/src/cli/context.js +257 -0
  37. package/src/cli/define-command.js +140 -0
  38. package/src/cli/errors.js +111 -0
  39. package/src/cli/format.js +31 -0
  40. package/src/cli/prompts.js +183 -0
  41. package/src/cli/reporter.js +194 -0
  42. package/src/cli/run.js +248 -0
  43. package/src/cli/templates/index.js +112 -0
  44. package/src/cli/theme.js +84 -0
  45. package/src/config-edit.js +280 -1
  46. package/src/config.js +137 -3
  47. package/src/extensions.js +72 -19
  48. package/src/index.js +21 -0
  49. package/src/vendor.js +1323 -0
  50. package/vendor/pm-packager-dist/.fx-build.json +1 -1
@@ -0,0 +1,179 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import { build } from "../../build.js";
4
+ import { sourceExtension } from "../../brand.js";
5
+ import { packageSignature, resolvePackageConfig } from "../../package.js";
6
+ import { packageProject } from "../../packager.js";
7
+ import { startDevServer } from "../../dev.js";
8
+ import { loadProjectState, resolveInput } from "../context.js";
9
+ import { EXIT, UsageError, errorToJson } from "../errors.js";
10
+
11
+ const EXTENSION = sourceExtension();
12
+
13
+ export const devCommand = {
14
+ name: "dev",
15
+ group: "Core Commands:",
16
+ summary: "Serve a live build and reload on changes",
17
+ description: `Build and serve the project as HTML, rebuilding when sources or
18
+ configuration change.`,
19
+ arguments: [`[input${EXTENSION}|dir]`],
20
+ options: [
21
+ { flags: "--host [host]", description: "Dev server host" },
22
+ { flags: "--lan", description: "Listen on all interfaces" },
23
+ { flags: "--port <port>", description: "Dev server port (default 3000)" },
24
+ {
25
+ flags: "--strict-port",
26
+ description: "Fail instead of choosing the next free port",
27
+ },
28
+ ],
29
+ examples: ["fx dev", "fx dev --lan --port 8080", "fx dev --json"],
30
+ jsonEvents: true,
31
+ async run({ args, options, cwd, reporter, prompts, product }) {
32
+ const target = resolveInput(args[0], cwd);
33
+ if (target.file && path.extname(target.file).toLowerCase() === ".pmp")
34
+ throw new UsageError(
35
+ "fx dev needs .tfx sources or a directory, not a .pmp archive",
36
+ );
37
+
38
+ let host = "127.0.0.1";
39
+ if (options.lan) host = "0.0.0.0";
40
+ else if (options.host === true) host = "0.0.0.0";
41
+ else if (typeof options.host === "string") host = options.host;
42
+
43
+ if (options.port === true) throw new UsageError("--port needs a number");
44
+ const port = options.port === undefined ? 3000 : Number(options.port);
45
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
46
+ throw new UsageError("--port must be an integer between 0 and 65535");
47
+
48
+ const initialState = await loadProjectState({
49
+ input: args[0],
50
+ cwd,
51
+ options,
52
+ reporter,
53
+ prompts,
54
+ includeExtensions: false,
55
+ });
56
+ const watchDir = initialState.configDir;
57
+
58
+ let lastSb3Hash = null;
59
+ let lastSignature = null;
60
+ let lastHtml = null;
61
+
62
+ const buildForDev = async () => {
63
+ const started = performance.now();
64
+ const state = await loadProjectState({
65
+ input: args[0],
66
+ cwd,
67
+ options,
68
+ reporter,
69
+ prompts,
70
+ includeExtensions: true,
71
+ });
72
+ const result = build({
73
+ ...state.target,
74
+ projectConfig: state.loaded.project,
75
+ fxConfig: state.loaded.fx,
76
+ catalog: state.catalog,
77
+ extensionUrls: state.extensionUrls,
78
+ useConfigFile: false,
79
+ });
80
+ for (const warning of result.warnings) reporter.warn(warning);
81
+
82
+ const pkg = resolvePackageConfig(state.loaded.package, {
83
+ baseDir: state.configDir,
84
+ projectName: result.config.name,
85
+ });
86
+ pkg.target = "html";
87
+
88
+ const signature = packageSignature(pkg);
89
+ const sb3Hash = crypto
90
+ .createHash("sha256")
91
+ .update(result.sb3)
92
+ .digest("hex");
93
+ if (
94
+ lastHtml != null &&
95
+ sb3Hash === lastSb3Hash &&
96
+ signature === lastSignature
97
+ )
98
+ return lastHtml;
99
+
100
+ const spinner = prompts.spinner();
101
+ spinner.start("Packaging project...");
102
+ let packaged;
103
+ try {
104
+ packaged = await packageProject({
105
+ sb3: result.sb3,
106
+ projectName: result.config.name,
107
+ packageConfig: pkg,
108
+ onProgress: (message) => spinner.message(String(message)),
109
+ });
110
+ } finally {
111
+ spinner.stop();
112
+ }
113
+ lastSb3Hash = sb3Hash;
114
+ lastSignature = signature;
115
+ lastHtml = packaged.html ?? packaged.data.toString("utf8");
116
+ reporter.event("build", {
117
+ durationMs: Math.round(performance.now() - started),
118
+ targets: result.project.targets.length,
119
+ });
120
+ return lastHtml;
121
+ };
122
+
123
+ const server = await startDevServer({
124
+ watchDir,
125
+ build: buildForDev,
126
+ host,
127
+ port,
128
+ strictPort: Boolean(options.strictPort),
129
+ log: {
130
+ error: (message) => {
131
+ reporter.error(message);
132
+ reporter.event("error", {
133
+ error: errorToJson(new Error(String(message))),
134
+ });
135
+ },
136
+ success: (message) => reporter.success(message),
137
+ warn: (message) => reporter.warn(message),
138
+ info: (message) => reporter.info(message),
139
+ },
140
+ });
141
+
142
+ const startup = {
143
+ url: server.url,
144
+ localUrls: server.localUrls,
145
+ networkUrls: server.networkUrls,
146
+ host,
147
+ port: server.port ?? port,
148
+ };
149
+ reporter.event("start", startup);
150
+
151
+ if (!reporter.json) {
152
+ reporter.text("");
153
+ reporter.text(
154
+ ` ${reporter.brand(product.name)} ${reporter.muted("dev")}`,
155
+ );
156
+ for (const url of server.localUrls)
157
+ reporter.detail("Local", url, { width: 9 });
158
+ for (const url of server.networkUrls)
159
+ reporter.detail("Network", url, { width: 9 });
160
+ reporter.text(` ${reporter.muted("Press Ctrl+C to stop.")}`);
161
+ }
162
+
163
+ let stopping = false;
164
+ const shutdown = (signal) => {
165
+ if (stopping) return;
166
+ stopping = true;
167
+ server
168
+ .close()
169
+ .finally(() =>
170
+ process.exit(signal === "SIGINT" ? EXIT.INTERRUPTED : EXIT.OK),
171
+ );
172
+ };
173
+ process.once("SIGINT", () => shutdown("SIGINT"));
174
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
175
+
176
+ await server.ready;
177
+ return startup;
178
+ },
179
+ };
@@ -0,0 +1,110 @@
1
+ import path from "node:path";
2
+ import { Command } from "commander";
3
+ import {
4
+ addProjectExtension,
5
+ findConfigPath,
6
+ readProjectExtensions,
7
+ removeProjectExtension,
8
+ } from "../../config.js";
9
+ import { defineCommand } from "../define-command.js";
10
+ import { UsageError } from "../errors.js";
11
+
12
+ function requireConfigPath(cwd, product) {
13
+ const found = findConfigPath(cwd);
14
+ if (!found)
15
+ throw new UsageError("No project config found.", {
16
+ hint: `run "${product.command} init" first`,
17
+ });
18
+ return found;
19
+ }
20
+
21
+ function deprecation(reporter, product) {
22
+ reporter.warn(
23
+ `"${product.command} extension ..." is deprecated and will be removed in a future major version.`,
24
+ );
25
+ reporter.hint(
26
+ `run "${product.command} vendor migrate", then use "${product.command} vendor ..." commands`,
27
+ );
28
+ }
29
+
30
+ const extensionAddCommand = {
31
+ name: "add",
32
+ summary: "Add a legacy project.extensions entry",
33
+ arguments: ["<source>"],
34
+ options: [
35
+ {
36
+ flags: "--unsandboxed <id>",
37
+ description: "Allow an extension id to run unsandboxed",
38
+ },
39
+ ],
40
+ run({ args, options, cwd, reporter, product }) {
41
+ deprecation(reporter, product);
42
+ const source = args[0];
43
+ const configPath = requireConfigPath(cwd, product);
44
+ const unsandboxed =
45
+ typeof options.unsandboxed === "string" ? options.unsandboxed : null;
46
+ const result = addProjectExtension(configPath, source, { unsandboxed });
47
+ if (result.added)
48
+ reporter.success(`Added ${source} to ${path.basename(configPath)}`);
49
+ else
50
+ reporter.info(
51
+ `${source} is already listed in ${path.basename(configPath)}`,
52
+ );
53
+ if (unsandboxed && result.unsandboxedAdded)
54
+ reporter.success(`Allowed ${unsandboxed} to run unsandboxed`);
55
+ return { source, config: configPath, ...result };
56
+ },
57
+ };
58
+
59
+ const extensionRemoveCommand = {
60
+ name: "remove",
61
+ summary: "Remove a legacy project.extensions entry",
62
+ arguments: ["<source|id>"],
63
+ run({ args, cwd, reporter, product }) {
64
+ deprecation(reporter, product);
65
+ const target = args[0];
66
+ const configPath = requireConfigPath(cwd, product);
67
+ const result = removeProjectExtension(configPath, target);
68
+ if (result.extensions.length > 0)
69
+ reporter.success(
70
+ `Removed ${result.extensions.join(", ")} from ${path.basename(configPath)}`,
71
+ );
72
+ if (result.unsandboxedRemoved)
73
+ reporter.success(`Removed ${target} from the unsandboxed list`);
74
+ return { target, config: configPath, ...result };
75
+ },
76
+ };
77
+
78
+ const extensionListCommand = {
79
+ name: "list",
80
+ summary: "List legacy project.extensions entries",
81
+ run({ cwd, reporter, product }) {
82
+ deprecation(reporter, product);
83
+ const configPath = requireConfigPath(cwd, product);
84
+ const { extensions, unsandboxed } = readProjectExtensions(configPath);
85
+ reporter.heading(
86
+ `${path.basename(configPath)}: ${extensions.length} extension(s)`,
87
+ );
88
+ for (const entry of extensions) reporter.item(entry, "", { width: 0 });
89
+ reporter.detail("Unsandboxed", unsandboxed.join(", ") || "(none)", {
90
+ width: 12,
91
+ });
92
+ return { extensions, unsandboxed };
93
+ },
94
+ };
95
+
96
+ export function registerExtension(program, options) {
97
+ const extension = new Command("extension")
98
+ .description("Deprecated legacy project.extensions commands.")
99
+ .helpGroup("Legacy Commands:");
100
+ extension.copyInheritedSettings(program);
101
+ extension.action(() => extension.outputHelp());
102
+ program.addCommand(extension, { hidden: true });
103
+ for (const spec of [
104
+ extensionAddCommand,
105
+ extensionRemoveCommand,
106
+ extensionListCommand,
107
+ ])
108
+ defineCommand(extension, spec, options);
109
+ return extension;
110
+ }
@@ -0,0 +1,39 @@
1
+ import { completeCommand, completionCommand } from "./completion.js";
2
+ import { buildCommand } from "./build.js";
3
+ import { catalogCommand } from "./catalog.js";
4
+ import { checkCommand } from "./check.js";
5
+ import { decompileCommand } from "./decompile.js";
6
+ import { devCommand } from "./dev.js";
7
+ import { registerExtension } from "./extension.js";
8
+ import { initCommand } from "./init.js";
9
+ import { packageCommand } from "./package.js";
10
+ import { registerSprite } from "./sprite/index.js";
11
+ import { registerVendor } from "./vendor/index.js";
12
+ import { defineCommand } from "../define-command.js";
13
+
14
+ const TOP_LEVEL = [
15
+ buildCommand,
16
+ devCommand,
17
+ packageCommand,
18
+ decompileCommand,
19
+ checkCommand,
20
+ catalogCommand,
21
+ initCommand,
22
+ completionCommand,
23
+ ];
24
+
25
+ /**
26
+ * Register every command on the root program.
27
+ *
28
+ * @param {import("commander").Command} program
29
+ * @param {{state: object, product: object}} options
30
+ */
31
+ export function registerCommands(program, options) {
32
+ program.commandsGroup("Core Commands:");
33
+ for (const spec of TOP_LEVEL) defineCommand(program, spec, options);
34
+ registerVendor(program, options);
35
+ registerSprite(program, options);
36
+ defineCommand(program, completeCommand, options);
37
+ registerExtension(program, options);
38
+ return program;
39
+ }
@@ -0,0 +1,169 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { normalizePackageTarget } from "../../config.js";
4
+ import { UsageError } from "../errors.js";
5
+ import {
6
+ initFiles,
7
+ STAGE_SIZES,
8
+ TARGETS,
9
+ TEMPLATES,
10
+ } from "../templates/index.js";
11
+
12
+ const TEMPLATE_VALUES = new Set(TEMPLATES.map((entry) => entry.value));
13
+ const TARGET_VALUES = new Set(TARGETS.map((entry) => entry.value));
14
+
15
+ function parseSize(value) {
16
+ const match = /^(\d+)x(\d+)$/i.exec(String(value).trim());
17
+ if (!match)
18
+ throw new UsageError(
19
+ `--size must look like 480x360, received ${JSON.stringify(value)}`,
20
+ );
21
+ return { width: Number(match[1]), height: Number(match[2]) };
22
+ }
23
+
24
+ function sizeValue(width, height) {
25
+ return `${width}x${height}`;
26
+ }
27
+
28
+ export const initCommand = {
29
+ name: "init",
30
+ aliases: ["i"],
31
+ group: "Core Commands:",
32
+ summary: "Create a new project",
33
+ description: `Scaffold a project config, a source file, and optional starter
34
+ assets. On a terminal this opens a short wizard; use --yes for defaults.`,
35
+ arguments: ["[dir]"],
36
+ options: [
37
+ { flags: "--name <name>", description: "Project name" },
38
+ {
39
+ flags: "--template <template>",
40
+ description: "Template: cat, empty, or blank",
41
+ },
42
+ {
43
+ flags: "--target <target>",
44
+ description: "Package target: none, html, or zip",
45
+ },
46
+ {
47
+ flags: "--size <WxH>",
48
+ description: "Stage size (default 480x360)",
49
+ },
50
+ { flags: "--force", description: "Overwrite existing files" },
51
+ ],
52
+ examples: [
53
+ "fx init",
54
+ "fx init examples/demo --yes",
55
+ "fx init --template blank --target zip",
56
+ ],
57
+ async run({ args, options, cwd, reporter, prompts, product, yes }) {
58
+ const directory = path.resolve(cwd, args[0] || ".");
59
+ const defaultName = `My ${product.name} Project`;
60
+ const interactive = prompts.enabled && !yes;
61
+
62
+ if (interactive) prompts.intro("Create a new project");
63
+
64
+ const name =
65
+ options.name ??
66
+ (interactive
67
+ ? await prompts.text({
68
+ message: "Project name",
69
+ defaultValue: defaultName,
70
+ })
71
+ : defaultName);
72
+
73
+ let template = options.template ?? "cat";
74
+ if (!TEMPLATE_VALUES.has(template))
75
+ throw new UsageError(
76
+ `--template must be one of: ${[...TEMPLATE_VALUES].join(", ")}`,
77
+ );
78
+ if (interactive && options.template === undefined)
79
+ template = await prompts.select({
80
+ message: "Template",
81
+ options: TEMPLATES,
82
+ initialValue: template,
83
+ });
84
+
85
+ let size = options.size ?? "480x360";
86
+ if (interactive && options.size === undefined)
87
+ size = await prompts.select({
88
+ message: "Stage size",
89
+ options: STAGE_SIZES,
90
+ initialValue: "480x360",
91
+ });
92
+ const { width, height } = parseSize(size);
93
+
94
+ let target = options.target ?? "html";
95
+ if (!TARGET_VALUES.has(target)) {
96
+ try {
97
+ target = normalizePackageTarget(target);
98
+ } catch {
99
+ throw new UsageError(
100
+ `--target must be one of: ${[...TARGET_VALUES].join(", ")}`,
101
+ );
102
+ }
103
+ }
104
+ if (interactive && options.target === undefined)
105
+ target = await prompts.select({
106
+ message: "Package target",
107
+ options: TARGETS,
108
+ initialValue: target,
109
+ });
110
+
111
+ const files = initFiles({
112
+ product,
113
+ name,
114
+ template,
115
+ width,
116
+ height,
117
+ target,
118
+ });
119
+ const conflicts = Object.keys(files).filter((relative) =>
120
+ fs.existsSync(path.join(directory, relative)),
121
+ );
122
+
123
+ let force = Boolean(options.force);
124
+ if (conflicts.length > 0 && !force) {
125
+ if (interactive) {
126
+ const overwrite = await prompts.confirm({
127
+ message: `Overwrite ${conflicts.length} existing file(s)?`,
128
+ initialValue: false,
129
+ });
130
+ force = Boolean(overwrite);
131
+ }
132
+ if (!force)
133
+ reporter.warn(
134
+ `Skipping ${conflicts.length} existing file(s); pass --force to overwrite`,
135
+ );
136
+ }
137
+
138
+ const written = [];
139
+ for (const relative of Object.keys(files).sort()) {
140
+ const destination = path.join(directory, relative);
141
+ if (fs.existsSync(destination) && !force) {
142
+ written.push({ path: relative, status: "skipped" });
143
+ continue;
144
+ }
145
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
146
+ fs.writeFileSync(destination, files[relative]);
147
+ written.push({ path: relative, status: "written" });
148
+ reporter.bullet(relative);
149
+ }
150
+
151
+ const count = written.filter((entry) => entry.status === "written").length;
152
+ reporter.success(
153
+ `Created ${count} file(s) in ${path.relative(cwd, directory) || "."}`,
154
+ );
155
+ if (interactive)
156
+ prompts.outro(
157
+ `Run "${product.command} build" or "${product.command} dev" next.`,
158
+ );
159
+
160
+ return {
161
+ directory,
162
+ name,
163
+ template,
164
+ target,
165
+ size: sizeValue(width, height),
166
+ files: written,
167
+ };
168
+ },
169
+ };
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { build } from "../../build.js";
4
+ import { normalizePackageTarget } from "../../config.js";
5
+ import { packageProject } from "../../packager.js";
6
+ import {
7
+ extractArchive,
8
+ packageOutputPath,
9
+ resolvePackageConfig,
10
+ } from "../../package.js";
11
+ import { loadProjectState } from "../context.js";
12
+ import { UsageError } from "../errors.js";
13
+ import { formatBytes, formatDuration } from "../format.js";
14
+
15
+ function isArchive(file) {
16
+ return path.extname(file).toLowerCase() === ".pmp";
17
+ }
18
+
19
+ export const packageCommand = {
20
+ name: "package",
21
+ aliases: ["p", "pack"],
22
+ group: "Core Commands:",
23
+ summary: "Package into HTML, ZIP, or a desktop build",
24
+ description: `Package a source, project archive, or directory into HTML, a ZIP
25
+ archive, or a desktop build (Windows/macOS/Linux use Electron).`,
26
+ arguments: ["[input.tfx|input.pmp|dir]"],
27
+ options: [
28
+ { flags: "-o, --output <file>", description: "Output file or directory" },
29
+ { flags: "--target <target>", description: "Packaging target" },
30
+ { flags: "--extract", description: "Extract a packaged archive" },
31
+ ],
32
+ examples: [
33
+ "fx package",
34
+ "fx package --target html",
35
+ "fx package build/game.pmp --target windows --extract",
36
+ ],
37
+ async run({ args, options, cwd, reporter, prompts }) {
38
+ const started = performance.now();
39
+ const input = args[0];
40
+ const archiveInput = input ? isArchive(path.resolve(cwd, input)) : false;
41
+ const state = await loadProjectState({
42
+ input,
43
+ cwd,
44
+ options,
45
+ reporter,
46
+ prompts,
47
+ includeExtensions: !archiveInput,
48
+ });
49
+ const { target, loaded, searchDir } = state;
50
+
51
+ let sb3;
52
+ let projectName;
53
+ if (target.file && isArchive(target.file)) {
54
+ sb3 = fs.readFileSync(target.file);
55
+ projectName = path.basename(target.file, path.extname(target.file));
56
+ } else {
57
+ const result = build({
58
+ ...target,
59
+ projectConfig: loaded.project,
60
+ fxConfig: loaded.fx,
61
+ catalog: state.catalog,
62
+ extensionUrls: state.extensionUrls,
63
+ useConfigFile: false,
64
+ });
65
+ for (const warning of result.warnings) reporter.warn(warning);
66
+ sb3 = result.sb3;
67
+ projectName = result.config.name;
68
+ }
69
+
70
+ const baseDir = loaded.path ? path.dirname(loaded.path) : searchDir;
71
+ const pkg = resolvePackageConfig(loaded.package, {
72
+ baseDir,
73
+ projectName,
74
+ });
75
+
76
+ if (typeof options.target === "string")
77
+ pkg.target = normalizePackageTarget(options.target);
78
+ if (typeof options.output === "string")
79
+ pkg.output = path.resolve(cwd, options.output);
80
+ const extract = Boolean(options.extract) || pkg.extract;
81
+ if (extract && pkg.target === "html")
82
+ throw new UsageError("--extract only applies to zip and desktop targets");
83
+
84
+ const output = packageOutputPath({
85
+ target: pkg.target,
86
+ output: pkg.output,
87
+ name: pkg.name || projectName,
88
+ dir: baseDir,
89
+ });
90
+
91
+ const spinner = prompts.spinner();
92
+ spinner.start("Packaging project...");
93
+ let result;
94
+ try {
95
+ result = await packageProject({
96
+ sb3,
97
+ projectName,
98
+ packageConfig: pkg,
99
+ onProgress: (message) => spinner.message(String(message)),
100
+ });
101
+ } finally {
102
+ spinner.stop();
103
+ }
104
+
105
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
106
+ fs.writeFileSync(output, result.data);
107
+ const durationMs = Math.round(performance.now() - started);
108
+
109
+ reporter.success(`Packaged ${output}`);
110
+ reporter.text("");
111
+ reporter.detail("Target", result.target);
112
+ reporter.detail("Type", result.type);
113
+ reporter.detail("Size", formatBytes(result.data.length));
114
+ reporter.detail("Time", formatDuration(durationMs));
115
+
116
+ let extracted = null;
117
+ if (extract) {
118
+ const extension = path.extname(output);
119
+ const extractDir = path.join(
120
+ path.dirname(output),
121
+ path.basename(output, extension),
122
+ );
123
+ const files = await extractArchive(result.data, extractDir);
124
+ extracted = { directory: extractDir, files };
125
+ reporter.success(`Extracted ${files.length} file(s) to ${extractDir}`);
126
+ }
127
+
128
+ return {
129
+ output,
130
+ target: result.target,
131
+ type: result.type,
132
+ size: result.data.length,
133
+ extracted,
134
+ durationMs,
135
+ };
136
+ },
137
+ };