@moewolf/moe-icons-cli 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.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +32 -0
  3. package/bin/moeicons.js +20 -0
  4. package/dist/api/client.d.ts +42 -0
  5. package/dist/api/client.js +105 -0
  6. package/dist/auth/callback.d.ts +37 -0
  7. package/dist/auth/callback.js +72 -0
  8. package/dist/auth/device-login.d.ts +29 -0
  9. package/dist/auth/device-login.js +74 -0
  10. package/dist/auth/login.d.ts +41 -0
  11. package/dist/auth/login.js +85 -0
  12. package/dist/auth/open-browser.d.ts +2 -0
  13. package/dist/auth/open-browser.js +15 -0
  14. package/dist/auth/pkce.d.ts +17 -0
  15. package/dist/auth/pkce.js +20 -0
  16. package/dist/auth/token-store.d.ts +38 -0
  17. package/dist/auth/token-store.js +126 -0
  18. package/dist/catalog/catalog.d.ts +32 -0
  19. package/dist/catalog/catalog.js +80 -0
  20. package/dist/catalog/catalog.json +9371 -0
  21. package/dist/cli.d.ts +23 -0
  22. package/dist/cli.js +507 -0
  23. package/dist/commands/parser.d.ts +41 -0
  24. package/dist/commands/parser.js +146 -0
  25. package/dist/core/auth.d.ts +32 -0
  26. package/dist/core/auth.js +131 -0
  27. package/dist/core/bitmap-assets.d.ts +29 -0
  28. package/dist/core/bitmap-assets.js +77 -0
  29. package/dist/core/cli-update.d.ts +43 -0
  30. package/dist/core/cli-update.js +71 -0
  31. package/dist/core/context.d.ts +22 -0
  32. package/dist/core/context.js +1 -0
  33. package/dist/core/free-download.d.ts +66 -0
  34. package/dist/core/free-download.js +193 -0
  35. package/dist/core/generate.d.ts +33 -0
  36. package/dist/core/generate.js +285 -0
  37. package/dist/core/icon-names.d.ts +14 -0
  38. package/dist/core/icon-names.js +54 -0
  39. package/dist/core/init.d.ts +12 -0
  40. package/dist/core/init.js +23 -0
  41. package/dist/core/install.d.ts +67 -0
  42. package/dist/core/install.js +117 -0
  43. package/dist/core/library-update.d.ts +24 -0
  44. package/dist/core/library-update.js +161 -0
  45. package/dist/core/manage.d.ts +24 -0
  46. package/dist/core/manage.js +38 -0
  47. package/dist/core/pro-download.d.ts +29 -0
  48. package/dist/core/pro-download.js +112 -0
  49. package/dist/core/pro-install.d.ts +24 -0
  50. package/dist/core/pro-install.js +72 -0
  51. package/dist/core/release-descriptor.d.ts +40 -0
  52. package/dist/core/release-descriptor.js +114 -0
  53. package/dist/core/resource-variant.d.ts +35 -0
  54. package/dist/core/resource-variant.js +70 -0
  55. package/dist/core/signed-artifact.d.ts +29 -0
  56. package/dist/core/signed-artifact.js +81 -0
  57. package/dist/core/target-subtree.d.ts +49 -0
  58. package/dist/core/target-subtree.js +132 -0
  59. package/dist/core/update-policy.d.ts +31 -0
  60. package/dist/core/update-policy.js +27 -0
  61. package/dist/core/version-service.d.ts +26 -0
  62. package/dist/core/version-service.js +71 -0
  63. package/dist/core/wizard.d.ts +49 -0
  64. package/dist/core/wizard.js +71 -0
  65. package/dist/errors/errors.d.ts +34 -0
  66. package/dist/errors/errors.js +39 -0
  67. package/dist/errors/index.d.ts +1 -0
  68. package/dist/errors/index.js +1 -0
  69. package/dist/generator/cn.d.ts +5 -0
  70. package/dist/generator/cn.js +14 -0
  71. package/dist/generator/generate.d.ts +34 -0
  72. package/dist/generator/generate.js +562 -0
  73. package/dist/generator/theme-resolve.d.ts +26 -0
  74. package/dist/generator/theme-resolve.js +100 -0
  75. package/dist/mcp/server.d.ts +58 -0
  76. package/dist/mcp/server.js +176 -0
  77. package/dist/project/config.d.ts +74 -0
  78. package/dist/project/config.js +353 -0
  79. package/dist/project/detect.d.ts +26 -0
  80. package/dist/project/detect.js +104 -0
  81. package/dist/project/install-metadata.d.ts +29 -0
  82. package/dist/project/install-metadata.js +108 -0
  83. package/dist/project/install.d.ts +115 -0
  84. package/dist/project/install.js +537 -0
  85. package/dist/project/project-lock.d.ts +11 -0
  86. package/dist/project/project-lock.js +81 -0
  87. package/dist/project/tailwind.d.ts +51 -0
  88. package/dist/project/tailwind.js +188 -0
  89. package/dist/project/tar-gz.d.ts +12 -0
  90. package/dist/project/tar-gz.js +100 -0
  91. package/dist/tui/primitives.d.ts +36 -0
  92. package/dist/tui/primitives.js +88 -0
  93. package/dist/ui/banner.d.ts +11 -0
  94. package/dist/ui/banner.js +105 -0
  95. package/dist/ui/clack.d.ts +8 -0
  96. package/dist/ui/clack.js +53 -0
  97. package/dist/ui/create-ui.d.ts +14 -0
  98. package/dist/ui/create-ui.js +16 -0
  99. package/dist/ui/non-interactive.d.ts +5 -0
  100. package/dist/ui/non-interactive.js +30 -0
  101. package/dist/ui/stream.d.ts +11 -0
  102. package/dist/ui/stream.js +50 -0
  103. package/package.json +70 -0
@@ -0,0 +1,81 @@
1
+ import { closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CliError } from "../errors/index.js";
4
+ const defaultIo = {
5
+ exists: existsSync,
6
+ read: (path) => readFileSync(path, "utf8"),
7
+ createExclusive: (path, content) => { const fd = openSync(path, "wx", 0o600); try {
8
+ writeFileSync(fd, content);
9
+ }
10
+ finally {
11
+ closeSync(fd);
12
+ } },
13
+ remove: unlinkSync,
14
+ isProcessAlive: (pid) => { try {
15
+ process.kill(pid, 0);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ } },
21
+ now: Date.now,
22
+ pid: process.pid,
23
+ };
24
+ export async function withProjectLock(projectRoot, operation, work, io = defaultIo) {
25
+ const path = join(projectRoot, ".moeicons.lock");
26
+ if (io.exists(path)) {
27
+ let stale = false;
28
+ try {
29
+ const value = JSON.parse(io.read(path));
30
+ stale = typeof value.pid === "number" && typeof value.createdAt === "number" && io.now() - value.createdAt > 30 * 60_000 && !io.isProcessAlive(value.pid);
31
+ }
32
+ catch {
33
+ stale = false;
34
+ }
35
+ if (!stale)
36
+ throw new CliError("VALIDATION_ERROR", "another moeicons install/update/reload operation is already running");
37
+ io.remove(path);
38
+ }
39
+ try {
40
+ io.createExclusive(path, JSON.stringify({ schemaVersion: 1, pid: io.pid, operation, createdAt: io.now() }));
41
+ }
42
+ catch {
43
+ throw new CliError("VALIDATION_ERROR", "another moeicons install/update/reload operation is already running");
44
+ }
45
+ try {
46
+ return await work();
47
+ }
48
+ finally {
49
+ if (io.exists(path))
50
+ io.remove(path);
51
+ }
52
+ }
53
+ export function withProjectLockSync(projectRoot, operation, work, io = defaultIo) {
54
+ const path = join(projectRoot, ".moeicons.lock");
55
+ if (io.exists(path)) {
56
+ let stale = false;
57
+ try {
58
+ const value = JSON.parse(io.read(path));
59
+ stale = typeof value.pid === "number" && typeof value.createdAt === "number" && io.now() - value.createdAt > 30 * 60_000 && !io.isProcessAlive(value.pid);
60
+ }
61
+ catch {
62
+ stale = false;
63
+ }
64
+ if (!stale)
65
+ throw new CliError("VALIDATION_ERROR", "another moeicons install/update/reload operation is already running");
66
+ io.remove(path);
67
+ }
68
+ try {
69
+ io.createExclusive(path, JSON.stringify({ schemaVersion: 1, pid: io.pid, operation, createdAt: io.now() }));
70
+ }
71
+ catch {
72
+ throw new CliError("VALIDATION_ERROR", "another moeicons install/update/reload operation is already running");
73
+ }
74
+ try {
75
+ return work();
76
+ }
77
+ finally {
78
+ if (io.exists(path))
79
+ io.remove(path);
80
+ }
81
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * H3: only rewrite Tailwind v3 configs whose `content` is a static string-array
3
+ * literal. Other shapes get a precise manual hint. Tailwind v4 → typed error.
4
+ */
5
+ export type TailwindDetectResult = {
6
+ readonly kind: "missing";
7
+ } | {
8
+ readonly kind: "v3";
9
+ readonly configPath: string;
10
+ readonly source: string;
11
+ } | {
12
+ readonly kind: "v4";
13
+ readonly configPath: string;
14
+ } | {
15
+ readonly kind: "unknown";
16
+ readonly configPath: string;
17
+ readonly source: string;
18
+ };
19
+ export declare function findTailwindConfig(projectRoot: string): string | undefined;
20
+ export declare function detectTailwind(projectRoot: string): TailwindDetectResult;
21
+ export type ContentInjectResult = {
22
+ readonly ok: true;
23
+ readonly changed: boolean;
24
+ readonly nextSource: string;
25
+ } | {
26
+ readonly ok: false;
27
+ readonly reason: "unsupported-shape";
28
+ readonly hint: string;
29
+ };
30
+ /** Idempotently append a glob to a static `content: [ ... ]` array. */
31
+ export declare function injectContentGlob(source: string, glob: string): ContentInjectResult;
32
+ export declare function moeiconsContentGlob(outputDir: string, target?: "react" | "vue" | "vanilla" | "assets"): string;
33
+ /**
34
+ * Apply Tailwind content injection. Throws CliError(TAILWIND_VERSION_UNSUPPORTED)
35
+ * for v4 when integration is requested.
36
+ */
37
+ export declare function planTailwindIntegration(projectRoot: string, outputDir: string, options: {
38
+ readonly noTailwind: boolean;
39
+ readonly target?: "react" | "vue" | "vanilla" | "assets";
40
+ }): {
41
+ readonly files: readonly {
42
+ path: string;
43
+ content: string;
44
+ }[];
45
+ readonly notes: readonly string[];
46
+ };
47
+ export declare function ensureClassMergeDependencies(source: string): {
48
+ readonly nextSource: string;
49
+ readonly changed: boolean;
50
+ readonly notes: string[];
51
+ };
@@ -0,0 +1,188 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CliError } from "../errors/index.js";
4
+ import { CLSX_VERSION_RANGE, TAILWIND_MERGE_VERSION_RANGE } from "../generator/cn.js";
5
+ import { applyEdits, modify } from "jsonc-parser";
6
+ const CONFIG_CANDIDATES = [
7
+ "tailwind.config.js",
8
+ "tailwind.config.cjs",
9
+ "tailwind.config.mjs",
10
+ "tailwind.config.ts",
11
+ ];
12
+ export function findTailwindConfig(projectRoot) {
13
+ for (const name of CONFIG_CANDIDATES) {
14
+ const path = join(projectRoot, name);
15
+ if (existsSync(path))
16
+ return path;
17
+ }
18
+ return undefined;
19
+ }
20
+ function packageDeclaresTailwindV4(projectRoot) {
21
+ const pkgPath = join(projectRoot, "package.json");
22
+ if (!existsSync(pkgPath))
23
+ return false;
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
26
+ const range = pkg.dependencies?.tailwindcss ?? pkg.devDependencies?.tailwindcss;
27
+ if (!range)
28
+ return false;
29
+ return (range.startsWith("^4") ||
30
+ range.startsWith("~4") ||
31
+ range.startsWith(">=4") ||
32
+ /^4(\.|$)/.test(range));
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ export function detectTailwind(projectRoot) {
39
+ const configPath = findTailwindConfig(projectRoot);
40
+ if (packageDeclaresTailwindV4(projectRoot)) {
41
+ return { kind: "v4", configPath: configPath ?? join(projectRoot, "package.json") };
42
+ }
43
+ if (!configPath)
44
+ return { kind: "missing" };
45
+ const source = readFileSync(configPath, "utf8");
46
+ if (/\b@tailwindcss\/(vite|postcss)\b/.test(source)) {
47
+ return { kind: "v4", configPath };
48
+ }
49
+ if (/content\s*:/.test(source))
50
+ return { kind: "v3", configPath, source };
51
+ return { kind: "unknown", configPath, source };
52
+ }
53
+ const CONTENT_ARRAY_RE = /(content\s*:\s*)\[([\s\S]*?)\]/;
54
+ /** Idempotently append a glob to a static `content: [ ... ]` array. */
55
+ export function injectContentGlob(source, glob) {
56
+ if (source.includes(glob)) {
57
+ return { ok: true, changed: false, nextSource: source };
58
+ }
59
+ const match = CONTENT_ARRAY_RE.exec(source);
60
+ if (!match || match.index === undefined) {
61
+ return {
62
+ ok: false,
63
+ reason: "unsupported-shape",
64
+ hint: `Could not safely edit Tailwind content; add ${JSON.stringify(glob)} to content manually.`,
65
+ };
66
+ }
67
+ const full = match[0];
68
+ if (/require\(|import\(|\.\.\./.test(full)) {
69
+ return {
70
+ ok: false,
71
+ reason: "unsupported-shape",
72
+ hint: `Tailwind content is not a static string array; add ${JSON.stringify(glob)} manually.`,
73
+ };
74
+ }
75
+ const prefix = match[1] ?? "content: ";
76
+ const body = match[2] ?? "";
77
+ const trimmed = body.replace(/\s+$/, "");
78
+ const needsComma = trimmed.length > 0 && !trimmed.trimEnd().endsWith(",");
79
+ const insertion = `${needsComma ? "," : ""}\n ${JSON.stringify(glob)}`;
80
+ const nextArray = `${prefix}[${trimmed}${insertion}\n ]`;
81
+ return { ok: true, changed: true, nextSource: source.replace(full, nextArray) };
82
+ }
83
+ export function moeiconsContentGlob(outputDir, target) {
84
+ const normalized = outputDir.replace(/\\/g, "/").replace(/\/$/, "");
85
+ const extensions = target === undefined
86
+ ? "js,ts,jsx,tsx,vue"
87
+ : target === "react"
88
+ ? "js,ts,jsx,tsx"
89
+ : target === "vue"
90
+ ? "js,ts,vue"
91
+ : "js,ts,html,svg";
92
+ return `./${normalized}/**/*.{${extensions}}`;
93
+ }
94
+ /**
95
+ * Apply Tailwind content injection. Throws CliError(TAILWIND_VERSION_UNSUPPORTED)
96
+ * for v4 when integration is requested.
97
+ */
98
+ export function planTailwindIntegration(projectRoot, outputDir, options) {
99
+ if (options.noTailwind) {
100
+ return { files: [], notes: ["skipped Tailwind config (--no-tailwind)"] };
101
+ }
102
+ if (options.target === "vanilla" || options.target === "assets") {
103
+ return { files: [], notes: [`skipped Tailwind integration for ${options.target} target`] };
104
+ }
105
+ const detected = detectTailwind(projectRoot);
106
+ if (detected.kind === "missing") {
107
+ return { files: [], notes: ["no Tailwind config found; skipped content injection"] };
108
+ }
109
+ if (detected.kind === "v4") {
110
+ throw new CliError("TAILWIND_VERSION_UNSUPPORTED", "Tailwind CSS v4 was detected; this CLI release only auto-integrates Tailwind v3. Pass --no-tailwind to skip Tailwind config changes.");
111
+ }
112
+ if (detected.kind === "unknown") {
113
+ return {
114
+ files: [],
115
+ notes: [
116
+ `Tailwind config ${detected.configPath} has no static content array; add ${JSON.stringify(moeiconsContentGlob(outputDir))} manually.`,
117
+ ],
118
+ };
119
+ }
120
+ const glob = moeiconsContentGlob(outputDir, options.target);
121
+ const injected = injectContentGlob(detected.source, glob);
122
+ if (!injected.ok) {
123
+ return { files: [], notes: [injected.hint] };
124
+ }
125
+ if (!injected.changed) {
126
+ return { files: [], notes: [`Tailwind content already includes ${glob}`] };
127
+ }
128
+ return {
129
+ files: [{ path: detected.configPath, content: injected.nextSource }],
130
+ notes: [`updated ${detected.configPath} content with ${glob}`],
131
+ };
132
+ }
133
+ export function ensureClassMergeDependencies(source) {
134
+ const notes = [];
135
+ let pkg;
136
+ try {
137
+ pkg = JSON.parse(source);
138
+ }
139
+ catch {
140
+ return {
141
+ nextSource: source,
142
+ changed: false,
143
+ notes: ["package.json is not valid JSON; skipped clsx/tailwind-merge"],
144
+ };
145
+ }
146
+ const deps = { ...(pkg.dependencies ?? {}) };
147
+ let changed = false;
148
+ if (!deps.clsx) {
149
+ deps.clsx = CLSX_VERSION_RANGE;
150
+ changed = true;
151
+ notes.push(`add dependencies.clsx ${CLSX_VERSION_RANGE}`);
152
+ }
153
+ if (!deps["tailwind-merge"]) {
154
+ deps["tailwind-merge"] = TAILWIND_MERGE_VERSION_RANGE;
155
+ changed = true;
156
+ notes.push(`add dependencies.tailwind-merge ${TAILWIND_MERGE_VERSION_RANGE}`);
157
+ }
158
+ if (!changed)
159
+ return { nextSource: source, changed: false, notes: ["clsx/tailwind-merge already declared"] };
160
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
161
+ const indent = source.match(/\n([ \t]+)"/)?.[1] ?? " ";
162
+ const formattingOptions = {
163
+ insertSpaces: !indent.includes("\t"),
164
+ tabSize: indent.includes("\t") ? 1 : indent.length,
165
+ eol,
166
+ };
167
+ if (!pkg.dependencies) {
168
+ const closing = source.lastIndexOf("}");
169
+ const beforeClosing = source.slice(0, closing);
170
+ const whitespace = beforeClosing.match(/\s*$/)?.[0] ?? "";
171
+ const body = beforeClosing.slice(0, beforeClosing.length - whitespace.length);
172
+ const comma = Object.keys(pkg).length > 0 ? "," : "";
173
+ const block = `${comma}${eol}${indent}"dependencies": {${eol}${indent}${indent}"clsx": ${JSON.stringify(CLSX_VERSION_RANGE)},${eol}${indent}${indent}"tailwind-merge": ${JSON.stringify(TAILWIND_MERGE_VERSION_RANGE)}${eol}${indent}}`;
174
+ return {
175
+ nextSource: `${body}${block}${whitespace}${source.slice(closing)}`,
176
+ changed: true,
177
+ notes,
178
+ };
179
+ }
180
+ let nextSource = source;
181
+ if (!pkg.dependencies?.clsx)
182
+ nextSource = applyEdits(nextSource, modify(nextSource, ["dependencies", "clsx"], CLSX_VERSION_RANGE, { formattingOptions }));
183
+ if (!pkg.dependencies?.["tailwind-merge"])
184
+ nextSource = applyEdits(nextSource, modify(nextSource, ["dependencies", "tailwind-merge"], TAILWIND_MERGE_VERSION_RANGE, {
185
+ formattingOptions,
186
+ }));
187
+ return { nextSource, changed: true, notes };
188
+ }
@@ -0,0 +1,12 @@
1
+ /** Build a gzipped POSIX tar from in-memory files (test fixtures and unpacking). */
2
+ export declare function createTarGz(files: Readonly<Record<string, string | Uint8Array>>): Uint8Array;
3
+ export type ExtractedTarFiles = {
4
+ readonly files: Record<string, Uint8Array>;
5
+ readonly errors: string[];
6
+ };
7
+ /** Gunzip + unpack tar; reject absolute paths, `..`, and oversized archives. */
8
+ export declare function extractTarGz(bytes: Uint8Array, limits: {
9
+ maxEntries: number;
10
+ maxExpandedBytes: number;
11
+ }): ExtractedTarFiles;
12
+ export declare function decodeUtf8(bytes: Uint8Array): string;
@@ -0,0 +1,100 @@
1
+ import { gzipSync, gunzipSync } from "node:zlib";
2
+ const BLOCK = 512;
3
+ function checksumHeader(header) {
4
+ let sum = 0;
5
+ for (let i = 0; i < BLOCK; i += 1)
6
+ sum += header[i] ?? 0;
7
+ return sum;
8
+ }
9
+ function writeOctal(buf, offset, length, value) {
10
+ const text = `${value.toString(8).padStart(length - 1, "0")}\0`;
11
+ buf.write(text.slice(0, length), offset, length, "utf8");
12
+ }
13
+ /** Build a gzipped POSIX tar from in-memory files (test fixtures and unpacking). */
14
+ export function createTarGz(files) {
15
+ const parts = [];
16
+ const names = Object.keys(files).sort((a, b) => a.localeCompare(b));
17
+ for (const name of names) {
18
+ if (name.includes("..") || name.startsWith("/")) {
19
+ throw new Error(`unsafe tar path: ${name}`);
20
+ }
21
+ const raw = files[name];
22
+ const body = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw ?? []);
23
+ const header = Buffer.alloc(BLOCK, 0);
24
+ Buffer.from(name).copy(header, 0, 0, Math.min(name.length, 100));
25
+ writeOctal(header, 100, 8, 0o644);
26
+ writeOctal(header, 108, 8, 0);
27
+ writeOctal(header, 116, 8, 0);
28
+ writeOctal(header, 124, 12, body.byteLength);
29
+ writeOctal(header, 136, 12, 0);
30
+ header.write(" ", 148, 8, "utf8");
31
+ header[156] = 0x30;
32
+ header.write("ustar", 257, 5, "utf8");
33
+ header[262] = 0;
34
+ header.write("00", 263, 2, "utf8");
35
+ const sum = checksumHeader(header);
36
+ header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "utf8");
37
+ parts.push(header, body);
38
+ const pad = (BLOCK - (body.byteLength % BLOCK)) % BLOCK;
39
+ if (pad > 0)
40
+ parts.push(Buffer.alloc(pad, 0));
41
+ }
42
+ parts.push(Buffer.alloc(BLOCK * 2, 0));
43
+ return gzipSync(Buffer.concat(parts), { level: 9 });
44
+ }
45
+ function readOctal(buf, offset, length) {
46
+ const raw = buf.subarray(offset, offset + length).toString("utf8").replace(/\0.*$/, "").trim();
47
+ if (!raw)
48
+ return 0;
49
+ return Number.parseInt(raw, 8);
50
+ }
51
+ /** Gunzip + unpack tar; reject absolute paths, `..`, and oversized archives. */
52
+ export function extractTarGz(bytes, limits) {
53
+ const files = {};
54
+ const errors = [];
55
+ let data;
56
+ try {
57
+ data = gunzipSync(bytes);
58
+ }
59
+ catch {
60
+ return { files, errors: ["invalid gzip"] };
61
+ }
62
+ let offset = 0;
63
+ let entries = 0;
64
+ let expanded = 0;
65
+ while (offset + BLOCK <= data.byteLength) {
66
+ const header = data.subarray(offset, offset + BLOCK);
67
+ offset += BLOCK;
68
+ if (header.every((byte) => byte === 0))
69
+ break;
70
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "").replace(/^\.\//, "");
71
+ const size = readOctal(header, 124, 12);
72
+ const type = header[156];
73
+ const padded = Math.ceil(size / BLOCK) * BLOCK;
74
+ const content = data.subarray(offset, offset + size);
75
+ offset += padded;
76
+ if (type !== 0 && type !== 0x30)
77
+ continue;
78
+ if (!name || name.endsWith("/"))
79
+ continue;
80
+ entries += 1;
81
+ if (entries > limits.maxEntries) {
82
+ errors.push(`too many entries (> ${limits.maxEntries})`);
83
+ break;
84
+ }
85
+ if (name.startsWith("/") || name.split("/").includes("..")) {
86
+ errors.push(`unsafe path "${name}"`);
87
+ continue;
88
+ }
89
+ expanded += content.byteLength;
90
+ if (expanded > limits.maxExpandedBytes) {
91
+ errors.push("expanded size exceeds limit");
92
+ break;
93
+ }
94
+ files[name] = new Uint8Array(content);
95
+ }
96
+ return { files, errors };
97
+ }
98
+ export function decodeUtf8(bytes) {
99
+ return Buffer.from(bytes).toString("utf8");
100
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * CLI-04: TUI primitives with injected streams. Business modules never import
3
+ * this; command handlers inject their own streams. Non-TTY detection and
4
+ * cancellation are first-class. Avoids brittle terminal timing.
5
+ */
6
+ export interface TuiStreams {
7
+ readonly isTTY: () => boolean;
8
+ readonly write: (text: string) => void;
9
+ readonly readLine: (prompt: string) => Promise<string>;
10
+ readonly readKey: () => Promise<string>;
11
+ }
12
+ export interface TuiDeps {
13
+ readonly streams: TuiStreams;
14
+ readonly signal?: AbortSignal;
15
+ /** `--yes`: skip confirmations without prompting. */
16
+ readonly yes?: boolean;
17
+ }
18
+ export type Choice<T> = {
19
+ readonly value: T;
20
+ readonly label: string;
21
+ };
22
+ export interface SelectOptions<T> {
23
+ readonly choices: readonly Choice<T>[];
24
+ readonly canCancel?: boolean;
25
+ }
26
+ export declare class CancelledError extends Error {
27
+ constructor();
28
+ }
29
+ /** Render a banner. No TTY required. */
30
+ export declare function renderBanner(deps: TuiDeps, banner: string): void;
31
+ /** Interactive single-choice selection; non-TTY rejects with guidance. */
32
+ export declare function select<T>(deps: TuiDeps, prompt: string, options: SelectOptions<T>): Promise<T>;
33
+ /** Yes/no confirmation; default from options. `--yes` skips the prompt. */
34
+ export declare function confirm(deps: TuiDeps, prompt: string, defaultValue: boolean): Promise<boolean>;
35
+ /** Masked key input (echo replaced with bullets). Non-TTY rejects. */
36
+ export declare function maskedInput(deps: TuiDeps, prompt: string): Promise<string>;
@@ -0,0 +1,88 @@
1
+ import { CliError } from "../errors/index.js";
2
+ export class CancelledError extends Error {
3
+ constructor() {
4
+ super("cancelled");
5
+ this.name = "CancelledError";
6
+ }
7
+ }
8
+ function assertNotAborted(deps) {
9
+ if (deps.signal?.aborted)
10
+ throw new CancelledError();
11
+ }
12
+ /** Render a banner. No TTY required. */
13
+ export function renderBanner(deps, banner) {
14
+ deps.streams.write(banner);
15
+ deps.streams.write("\n");
16
+ }
17
+ /** Interactive single-choice selection; non-TTY rejects with guidance. */
18
+ export async function select(deps, prompt, options) {
19
+ assertNotAborted(deps);
20
+ if (!deps.streams.isTTY()) {
21
+ throw new CliError("NOT_TTY", "interactive selection requires a TTY; use a specific command or --json");
22
+ }
23
+ deps.streams.write(`${prompt}\n`);
24
+ options.choices.forEach((choice, i) => {
25
+ deps.streams.write(` ${i + 1}) ${choice.label}\n`);
26
+ });
27
+ if (options.canCancel)
28
+ deps.streams.write(" 0) Cancel\n");
29
+ for (;;) {
30
+ const line = (await deps.streams.readLine("> ")).trim();
31
+ assertNotAborted(deps);
32
+ if (options.canCancel && (line === "0" || line.toLowerCase() === "cancel")) {
33
+ throw new CancelledError();
34
+ }
35
+ const index = Number(line);
36
+ const choice = options.choices[index - 1];
37
+ if (choice)
38
+ return choice.value;
39
+ deps.streams.write("Invalid choice. Try again.\n");
40
+ }
41
+ }
42
+ /** Yes/no confirmation; default from options. `--yes` skips the prompt. */
43
+ export async function confirm(deps, prompt, defaultValue) {
44
+ assertNotAborted(deps);
45
+ if (deps.yes)
46
+ return true;
47
+ if (!deps.streams.isTTY()) {
48
+ throw new CliError("NOT_TTY", "confirmation requires a TTY; pass --yes to skip");
49
+ }
50
+ for (;;) {
51
+ const line = (await deps.streams.readLine(`${prompt} [y/N] `)).trim().toLowerCase();
52
+ assertNotAborted(deps);
53
+ if (line === "y" || line === "yes")
54
+ return true;
55
+ if (line === "n" || line === "no")
56
+ return false;
57
+ if (line === "")
58
+ return defaultValue;
59
+ deps.streams.write("Please answer y or n.\n");
60
+ }
61
+ }
62
+ /** Masked key input (echo replaced with bullets). Non-TTY rejects. */
63
+ export async function maskedInput(deps, prompt) {
64
+ assertNotAborted(deps);
65
+ if (!deps.streams.isTTY()) {
66
+ throw new CliError("NOT_TTY", "masked input requires a TTY");
67
+ }
68
+ deps.streams.write(prompt);
69
+ let value = "";
70
+ for (;;) {
71
+ const key = await deps.streams.readKey();
72
+ if (key === "\r" || key === "\n")
73
+ break;
74
+ if (key === "\u0003")
75
+ throw new CancelledError(); // Ctrl+C
76
+ if (key === "\u007f" || key === "\b") {
77
+ value = value.slice(0, -1);
78
+ deps.streams.write("\b \b");
79
+ continue;
80
+ }
81
+ if (key.length === 1 && key >= " ") {
82
+ value += key;
83
+ deps.streams.write("*");
84
+ }
85
+ }
86
+ deps.streams.write("\n");
87
+ return value;
88
+ }
@@ -0,0 +1,11 @@
1
+ export declare const MOEICONS_BANNER: string;
2
+ export declare const CLI_NOTICE_LINES: readonly ["Run moeicons from your project root.", "React/Vue targets require the matching framework.", "Automatic Tailwind integration supports v3 only.", "Vanilla and Assets do not require React or Vue."];
3
+ /** Visible terminal width: ANSI sequences and combining marks take no columns. */
4
+ export declare function visibleWidth(value: string): number;
5
+ /** Render an adaptive notice box without splitting ANSI escapes or Unicode characters. */
6
+ export declare function renderNoticeBox(message: string | readonly string[], options?: {
7
+ readonly width?: number;
8
+ readonly unicode?: boolean;
9
+ }): string;
10
+ export declare function renderBannerText(): string;
11
+ export declare const CLI_VERSION: string;
@@ -0,0 +1,105 @@
1
+ import packageJson from "../../package.json" with { type: "json" };
2
+ /** ASCII banner generated by scripts/generate-banner.mjs (figlet, Standard font). */
3
+ const slash = String.fromCharCode(92);
4
+ export const MOEICONS_BANNER = [
5
+ "",
6
+ " __ __ ___ _____ ___ ____ ___ _ _ ____",
7
+ ` | ${slash}/ |/ _ ${slash}| ____|_ _/ ___/ _ ${slash}| ${slash} | / ___|`,
8
+ " | |" + slash + "/| | | | | _| | | | | | | " + slash + "| " + slash + "___ " + slash,
9
+ ` | | | | |_| | |___ | | |__| |_| | |${slash} |___) |`,
10
+ ` |_| |_|${slash}___/|_____|___${slash}____${slash}___/|_| ${slash}_|____/`,
11
+ "",
12
+ ].join("\n");
13
+ const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
14
+ const COMBINING_MARK = /\p{Mark}/u;
15
+ const CONTROL = /[\p{Cc}\p{Cf}]/u;
16
+ const WIDE_CODEPOINT = "[" +
17
+ "\u1100-\u115f" + // Hangul jamo
18
+ "\u2e80-\u303e" + // CJK radicals, Kangxi, punctuation
19
+ "\u3041-\u33ff" + // Hiragana/Katakana, CJK symbols, enclosed
20
+ "\u3400-\u4dbf" + // CJK ext A
21
+ "\u4e00-\u9fff" + // CJK unified
22
+ "\ua000-\ua4cf" + // Yi
23
+ "\uac00-\ud7a3" + // Hangul syllables
24
+ "\uf900-\ufaff" + // CJK compat
25
+ "\ufe30-\ufe4f" + // CJK compat forms
26
+ "\uff00-\uff60" + // fullwidth forms
27
+ "\uffe0-\uffe6" + // fullwidth signs
28
+ "]";
29
+ const WIDE_CHAR = new RegExp(WIDE_CODEPOINT, "u");
30
+ export const CLI_NOTICE_LINES = [
31
+ "Run moeicons from your project root.",
32
+ "React/Vue targets require the matching framework.",
33
+ "Automatic Tailwind integration supports v3 only.",
34
+ "Vanilla and Assets do not require React or Vue.",
35
+ ];
36
+ /** wcwidth-style display width for a single Unicode code point. */
37
+ function codepointWidth(character) {
38
+ if (COMBINING_MARK.test(character))
39
+ return 0;
40
+ if (CONTROL.test(character))
41
+ return 0;
42
+ if (WIDE_CHAR.test(character))
43
+ return 2;
44
+ return 1;
45
+ }
46
+ function stripAnsi(value) {
47
+ return value.replace(ANSI_ESCAPE, "");
48
+ }
49
+ /** Visible terminal width: ANSI sequences and combining marks take no columns. */
50
+ export function visibleWidth(value) {
51
+ let width = 0;
52
+ for (const character of stripAnsi(value))
53
+ width += codepointWidth(character);
54
+ return width;
55
+ }
56
+ const ANSI_OR_CHAR = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]|[\\s\\S]`, "gu");
57
+ /** Truncate to `maxWidth` visible columns, preserving ANSI sequences seen so far. */
58
+ function truncateLine(value, maxWidth) {
59
+ if (visibleWidth(value) <= maxWidth)
60
+ return value;
61
+ const budget = maxWidth - 1;
62
+ let result = "";
63
+ let width = 0;
64
+ for (const token of value.matchAll(ANSI_OR_CHAR)) {
65
+ const part = token[0];
66
+ if (part.startsWith(String.fromCharCode(27))) {
67
+ result += part;
68
+ continue;
69
+ }
70
+ const tokenWidth = codepointWidth(part);
71
+ if (width + tokenWidth > budget) {
72
+ result += "…";
73
+ break;
74
+ }
75
+ result += part;
76
+ width += tokenWidth;
77
+ }
78
+ return result;
79
+ }
80
+ /** Pad to `width` visible columns, ignoring ANSI escapes. */
81
+ function padLine(value, width) {
82
+ const missing = width - visibleWidth(value);
83
+ return missing > 0 ? value + " ".repeat(missing) : value;
84
+ }
85
+ /** Render an adaptive notice box without splitting ANSI escapes or Unicode characters. */
86
+ export function renderNoticeBox(message, options = {}) {
87
+ const lines = typeof message === "string" ? message.split("\n") : [...message];
88
+ const available = Math.max(4, options.width ?? 80);
89
+ const widest = lines.length > 0 ? Math.max(...lines.map(visibleWidth)) : 1;
90
+ const contentWidth = Math.min(Math.max(widest, 1), Math.max(1, available - 4));
91
+ const unicode = options.unicode ?? true;
92
+ const [top, side, bottom, topRight, bottomRight] = unicode
93
+ ? ["┌", "│", "└", "┐", "┘"]
94
+ : ["+", "|", "+", "+", "+"];
95
+ const horizontal = unicode ? "─" : "-";
96
+ return [
97
+ `${top}${horizontal.repeat(contentWidth + 2)}${topRight}`,
98
+ ...lines.map((line) => `${side} ${padLine(truncateLine(line, contentWidth), contentWidth)} ${side}`),
99
+ `${bottom}${horizontal.repeat(contentWidth + 2)}${bottomRight}`,
100
+ ].join("\n");
101
+ }
102
+ export function renderBannerText() {
103
+ return `${MOEICONS_BANNER}\n${renderNoticeBox(CLI_NOTICE_LINES)}\n`;
104
+ }
105
+ export const CLI_VERSION = packageJson.version;