@moewolf/moe-icons-cli 0.0.1 → 0.0.2
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 +243 -9
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +538 -3395
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReleaseTarget } from "../core/release-descriptor.js";
|
|
2
|
+
export interface MetadataFileDigest {
|
|
3
|
+
readonly size: number;
|
|
4
|
+
readonly sha256: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MetadataManifest {
|
|
7
|
+
readonly schemaVersion: 1;
|
|
8
|
+
readonly tier: "free" | "pro";
|
|
9
|
+
readonly libraryVersion: string;
|
|
10
|
+
readonly manualVersion: string;
|
|
11
|
+
readonly catalogVersion: string;
|
|
12
|
+
readonly cliVersion: string;
|
|
13
|
+
readonly generatedAt: {
|
|
14
|
+
readonly sourceCommit: string;
|
|
15
|
+
readonly generatorCommit: string;
|
|
16
|
+
};
|
|
17
|
+
readonly targets: readonly ReleaseTarget[];
|
|
18
|
+
readonly dependencies: Readonly<Partial<Record<ReleaseTarget, Readonly<Record<string, string>>>>>;
|
|
19
|
+
readonly files: {
|
|
20
|
+
readonly "MANUAL.md": MetadataFileDigest;
|
|
21
|
+
readonly "catalog.json": MetadataFileDigest;
|
|
22
|
+
};
|
|
23
|
+
/** ICON-E2E-0907 / E2E-E2: local-test candidates only. */
|
|
24
|
+
readonly channel?: "local-test";
|
|
25
|
+
readonly publishable?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Strict parse of a metadata manifest. Unknown fields, unknown schemaVersion,
|
|
29
|
+
* a tier/version/hash mismatch or malformed file digests are hard errors.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseMetadataManifest(raw: string | Uint8Array, opts?: {
|
|
32
|
+
readonly allowLocalTest?: boolean;
|
|
33
|
+
}): MetadataManifest;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
2
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-(?:alpha|beta))?$/;
|
|
3
|
+
const LOCAL_TEST_VERSION = /^\d+\.\d+\.\d+-test$/;
|
|
4
|
+
const COMMIT = /^[0-9a-f]{40}$/;
|
|
5
|
+
const RELEASE_TARGETS = ["react", "vue", "vanilla", "assets"];
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function parseFileDigest(value, field) {
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
throw new Error(`${field} must be an object`);
|
|
12
|
+
if (typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 1) {
|
|
13
|
+
throw new Error(`${field}.size must be a positive integer`);
|
|
14
|
+
}
|
|
15
|
+
if (typeof value.sha256 !== "string" || !SHA256_HEX.test(value.sha256)) {
|
|
16
|
+
throw new Error(`${field}.sha256 must be a 64-char sha256 hex`);
|
|
17
|
+
}
|
|
18
|
+
return { size: value.size, sha256: value.sha256.toLowerCase() };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Strict parse of a metadata manifest. Unknown fields, unknown schemaVersion,
|
|
22
|
+
* a tier/version/hash mismatch or malformed file digests are hard errors.
|
|
23
|
+
*/
|
|
24
|
+
export function parseMetadataManifest(raw, opts = {}) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error("manifest.json is not valid JSON");
|
|
31
|
+
}
|
|
32
|
+
if (!isRecord(parsed))
|
|
33
|
+
throw new Error("manifest.json must be an object");
|
|
34
|
+
const allowLocalTest = opts.allowLocalTest === true;
|
|
35
|
+
const allowed = new Set([
|
|
36
|
+
"schemaVersion",
|
|
37
|
+
"tier",
|
|
38
|
+
"libraryVersion",
|
|
39
|
+
"manualVersion",
|
|
40
|
+
"catalogVersion",
|
|
41
|
+
"cliVersion",
|
|
42
|
+
"generatedAt",
|
|
43
|
+
"targets",
|
|
44
|
+
"dependencies",
|
|
45
|
+
"files",
|
|
46
|
+
"channel",
|
|
47
|
+
"publishable",
|
|
48
|
+
]);
|
|
49
|
+
for (const key of Object.keys(parsed)) {
|
|
50
|
+
if (!allowed.has(key))
|
|
51
|
+
throw new Error(`manifest.json has an unknown field: ${key}`);
|
|
52
|
+
}
|
|
53
|
+
if (parsed.schemaVersion !== 1)
|
|
54
|
+
throw new Error(`unsupported manifest schemaVersion: ${String(parsed.schemaVersion)}`);
|
|
55
|
+
if (parsed.tier !== "free" && parsed.tier !== "pro") {
|
|
56
|
+
throw new Error(`manifest.json has an invalid tier: ${String(parsed.tier)}`);
|
|
57
|
+
}
|
|
58
|
+
const channel = parsed.channel === "local-test" ? "local-test" : undefined;
|
|
59
|
+
const publishable = parsed.publishable === true || parsed.publishable === false
|
|
60
|
+
? parsed.publishable
|
|
61
|
+
: undefined;
|
|
62
|
+
const localTestMarked = channel === "local-test" && publishable === false;
|
|
63
|
+
if (allowLocalTest && !localTestMarked && parsed.channel !== undefined) {
|
|
64
|
+
throw new Error("manifest.json local context requires channel local-test and publishable: false");
|
|
65
|
+
}
|
|
66
|
+
const versionAllowed = (value) => {
|
|
67
|
+
if (typeof value !== "string")
|
|
68
|
+
return false;
|
|
69
|
+
if (VERSION.test(value))
|
|
70
|
+
return true;
|
|
71
|
+
return allowLocalTest && localTestMarked && LOCAL_TEST_VERSION.test(value);
|
|
72
|
+
};
|
|
73
|
+
for (const field of ["libraryVersion", "manualVersion", "catalogVersion", "cliVersion"]) {
|
|
74
|
+
if (!versionAllowed(parsed[field])) {
|
|
75
|
+
throw new Error(`manifest.json ${field} must be a valid semver string`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(parsed.generatedAt))
|
|
79
|
+
throw new Error("manifest.json generatedAt must be an object");
|
|
80
|
+
const generatedAt = parsed.generatedAt;
|
|
81
|
+
for (const field of ["sourceCommit", "generatorCommit"]) {
|
|
82
|
+
if (typeof generatedAt[field] !== "string" || !COMMIT.test(generatedAt[field])) {
|
|
83
|
+
throw new Error(`manifest.json generatedAt.${field} must be a 40-char commit sha`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(parsed.targets) ||
|
|
87
|
+
parsed.targets.length === 0 ||
|
|
88
|
+
!parsed.targets.every((target) => RELEASE_TARGETS.includes(target))) {
|
|
89
|
+
throw new Error("manifest.json targets must be a non-empty array of release targets");
|
|
90
|
+
}
|
|
91
|
+
const targets = parsed.targets;
|
|
92
|
+
if (!isRecord(parsed.dependencies))
|
|
93
|
+
throw new Error("manifest.json dependencies must be an object");
|
|
94
|
+
const dependencies = {
|
|
95
|
+
react: {},
|
|
96
|
+
vue: {},
|
|
97
|
+
vanilla: {},
|
|
98
|
+
assets: {},
|
|
99
|
+
};
|
|
100
|
+
for (const [target, value] of Object.entries(parsed.dependencies)) {
|
|
101
|
+
if (!RELEASE_TARGETS.includes(target)) {
|
|
102
|
+
throw new Error(`manifest.json has an unknown target dependency: ${target}`);
|
|
103
|
+
}
|
|
104
|
+
if (!isRecord(value))
|
|
105
|
+
throw new Error(`manifest.json dependencies.${target} must be an object`);
|
|
106
|
+
dependencies[target] = Object.fromEntries(Object.entries(value).map(([name, range]) => [name, String(range)]));
|
|
107
|
+
}
|
|
108
|
+
if (!isRecord(parsed.files))
|
|
109
|
+
throw new Error("manifest.json files must be an object");
|
|
110
|
+
const files = {
|
|
111
|
+
"MANUAL.md": parseFileDigest(parsed.files["MANUAL.md"], "files.MANUAL.md"),
|
|
112
|
+
"catalog.json": parseFileDigest(parsed.files["catalog.json"], "files.catalog.json"),
|
|
113
|
+
};
|
|
114
|
+
for (const key of Object.keys(parsed.files)) {
|
|
115
|
+
if (key !== "MANUAL.md" && key !== "catalog.json") {
|
|
116
|
+
throw new Error(`manifest.json files has an unknown entry: ${key}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
schemaVersion: 1,
|
|
121
|
+
tier: parsed.tier,
|
|
122
|
+
libraryVersion: parsed.libraryVersion,
|
|
123
|
+
manualVersion: parsed.manualVersion,
|
|
124
|
+
catalogVersion: parsed.catalogVersion,
|
|
125
|
+
cliVersion: parsed.cliVersion,
|
|
126
|
+
generatedAt: {
|
|
127
|
+
sourceCommit: generatedAt.sourceCommit,
|
|
128
|
+
generatorCommit: generatedAt.generatorCommit,
|
|
129
|
+
},
|
|
130
|
+
targets,
|
|
131
|
+
dependencies,
|
|
132
|
+
files,
|
|
133
|
+
...(channel ? { channel } : {}),
|
|
134
|
+
...(publishable !== undefined ? { publishable } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ParsedVersion {
|
|
2
|
+
readonly major: number;
|
|
3
|
+
readonly minor: number;
|
|
4
|
+
readonly patch: number;
|
|
5
|
+
readonly channel: "stable" | "alpha" | "beta";
|
|
6
|
+
}
|
|
7
|
+
export declare function parseVersion(value: string): ParsedVersion;
|
|
8
|
+
/** Three-way semver comparison; never string-dictionary compares prerelease tags. */
|
|
9
|
+
export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
10
|
+
export declare function isVersionNewer(candidate: string, current: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Per-resource version state for code and metadata. Distinguishes a CLI that is
|
|
13
|
+
* too old to consume the remote (incompatible), a corrupt local install, and a
|
|
14
|
+
* plain missing/current/update state.
|
|
15
|
+
*/
|
|
16
|
+
export type ResourceStatus = "missing" | "current" | "update-available" | "incompatible" | "corrupt";
|
|
17
|
+
export declare function cliCompatible(minRequiredCli: string, cliVersion: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Format a byte count for user-facing size hints (MiB, 1024-based, one decimal;
|
|
20
|
+
* KiB/B below one MiB). Frozen contract shared by the free and pro flows.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatBytes(bytes: number): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta))?$/;
|
|
2
|
+
export function parseVersion(value) {
|
|
3
|
+
const match = VERSION.exec(value);
|
|
4
|
+
if (!match)
|
|
5
|
+
throw new Error(`invalid version: ${value}`);
|
|
6
|
+
return {
|
|
7
|
+
major: Number(match[1]),
|
|
8
|
+
minor: Number(match[2]),
|
|
9
|
+
patch: Number(match[3]),
|
|
10
|
+
channel: match[4] === "alpha" ? "alpha" : match[4] === "beta" ? "beta" : "stable",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Three-way semver comparison; never string-dictionary compares prerelease tags. */
|
|
14
|
+
export function compareVersions(a, b) {
|
|
15
|
+
const left = parseVersion(a);
|
|
16
|
+
const right = parseVersion(b);
|
|
17
|
+
if (left.major !== right.major)
|
|
18
|
+
return left.major < right.major ? -1 : 1;
|
|
19
|
+
if (left.minor !== right.minor)
|
|
20
|
+
return left.minor < right.minor ? -1 : 1;
|
|
21
|
+
if (left.patch !== right.patch)
|
|
22
|
+
return left.patch < right.patch ? -1 : 1;
|
|
23
|
+
const channelRank = (channel) => channel === "alpha" ? 0 : channel === "beta" ? 1 : 2;
|
|
24
|
+
const l = channelRank(left.channel);
|
|
25
|
+
const r = channelRank(right.channel);
|
|
26
|
+
if (l !== r)
|
|
27
|
+
return l < r ? -1 : 1;
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
export function isVersionNewer(candidate, current) {
|
|
31
|
+
return compareVersions(candidate, current) > 0;
|
|
32
|
+
}
|
|
33
|
+
export function cliCompatible(minRequiredCli, cliVersion) {
|
|
34
|
+
try {
|
|
35
|
+
return compareVersions(cliVersion, minRequiredCli) >= 0;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format a byte count for user-facing size hints (MiB, 1024-based, one decimal;
|
|
43
|
+
* KiB/B below one MiB). Frozen contract shared by the free and pro flows.
|
|
44
|
+
*/
|
|
45
|
+
export function formatBytes(bytes) {
|
|
46
|
+
const kib = 1024;
|
|
47
|
+
const mib = kib * 1024;
|
|
48
|
+
if (bytes >= mib)
|
|
49
|
+
return `${(bytes / mib).toFixed(1)} MiB`;
|
|
50
|
+
if (bytes >= kib)
|
|
51
|
+
return `${(bytes / kib).toFixed(1)} KiB`;
|
|
52
|
+
return `${bytes} B`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnchorResult, PlannedFileChange } from "./types.js";
|
|
2
|
+
import type { DetectorIo } from "./helpers.js";
|
|
3
|
+
export type { PlannedFileChange };
|
|
4
|
+
export interface ApplicationAnchorOptions {
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly adapter?: string;
|
|
7
|
+
readonly io: DetectorIo;
|
|
8
|
+
/** true when the target is assets-only and application registration is not required. */
|
|
9
|
+
readonly assetsOnly?: boolean;
|
|
10
|
+
readonly outputDir?: string;
|
|
11
|
+
readonly confirmedEntry?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function inspectApplicationAnchor(options: ApplicationAnchorOptions): AnchorResult;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { dirname, join, posix } from "node:path";
|
|
2
|
+
import { parseSource } from "./parse.js";
|
|
3
|
+
/**
|
|
4
|
+
* Application anchor (E2E-B3).
|
|
5
|
+
*
|
|
6
|
+
* Only three auto-patchable shapes are supported and each gets its own
|
|
7
|
+
* analyzer/plan in this file:
|
|
8
|
+
* - Vite React `createRoot(...).render(<App />)`
|
|
9
|
+
* - Vite Vue `createApp(App).mount(...)`
|
|
10
|
+
* - Vanilla a single module entry
|
|
11
|
+
* Next App/Pages, Nuxt and custom bootstrap are `unsupported` (detection +
|
|
12
|
+
* manual instructions only, fixes empty). Detectors never write; they return a
|
|
13
|
+
* plan of PlannedFileChange built from parsed AST positions.
|
|
14
|
+
*/
|
|
15
|
+
const ENTRY_CANDIDATES = {
|
|
16
|
+
"vite-react": ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx"],
|
|
17
|
+
"vite-vue": ["src/main.ts", "src/main.js"],
|
|
18
|
+
vanilla: ["src/main.ts", "src/main.js", "main.ts", "main.js"],
|
|
19
|
+
"next-app": ["app/layout.tsx", "src/app/layout.tsx"],
|
|
20
|
+
"next-pages": ["pages/_app.tsx", "src/pages/_app.tsx"],
|
|
21
|
+
nuxt: ["plugins/moeicons.ts", "app.vue"],
|
|
22
|
+
};
|
|
23
|
+
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
24
|
+
function namedImportBinding(source, rel, specifier, imported) {
|
|
25
|
+
const parsed = parseSource(source, rel);
|
|
26
|
+
if (!parsed.ok)
|
|
27
|
+
return undefined;
|
|
28
|
+
for (const node of parsed.ast.program.body) {
|
|
29
|
+
if (node.type !== "ImportDeclaration" || node.source.value !== specifier)
|
|
30
|
+
continue;
|
|
31
|
+
for (const item of node.specifiers) {
|
|
32
|
+
if (item.type === "ImportSpecifier" &&
|
|
33
|
+
(item.imported.type === "Identifier" ? item.imported.name : item.imported.value) === imported)
|
|
34
|
+
return item.local.name;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function escapeRegExp(value) {
|
|
40
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
/** Find the single `createRoot(...).render(...)` statement text region. */
|
|
43
|
+
function readEntry(io, root, rel) {
|
|
44
|
+
const full = join(root, rel);
|
|
45
|
+
if (!io.existsSync(full))
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
return { source: io.readFileSync(full) };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function locateCandidate(io, root, candidates) {
|
|
55
|
+
const present = candidates.filter((c) => readEntry(io, root, c) !== undefined);
|
|
56
|
+
if (present.length === 0)
|
|
57
|
+
return { ambiguous: true };
|
|
58
|
+
if (present.length > 1)
|
|
59
|
+
return { ambiguous: true };
|
|
60
|
+
return { rel: present[0] ?? "", source: readEntry(io, root, present[0] ?? "")?.source ?? "" };
|
|
61
|
+
}
|
|
62
|
+
function unsupportedManual(adapter) {
|
|
63
|
+
return {
|
|
64
|
+
kind: "application",
|
|
65
|
+
status: "unsupported",
|
|
66
|
+
candidates: ENTRY_CANDIDATES[adapter] ?? [],
|
|
67
|
+
evidence: [
|
|
68
|
+
`${adapter} registration is detect-only in this CLI release; a manual patch is required`,
|
|
69
|
+
],
|
|
70
|
+
fixes: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Vite React plan: wrap the unique root in `MoeiconsProvider`. The plan is
|
|
75
|
+
* generated from a parsed AST but applied as a small deterministic text patch so
|
|
76
|
+
* it survives formatting differences.
|
|
77
|
+
*/
|
|
78
|
+
function planViteReact(source, rel, specifier) {
|
|
79
|
+
const binding = namedImportBinding(source, rel, specifier, "MoeiconsProvider");
|
|
80
|
+
const parsed = parseSource(source, rel);
|
|
81
|
+
if (!parsed.ok) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "application",
|
|
84
|
+
status: "invalid",
|
|
85
|
+
path: rel,
|
|
86
|
+
candidates: [rel],
|
|
87
|
+
evidence: [`cannot parse ${rel}: ${parsed.error}`],
|
|
88
|
+
fixes: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const root = findUniqueRender(parsed.ast);
|
|
92
|
+
if (root === "multiple") {
|
|
93
|
+
return {
|
|
94
|
+
kind: "application",
|
|
95
|
+
status: "ambiguous",
|
|
96
|
+
path: rel,
|
|
97
|
+
candidates: [rel],
|
|
98
|
+
evidence: ["multiple createRoot(...).render(...) calls found"],
|
|
99
|
+
fixes: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (root === undefined) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "application",
|
|
105
|
+
status: "unsupported",
|
|
106
|
+
path: rel,
|
|
107
|
+
candidates: [rel],
|
|
108
|
+
evidence: ["no unique createRoot(...).render(<App />) shape found"],
|
|
109
|
+
fixes: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { childStart, childEnd } = root;
|
|
113
|
+
const child = source.slice(childStart, childEnd);
|
|
114
|
+
if (binding && new RegExp(`^\\s*<${escapeRegExp(binding)}(?:\\s|>)`).test(child)) {
|
|
115
|
+
return { kind: "application", status: "ok", path: rel, candidates: [rel], evidence: ["render root is wrapped in the generated Provider"], fixes: [] };
|
|
116
|
+
}
|
|
117
|
+
const providerName = binding ?? "MoeiconsProvider";
|
|
118
|
+
const after = source.slice(0, childStart) + `<${providerName}>${child}</${providerName}>` + source.slice(childEnd);
|
|
119
|
+
const imported = binding ? after : insertImport(after, `import { MoeiconsProvider } from "${specifier}";`, rel);
|
|
120
|
+
const valid = parseSource(imported, rel);
|
|
121
|
+
if (!valid.ok)
|
|
122
|
+
return { kind: "application", status: "invalid", path: rel, candidates: [rel], evidence: [`generated React patch is invalid: ${valid.error}`], fixes: [] };
|
|
123
|
+
return {
|
|
124
|
+
kind: "application",
|
|
125
|
+
status: "missing",
|
|
126
|
+
path: rel,
|
|
127
|
+
candidates: [rel],
|
|
128
|
+
evidence: ["unique createRoot render found; Provider wrap plan generated"],
|
|
129
|
+
fixes: [{ kind: "replace", path: rel, before: source, after: imported }],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Find the unique JSX child range of a `.render(...)` call. */
|
|
133
|
+
function findUniqueRender(ast) {
|
|
134
|
+
const renders = [];
|
|
135
|
+
const visit = (node) => {
|
|
136
|
+
if (!isRecord(node) || typeof node !== "object")
|
|
137
|
+
return;
|
|
138
|
+
const type = typeof node.type === "string" ? node.type : "";
|
|
139
|
+
if (type === "CallExpression" &&
|
|
140
|
+
isRecord(node.callee) &&
|
|
141
|
+
node.callee.type === "MemberExpression" &&
|
|
142
|
+
isRecord(node.callee.object) &&
|
|
143
|
+
node.callee.object.type === "CallExpression" &&
|
|
144
|
+
isRecord(node.callee.object.callee) &&
|
|
145
|
+
node.callee.object.callee.type === "Identifier" &&
|
|
146
|
+
node.callee.object.callee.name === "createRoot" &&
|
|
147
|
+
isRecord(node.callee.property) &&
|
|
148
|
+
node.callee.property.type === "Identifier" &&
|
|
149
|
+
node.callee.property.name === "render" &&
|
|
150
|
+
Array.isArray(node.arguments) &&
|
|
151
|
+
node.arguments.length === 1) {
|
|
152
|
+
const child = node.arguments[0];
|
|
153
|
+
if (child && typeof child.start === "number" && typeof child.end === "number") {
|
|
154
|
+
renders.push({ start: child.start, end: child.end });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const value of Object.values(node)) {
|
|
158
|
+
if (Array.isArray(value))
|
|
159
|
+
for (const item of value)
|
|
160
|
+
visit(item);
|
|
161
|
+
else if (value && typeof value === "object")
|
|
162
|
+
visit(value);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
visit(ast);
|
|
166
|
+
if (renders.length === 0)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (renders.length > 1)
|
|
169
|
+
return "multiple";
|
|
170
|
+
const only = renders[0];
|
|
171
|
+
return only ? { childStart: only.start, childEnd: only.end } : undefined;
|
|
172
|
+
}
|
|
173
|
+
function insertImport(source, line, rel = "src/main.tsx") {
|
|
174
|
+
if (source.includes(line))
|
|
175
|
+
return source;
|
|
176
|
+
const parsed = parseSource(source, rel);
|
|
177
|
+
if (!parsed.ok)
|
|
178
|
+
return source;
|
|
179
|
+
const declarations = parsed.ast.program.body.filter((node) => node.type === "ImportDeclaration");
|
|
180
|
+
const anchor = declarations.length ? declarations[declarations.length - 1]?.end ?? 0 : 0;
|
|
181
|
+
return `${source.slice(0, anchor)}${anchor ? "\n" : ""}${line}\n${source.slice(anchor)}`;
|
|
182
|
+
}
|
|
183
|
+
/** Vite Vue plan: create a Provider host and use it as the app root (no app.use). */
|
|
184
|
+
function planViteVue(source, rel, specifier) {
|
|
185
|
+
const binding = namedImportBinding(source, rel, specifier, "MoeiconsProvider");
|
|
186
|
+
const hBinding = namedImportBinding(source, rel, "vue", "h");
|
|
187
|
+
const hName = hBinding ?? "h";
|
|
188
|
+
const parsed = parseSource(source, rel);
|
|
189
|
+
if (!parsed.ok) {
|
|
190
|
+
return {
|
|
191
|
+
kind: "application",
|
|
192
|
+
status: "invalid",
|
|
193
|
+
path: rel,
|
|
194
|
+
candidates: [rel],
|
|
195
|
+
evidence: [`cannot parse ${rel}: ${parsed.error}`],
|
|
196
|
+
fixes: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const mount = findCreateAppMount(parsed.ast);
|
|
200
|
+
if (mount === "multiple") {
|
|
201
|
+
return {
|
|
202
|
+
kind: "application",
|
|
203
|
+
status: "ambiguous",
|
|
204
|
+
path: rel,
|
|
205
|
+
candidates: [rel],
|
|
206
|
+
evidence: ["multiple createApp(...).mount(...) calls found"],
|
|
207
|
+
fixes: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (mount === undefined) {
|
|
211
|
+
return {
|
|
212
|
+
kind: "application",
|
|
213
|
+
status: "unsupported",
|
|
214
|
+
path: rel,
|
|
215
|
+
candidates: [rel],
|
|
216
|
+
evidence: ["no unique createApp(...).mount(...) shape found"],
|
|
217
|
+
fixes: [],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const { rootStart, rootEnd } = mount;
|
|
221
|
+
const rootText = source.slice(rootStart, rootEnd);
|
|
222
|
+
if (binding && new RegExp(`${escapeRegExp(hName)}\\(${escapeRegExp(binding)}\\b`).test(rootText)) {
|
|
223
|
+
return { kind: "application", status: "ok", path: rel, candidates: [rel], evidence: ["createApp root renders the generated Provider"], fixes: [] };
|
|
224
|
+
}
|
|
225
|
+
// Host component renders the original root inside MoeiconsProvider.
|
|
226
|
+
const providerName = binding ?? "MoeiconsProvider";
|
|
227
|
+
const after = source.slice(0, rootStart) +
|
|
228
|
+
`{ render: () => ${hName}(${providerName}, null, { default: () => ${hName}(${rootText}) }) }` +
|
|
229
|
+
source.slice(rootEnd);
|
|
230
|
+
const withProvider = binding ? after : insertImport(after, `import { MoeiconsProvider } from "${specifier}";`, rel);
|
|
231
|
+
const withH = hBinding ? withProvider : insertImport(withProvider, `import { h } from "vue";`, rel);
|
|
232
|
+
const valid = parseSource(withH, rel);
|
|
233
|
+
if (!valid.ok)
|
|
234
|
+
return { kind: "application", status: "invalid", path: rel, candidates: [rel], evidence: [`generated Vue patch is invalid: ${valid.error}`], fixes: [] };
|
|
235
|
+
return {
|
|
236
|
+
kind: "application",
|
|
237
|
+
status: "missing",
|
|
238
|
+
path: rel,
|
|
239
|
+
candidates: [rel],
|
|
240
|
+
evidence: ["unique createApp found; Provider-host plan generated (no app.use plugin)"],
|
|
241
|
+
fixes: [
|
|
242
|
+
{
|
|
243
|
+
kind: "replace",
|
|
244
|
+
path: rel,
|
|
245
|
+
before: source,
|
|
246
|
+
after: withH,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function findCreateAppMount(ast) {
|
|
252
|
+
const found = [];
|
|
253
|
+
const visit = (node) => {
|
|
254
|
+
if (!isRecord(node))
|
|
255
|
+
return;
|
|
256
|
+
const type = typeof node.type === "string" ? node.type : "";
|
|
257
|
+
if (type === "CallExpression" &&
|
|
258
|
+
isRecord(node.callee) &&
|
|
259
|
+
node.callee.type === "MemberExpression" &&
|
|
260
|
+
isRecord(node.callee.property) &&
|
|
261
|
+
node.callee.property.name === "mount" &&
|
|
262
|
+
isRecord(node.callee.object) &&
|
|
263
|
+
node.callee.object.type === "CallExpression" &&
|
|
264
|
+
isRecord(node.callee.object.callee) &&
|
|
265
|
+
node.callee.object.callee.type === "Identifier" &&
|
|
266
|
+
node.callee.object.callee.name === "createApp" &&
|
|
267
|
+
Array.isArray(node.callee.object.arguments) &&
|
|
268
|
+
node.callee.object.arguments.length >= 1) {
|
|
269
|
+
const root = node.callee.object.arguments[0];
|
|
270
|
+
if (root && typeof root.start === "number" && typeof root.end === "number") {
|
|
271
|
+
found.push({ start: root.start, end: root.end });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const value of Object.values(node)) {
|
|
275
|
+
if (Array.isArray(value))
|
|
276
|
+
for (const item of value)
|
|
277
|
+
visit(item);
|
|
278
|
+
else if (value && typeof value === "object")
|
|
279
|
+
visit(value);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
visit(ast);
|
|
283
|
+
if (found.length === 0)
|
|
284
|
+
return undefined;
|
|
285
|
+
if (found.length > 1)
|
|
286
|
+
return "multiple";
|
|
287
|
+
const only = found[0];
|
|
288
|
+
return only ? { rootStart: only.start, rootEnd: only.end } : undefined;
|
|
289
|
+
}
|
|
290
|
+
function planVanilla(source, rel, specifier) {
|
|
291
|
+
const parsed = parseSource(source, rel);
|
|
292
|
+
if (!parsed.ok) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "application",
|
|
295
|
+
status: "invalid",
|
|
296
|
+
path: rel,
|
|
297
|
+
candidates: [rel],
|
|
298
|
+
evidence: [`cannot parse ${rel}: ${parsed.error}`],
|
|
299
|
+
fixes: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (/createMoeiconsRuntime|mountIcon/.test(source)) {
|
|
303
|
+
return {
|
|
304
|
+
kind: "application",
|
|
305
|
+
status: "ok",
|
|
306
|
+
path: rel,
|
|
307
|
+
candidates: [rel],
|
|
308
|
+
evidence: ["Vanilla runtime init is present"],
|
|
309
|
+
fixes: [],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
kind: "application",
|
|
314
|
+
status: "missing",
|
|
315
|
+
path: rel,
|
|
316
|
+
candidates: [rel],
|
|
317
|
+
evidence: ["Vanilla module entry found; runtime init plan generated"],
|
|
318
|
+
fixes: [
|
|
319
|
+
{
|
|
320
|
+
kind: "replace",
|
|
321
|
+
path: rel,
|
|
322
|
+
before: source,
|
|
323
|
+
after: `${source}\nimport { createMoeiconsRuntime } from "${specifier}";\ncreateMoeiconsRuntime();\n`,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
export function inspectApplicationAnchor(options) {
|
|
329
|
+
const { io, root, adapter, assetsOnly } = options;
|
|
330
|
+
if (assetsOnly) {
|
|
331
|
+
return {
|
|
332
|
+
kind: "application",
|
|
333
|
+
status: "not-required",
|
|
334
|
+
candidates: [],
|
|
335
|
+
evidence: ["assets-only target requires no application registration"],
|
|
336
|
+
fixes: [],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (!adapter) {
|
|
340
|
+
return {
|
|
341
|
+
kind: "application",
|
|
342
|
+
status: "unsupported",
|
|
343
|
+
candidates: [],
|
|
344
|
+
evidence: ["adapter is unknown; no application entry can be located"],
|
|
345
|
+
fixes: [],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (adapter === "next-app" || adapter === "next-pages" || adapter === "nuxt") {
|
|
349
|
+
return unsupportedManual(adapter);
|
|
350
|
+
}
|
|
351
|
+
const candidates = options.confirmedEntry ? [options.confirmedEntry] : ENTRY_CANDIDATES[adapter] ?? [];
|
|
352
|
+
const located = locateCandidate(io, root, candidates);
|
|
353
|
+
if ("ambiguous" in located) {
|
|
354
|
+
const present = candidates.filter((c) => readEntry(io, root, c) !== undefined);
|
|
355
|
+
const path = present.length === 1 ? join(root, present[0] ?? "") : undefined;
|
|
356
|
+
return {
|
|
357
|
+
kind: "application",
|
|
358
|
+
status: present.length === 0 ? "missing" : "ambiguous",
|
|
359
|
+
...(path !== undefined ? { path } : {}),
|
|
360
|
+
candidates: present.length ? present : candidates,
|
|
361
|
+
evidence: present.length
|
|
362
|
+
? [`multiple entry candidates: ${present.join(", ")}`]
|
|
363
|
+
: [`no entry candidate found among: ${candidates.join(", ")}`],
|
|
364
|
+
fixes: [],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const outputDir = (options.outputDir ?? "src/moeicons").replace(/\\/g, "/").replace(/\/$/, "");
|
|
368
|
+
const importPath = posix.relative(dirname(located.rel.replace(/\\/g, "/")), outputDir);
|
|
369
|
+
const specifier = importPath.startsWith(".") ? importPath : `./${importPath}`;
|
|
370
|
+
if (adapter === "vite-react")
|
|
371
|
+
return planViteReact(located.source, located.rel, specifier);
|
|
372
|
+
if (adapter === "vite-vue")
|
|
373
|
+
return planViteVue(located.source, located.rel, specifier);
|
|
374
|
+
if (adapter === "vanilla")
|
|
375
|
+
return planVanilla(located.source, located.rel, specifier);
|
|
376
|
+
return unsupportedManual(adapter);
|
|
377
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PlannedFileChange } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Transactional application of PlannedFileChange set (E2E-B5). Writes all
|
|
4
|
+
* changes atomically and restores originals on any failure. A second identical
|
|
5
|
+
* apply is a no-op and reports `alreadyConfigured`.
|
|
6
|
+
*/
|
|
7
|
+
export interface ApplyFs {
|
|
8
|
+
readonly existsSync: (path: string) => boolean;
|
|
9
|
+
readonly readTextFileSync: (path: string) => string;
|
|
10
|
+
readonly mkdirSync: (path: string, options?: {
|
|
11
|
+
recursive?: boolean;
|
|
12
|
+
}) => void;
|
|
13
|
+
readonly writeTextFileSync: (path: string, content: string) => void;
|
|
14
|
+
readonly renameSync: (from: string, to: string) => void;
|
|
15
|
+
readonly rmSync: (path: string, options?: {
|
|
16
|
+
recursive?: boolean;
|
|
17
|
+
force?: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
}
|
|
20
|
+
export type ApplyOutcome = {
|
|
21
|
+
readonly ok: true;
|
|
22
|
+
readonly alreadyConfigured: boolean;
|
|
23
|
+
readonly written: readonly string[];
|
|
24
|
+
} | {
|
|
25
|
+
readonly ok: false;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function applyPlannedChanges(projectRoot: string, changes: readonly PlannedFileChange[], fs_: ApplyFs): ApplyOutcome;
|