bytecut 0.0.0-stage → 0.0.1

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/cli.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ collectDoctorReport,
4
+ formatDoctorReport
5
+ } from "./chunk-326CQQWX.js";
6
+ import {
7
+ svgoEngine
8
+ } from "./chunk-LTWUPQDT.js";
9
+ import {
10
+ audit
11
+ } from "./chunk-N536JFSF.js";
12
+ import {
13
+ package_default
14
+ } from "./chunk-G53LNJ5O.js";
15
+ import {
16
+ formatAuditReport
17
+ } from "./chunk-CEZK3RQJ.js";
18
+ import "./chunk-3DBJKHOD.js";
19
+ import "./chunk-5R7LR5BB.js";
20
+ import {
21
+ sharpImageEngine
22
+ } from "./chunk-XFJ3HW4X.js";
23
+ import "./chunk-NAKV7HS5.js";
24
+ import {
25
+ createVideoEngine,
26
+ discover,
27
+ ffmpegInstallText,
28
+ ffmpegStatus,
29
+ makeScorerFactory,
30
+ noAssetsMessage
31
+ } from "./chunk-4XOB3C34.js";
32
+ import {
33
+ doctorDeps
34
+ } from "./chunk-M6ZDSATP.js";
35
+ import "./chunk-WE7AEDVI.js";
36
+ import "./chunk-YBEC5QGV.js";
37
+ import "./chunk-LHTI3JMC.js";
38
+ import {
39
+ guarded
40
+ } from "./chunk-J2XJO4JZ.js";
41
+ import "./chunk-TWI2OZJZ.js";
42
+
43
+ // src/cli/index.ts
44
+ import { defineCommand, runMain } from "citty";
45
+
46
+ // src/cli/interactive.ts
47
+ function isInteractive(env = process.env, stdin = process.stdin, stdout = process.stdout) {
48
+ const ci = env.CI;
49
+ const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
50
+ return Boolean(stdin.isTTY && stdout.isTTY) && !inCi;
51
+ }
52
+ async function runDefault() {
53
+ const found = await discover([]);
54
+ if (found.scanInputs.length === 0) {
55
+ console.error(noAssetsMessage("", found.project));
56
+ process.exitCode = 2;
57
+ return;
58
+ }
59
+ if (isInteractive()) {
60
+ const { runTui } = await import("./tui-J4Y44IL7.js");
61
+ await runTui({
62
+ projectRoot: found.projectRoot,
63
+ project: found.project,
64
+ config: found.config,
65
+ inputs: found.scanInputs,
66
+ outDir: found.outDir,
67
+ engine: sharpImageEngine,
68
+ svgEngine: svgoEngine,
69
+ toolVersion: package_default.version,
70
+ ffmpeg: ffmpegStatus(found.config),
71
+ videoEngine: createVideoEngine({}, found.config),
72
+ makeScorer: makeScorerFactory(void 0, found.config),
73
+ needsFfmpeg: ffmpegInstallText(),
74
+ loadDoctor: async () => formatDoctorReport(await collectDoctorReport(doctorDeps()))
75
+ });
76
+ return;
77
+ }
78
+ const report = await audit({
79
+ projectRoot: found.projectRoot,
80
+ engine: sharpImageEngine,
81
+ svgEngine: svgoEngine,
82
+ inputs: found.scanInputs,
83
+ outDir: found.outDir,
84
+ staticRoot: found.project.staticRoot,
85
+ ffmpeg: ffmpegStatus(found.config),
86
+ videoEngine: createVideoEngine({}, found.config),
87
+ needsFfmpeg: ffmpegInstallText(),
88
+ policy: found.config.policy,
89
+ exclude: found.config.exclude,
90
+ ignore: found.config.auditIgnore,
91
+ settings: found.config.rules,
92
+ budgets: found.config.budgets,
93
+ // A bare `npx bytecut` in CI should not create folders in the repo.
94
+ persistCache: false
95
+ });
96
+ console.log(formatAuditReport(report));
97
+ console.log(
98
+ "\nNot an interactive terminal. Run `bytecut optimize -y` to write the optimized files."
99
+ );
100
+ }
101
+
102
+ // src/cli/index.ts
103
+ var SUBCOMMANDS = [
104
+ "audit",
105
+ "optimize",
106
+ "snippet",
107
+ "compare",
108
+ "explain",
109
+ "init",
110
+ "check",
111
+ "doctor"
112
+ ];
113
+ var main = defineCommand({
114
+ meta: {
115
+ name: "bytecut",
116
+ version: package_default.version,
117
+ description: "Audit and optimize images and videos in web projects"
118
+ },
119
+ // Plain `bytecut` (no subcommand): interactive UI on a TTY, audit report otherwise.
120
+ // citty also invokes the parent's `run` after a subcommand finishes, so bail out then, or
121
+ // every subcommand would be followed by a second, unrelated audit of the project.
122
+ run: guarded(async ({ rawArgs }) => {
123
+ const first = rawArgs.find((arg) => !arg.startsWith("-"));
124
+ if (first !== void 0 && SUBCOMMANDS.includes(first)) return;
125
+ await runDefault();
126
+ }),
127
+ subCommands: {
128
+ audit: () => import("./audit-HVW4EOLP.js").then((m) => m.default),
129
+ optimize: () => import("./optimize-UXM6JEDA.js").then((m) => m.default),
130
+ snippet: () => import("./snippet-2WVGMCNV.js").then((m) => m.default),
131
+ compare: () => import("./compare-6PCE6533.js").then((m) => m.default),
132
+ explain: () => import("./explain-SGN5BOR7.js").then((m) => m.default),
133
+ init: () => import("./init-WHWUANOB.js").then((m) => m.default),
134
+ check: () => import("./check-YONIQTM7.js").then((m) => m.default),
135
+ doctor: () => import("./doctor-R226JU6P.js").then((m) => m.default)
136
+ }
137
+ });
138
+ await runMain(main);
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ startCompareServer
4
+ } from "./chunk-7IECTPJB.js";
5
+ import {
6
+ MANIFEST_FILE,
7
+ parseManifest
8
+ } from "./chunk-5R7LR5BB.js";
9
+ import {
10
+ configArg,
11
+ discover
12
+ } from "./chunk-4XOB3C34.js";
13
+ import "./chunk-M6ZDSATP.js";
14
+ import {
15
+ toPosix
16
+ } from "./chunk-WE7AEDVI.js";
17
+ import "./chunk-YBEC5QGV.js";
18
+ import {
19
+ guarded
20
+ } from "./chunk-J2XJO4JZ.js";
21
+ import {
22
+ BytecutError
23
+ } from "./chunk-TWI2OZJZ.js";
24
+
25
+ // src/cli/commands/compare.ts
26
+ import { readFile } from "fs/promises";
27
+ import { relative, resolve } from "path";
28
+ import { defineCommand } from "citty";
29
+ var compare_default = defineCommand({
30
+ meta: {
31
+ name: "compare",
32
+ description: "Serve a before/after page for one optimized file on localhost (127.0.0.1, random port)"
33
+ },
34
+ args: {
35
+ ...configArg,
36
+ port: { type: "string", description: "Port to listen on (default: a free one)" },
37
+ "out-dir": { type: "string", description: "Output folder (default: from the framework)" }
38
+ },
39
+ run: guarded(async ({ args }) => {
40
+ const inputs = args._.map(String);
41
+ if (inputs.length !== 1) {
42
+ throw new BytecutError(
43
+ "BAD_ARGUMENTS",
44
+ "Name one optimized file, e.g. `bytecut compare public/img/hero.jpg`."
45
+ );
46
+ }
47
+ let port = 0;
48
+ if (args.port !== void 0) {
49
+ port = Number(args.port);
50
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
51
+ throw new BytecutError("BAD_ARGUMENTS", `--port must be 0-65535, not "${args.port}".`);
52
+ }
53
+ }
54
+ const found = await discover([], args["out-dir"], args.config);
55
+ let text;
56
+ try {
57
+ text = await readFile(resolve(found.projectRoot, found.outDir, MANIFEST_FILE), "utf8");
58
+ } catch {
59
+ throw new BytecutError(
60
+ "INPUT_NOT_FOUND",
61
+ `No manifest at ${found.outDir}/${MANIFEST_FILE}. Run \`bytecut optimize -y\` first.`
62
+ );
63
+ }
64
+ const source = toPosix(relative(found.projectRoot, resolve(found.projectRoot, inputs[0])));
65
+ const server = await startCompareServer({
66
+ projectRoot: found.projectRoot,
67
+ outDir: found.outDir,
68
+ manifest: parseManifest(text),
69
+ source,
70
+ port
71
+ });
72
+ console.log(`Comparing ${source}`);
73
+ console.log(` ${server.url}`);
74
+ console.log("Local only (127.0.0.1). Press Ctrl-C to stop.");
75
+ await new Promise((done) => {
76
+ process.once("SIGINT", () => done());
77
+ process.once("SIGTERM", () => done());
78
+ });
79
+ await server.close();
80
+ })
81
+ });
82
+ export {
83
+ compare_default as default
84
+ };
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ collectDoctorReport,
4
+ formatDoctorReport
5
+ } from "./chunk-326CQQWX.js";
6
+ import {
7
+ doctorDeps
8
+ } from "./chunk-M6ZDSATP.js";
9
+ import {
10
+ resolveConfig
11
+ } from "./chunk-YBEC5QGV.js";
12
+ import {
13
+ isGitIgnored
14
+ } from "./chunk-DJ45NQZD.js";
15
+ import {
16
+ guarded
17
+ } from "./chunk-J2XJO4JZ.js";
18
+ import {
19
+ BytecutError,
20
+ detectProject,
21
+ loadConfig
22
+ } from "./chunk-TWI2OZJZ.js";
23
+
24
+ // src/cli/commands/doctor.ts
25
+ import { defineCommand } from "citty";
26
+ import { resolve } from "path";
27
+ var doctor_default = defineCommand({
28
+ meta: {
29
+ name: "doctor",
30
+ description: "Check Node, sharp and ffmpeg, and report what is degraded"
31
+ },
32
+ args: {
33
+ config: {
34
+ type: "string",
35
+ description: "Path to a config file (default: bytecut.config.* in the project)"
36
+ },
37
+ ffmpeg: { type: "string", description: "Path to the ffmpeg binary" },
38
+ ffprobe: { type: "string", description: "Path to the ffprobe binary" },
39
+ ssimulacra2: {
40
+ type: "string",
41
+ description: "Path to a native ssimulacra2 (optional; a WebAssembly scorer is built in)"
42
+ },
43
+ json: { type: "boolean", description: "Print the report as JSON" }
44
+ },
45
+ run: guarded(async ({ args }) => {
46
+ const projectRoot = process.cwd();
47
+ let configError;
48
+ let resolved = await resolveConfig(projectRoot, null);
49
+ try {
50
+ resolved = await resolveConfig(projectRoot, await loadConfig(projectRoot, args.config));
51
+ } catch (error) {
52
+ if (!(error instanceof BytecutError)) throw error;
53
+ configError = error.message;
54
+ resolved = { ...resolved, project: await detectProject(projectRoot) };
55
+ }
56
+ const report = await collectDoctorReport(doctorDeps(), {
57
+ ffmpegFlag: args.ffmpeg,
58
+ ffprobeFlag: args.ffprobe,
59
+ ffmpegConfig: resolved.ffmpegPath,
60
+ ffprobeConfig: resolved.ffprobePath,
61
+ ssimulacra2Flag: args.ssimulacra2,
62
+ ssimulacra2Config: resolved.ssimulacra2Path
63
+ });
64
+ report.project = {
65
+ framework: resolved.project.framework,
66
+ configFile: resolved.loaded?.name ?? null,
67
+ ...configError ? { configError } : {},
68
+ outDir: resolved.project.outDir,
69
+ outDirIgnored: await isGitIgnored(projectRoot, resolve(projectRoot, resolved.project.outDir))
70
+ };
71
+ console.log(args.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
72
+ if (!report.ok) process.exitCode = 1;
73
+ })
74
+ });
75
+ export {
76
+ doctor_default as default
77
+ };
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ sharpImageEngine
4
+ } from "./chunk-XFJ3HW4X.js";
5
+ import {
6
+ classify,
7
+ classifyVideo,
8
+ mirroredDir,
9
+ plannedVideoOutputs,
10
+ targetWidths
11
+ } from "./chunk-NAKV7HS5.js";
12
+ import {
13
+ configArg,
14
+ createVideoEngine,
15
+ discover
16
+ } from "./chunk-4XOB3C34.js";
17
+ import "./chunk-M6ZDSATP.js";
18
+ import {
19
+ toPosix
20
+ } from "./chunk-WE7AEDVI.js";
21
+ import {
22
+ getVideoPreset
23
+ } from "./chunk-YBEC5QGV.js";
24
+ import {
25
+ guarded
26
+ } from "./chunk-J2XJO4JZ.js";
27
+ import {
28
+ BytecutError,
29
+ formatBytes,
30
+ matchesGlob
31
+ } from "./chunk-TWI2OZJZ.js";
32
+
33
+ // src/cli/commands/explain.ts
34
+ import { stat } from "fs/promises";
35
+ import { resolve as resolve2 } from "path";
36
+ import { defineCommand } from "citty";
37
+
38
+ // src/core/config/explain.ts
39
+ import { readFile } from "fs/promises";
40
+ import { extname, posix, relative, resolve } from "path";
41
+ var RASTER = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif"]);
42
+ var VIDEO = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".m4v", ".mkv"]);
43
+ async function explainFile(options) {
44
+ const { config, projectRoot } = options;
45
+ const absolute = resolve(projectRoot, options.file);
46
+ const source = toPosix(relative(projectRoot, absolute));
47
+ const ext = extname(source).toLowerCase();
48
+ const { catalog } = config;
49
+ const base = {
50
+ source,
51
+ outDir: config.project.outDir,
52
+ publicPath: config.project.publicPath,
53
+ excluded: false,
54
+ auditIgnored: config.auditIgnore.some((p) => matchesGlob(p, source))
55
+ };
56
+ const none = (kind, reason) => ({
57
+ ...base,
58
+ kind,
59
+ decision: { by: "unavailable", reason },
60
+ preset: null,
61
+ chain: [],
62
+ changedBy: [],
63
+ resolved: null,
64
+ outputs: []
65
+ });
66
+ if (ext === ".svg") return none("svg", "SVG output is not implemented yet");
67
+ const isVideo = VIDEO.has(ext);
68
+ if (!isVideo && !RASTER.has(ext))
69
+ return none("unsupported", `${ext || "this"} is not a supported type`);
70
+ const matched = config.policy.assetFor(source);
71
+ const detail = (preset2) => ({
72
+ chain: catalog.origin[preset2]?.chain ?? [preset2],
73
+ changedBy: catalog.origin[preset2]?.overrides ?? []
74
+ });
75
+ const pick = (kind, classifier) => {
76
+ const known = (name) => kind === "image" ? catalog.image[name] : catalog.video[name];
77
+ if (options.presetFlag) {
78
+ if (!known(options.presetFlag)) {
79
+ return {
80
+ preset: null,
81
+ decision: {
82
+ by: "unavailable",
83
+ reason: `--preset ${options.presetFlag} is not a ${kind} preset`
84
+ }
85
+ };
86
+ }
87
+ return {
88
+ preset: options.presetFlag,
89
+ decision: { by: "flag", reason: `forced with --preset ${options.presetFlag}` }
90
+ };
91
+ }
92
+ if (matched && known(matched.preset)) {
93
+ return {
94
+ preset: matched.preset,
95
+ decision: {
96
+ by: "assets",
97
+ pattern: matched.pattern,
98
+ reason: `matched assets "${matched.pattern}"`
99
+ }
100
+ };
101
+ }
102
+ const verdict = classifier();
103
+ if (!verdict) {
104
+ return {
105
+ preset: null,
106
+ decision: {
107
+ by: "unavailable",
108
+ reason: "classifying a video needs ffprobe, which was not found"
109
+ }
110
+ };
111
+ }
112
+ return {
113
+ preset: verdict.preset,
114
+ decision: { by: "classifier", reason: verdict.reason, confidence: verdict.confidence }
115
+ };
116
+ };
117
+ if (!isVideo) {
118
+ const data = await readFile(absolute);
119
+ const analysis = await options.engine.analyze(data);
120
+ if (!(analysis.animated && analysis.format === "gif")) {
121
+ const { preset: preset2, decision: decision2 } = pick("image", () => classify(source, analysis));
122
+ const resolved2 = preset2 ? catalog.image[preset2] : null;
123
+ const dir = mirroredDir(source, config.project.staticRoot);
124
+ const name = posix.basename(source, posix.extname(source));
125
+ const outputs = resolved2 ? targetWidths(resolved2.widths, analysis.width).flatMap(
126
+ (w) => resolved2.formats.map((f) => posix.join(dir, name, `${name}-${w}.${f}`))
127
+ ) : [];
128
+ return {
129
+ ...base,
130
+ kind: "image",
131
+ decision: decision2,
132
+ preset: preset2,
133
+ ...preset2 ? detail(preset2) : { chain: [], changedBy: [] },
134
+ resolved: resolved2,
135
+ outputs
136
+ };
137
+ }
138
+ }
139
+ const probe = options.videoEngine ? await options.videoEngine.probe(absolute).catch(() => null) : null;
140
+ const isGif = ext === ".gif";
141
+ const { preset, decision } = pick("video", () => {
142
+ if (isGif) return { preset: "gif-to-video", reason: "animated GIF", confidence: "high" };
143
+ return probe ? classifyVideo(source, probe) : null;
144
+ });
145
+ const resolved = preset ? getVideoPreset(preset, catalog) ?? null : null;
146
+ return {
147
+ ...base,
148
+ kind: "video",
149
+ decision,
150
+ preset,
151
+ ...preset ? detail(preset) : { chain: [], changedBy: [] },
152
+ resolved,
153
+ outputs: resolved && probe ? plannedVideoOutputs(source, config.project.staticRoot, resolved, probe).map((o) => o.path) : []
154
+ };
155
+ }
156
+ function formatExplanation(e) {
157
+ const lines = [e.source];
158
+ lines.push(` type: ${e.kind}`);
159
+ if (e.preset === null) {
160
+ lines.push(` preset: none (${e.decision.reason})`);
161
+ } else {
162
+ const how = e.decision.by === "assets" ? `assets rule "${e.decision.pattern}"` : e.decision.by === "flag" ? "the --preset flag" : e.decision.by === "classifier" ? `classifier, ${e.decision.confidence} confidence: ${e.decision.reason}` : e.decision.reason;
163
+ lines.push(` preset: ${e.preset} (from ${how})`);
164
+ if (e.chain.length > 1 || e.changedBy.length > 0) {
165
+ lines.push(` extends: ${e.chain.join(" \u2192 ")}`);
166
+ if (e.changedBy.length > 0) lines.push(` changed: ${e.changedBy.join(", ")}`);
167
+ }
168
+ }
169
+ const r = e.resolved;
170
+ if (r && "widths" in r) {
171
+ lines.push(` widths: ${r.widths.join(", ")} (never above the source)`);
172
+ lines.push(` formats: ${r.formats.join(", ")}`);
173
+ for (const f of r.formats) {
174
+ const lossless = r.lossless ? ", lossless" : "";
175
+ lines.push(
176
+ ` ${f}: quality ${r.encoders[f].quality}, effort ${r.encoders[f].effort}${lossless}`
177
+ );
178
+ }
179
+ lines.push(` priority: ${r.priority ? "yes (fetchpriority=high, never lazy)" : "no"}`);
180
+ if (r.search) {
181
+ const parts = [`ssimulacra2 >= ${r.search.target}`];
182
+ if (r.search.maxBytes !== void 0) {
183
+ parts.push(
184
+ `at most ${formatBytes(r.search.maxBytes)} per file, quality may fall to a score of ${r.search.floor}`
185
+ );
186
+ }
187
+ lines.push(
188
+ ` search: ${parts.join("; ")} (quality is found per image; lossless outputs are exact)`
189
+ );
190
+ }
191
+ } else if (r) {
192
+ lines.push(` codecs: ${r.codecs.join(", ")}`);
193
+ lines.push(` audio: ${r.audio === "strip" ? "removed" : "kept"}`);
194
+ lines.push(
195
+ ` cap: ${r.maxWidth ? `${r.maxWidth}px wide` : "no width cap"}, ${r.maxFps ? `${r.maxFps} fps` : "no fps cap"}`
196
+ );
197
+ lines.push(` crf: av1 ${r.crf.av1}, h264 ${r.crf.h264}`);
198
+ if (r.search) {
199
+ lines.push(
200
+ ` search: ${r.search.metric} >= ${r.search.target} on the worst of 3 sampled clips (crf above is the starting point)`
201
+ );
202
+ }
203
+ }
204
+ if (e.outputs.length > 0) {
205
+ lines.push(` outputs: ${e.outDir}/`);
206
+ for (const o of e.outputs) lines.push(` ${o}`);
207
+ }
208
+ if (e.auditIgnored)
209
+ lines.push(" note: matched audit.ignore, so audit raises no findings for it");
210
+ return lines.join("\n");
211
+ }
212
+
213
+ // src/cli/commands/explain.ts
214
+ var explain_default = defineCommand({
215
+ meta: {
216
+ name: "explain",
217
+ description: "Show which rule chose a file's preset and what that preset resolves to"
218
+ },
219
+ args: {
220
+ ...configArg,
221
+ preset: { type: "string", description: "Show the result of forcing this preset" },
222
+ json: { type: "boolean", description: "Print the explanation as JSON" },
223
+ ffmpeg: { type: "string", description: "Path to the ffmpeg binary" },
224
+ ffprobe: { type: "string", description: "Path to the ffprobe binary" }
225
+ },
226
+ run: guarded(async ({ args }) => {
227
+ const files = args._.map(String);
228
+ if (files.length === 0) {
229
+ throw new BytecutError(
230
+ "BAD_ARGUMENTS",
231
+ "Name a file, e.g. `bytecut explain public/img/hero.jpg`."
232
+ );
233
+ }
234
+ const found = await discover([], void 0, args.config);
235
+ const engine = createVideoEngine({ ffmpeg: args.ffmpeg, ffprobe: args.ffprobe }, found.config);
236
+ const results = [];
237
+ for (const file of files) {
238
+ if (!(await stat(resolve2(found.projectRoot, file)).catch(() => null))?.isFile()) {
239
+ throw new BytecutError("INPUT_NOT_FOUND", `No such file: ${file}`);
240
+ }
241
+ results.push(
242
+ await explainFile({
243
+ projectRoot: found.projectRoot,
244
+ file,
245
+ config: found.config,
246
+ engine: sharpImageEngine,
247
+ videoEngine: engine,
248
+ presetFlag: args.preset
249
+ })
250
+ );
251
+ }
252
+ if (args.json) {
253
+ process.stdout.write(`${JSON.stringify({ version: 1, files: results }, null, 2)}
254
+ `);
255
+ return;
256
+ }
257
+ const where = found.config.loaded ? found.config.loaded.name : "no config (zero-config)";
258
+ console.log(`Config: ${where}
259
+ `);
260
+ console.log(results.map(formatExplanation).join("\n\n"));
261
+ })
262
+ });
263
+ export {
264
+ explain_default as default
265
+ };
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isGitIgnored
4
+ } from "./chunk-DJ45NQZD.js";
5
+ import {
6
+ applyChoices,
7
+ isEmpty,
8
+ renderConfig
9
+ } from "./chunk-GMHOU4Y5.js";
10
+ import {
11
+ writeConfigFile
12
+ } from "./chunk-LHTI3JMC.js";
13
+ import {
14
+ guarded
15
+ } from "./chunk-J2XJO4JZ.js";
16
+ import {
17
+ BytecutError,
18
+ FRAMEWORKS,
19
+ PLACEHOLDERS,
20
+ detectProject,
21
+ findConfigFiles
22
+ } from "./chunk-TWI2OZJZ.js";
23
+
24
+ // src/cli/commands/init.ts
25
+ import { relative } from "path";
26
+ import { defineCommand } from "citty";
27
+ import { resolve } from "path";
28
+ var FILE = "bytecut.config.json";
29
+ var init_default = defineCommand({
30
+ meta: {
31
+ name: "init",
32
+ description: "Write a minimal bytecut.config.json containing only non-default choices"
33
+ },
34
+ args: {
35
+ "out-dir": { type: "string", description: "Output folder to record" },
36
+ "public-path": { type: "string", description: "URL prefix to record" },
37
+ framework: { type: "string", description: `Framework to record (${FRAMEWORKS.join(", ")})` },
38
+ placeholder: {
39
+ type: "string",
40
+ description: `Placeholder to record (${PLACEHOLDERS.join(", ")})`
41
+ }
42
+ },
43
+ run: guarded(async ({ args }) => {
44
+ const projectRoot = process.cwd();
45
+ const existing = await findConfigFiles(projectRoot);
46
+ if (existing.length > 0) {
47
+ throw new BytecutError(
48
+ "CONFIG_EXISTS",
49
+ `${existing[0]} already exists; edit it instead. init never overwrites a config.`
50
+ );
51
+ }
52
+ const framework = args.framework;
53
+ if (framework !== void 0 && !FRAMEWORKS.includes(framework)) {
54
+ throw new BytecutError(
55
+ "BAD_ARGUMENTS",
56
+ `--framework must be one of ${FRAMEWORKS.join(", ")}.`
57
+ );
58
+ }
59
+ const placeholder = args.placeholder;
60
+ if (placeholder !== void 0 && !PLACEHOLDERS.includes(placeholder)) {
61
+ throw new BytecutError(
62
+ "BAD_ARGUMENTS",
63
+ `--placeholder must be one of ${PLACEHOLDERS.join(", ")}.`
64
+ );
65
+ }
66
+ const detected = await detectProject(projectRoot);
67
+ const effective = await detectProject(projectRoot, { framework });
68
+ const choices = {
69
+ ...args["out-dir"] && args["out-dir"] !== effective.outDir ? { outDir: args["out-dir"] } : {},
70
+ ...args["public-path"] && args["public-path"] !== effective.publicPath ? { publicPath: args["public-path"] } : {},
71
+ ...placeholder && placeholder !== "dominant-color" ? { placeholder } : {}
72
+ };
73
+ const data = applyChoices(
74
+ framework && framework !== detected.framework ? { framework } : {},
75
+ choices
76
+ );
77
+ const path = await writeConfigFile(projectRoot, FILE, renderConfig("json", data));
78
+ console.log(
79
+ isEmpty(choices) && !data.framework ? `Wrote ${relative(projectRoot, path)} with no overrides: everything is at its default.` : `Wrote ${relative(projectRoot, path)}:
80
+ ${renderConfig("json", data)}`
81
+ );
82
+ const outDir = args["out-dir"] ?? effective.outDir;
83
+ if (await isGitIgnored(projectRoot, resolve(projectRoot, outDir))) {
84
+ console.warn(
85
+ `Note: ${outDir} is matched by a .gitignore. bytecut's outputs are meant to be committed so deploys need no encoding step. Nothing was changed.`
86
+ );
87
+ }
88
+ })
89
+ });
90
+ export {
91
+ init_default as default
92
+ };