@devdogsuga/backstage 0.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/README.md +147 -0
- package/bin/backstage.mjs +28 -0
- package/dist/args-Cjr_Iqts.js +66 -0
- package/dist/build-info.json +3 -0
- package/dist/catalog-BOBE4ee7.js +1570 -0
- package/dist/cli-YLCe3Zrx.js +6300 -0
- package/dist/commands-Bk7IbhPD.js +220 -0
- package/dist/commands-D345F-Od.js +800 -0
- package/dist/commands-Dv5TXjvb.js +545 -0
- package/dist/dispatch-D048O65I.js +13 -0
- package/dist/launch.js +329 -0
- package/dist/options-BTjOf5KP.js +183 -0
- package/dist/peer-redirect-hooks.js +15 -0
- package/dist/telemetry-Bjoz29Hl.js +609 -0
- package/dist/telemetry.js +2 -0
- package/dist/ui-CdKo8mLw.js +66 -0
- package/env.ts +163 -0
- package/package.json +82 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
|
|
2
|
+
import { i as qrParseOptions, n as QR_FLAGS, o as isDryRun, t as QR_EXTRA_FLAGS } from "./options-BTjOf5KP.js";
|
|
3
|
+
import { a as explainError, o as unwrap, t as UsageError } from "./ui-CdKo8mLw.js";
|
|
4
|
+
import { t as DONE } from "./dispatch-D048O65I.js";
|
|
5
|
+
import { dirname, extname, relative, resolve } from "node:path";
|
|
6
|
+
import { log, text } from "@clack/prompts";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { renderQr } from "@devdogsuga/brand/render";
|
|
11
|
+
import { QR_LOGO_PRESETS, qrRequestSchema, qrVersionIssue, resolveQrRequest } from "@devdogsuga/brand/qr";
|
|
12
|
+
//#region src/qr/commands.ts
|
|
13
|
+
/**
|
|
14
|
+
* `pnpm backstage qr <text> [options]`
|
|
15
|
+
*
|
|
16
|
+
* The QR generator from `/console/qr`, on the command line, with every option
|
|
17
|
+
* the page has: content, size and margin, ink colour, background, a two-colour
|
|
18
|
+
* gradient, module shape, error-correction level, QR version, a centre logo
|
|
19
|
+
* (a brand preset, or an image file with size, padding and crop) and every
|
|
20
|
+
* output format at once. The page stays, deprecated; new options are
|
|
21
|
+
* CLI-only.
|
|
22
|
+
*
|
|
23
|
+
* Both read one request schema from `@devdogsuga/brand/qr`, so what a request
|
|
24
|
+
* means cannot differ between them. The scannability warning is the page's
|
|
25
|
+
* too: `qrVersionIssue`.
|
|
26
|
+
*
|
|
27
|
+
* No credentials, no checkout.
|
|
28
|
+
*/
|
|
29
|
+
const MIME = {
|
|
30
|
+
".svg": "image/svg+xml",
|
|
31
|
+
".png": "image/png",
|
|
32
|
+
".jpg": "image/jpeg",
|
|
33
|
+
".jpeg": "image/jpeg",
|
|
34
|
+
".webp": "image/webp",
|
|
35
|
+
".gif": "image/gif",
|
|
36
|
+
".avif": "image/avif"
|
|
37
|
+
};
|
|
38
|
+
function expandHome(path) {
|
|
39
|
+
return path === "~" || path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : path;
|
|
40
|
+
}
|
|
41
|
+
function number(flag, value) {
|
|
42
|
+
if (value === void 0) return void 0;
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
if (value.trim() === "" || Number.isNaN(parsed)) throw new UsageError(`--${flag} takes a number, not "${value}".`);
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
/** An empty flag value counts as absent. */
|
|
48
|
+
function nonBlank(value) {
|
|
49
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
50
|
+
}
|
|
51
|
+
function list(value) {
|
|
52
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
function parseCrop(value) {
|
|
55
|
+
const parts = list(value).map(Number);
|
|
56
|
+
const [x, y, width, height] = parts;
|
|
57
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0) || !width || !height) throw new UsageError(`--${QR_EXTRA_FLAGS.logoCrop.name} takes four whole numbers: x,y,width,height (source pixels).`);
|
|
58
|
+
return {
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
width,
|
|
62
|
+
height
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads the command line into a request the shared schema accepts.
|
|
67
|
+
*
|
|
68
|
+
* Throws {@link UsageError} for anything it can name: a bad number, a logo
|
|
69
|
+
* that is neither a preset nor a file, or a value the schema refuses (reported
|
|
70
|
+
* against the flag that carries it).
|
|
71
|
+
*/
|
|
72
|
+
/** The parsed flags by name: all string-valued, bar `--dry-run`. */
|
|
73
|
+
function flagValues(values) {
|
|
74
|
+
return values;
|
|
75
|
+
}
|
|
76
|
+
function readArgs(argv) {
|
|
77
|
+
try {
|
|
78
|
+
return parseArgs({
|
|
79
|
+
args: [...argv],
|
|
80
|
+
options: {
|
|
81
|
+
...qrParseOptions(),
|
|
82
|
+
"dry-run": { type: "boolean" }
|
|
83
|
+
},
|
|
84
|
+
allowPositionals: true
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The text on the command line, if it carries any. */
|
|
91
|
+
function textGiven(argv) {
|
|
92
|
+
const { values, positionals } = readArgs(argv);
|
|
93
|
+
return positionals[0] ?? flagValues(values)[QR_FLAGS.text.name];
|
|
94
|
+
}
|
|
95
|
+
function parseQrArgs(argv, cwd, asked) {
|
|
96
|
+
const parsed = readArgs(argv);
|
|
97
|
+
const { positionals } = parsed;
|
|
98
|
+
const values = flagValues(parsed.values);
|
|
99
|
+
const get = (flag) => values[flag.name];
|
|
100
|
+
if (positionals.length > 1) throw new UsageError(`Give the text to encode as one argument (quote it); got ${positionals.length}.`);
|
|
101
|
+
const text = positionals[0] ?? get(QR_FLAGS.text) ?? asked;
|
|
102
|
+
const gradient = get(QR_FLAGS.gradient);
|
|
103
|
+
const gradientColours = gradient === void 0 ? void 0 : list(gradient);
|
|
104
|
+
if (gradientColours && gradientColours.length !== 2) throw new UsageError("--gradient takes two colours: from,to.");
|
|
105
|
+
const logo = get(QR_FLAGS.logo);
|
|
106
|
+
const isPreset = logo !== void 0 && logo in QR_LOGO_PRESETS;
|
|
107
|
+
const logoPath = logo !== void 0 && !isPreset && logo !== "none" ? resolve(cwd, expandHome(logo)) : void 0;
|
|
108
|
+
const cropValue = get(QR_EXTRA_FLAGS.logoCrop);
|
|
109
|
+
if (cropValue !== void 0 && !logoPath) throw new UsageError("--logo-crop crops a logo file; pass --logo <file>.");
|
|
110
|
+
const background = get(QR_FLAGS.background);
|
|
111
|
+
const formats = get(QR_FLAGS.formats);
|
|
112
|
+
const request = {
|
|
113
|
+
text: text ?? "",
|
|
114
|
+
theme: get(QR_FLAGS.theme),
|
|
115
|
+
size: number("size", get(QR_FLAGS.size)),
|
|
116
|
+
margin: number("margin", get(QR_FLAGS.margin)),
|
|
117
|
+
color: get(QR_FLAGS.color),
|
|
118
|
+
background: background === "transparent" || background === "none" ? "" : background,
|
|
119
|
+
gradient: gradientColours,
|
|
120
|
+
shape: get(QR_FLAGS.shape),
|
|
121
|
+
logo: logoPath ? "none" : logo,
|
|
122
|
+
logoSize: number("logo-size", get(QR_FLAGS.logoSize)),
|
|
123
|
+
logoPadding: number("logo-padding", get(QR_FLAGS.logoPadding)),
|
|
124
|
+
errorLevel: get(QR_FLAGS.errorLevel),
|
|
125
|
+
version: number("qr-version", get(QR_FLAGS.version)),
|
|
126
|
+
formats: formats === void 0 ? void 0 : list(formats)
|
|
127
|
+
};
|
|
128
|
+
const defined = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== void 0));
|
|
129
|
+
const checked = qrRequestSchema.safeParse(defined);
|
|
130
|
+
if (!checked.success) {
|
|
131
|
+
const names = new Map(Object.entries(QR_FLAGS).map(([key, flag]) => [key, flag.name]));
|
|
132
|
+
throw new UsageError(checked.error.issues.map((issue) => {
|
|
133
|
+
const key = String(issue.path[0] ?? "");
|
|
134
|
+
const flag = names.get(key);
|
|
135
|
+
return key === "text" || !flag ? issue.message : `--${flag}: ${issue.message}`;
|
|
136
|
+
}).join("\n"));
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
request: defined,
|
|
140
|
+
logoFile: logoPath ? {
|
|
141
|
+
path: logoPath,
|
|
142
|
+
crop: cropValue === void 0 ? void 0 : parseCrop(cropValue)
|
|
143
|
+
} : void 0,
|
|
144
|
+
out: resolve(cwd, expandHome(nonBlank(get(QR_EXTRA_FLAGS.out)) ?? ".")),
|
|
145
|
+
name: nonBlank(get(QR_EXTRA_FLAGS.name)) ?? "qr",
|
|
146
|
+
dryRun: parsed.values["dry-run"] === true || isDryRun()
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The logo file as a data URI, cropped first when asked.
|
|
151
|
+
*
|
|
152
|
+
* Cropped with sharp rather than by the renderer's own `logoCrop`, because the
|
|
153
|
+
* schema takes that from a preset only; the result is the same square of
|
|
154
|
+
* pixels either way.
|
|
155
|
+
*/
|
|
156
|
+
async function logoHref({ path, crop }) {
|
|
157
|
+
const mime = MIME[extname(path).toLowerCase()];
|
|
158
|
+
if (!mime) throw new UsageError(`Cannot use ${path} as a logo: expected ${Object.keys(MIME).join(", ")}.`);
|
|
159
|
+
let bytes;
|
|
160
|
+
try {
|
|
161
|
+
bytes = await readFile(path);
|
|
162
|
+
} catch {
|
|
163
|
+
throw new UsageError(`--logo is neither a preset (${Object.keys(QR_LOGO_PRESETS).join(", ")}, none) nor a readable file: ${path}`);
|
|
164
|
+
}
|
|
165
|
+
if (!crop) return `data:${mime};base64,${bytes.toString("base64")}`;
|
|
166
|
+
const { default: sharp } = await import("sharp");
|
|
167
|
+
const image = sharp(bytes);
|
|
168
|
+
const { width = 0, height = 0 } = await image.metadata();
|
|
169
|
+
if (crop.x + crop.width > width || crop.y + crop.height > height) throw new UsageError(`--logo-crop ${crop.x},${crop.y},${crop.width},${crop.height} runs outside the ${width}x${height} logo.`);
|
|
170
|
+
return `data:image/png;base64,${(await image.extract({
|
|
171
|
+
left: crop.x,
|
|
172
|
+
top: crop.y,
|
|
173
|
+
width: crop.width,
|
|
174
|
+
height: crop.height
|
|
175
|
+
}).png().toBuffer()).toString("base64")}`;
|
|
176
|
+
}
|
|
177
|
+
async function runQr(argv, deps = {}) {
|
|
178
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
179
|
+
try {
|
|
180
|
+
await qr(argv, cwd);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
explainError("Could not make that code.", err, ["pnpm backstage qr https://devdogsuga.org --format svg,png --out ~/codes", "pnpm backstage qr https://devdogsuga.org --logo ./mark.png --logo-crop 0,24,265,265"]);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function qr(argv, cwd) {
|
|
187
|
+
let asked;
|
|
188
|
+
if (textGiven(argv) === void 0 && !isNonInteractive()) asked = unwrap(await text({
|
|
189
|
+
message: "What should the code say?",
|
|
190
|
+
placeholder: "https://devdogsuga.org",
|
|
191
|
+
validate: (value) => value?.trim() ? void 0 : "Enter something."
|
|
192
|
+
}));
|
|
193
|
+
const invocation = parseQrArgs(argv, cwd, asked);
|
|
194
|
+
const { text: text$1, options, logo, formats } = resolveQrRequest(invocation.request);
|
|
195
|
+
const href = invocation.logoFile ? await logoHref(invocation.logoFile) : void 0;
|
|
196
|
+
const issue = qrVersionIssue(text$1, options.errorLevel, options.version, options.logoSize, options.logoPadding, logo !== "none" || href !== void 0);
|
|
197
|
+
if (issue) log.warn(`Might not scan: ${issue}.`);
|
|
198
|
+
const display = (file) => {
|
|
199
|
+
const rel = relative(cwd, file);
|
|
200
|
+
return rel.startsWith("..") ? file : rel;
|
|
201
|
+
};
|
|
202
|
+
const target = (format) => resolve(invocation.out, `${invocation.name}.${format}`);
|
|
203
|
+
if (invocation.dryRun) {
|
|
204
|
+
log.info(`Would write ${formats.map((format) => display(target(format))).join(", ")}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const files = await renderQr(invocation.request, href);
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const path = target(file.format);
|
|
210
|
+
await mkdir(dirname(path), { recursive: true });
|
|
211
|
+
await writeFile(path, file.data);
|
|
212
|
+
log.success(`${display(path)} (${Math.round(file.data.length / 1024)} KB)`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const handleQr = async (rest) => {
|
|
216
|
+
await runQr(rest);
|
|
217
|
+
return process.exitCode ? null : DONE;
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
export { handleQr };
|