@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.
- package/LICENSE +15 -0
- package/README.md +32 -0
- package/bin/moeicons.js +20 -0
- package/dist/api/client.d.ts +42 -0
- package/dist/api/client.js +105 -0
- package/dist/auth/callback.d.ts +37 -0
- package/dist/auth/callback.js +72 -0
- package/dist/auth/device-login.d.ts +29 -0
- package/dist/auth/device-login.js +74 -0
- package/dist/auth/login.d.ts +41 -0
- package/dist/auth/login.js +85 -0
- package/dist/auth/open-browser.d.ts +2 -0
- package/dist/auth/open-browser.js +15 -0
- package/dist/auth/pkce.d.ts +17 -0
- package/dist/auth/pkce.js +20 -0
- package/dist/auth/token-store.d.ts +38 -0
- package/dist/auth/token-store.js +126 -0
- package/dist/catalog/catalog.d.ts +32 -0
- package/dist/catalog/catalog.js +80 -0
- package/dist/catalog/catalog.json +9371 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +507 -0
- package/dist/commands/parser.d.ts +41 -0
- package/dist/commands/parser.js +146 -0
- package/dist/core/auth.d.ts +32 -0
- package/dist/core/auth.js +131 -0
- package/dist/core/bitmap-assets.d.ts +29 -0
- package/dist/core/bitmap-assets.js +77 -0
- package/dist/core/cli-update.d.ts +43 -0
- package/dist/core/cli-update.js +71 -0
- package/dist/core/context.d.ts +22 -0
- package/dist/core/context.js +1 -0
- package/dist/core/free-download.d.ts +66 -0
- package/dist/core/free-download.js +193 -0
- package/dist/core/generate.d.ts +33 -0
- package/dist/core/generate.js +285 -0
- package/dist/core/icon-names.d.ts +14 -0
- package/dist/core/icon-names.js +54 -0
- package/dist/core/init.d.ts +12 -0
- package/dist/core/init.js +23 -0
- package/dist/core/install.d.ts +67 -0
- package/dist/core/install.js +117 -0
- package/dist/core/library-update.d.ts +24 -0
- package/dist/core/library-update.js +161 -0
- package/dist/core/manage.d.ts +24 -0
- package/dist/core/manage.js +38 -0
- package/dist/core/pro-download.d.ts +29 -0
- package/dist/core/pro-download.js +112 -0
- package/dist/core/pro-install.d.ts +24 -0
- package/dist/core/pro-install.js +72 -0
- package/dist/core/release-descriptor.d.ts +40 -0
- package/dist/core/release-descriptor.js +114 -0
- package/dist/core/resource-variant.d.ts +35 -0
- package/dist/core/resource-variant.js +70 -0
- package/dist/core/signed-artifact.d.ts +29 -0
- package/dist/core/signed-artifact.js +81 -0
- package/dist/core/target-subtree.d.ts +49 -0
- package/dist/core/target-subtree.js +132 -0
- package/dist/core/update-policy.d.ts +31 -0
- package/dist/core/update-policy.js +27 -0
- package/dist/core/version-service.d.ts +26 -0
- package/dist/core/version-service.js +71 -0
- package/dist/core/wizard.d.ts +49 -0
- package/dist/core/wizard.js +71 -0
- package/dist/errors/errors.d.ts +34 -0
- package/dist/errors/errors.js +39 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/generator/cn.d.ts +5 -0
- package/dist/generator/cn.js +14 -0
- package/dist/generator/generate.d.ts +34 -0
- package/dist/generator/generate.js +562 -0
- package/dist/generator/theme-resolve.d.ts +26 -0
- package/dist/generator/theme-resolve.js +100 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.js +176 -0
- package/dist/project/config.d.ts +74 -0
- package/dist/project/config.js +353 -0
- package/dist/project/detect.d.ts +26 -0
- package/dist/project/detect.js +104 -0
- package/dist/project/install-metadata.d.ts +29 -0
- package/dist/project/install-metadata.js +108 -0
- package/dist/project/install.d.ts +115 -0
- package/dist/project/install.js +537 -0
- package/dist/project/project-lock.d.ts +11 -0
- package/dist/project/project-lock.js +81 -0
- package/dist/project/tailwind.d.ts +51 -0
- package/dist/project/tailwind.js +188 -0
- package/dist/project/tar-gz.d.ts +12 -0
- package/dist/project/tar-gz.js +100 -0
- package/dist/tui/primitives.d.ts +36 -0
- package/dist/tui/primitives.js +88 -0
- package/dist/ui/banner.d.ts +11 -0
- package/dist/ui/banner.js +105 -0
- package/dist/ui/clack.d.ts +8 -0
- package/dist/ui/clack.js +53 -0
- package/dist/ui/create-ui.d.ts +14 -0
- package/dist/ui/create-ui.js +16 -0
- package/dist/ui/non-interactive.d.ts +5 -0
- package/dist/ui/non-interactive.js +30 -0
- package/dist/ui/stream.d.ts +11 -0
- package/dist/ui/stream.js +50 -0
- package/package.json +70 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { downloadArtifact, verifyArtifact } from "../project/install.js";
|
|
4
|
+
import { decodeUtf8, extractTarGz } from "../project/tar-gz.js";
|
|
5
|
+
import { catalog as bundledCatalog } from "../catalog/catalog.js";
|
|
6
|
+
import { cacheKey, FREE_DOWNLOAD_HOSTS, githubReleaseAssetUrl, parseReleaseDescriptor, parseSha256Sidecar, } from "./release-descriptor.js";
|
|
7
|
+
export const DESCRIPTOR_MAX_BYTES = 256 * 1024;
|
|
8
|
+
export const ARTIFACT_MAX_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
export const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
10
|
+
export const MAX_REDIRECTS = 5;
|
|
11
|
+
export const DESCRIPTOR_NAME = "release-descriptor.json";
|
|
12
|
+
export const DESCRIPTOR_SHA_NAME = "release-descriptor.json.sha256";
|
|
13
|
+
function sha256Hex(bytes) {
|
|
14
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
function userAgent(cliVersion) {
|
|
17
|
+
return `moeicons/${cliVersion} (+https://github.com/moewolf-dev/moe-icons-cli)`;
|
|
18
|
+
}
|
|
19
|
+
function descriptorLimits(cliVersion, onProgress, fixtureBaseUrl, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
20
|
+
return {
|
|
21
|
+
maxBytes: DESCRIPTOR_MAX_BYTES,
|
|
22
|
+
timeoutMs,
|
|
23
|
+
maxRedirects: MAX_REDIRECTS,
|
|
24
|
+
allowedHosts: fixtureBaseUrl ? [new URL(fixtureBaseUrl).host] : FREE_DOWNLOAD_HOSTS,
|
|
25
|
+
...(fixtureBaseUrl ? { allowHttpLoopback: true } : {}),
|
|
26
|
+
userAgent: userAgent(cliVersion),
|
|
27
|
+
...(onProgress ? { onProgress } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function artifactLimits(cliVersion, onProgress, fixtureBaseUrl, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
|
|
31
|
+
return {
|
|
32
|
+
maxBytes: ARTIFACT_MAX_BYTES,
|
|
33
|
+
timeoutMs,
|
|
34
|
+
maxRedirects: MAX_REDIRECTS,
|
|
35
|
+
allowedHosts: fixtureBaseUrl ? [new URL(fixtureBaseUrl).host] : FREE_DOWNLOAD_HOSTS,
|
|
36
|
+
...(fixtureBaseUrl ? { allowHttpLoopback: true } : {}),
|
|
37
|
+
userAgent: userAgent(cliVersion),
|
|
38
|
+
...(onProgress ? { onProgress } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function mapDownloadError(code, message) {
|
|
42
|
+
if (code === "HTTP_ERROR" && / 404$/.test(message)) {
|
|
43
|
+
return { ok: false, reason: "not-found", message };
|
|
44
|
+
}
|
|
45
|
+
if (code === "NETWORK_ERROR")
|
|
46
|
+
return { ok: false, reason: "network", message };
|
|
47
|
+
return { ok: false, reason: "validation", message };
|
|
48
|
+
}
|
|
49
|
+
function readFixture(io, name) {
|
|
50
|
+
return io.readFileSync(join(io.fixtureDir ?? "", name));
|
|
51
|
+
}
|
|
52
|
+
export function artifactCachePath(cacheDir, fullVersion, sha256) {
|
|
53
|
+
return join(cacheDir, ...cacheKey(fullVersion, sha256).split("/"), "artifact.tgz");
|
|
54
|
+
}
|
|
55
|
+
function cachePath(io, fullVersion, sha256) {
|
|
56
|
+
return artifactCachePath(io.cacheDir, fullVersion, sha256);
|
|
57
|
+
}
|
|
58
|
+
async function loadBytes(io, filename, limits, tag) {
|
|
59
|
+
if (io.signal.aborted)
|
|
60
|
+
return { ok: false, reason: "cancelled", message: "download cancelled" };
|
|
61
|
+
if (io.fixtureDir) {
|
|
62
|
+
try {
|
|
63
|
+
return { ok: true, bytes: readFixture(io, filename) };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { ok: false, reason: "validation", message: error instanceof Error ? error.message : String(error) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const url = io.fixtureBaseUrl ? new URL(filename, io.fixtureBaseUrl.endsWith("/") ? io.fixtureBaseUrl : `${io.fixtureBaseUrl}/`).toString() : githubReleaseAssetUrl(tag, filename);
|
|
70
|
+
const result = await downloadArtifact(url, limits, { fetchFn: io.fetchFn, signal: io.signal });
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
return mapDownloadError(result.code, result.message);
|
|
73
|
+
return { ok: true, bytes: result.bytes };
|
|
74
|
+
}
|
|
75
|
+
function catalogFromArchive(artifactBytes, catalogFilename, expectedSha) {
|
|
76
|
+
const unpacked = extractTarGz(artifactBytes, { maxEntries: 20_000, maxExpandedBytes: ARTIFACT_MAX_BYTES });
|
|
77
|
+
if (unpacked.errors.length > 0) {
|
|
78
|
+
return { ok: false, reason: "validation", message: unpacked.errors[0] ?? "extract failed" };
|
|
79
|
+
}
|
|
80
|
+
const catalogBytes = unpacked.files[catalogFilename] ?? unpacked.files[`./${catalogFilename}`];
|
|
81
|
+
if (!catalogBytes) {
|
|
82
|
+
return { ok: false, reason: "validation", message: `artifact is missing ${catalogFilename}` };
|
|
83
|
+
}
|
|
84
|
+
const actual = sha256Hex(catalogBytes);
|
|
85
|
+
if (actual !== expectedSha) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: "checksum-mismatch",
|
|
89
|
+
message: `catalog SHA-256 mismatch: expected ${expectedSha}, got ${actual}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, json: decodeUtf8(catalogBytes) };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch descriptor sidecar → verify descriptor → download free.filename from
|
|
96
|
+
* the descriptor (never guessed) → verify archive + nested catalog checksums.
|
|
97
|
+
* Local `fixtureDir` stands in for a GitHub Release directory.
|
|
98
|
+
*/
|
|
99
|
+
export async function downloadFreeRelease(io, sourceVersion) {
|
|
100
|
+
if (io.signal.aborted)
|
|
101
|
+
return { ok: false, reason: "cancelled", message: "download cancelled" };
|
|
102
|
+
const tag = `v${sourceVersion}`;
|
|
103
|
+
const shaFile = await loadBytes(io, DESCRIPTOR_SHA_NAME, descriptorLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs), tag);
|
|
104
|
+
if (!shaFile.ok) {
|
|
105
|
+
if (!io.fixtureDir && shaFile.reason === "network") {
|
|
106
|
+
return { ok: false, reason: "offline-no-cache", message: shaFile.message };
|
|
107
|
+
}
|
|
108
|
+
return shaFile;
|
|
109
|
+
}
|
|
110
|
+
let expectedDescriptorSha;
|
|
111
|
+
try {
|
|
112
|
+
expectedDescriptorSha = parseSha256Sidecar(decodeUtf8(shaFile.bytes));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return { ok: false, reason: "validation", message: error instanceof Error ? error.message : String(error) };
|
|
116
|
+
}
|
|
117
|
+
const descriptorFile = await loadBytes(io, DESCRIPTOR_NAME, descriptorLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs), tag);
|
|
118
|
+
if (!descriptorFile.ok)
|
|
119
|
+
return descriptorFile;
|
|
120
|
+
const actualDescriptorSha = sha256Hex(descriptorFile.bytes);
|
|
121
|
+
if (actualDescriptorSha !== expectedDescriptorSha) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: "checksum-mismatch",
|
|
125
|
+
message: `descriptor SHA-256 mismatch: expected ${expectedDescriptorSha}, got ${actualDescriptorSha}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
let descriptor;
|
|
129
|
+
try {
|
|
130
|
+
descriptor = parseReleaseDescriptor(descriptorFile.bytes);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { ok: false, reason: "validation", message: error instanceof Error ? error.message : String(error) };
|
|
134
|
+
}
|
|
135
|
+
if (descriptor.fullVersion !== sourceVersion) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: "validation",
|
|
139
|
+
message: `descriptor fullVersion ${descriptor.fullVersion} does not match catalog sourceVersion ${sourceVersion}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const artifactCache = cachePath(io, descriptor.fullVersion, descriptor.free.sha256);
|
|
143
|
+
if (io.existsSync(artifactCache)) {
|
|
144
|
+
const cached = io.readFileSync(artifactCache);
|
|
145
|
+
const verified = verifyArtifact(cached, descriptor.free.sha256);
|
|
146
|
+
if (verified.ok) {
|
|
147
|
+
const catalog = catalogFromArchive(cached, descriptor.catalog.filename, descriptor.catalog.sha256);
|
|
148
|
+
if (!catalog.ok)
|
|
149
|
+
return catalog;
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
descriptor,
|
|
153
|
+
descriptorSha256: actualDescriptorSha,
|
|
154
|
+
artifactBytes: cached,
|
|
155
|
+
catalogJson: catalog.json,
|
|
156
|
+
cacheHit: true,
|
|
157
|
+
tag,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const artifact = await loadBytes(io, descriptor.free.filename, artifactLimits(io.cliVersion, io.onProgress, io.fixtureBaseUrl, io.timeoutMs), tag);
|
|
162
|
+
if (!artifact.ok) {
|
|
163
|
+
if (!io.fixtureDir && artifact.reason === "network") {
|
|
164
|
+
return { ok: false, reason: "offline-no-cache", message: artifact.message };
|
|
165
|
+
}
|
|
166
|
+
return artifact;
|
|
167
|
+
}
|
|
168
|
+
const verified = verifyArtifact(artifact.bytes, descriptor.free.sha256);
|
|
169
|
+
if (!verified.ok) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: "checksum-mismatch",
|
|
173
|
+
message: `free artifact SHA-256 mismatch: expected ${descriptor.free.sha256}, got ${verified.actual}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const catalog = catalogFromArchive(artifact.bytes, descriptor.catalog.filename, descriptor.catalog.sha256);
|
|
177
|
+
if (!catalog.ok)
|
|
178
|
+
return catalog;
|
|
179
|
+
io.mkdirSync(join(artifactCache, ".."));
|
|
180
|
+
io.writeFileSync(artifactCache, artifact.bytes);
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
descriptor,
|
|
184
|
+
descriptorSha256: actualDescriptorSha,
|
|
185
|
+
artifactBytes: artifact.bytes,
|
|
186
|
+
catalogJson: catalog.json,
|
|
187
|
+
cacheHit: false,
|
|
188
|
+
tag,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function bundledSourceVersion() {
|
|
192
|
+
return bundledCatalog.sourceVersion;
|
|
193
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type TransactionalFsWithCopy } from "../project/install.js";
|
|
2
|
+
import type { CommandContext } from "./context.js";
|
|
3
|
+
import type { Target } from "../commands/parser.js";
|
|
4
|
+
export type GenerateResult = {
|
|
5
|
+
readonly ok: true;
|
|
6
|
+
readonly files: readonly string[];
|
|
7
|
+
readonly warnings?: readonly string[];
|
|
8
|
+
readonly notes?: readonly string[];
|
|
9
|
+
} | {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
readonly errors?: readonly string[];
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Load installed artifact bytes for generate. Prefers `.moeicons/artifact/<target>/`
|
|
17
|
+
* when that subtree already contains the assets the generator needs (assets target).
|
|
18
|
+
* Vanilla (and bitmap themes on react/vue) need the full cached archive so
|
|
19
|
+
* `assets/` is available — the installed package subtree alone is not enough.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadArchiveFiles(projectRoot: string, env: Readonly<Record<string, string | undefined>>, fs_: Pick<TransactionalFsWithCopy, "readFileSync" | "existsSync" | "readdirSync">, injected?: Readonly<Record<string, Uint8Array>>): {
|
|
22
|
+
readonly ok: true;
|
|
23
|
+
readonly files: Readonly<Record<string, Uint8Array>>;
|
|
24
|
+
} | {
|
|
25
|
+
readonly ok: false;
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function runGenerateUseCase(context: CommandContext, fs_: TransactionalFsWithCopy, options?: {
|
|
29
|
+
readonly noTailwind?: boolean;
|
|
30
|
+
readonly target?: Target;
|
|
31
|
+
readonly archiveFiles?: Readonly<Record<string, Uint8Array>>;
|
|
32
|
+
readonly reconcileInstalled?: boolean;
|
|
33
|
+
}): Promise<GenerateResult>;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { join, relative, resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { executeGeneratedFilesDir, executeManagedReconcile, } from "../project/install.js";
|
|
4
|
+
import { detectProject } from "../project/detect.js";
|
|
5
|
+
import { readMoeiconsConfig } from "../project/config.js";
|
|
6
|
+
import { planGeneratedFiles } from "../generator/generate.js";
|
|
7
|
+
import { ensureClassMergeDependencies, planTailwindIntegration } from "../project/tailwind.js";
|
|
8
|
+
import { CliError, isCliError } from "../errors/index.js";
|
|
9
|
+
import { extractTarGz } from "../project/tar-gz.js";
|
|
10
|
+
import { artifactCachePath } from "./free-download.js";
|
|
11
|
+
import { resolveThemes } from "../generator/theme-resolve.js";
|
|
12
|
+
import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
|
|
13
|
+
import { withProjectLockSync } from "../project/project-lock.js";
|
|
14
|
+
function hasBitmapThemes(config) {
|
|
15
|
+
const resolved = resolveThemes(config);
|
|
16
|
+
return resolved.ok && resolved.themes.some((theme) => theme.kind === "bitmap");
|
|
17
|
+
}
|
|
18
|
+
/** Vanilla/Assets generate from raw SVG; bitmap themes need variant binaries. */
|
|
19
|
+
function needsArchiveFiles(config) {
|
|
20
|
+
if (config.target === "vanilla" || config.target === "assets")
|
|
21
|
+
return true;
|
|
22
|
+
return hasBitmapThemes(config);
|
|
23
|
+
}
|
|
24
|
+
function archiveHasAssetsManifest(files) {
|
|
25
|
+
return Object.keys(files).some((path) => /(?:^|\/)assets\/manifest\.json$/.test(path));
|
|
26
|
+
}
|
|
27
|
+
function missingArchiveMessage(config) {
|
|
28
|
+
if (config.target === "vanilla" || config.target === "assets") {
|
|
29
|
+
return `${config.target} target requires an installed artifact; run \`moeicons install\` first`;
|
|
30
|
+
}
|
|
31
|
+
return "bitmap themes require a downloaded free artifact; run `moeicons install` or set MOEICONS_BITMAP_ARCHIVE";
|
|
32
|
+
}
|
|
33
|
+
function readInstallMetadata(projectRoot, readFileSync, existsSync) {
|
|
34
|
+
const path = join(projectRoot, ".moeicons", "install-metadata.json");
|
|
35
|
+
if (!existsSync(path))
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
+
return {
|
|
40
|
+
...(typeof parsed.artifactVersion === "string"
|
|
41
|
+
? { artifactVersion: parsed.artifactVersion }
|
|
42
|
+
: {}),
|
|
43
|
+
...(typeof parsed.artifactSha256 === "string"
|
|
44
|
+
? { artifactSha256: parsed.artifactSha256 }
|
|
45
|
+
: {}),
|
|
46
|
+
...(typeof parsed.target === "string" &&
|
|
47
|
+
["react", "vue", "vanilla", "assets"].includes(parsed.target)
|
|
48
|
+
? { target: parsed.target }
|
|
49
|
+
: {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readBinaryFile(fs_, path) {
|
|
57
|
+
const data = fs_.readFileSync(path);
|
|
58
|
+
return typeof data === "string" ? Buffer.from(data, "utf8") : new Uint8Array(data);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load installed artifact bytes for generate. Prefers `.moeicons/artifact/<target>/`
|
|
62
|
+
* when that subtree already contains the assets the generator needs (assets target).
|
|
63
|
+
* Vanilla (and bitmap themes on react/vue) need the full cached archive so
|
|
64
|
+
* `assets/` is available — the installed package subtree alone is not enough.
|
|
65
|
+
*/
|
|
66
|
+
export function loadArchiveFiles(projectRoot, env, fs_, injected) {
|
|
67
|
+
if (injected)
|
|
68
|
+
return { ok: true, files: injected };
|
|
69
|
+
const fixtureTgz = env.MOEICONS_BITMAP_ARCHIVE;
|
|
70
|
+
if (fixtureTgz && fs_.existsSync(fixtureTgz)) {
|
|
71
|
+
const unpacked = extractTarGz(readBinaryFile(fs_, fixtureTgz), {
|
|
72
|
+
maxEntries: 20_000,
|
|
73
|
+
maxExpandedBytes: 64 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
if (unpacked.errors.length > 0)
|
|
76
|
+
return { ok: false, reason: unpacked.errors[0] ?? "invalid bitmap archive fixture" };
|
|
77
|
+
return { ok: true, files: unpacked.files };
|
|
78
|
+
}
|
|
79
|
+
const meta = readInstallMetadata(projectRoot, fs_.readFileSync, fs_.existsSync);
|
|
80
|
+
if (meta?.target) {
|
|
81
|
+
const subtreeRoot = join(projectRoot, ".moeicons", "artifact", meta.target);
|
|
82
|
+
if (fs_.existsSync(subtreeRoot)) {
|
|
83
|
+
const files = {};
|
|
84
|
+
const walk = (dir, prefix) => {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = fs_.readdirSync(dir, { withFileTypes: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
93
|
+
const full = join(dir, entry.name);
|
|
94
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
walk(full, rel);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!entry.isFile())
|
|
100
|
+
continue;
|
|
101
|
+
const data = readBinaryFile(fs_, full);
|
|
102
|
+
files[`${meta.target}/${rel}`] = data;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
walk(subtreeRoot, "");
|
|
106
|
+
// assets install lands the raw tree; reuse it. Package subtrees (react/vue/
|
|
107
|
+
// vanilla) do not contain assets/manifest.json — fall through to the cache.
|
|
108
|
+
if (Object.keys(files).length > 0 && (meta.target === "assets" || archiveHasAssetsManifest(files))) {
|
|
109
|
+
return { ok: true, files };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const cacheDir = env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
114
|
+
if (meta?.artifactVersion && meta.artifactSha256) {
|
|
115
|
+
const cached = artifactCachePath(cacheDir, meta.artifactVersion, meta.artifactSha256);
|
|
116
|
+
if (fs_.existsSync(cached)) {
|
|
117
|
+
const unpacked = extractTarGz(readBinaryFile(fs_, cached), {
|
|
118
|
+
maxEntries: 20_000,
|
|
119
|
+
maxExpandedBytes: 64 * 1024 * 1024,
|
|
120
|
+
});
|
|
121
|
+
if (unpacked.errors.length > 0)
|
|
122
|
+
return { ok: false, reason: unpacked.errors[0] ?? "invalid cached artifact" };
|
|
123
|
+
return { ok: true, files: unpacked.files };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { ok: false, reason: "installed artifact not found" };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* B5: switching the configured target over an existing install is a destructive
|
|
130
|
+
* migration (generated files are replaced and the install subtree changes).
|
|
131
|
+
* Interactive TTY prompts for explicit confirmation; non-interactive/JSON mode
|
|
132
|
+
* fails with VALIDATION_ERROR unless `--yes` was passed (the non-interactive
|
|
133
|
+
* UI resolves `--yes` confirmations to true). Fresh projects with no install
|
|
134
|
+
* metadata never prompt.
|
|
135
|
+
*/
|
|
136
|
+
async function confirmTargetSwitch(context, installedTarget, configuredTarget) {
|
|
137
|
+
const message = `Existing install targets "${installedTarget}" but the config now targets "${configuredTarget}". ` +
|
|
138
|
+
"Switching targets is a destructive migration that replaces generated files. Continue?";
|
|
139
|
+
try {
|
|
140
|
+
return (await context.ui.confirm(message, context.signal)) === true;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (isCliError(error) && error.code === "NOT_TTY") {
|
|
144
|
+
throw new CliError("VALIDATION_ERROR", `target switch from "${installedTarget}" to "${configuredTarget}" is destructive and requires confirmation; pass --yes in non-interactive mode`);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export async function runGenerateUseCase(context, fs_, options = {}) {
|
|
150
|
+
const project = detectProject(context.cwd);
|
|
151
|
+
if (!project)
|
|
152
|
+
return { ok: false, reason: "no-project" };
|
|
153
|
+
const loaded = readMoeiconsConfig(project.root);
|
|
154
|
+
if (loaded.kind !== "ok")
|
|
155
|
+
return { ok: false, reason: `config state: ${loaded.kind}` };
|
|
156
|
+
const effectiveConfig = options.target
|
|
157
|
+
? { ...loaded.config, target: options.target }
|
|
158
|
+
: loaded.config;
|
|
159
|
+
const installedMeta = readInstallMetadata(project.root, fs_.readFileSync, fs_.existsSync);
|
|
160
|
+
if (installedMeta?.target && installedMeta.target !== effectiveConfig.target) {
|
|
161
|
+
const confirmed = await confirmTargetSwitch(context, installedMeta.target, effectiveConfig.target);
|
|
162
|
+
if (!confirmed)
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
reason: "cancelled",
|
|
166
|
+
errors: [`target switch from "${installedMeta.target}" to "${effectiveConfig.target}" cancelled`],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
let archiveFiles = options.archiveFiles;
|
|
170
|
+
if (needsArchiveFiles(effectiveConfig) && archiveFiles === undefined) {
|
|
171
|
+
const loadedArchive = loadArchiveFiles(project.root, context.env, fs_, undefined);
|
|
172
|
+
if (!loadedArchive.ok) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: "validation",
|
|
176
|
+
errors: [missingArchiveMessage(effectiveConfig)],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
archiveFiles = loadedArchive.files;
|
|
180
|
+
}
|
|
181
|
+
const plan = planGeneratedFiles(effectiveConfig, effectiveConfig.outputDir, archiveFiles ? { archiveFiles } : {});
|
|
182
|
+
if (!plan.ok)
|
|
183
|
+
return { ok: false, reason: "validation", errors: plan.errors };
|
|
184
|
+
const notes = [];
|
|
185
|
+
const sideFiles = [];
|
|
186
|
+
try {
|
|
187
|
+
const tw = planTailwindIntegration(project.root, loaded.config.outputDir, {
|
|
188
|
+
noTailwind: options.noTailwind === true,
|
|
189
|
+
target: effectiveConfig.target,
|
|
190
|
+
});
|
|
191
|
+
notes.push(...tw.notes);
|
|
192
|
+
sideFiles.push(...tw.files);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (isCliError(error) && error.code === "TAILWIND_VERSION_UNSUPPORTED") {
|
|
196
|
+
return { ok: false, reason: error.message, code: error.code };
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
const pkgPath = join(project.root, "package.json");
|
|
201
|
+
if (fs_.existsSync(pkgPath)) {
|
|
202
|
+
const pkgSource = fs_.readFileSync(pkgPath, "utf8");
|
|
203
|
+
const target = effectiveConfig.target;
|
|
204
|
+
const deps = target === "react" || target === "vue"
|
|
205
|
+
? ensureClassMergeDependencies(pkgSource)
|
|
206
|
+
: {
|
|
207
|
+
nextSource: pkgSource,
|
|
208
|
+
changed: false,
|
|
209
|
+
notes: [`skipped class merge dependencies for ${target} target`],
|
|
210
|
+
};
|
|
211
|
+
notes.push(...deps.notes);
|
|
212
|
+
if (deps.changed) {
|
|
213
|
+
sideFiles.push({ path: pkgPath, content: deps.nextSource });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
if (options.reconcileInstalled) {
|
|
218
|
+
const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
219
|
+
if (!fs_.existsSync(metadataPath))
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
reason: "managed install metadata is missing; run repair or reinstall",
|
|
223
|
+
};
|
|
224
|
+
const metadata = parseInstallMetadata(fs_.readFileSync(metadataPath, "utf8"));
|
|
225
|
+
if (!metadata || metadata.tier !== loaded.config.tier)
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
reason: "managed install metadata is invalid or does not match config tier; run repair or reinstall",
|
|
229
|
+
};
|
|
230
|
+
for (const [managedPath, expected] of Object.entries(metadata.managedFiles)) {
|
|
231
|
+
const absolute = join(project.root, managedPath);
|
|
232
|
+
if (!fs_.existsSync(absolute) ||
|
|
233
|
+
sha256Bytes(fs_.readFileSync(absolute)) !== expected) {
|
|
234
|
+
return { ok: false, reason: `managed file was modified or removed: ${managedPath}` };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (metadata.managedFiles[".moeicons/catalog.json"] !== metadata.catalogSha256)
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: "managed catalog hash is inconsistent; run repair or reinstall",
|
|
241
|
+
};
|
|
242
|
+
const outputPrefix = loaded.config.outputDir.replace(/\\/g, "/").replace(/\/$/, "") + "/";
|
|
243
|
+
const generated = Object.fromEntries(plan.files.map((file) => [file.path.replace(/\\/g, "/"), file.content]));
|
|
244
|
+
const nextManaged = {};
|
|
245
|
+
for (const [managedPath, hash] of Object.entries(metadata.managedFiles))
|
|
246
|
+
if (!managedPath.startsWith(outputPrefix))
|
|
247
|
+
nextManaged[managedPath] = hash;
|
|
248
|
+
for (const [path, content] of Object.entries(generated))
|
|
249
|
+
nextManaged[path] = sha256Bytes(content);
|
|
250
|
+
const nextMetadata = { ...metadata, managedFiles: nextManaged };
|
|
251
|
+
const writes = { ...generated };
|
|
252
|
+
for (const file of sideFiles) {
|
|
253
|
+
const absolute = resolve(file.path);
|
|
254
|
+
const rel = relative(project.root, absolute).replace(/\\/g, "/");
|
|
255
|
+
if (!rel || rel.startsWith("../") || rel === "..")
|
|
256
|
+
return { ok: false, reason: `side file escapes project: ${file.path}` };
|
|
257
|
+
writes[rel] = file.content;
|
|
258
|
+
}
|
|
259
|
+
writes[".moeicons/install-metadata.json"] = serializeInstallMetadata(nextMetadata);
|
|
260
|
+
const stale = Object.keys(metadata.managedFiles).filter((path) => path.startsWith(outputPrefix) && !(path in generated));
|
|
261
|
+
withProjectLockSync(project.root, "reload", () => executeManagedReconcile(project.root, writes, stale, fs_));
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
files: plan.files.map((file) => file.path),
|
|
265
|
+
...(loaded.warnings.length > 0 || notes.length > 0
|
|
266
|
+
? { warnings: [...loaded.warnings, ...notes] }
|
|
267
|
+
: {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
executeGeneratedFilesDir(plan.files, project.root, loaded.config.outputDir, fs_);
|
|
271
|
+
for (const file of sideFiles) {
|
|
272
|
+
fs_.writeFileSync(file.path, file.content);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
files: plan.files.map((file) => file.path),
|
|
277
|
+
...(loaded.warnings.length > 0 || notes.length > 0
|
|
278
|
+
? { warnings: [...loaded.warnings, ...notes] }
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical iconId → public name mapping.
|
|
3
|
+
*
|
|
4
|
+
* Must stay equivalent to moe-icons-code-library `scripts/generator-core.cjs`
|
|
5
|
+
* (`toComponentName` / `toExportName` / `reportNameCollisions`). The library
|
|
6
|
+
* keeps camelCase exports; CLI proxies keep PascalCase. Registry imports use
|
|
7
|
+
* an explicit alias between the two.
|
|
8
|
+
*/
|
|
9
|
+
/** Library/file/proxy PascalCase: `ui-search` → `UiSearch`. */
|
|
10
|
+
export declare function toProxyName(iconId: string): string;
|
|
11
|
+
/** Published library camelCase export: `ui-search` → `uiSearch`. */
|
|
12
|
+
export declare function toLibraryExportName(iconId: string): string;
|
|
13
|
+
export declare function reportProxyNameCollisions(iconIds: readonly string[]): string[];
|
|
14
|
+
export declare function reportLibraryExportNameCollisions(iconIds: readonly string[]): string[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical iconId → public name mapping.
|
|
3
|
+
*
|
|
4
|
+
* Must stay equivalent to moe-icons-code-library `scripts/generator-core.cjs`
|
|
5
|
+
* (`toComponentName` / `toExportName` / `reportNameCollisions`). The library
|
|
6
|
+
* keeps camelCase exports; CLI proxies keep PascalCase. Registry imports use
|
|
7
|
+
* an explicit alias between the two.
|
|
8
|
+
*/
|
|
9
|
+
const RESERVED_IDENTIFIERS = /^(?:default|class|function|var|let|const|new|return|delete|import|export)$/;
|
|
10
|
+
function nameSegments(value) {
|
|
11
|
+
return String(value)
|
|
12
|
+
.normalize("NFKD")
|
|
13
|
+
.split("")
|
|
14
|
+
.filter((character) => character.charCodeAt(0) <= 0x7f)
|
|
15
|
+
.join("")
|
|
16
|
+
.split("-")
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map((segment) => segment.replace(/[^A-Za-z0-9_$]/g, ""));
|
|
19
|
+
}
|
|
20
|
+
/** Library/file/proxy PascalCase: `ui-search` → `UiSearch`. */
|
|
21
|
+
export function toProxyName(iconId) {
|
|
22
|
+
const segments = nameSegments(iconId);
|
|
23
|
+
let result = segments.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("") || "Icon";
|
|
24
|
+
if (/^[0-9]/.test(result) || RESERVED_IDENTIFIERS.test(result.toLowerCase())) {
|
|
25
|
+
result = `Icon${result}`;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/** Published library camelCase export: `ui-search` → `uiSearch`. */
|
|
30
|
+
export function toLibraryExportName(iconId) {
|
|
31
|
+
const component = toProxyName(iconId);
|
|
32
|
+
return component.charAt(0).toLowerCase() + component.slice(1);
|
|
33
|
+
}
|
|
34
|
+
export function reportProxyNameCollisions(iconIds) {
|
|
35
|
+
return reportCollisions(iconIds, toProxyName, "PascalCase");
|
|
36
|
+
}
|
|
37
|
+
export function reportLibraryExportNameCollisions(iconIds) {
|
|
38
|
+
return reportCollisions(iconIds, toLibraryExportName, "library export");
|
|
39
|
+
}
|
|
40
|
+
function reportCollisions(iconIds, nameOf, kind) {
|
|
41
|
+
const seen = new Map();
|
|
42
|
+
const collisions = [];
|
|
43
|
+
for (const id of iconIds) {
|
|
44
|
+
const name = nameOf(id);
|
|
45
|
+
const existing = seen.get(name);
|
|
46
|
+
if (existing) {
|
|
47
|
+
collisions.push(`duplicate ${kind} name "${name}" from "${existing}" and "${id}"`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
seen.set(name, id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return collisions;
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TransactionalFs } from "../project/install.js";
|
|
2
|
+
import type { CommandContext } from "./context.js";
|
|
3
|
+
import type { Target } from "../commands/parser.js";
|
|
4
|
+
export type InitResult = {
|
|
5
|
+
readonly ok: true;
|
|
6
|
+
readonly created: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly ok: false;
|
|
9
|
+
readonly reason: "no-project" | "exists" | "write-failed";
|
|
10
|
+
readonly path?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function runInitUseCase(context: CommandContext, fs_: TransactionalFs, target?: Target): InitResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createFileIfAbsent } from "../project/install.js";
|
|
3
|
+
import { detectProject } from "../project/detect.js";
|
|
4
|
+
import { findConfigFile, renderMoeiconsConfigJsonc } from "../project/config.js";
|
|
5
|
+
export function runInitUseCase(context, fs_, target) {
|
|
6
|
+
const project = detectProject(context.cwd);
|
|
7
|
+
if (!project)
|
|
8
|
+
return { ok: false, reason: "no-project" };
|
|
9
|
+
if (findConfigFile(project.root))
|
|
10
|
+
return { ok: false, reason: "exists" };
|
|
11
|
+
const configPath = join(project.root, "moeicons.config.jsonc");
|
|
12
|
+
try {
|
|
13
|
+
const created = createFileIfAbsent(configPath, renderMoeiconsConfigJsonc({
|
|
14
|
+
target: target ?? (project.framework === "vue" ? "vue" : "react"),
|
|
15
|
+
}), fs_);
|
|
16
|
+
return created
|
|
17
|
+
? { ok: true, created: configPath }
|
|
18
|
+
: { ok: false, reason: "exists", path: configPath };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ok: false, reason: "write-failed", path: configPath };
|
|
22
|
+
}
|
|
23
|
+
}
|