@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,67 @@
|
|
|
1
|
+
import { type TransactionalFs } from "../project/install.js";
|
|
2
|
+
import type { CommandContext } from "./context.js";
|
|
3
|
+
import type { PackageManager } from "../project/detect.js";
|
|
4
|
+
import { type FreeDownloadIo } from "./free-download.js";
|
|
5
|
+
import type { Target } from "../commands/parser.js";
|
|
6
|
+
export type InstallResult = {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly projectRoot: string;
|
|
9
|
+
readonly packageManager: PackageManager;
|
|
10
|
+
readonly group: "free";
|
|
11
|
+
readonly target: Target;
|
|
12
|
+
readonly artifactBytes: number;
|
|
13
|
+
readonly planItems: number;
|
|
14
|
+
readonly config: string;
|
|
15
|
+
readonly artifactVersion: string;
|
|
16
|
+
readonly descriptorSha256: string;
|
|
17
|
+
readonly catalogSha256: string;
|
|
18
|
+
readonly cacheHit: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly reason: "no-project";
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly reason: "cancelled";
|
|
25
|
+
readonly message: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly reason: "checksum-mismatch";
|
|
29
|
+
readonly message: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
readonly reason: "network";
|
|
33
|
+
readonly message: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly ok: false;
|
|
36
|
+
readonly reason: "not-found";
|
|
37
|
+
readonly message: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly ok: false;
|
|
40
|
+
readonly reason: "offline-no-cache";
|
|
41
|
+
readonly message: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly ok: false;
|
|
44
|
+
readonly reason: "validation";
|
|
45
|
+
readonly message: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly ok: false;
|
|
48
|
+
readonly reason: "write-failed";
|
|
49
|
+
readonly message: string;
|
|
50
|
+
};
|
|
51
|
+
export interface InstallUseCaseDeps {
|
|
52
|
+
readonly fs: TransactionalFs;
|
|
53
|
+
readonly download: Omit<FreeDownloadIo, "signal">;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Free install: resolve the catalog sourceVersion tag, download/verify the
|
|
57
|
+
* GitHub Release (or a local release fixture), verify the selected target's
|
|
58
|
+
* descriptor subtree, then transactionally write managed metadata plus the
|
|
59
|
+
* selected target subtree. Pro installs route through the authenticated
|
|
60
|
+
* `runProInstallUseCase`; this use case rejects them explicitly.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runInstallUseCase(context: CommandContext, deps: InstallUseCaseDeps, options: {
|
|
63
|
+
readonly group?: string;
|
|
64
|
+
readonly target?: Target;
|
|
65
|
+
readonly sourceVersion?: string;
|
|
66
|
+
readonly expectedDescriptorSha256?: string;
|
|
67
|
+
}): Promise<InstallResult>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createInstallPlan, executeInstallPlan } from "../project/install.js";
|
|
2
|
+
import { detectProject } from "../project/detect.js";
|
|
3
|
+
import { readMoeiconsConfig } from "../project/config.js";
|
|
4
|
+
import { bundledSourceVersion, downloadFreeRelease } from "./free-download.js";
|
|
5
|
+
import { serializeInstallMetadata, sha256Bytes } from "../project/install-metadata.js";
|
|
6
|
+
import { withProjectLock } from "../project/project-lock.js";
|
|
7
|
+
import { selectTargetSubtree } from "./target-subtree.js";
|
|
8
|
+
function normalizeGroup(group) {
|
|
9
|
+
if (group === undefined || group === "free")
|
|
10
|
+
return "free";
|
|
11
|
+
if (group === "ent")
|
|
12
|
+
return "pro";
|
|
13
|
+
return group === "pro" ? "pro" : "free";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Free install: resolve the catalog sourceVersion tag, download/verify the
|
|
17
|
+
* GitHub Release (or a local release fixture), verify the selected target's
|
|
18
|
+
* descriptor subtree, then transactionally write managed metadata plus the
|
|
19
|
+
* selected target subtree. Pro installs route through the authenticated
|
|
20
|
+
* `runProInstallUseCase`; this use case rejects them explicitly.
|
|
21
|
+
*/
|
|
22
|
+
export async function runInstallUseCase(context, deps, options) {
|
|
23
|
+
const project = detectProject(context.cwd);
|
|
24
|
+
if (!project)
|
|
25
|
+
return { ok: false, reason: "no-project" };
|
|
26
|
+
const groupArg = options.group;
|
|
27
|
+
if (groupArg !== undefined && groupArg !== "free" && groupArg !== "pro" && groupArg !== "ent") {
|
|
28
|
+
return { ok: false, reason: "validation", message: `unknown install group: ${groupArg}` };
|
|
29
|
+
}
|
|
30
|
+
const group = normalizeGroup(groupArg);
|
|
31
|
+
if (group === "pro") {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: "validation",
|
|
35
|
+
message: "pro install requires the authenticated pro flow",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const config = readMoeiconsConfig(project.root);
|
|
39
|
+
if (config.kind === "invalid" || config.kind === "unsupported") {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: "validation",
|
|
43
|
+
message: config.kind === "invalid" ? config.message : `unsupported config schema version ${config.version}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const target = options.target ?? (config.kind === "ok" ? config.config.target : "react");
|
|
47
|
+
const downloaded = await downloadFreeRelease({ ...deps.download, signal: context.signal }, options.sourceVersion ?? bundledSourceVersion());
|
|
48
|
+
if (!downloaded.ok) {
|
|
49
|
+
return downloaded.reason === "cancelled"
|
|
50
|
+
? { ok: false, reason: "cancelled", message: downloaded.message }
|
|
51
|
+
: downloaded;
|
|
52
|
+
}
|
|
53
|
+
if ((options.sourceVersion && downloaded.descriptor.fullVersion !== options.sourceVersion) ||
|
|
54
|
+
(options.expectedDescriptorSha256 &&
|
|
55
|
+
downloaded.descriptorSha256 !== options.expectedDescriptorSha256)) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: "validation",
|
|
59
|
+
message: "downloaded release identity changed after version check; retry the update",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const subtree = selectTargetSubtree(downloaded.artifactBytes, downloaded.descriptor.free, target);
|
|
63
|
+
if (!subtree.ok) {
|
|
64
|
+
return subtree.reason === "checksum-mismatch"
|
|
65
|
+
? { ok: false, reason: "checksum-mismatch", message: subtree.message }
|
|
66
|
+
: { ok: false, reason: "validation", message: subtree.message };
|
|
67
|
+
}
|
|
68
|
+
const catalogJson = downloaded.catalogJson;
|
|
69
|
+
const files = {
|
|
70
|
+
".moeicons/catalog.json": catalogJson,
|
|
71
|
+
"src/moeicons/types.ts": `export type { ReactIconProps } from "moe-icons";\n`,
|
|
72
|
+
"src/moeicons/.moeicons-free.marker": "free\n",
|
|
73
|
+
};
|
|
74
|
+
for (const [rel, bytes] of Object.entries(subtree.files)) {
|
|
75
|
+
files[`.moeicons/artifact/${target}/${rel}`] = bytes;
|
|
76
|
+
}
|
|
77
|
+
const managedFiles = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, sha256Bytes(content)]));
|
|
78
|
+
files[".moeicons/install-metadata.json"] = serializeInstallMetadata({
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
artifactVersion: downloaded.descriptor.fullVersion,
|
|
81
|
+
tier: "free",
|
|
82
|
+
target,
|
|
83
|
+
descriptorSha256: downloaded.descriptorSha256,
|
|
84
|
+
catalogSha256: downloaded.descriptor.catalog.sha256,
|
|
85
|
+
artifactSha256: downloaded.descriptor.free.sha256,
|
|
86
|
+
installedAt: context.now().toISOString(),
|
|
87
|
+
managedFiles,
|
|
88
|
+
targetSha256: subtree.sha256,
|
|
89
|
+
targetFileCount: subtree.fileCount,
|
|
90
|
+
targetByteCount: subtree.byteCount,
|
|
91
|
+
});
|
|
92
|
+
const plan = createInstallPlan(project.root, files);
|
|
93
|
+
try {
|
|
94
|
+
await withProjectLock(project.root, "install", () => executeInstallPlan(plan, deps.fs));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: "write-failed",
|
|
100
|
+
message: error instanceof Error ? error.message : String(error),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
projectRoot: project.root,
|
|
106
|
+
packageManager: project.packageManager,
|
|
107
|
+
group: "free",
|
|
108
|
+
target,
|
|
109
|
+
artifactBytes: downloaded.artifactBytes.byteLength,
|
|
110
|
+
planItems: plan.items.length,
|
|
111
|
+
config: config.kind,
|
|
112
|
+
artifactVersion: downloaded.descriptor.fullVersion,
|
|
113
|
+
descriptorSha256: downloaded.descriptorSha256,
|
|
114
|
+
catalogSha256: downloaded.descriptor.catalog.sha256,
|
|
115
|
+
cacheHit: downloaded.cacheHit,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type TransactionalFsWithCopy } from "../project/install.js";
|
|
2
|
+
import type { AuthUseCaseDependencies } from "./auth.js";
|
|
3
|
+
import type { CommandContext } from "./context.js";
|
|
4
|
+
import { type FreeDownloadIo } from "./free-download.js";
|
|
5
|
+
export interface LibraryUpdateDeps {
|
|
6
|
+
readonly fs: TransactionalFsWithCopy;
|
|
7
|
+
readonly free: Omit<FreeDownloadIo, "signal">;
|
|
8
|
+
readonly auth: AuthUseCaseDependencies;
|
|
9
|
+
readonly fetch?: typeof fetch;
|
|
10
|
+
readonly allowedProHosts?: readonly string[];
|
|
11
|
+
readonly onProgress?: (event: {
|
|
12
|
+
readonly downloadedBytes: number;
|
|
13
|
+
readonly totalBytes?: number;
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function runLibraryUpdateUseCase(context: CommandContext, deps: LibraryUpdateDeps, expected: {
|
|
17
|
+
readonly tier: "free" | "pro";
|
|
18
|
+
readonly version: string;
|
|
19
|
+
readonly descriptorSha256: string;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
readonly projectRoot: string;
|
|
22
|
+
readonly artifactVersion: string;
|
|
23
|
+
readonly files: readonly string[];
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { join, relative, resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { parseCatalog } from "../catalog/catalog.js";
|
|
4
|
+
import { CliError } from "../errors/index.js";
|
|
5
|
+
import { planGeneratedFiles } from "../generator/generate.js";
|
|
6
|
+
import { readMoeiconsConfig } from "../project/config.js";
|
|
7
|
+
import { detectProject } from "../project/detect.js";
|
|
8
|
+
import { executeManagedReconcile } from "../project/install.js";
|
|
9
|
+
import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
|
|
10
|
+
import { withProjectLock } from "../project/project-lock.js";
|
|
11
|
+
import { ensureClassMergeDependencies, planTailwindIntegration } from "../project/tailwind.js";
|
|
12
|
+
import { extractTarGz } from "../project/tar-gz.js";
|
|
13
|
+
import { artifactCachePath, downloadFreeRelease } from "./free-download.js";
|
|
14
|
+
import { downloadProArtifact } from "./pro-download.js";
|
|
15
|
+
import { selectTargetSubtree } from "./target-subtree.js";
|
|
16
|
+
export async function runLibraryUpdateUseCase(context, deps, expected) {
|
|
17
|
+
const project = detectProject(context.cwd);
|
|
18
|
+
if (!project)
|
|
19
|
+
throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents");
|
|
20
|
+
let catalogJson;
|
|
21
|
+
let archiveBytes;
|
|
22
|
+
let artifactSha256;
|
|
23
|
+
let catalogSha256;
|
|
24
|
+
let tierSource;
|
|
25
|
+
if (expected.tier === "free") {
|
|
26
|
+
const downloaded = await downloadFreeRelease({
|
|
27
|
+
...deps.free,
|
|
28
|
+
signal: context.signal,
|
|
29
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
30
|
+
}, expected.version);
|
|
31
|
+
if (!downloaded.ok)
|
|
32
|
+
throw new CliError(downloaded.reason === "cancelled" ? "CANCELLED" : "VALIDATION_ERROR", downloaded.message);
|
|
33
|
+
if (downloaded.descriptorSha256 !== expected.descriptorSha256 ||
|
|
34
|
+
downloaded.descriptor.fullVersion !== expected.version) {
|
|
35
|
+
throw new CliError("VALIDATION_ERROR", "downloaded release identity changed after version check; retry the update");
|
|
36
|
+
}
|
|
37
|
+
catalogJson = downloaded.catalogJson;
|
|
38
|
+
archiveBytes = downloaded.artifactBytes;
|
|
39
|
+
artifactSha256 = downloaded.descriptor.free.sha256;
|
|
40
|
+
catalogSha256 = downloaded.descriptor.catalog.sha256;
|
|
41
|
+
tierSource = downloaded.descriptor.free;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const downloaded = await downloadProArtifact(context, deps.auth, expected, {
|
|
45
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
46
|
+
...(deps.allowedProHosts ? { allowedHosts: deps.allowedProHosts } : {}),
|
|
47
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
48
|
+
});
|
|
49
|
+
catalogJson = downloaded.catalogJson;
|
|
50
|
+
archiveBytes = downloaded.artifactBytes;
|
|
51
|
+
artifactSha256 = downloaded.descriptor.sha256;
|
|
52
|
+
catalogSha256 = downloaded.descriptor.catalogSha256;
|
|
53
|
+
tierSource = downloaded.descriptor;
|
|
54
|
+
const cacheDir = context.env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
55
|
+
const cached = artifactCachePath(cacheDir, downloaded.descriptor.version, downloaded.descriptor.sha256);
|
|
56
|
+
if (!deps.fs.existsSync(cached)) {
|
|
57
|
+
deps.fs.mkdirSync(join(cached, ".."), { recursive: true });
|
|
58
|
+
deps.fs.writeFileSync(cached, downloaded.artifactBytes);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let candidateCatalog;
|
|
62
|
+
try {
|
|
63
|
+
candidateCatalog = parseCatalog(JSON.parse(catalogJson));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new CliError("VALIDATION_ERROR", error instanceof Error ? error.message : String(error));
|
|
67
|
+
}
|
|
68
|
+
if (candidateCatalog.sourceVersion !== expected.version ||
|
|
69
|
+
sha256Bytes(catalogJson) !== catalogSha256) {
|
|
70
|
+
throw new CliError("VALIDATION_ERROR", "candidate catalog identity does not match the selected release");
|
|
71
|
+
}
|
|
72
|
+
const loaded = readMoeiconsConfig(project.root, candidateCatalog);
|
|
73
|
+
if (loaded.kind !== "ok" || loaded.config.tier !== expected.tier)
|
|
74
|
+
throw new CliError("VALIDATION_ERROR", `config is invalid for the ${expected.tier} candidate`);
|
|
75
|
+
const unpacked = extractTarGz(archiveBytes, {
|
|
76
|
+
maxEntries: 20_000,
|
|
77
|
+
maxExpandedBytes: 64 * 1024 * 1024,
|
|
78
|
+
});
|
|
79
|
+
if (unpacked.errors.length)
|
|
80
|
+
throw new CliError("VALIDATION_ERROR", unpacked.errors[0] ?? "invalid artifact");
|
|
81
|
+
const target = loaded.config.target;
|
|
82
|
+
const subtree = selectTargetSubtree(archiveBytes, tierSource, target);
|
|
83
|
+
if (!subtree.ok)
|
|
84
|
+
throw new CliError("VALIDATION_ERROR", subtree.message);
|
|
85
|
+
const generated = planGeneratedFiles(loaded.config, loaded.config.outputDir, {
|
|
86
|
+
archiveFiles: unpacked.files,
|
|
87
|
+
catalog: candidateCatalog,
|
|
88
|
+
});
|
|
89
|
+
if (!generated.ok)
|
|
90
|
+
throw new CliError("VALIDATION_ERROR", generated.errors.join("; "));
|
|
91
|
+
const writes = {
|
|
92
|
+
".moeicons/catalog.json": catalogJson,
|
|
93
|
+
[`${loaded.config.outputDir.replace(/\\/g, "/").replace(/\/$/, "")}/.moeicons-${expected.tier}.marker`]: `${expected.tier}\n`,
|
|
94
|
+
};
|
|
95
|
+
for (const file of generated.files)
|
|
96
|
+
writes[file.path.replace(/\\/g, "/")] = file.content;
|
|
97
|
+
for (const [rel, bytes] of Object.entries(subtree.files)) {
|
|
98
|
+
writes[`.moeicons/artifact/${target}/${rel}`] = bytes;
|
|
99
|
+
}
|
|
100
|
+
const tailwindPlan = planTailwindIntegration(project.root, loaded.config.outputDir, {
|
|
101
|
+
noTailwind: false,
|
|
102
|
+
target,
|
|
103
|
+
});
|
|
104
|
+
for (const side of tailwindPlan.files) {
|
|
105
|
+
const rel = relative(project.root, resolve(side.path)).replace(/\\/g, "/");
|
|
106
|
+
if (!rel || rel === ".." || rel.startsWith("../"))
|
|
107
|
+
throw new CliError("VALIDATION_ERROR", `side file escapes project: ${side.path}`);
|
|
108
|
+
writes[rel] = side.content;
|
|
109
|
+
}
|
|
110
|
+
const pkgPath = join(project.root, "package.json");
|
|
111
|
+
if (deps.fs.existsSync(pkgPath)) {
|
|
112
|
+
const dependencyPlan = target === "react" || target === "vue"
|
|
113
|
+
? ensureClassMergeDependencies(deps.fs.readFileSync(pkgPath, "utf8"))
|
|
114
|
+
: { nextSource: deps.fs.readFileSync(pkgPath, "utf8"), changed: false, notes: [] };
|
|
115
|
+
if (dependencyPlan.changed)
|
|
116
|
+
writes["package.json"] = dependencyPlan.nextSource;
|
|
117
|
+
}
|
|
118
|
+
const sidePaths = new Set([
|
|
119
|
+
"package.json",
|
|
120
|
+
...tailwindPlan.files.map((file) => relative(project.root, resolve(file.path)).replace(/\\/g, "/")),
|
|
121
|
+
]);
|
|
122
|
+
const managedFiles = Object.fromEntries(Object.entries(writes)
|
|
123
|
+
.filter(([path]) => !sidePaths.has(path))
|
|
124
|
+
.map(([path, content]) => [path, sha256Bytes(content)]));
|
|
125
|
+
const nextMetadata = {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
artifactVersion: expected.version,
|
|
128
|
+
tier: expected.tier,
|
|
129
|
+
target,
|
|
130
|
+
descriptorSha256: expected.descriptorSha256,
|
|
131
|
+
artifactSha256,
|
|
132
|
+
catalogSha256,
|
|
133
|
+
installedAt: context.now().toISOString(),
|
|
134
|
+
managedFiles,
|
|
135
|
+
targetSha256: subtree.sha256,
|
|
136
|
+
targetFileCount: subtree.fileCount,
|
|
137
|
+
targetByteCount: subtree.byteCount,
|
|
138
|
+
};
|
|
139
|
+
writes[".moeicons/install-metadata.json"] = serializeInstallMetadata(nextMetadata);
|
|
140
|
+
await withProjectLock(project.root, "update", () => {
|
|
141
|
+
const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
142
|
+
if (!deps.fs.existsSync(metadataPath))
|
|
143
|
+
throw new CliError("VALIDATION_ERROR", "managed install metadata is missing");
|
|
144
|
+
const old = parseInstallMetadata(deps.fs.readFileSync(metadataPath, "utf8"));
|
|
145
|
+
if (!old || old.tier !== expected.tier)
|
|
146
|
+
throw new CliError("VALIDATION_ERROR", "managed install metadata is invalid");
|
|
147
|
+
for (const [path, hash] of Object.entries(old.managedFiles)) {
|
|
148
|
+
const absolute = join(project.root, path);
|
|
149
|
+
if (!deps.fs.existsSync(absolute) ||
|
|
150
|
+
sha256Bytes(deps.fs.readFileSync(absolute)) !== hash) {
|
|
151
|
+
throw new CliError("VALIDATION_ERROR", `managed file was modified or removed: ${path}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
executeManagedReconcile(project.root, writes, Object.keys(old.managedFiles).filter((path) => !(path in managedFiles)), deps.fs);
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
projectRoot: project.root,
|
|
158
|
+
artifactVersion: expected.version,
|
|
159
|
+
files: generated.files.map((file) => file.path),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readInstalledResourceState, type InstallMetadata } from "../project/install-metadata.js";
|
|
2
|
+
import { type PublicLibraryVersions } from "./version-service.js";
|
|
3
|
+
export type LibraryVersionStatus = {
|
|
4
|
+
readonly kind: "installation-invalid";
|
|
5
|
+
readonly message: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "check-failed";
|
|
8
|
+
readonly metadata: InstallMetadata;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "unavailable";
|
|
12
|
+
readonly metadata: InstallMetadata;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "current" | "update" | "channel-mismatch" | "remote-older";
|
|
16
|
+
readonly metadata: InstallMetadata;
|
|
17
|
+
readonly latestVersion: string;
|
|
18
|
+
readonly latestDescriptorSha256: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function getLibraryVersionStatus(projectRoot: string, configTier: "free" | "pro", deps?: {
|
|
21
|
+
readonly readState?: typeof readInstalledResourceState;
|
|
22
|
+
readonly fetchVersions?: () => Promise<PublicLibraryVersions>;
|
|
23
|
+
}): Promise<LibraryVersionStatus>;
|
|
24
|
+
export declare function formatLibraryVersionStatus(status: LibraryVersionStatus): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readInstalledResourceState } from "../project/install-metadata.js";
|
|
2
|
+
import { decideUpdate } from "./update-policy.js";
|
|
3
|
+
import { fetchLibraryVersions } from "./version-service.js";
|
|
4
|
+
export async function getLibraryVersionStatus(projectRoot, configTier, deps = {}) {
|
|
5
|
+
const installed = (deps.readState ?? readInstalledResourceState)(projectRoot, configTier);
|
|
6
|
+
if (installed.kind !== "ok")
|
|
7
|
+
return { kind: "installation-invalid", message: installed.message };
|
|
8
|
+
let versions;
|
|
9
|
+
try {
|
|
10
|
+
versions = await (deps.fetchVersions ?? (() => fetchLibraryVersions()))();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return { kind: "check-failed", metadata: installed.metadata, message: error instanceof Error ? error.message : "version check failed" };
|
|
14
|
+
}
|
|
15
|
+
const remote = versions[installed.metadata.tier];
|
|
16
|
+
if (!remote)
|
|
17
|
+
return { kind: "unavailable", metadata: installed.metadata, message: `no ${installed.metadata.tier} release is published` };
|
|
18
|
+
const decision = decideUpdate({
|
|
19
|
+
installedTier: installed.metadata.tier, remoteTier: installed.metadata.tier,
|
|
20
|
+
installedVersion: installed.metadata.artifactVersion, remoteVersion: remote.version,
|
|
21
|
+
});
|
|
22
|
+
const identity = { metadata: installed.metadata, latestVersion: remote.version, latestDescriptorSha256: remote.descriptorSha256 };
|
|
23
|
+
if (decision.kind === "current")
|
|
24
|
+
return { kind: "current", ...identity };
|
|
25
|
+
if (decision.kind === "update")
|
|
26
|
+
return { kind: "update", ...identity };
|
|
27
|
+
if (decision.reason === "channel-mismatch")
|
|
28
|
+
return { kind: "channel-mismatch", ...identity };
|
|
29
|
+
return { kind: "remote-older", ...identity };
|
|
30
|
+
}
|
|
31
|
+
export function formatLibraryVersionStatus(status) {
|
|
32
|
+
if (status.kind === "installation-invalid")
|
|
33
|
+
return `Current: invalid / Latest: unavailable / Status: installation invalid (${status.message})`;
|
|
34
|
+
if (status.kind === "check-failed" || status.kind === "unavailable")
|
|
35
|
+
return `Current: ${status.metadata.artifactVersion} / Latest: unavailable / Status: ${status.message}`;
|
|
36
|
+
const label = status.kind === "current" ? "up to date" : status.kind === "update" ? "update available" : status.kind === "channel-mismatch" ? "prerelease channel differs" : "remote version is older";
|
|
37
|
+
return `Current: ${status.metadata.artifactVersion} / Latest: ${status.latestVersion} / Status: ${label}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SignedArtifactDescriptor } from "./signed-artifact.js";
|
|
2
|
+
import { type AuthUseCaseDependencies } from "./auth.js";
|
|
3
|
+
import type { CommandContext } from "./context.js";
|
|
4
|
+
import type { ReleaseTarget, ReleaseTargetMetadata } from "./release-descriptor.js";
|
|
5
|
+
export declare const PRO_DOWNLOAD_HOSTS: readonly ["06898acc14d0b9633f259fe20145fd49.r2.cloudflarestorage.com"];
|
|
6
|
+
export interface ProArtifactDescriptor extends SignedArtifactDescriptor {
|
|
7
|
+
readonly tier: "pro";
|
|
8
|
+
readonly version: string;
|
|
9
|
+
readonly descriptorSha256: string;
|
|
10
|
+
readonly catalogFilename: "catalog.json";
|
|
11
|
+
readonly catalogSha256: string;
|
|
12
|
+
/** Optional per-target subtree metadata; verified when the API provides it. */
|
|
13
|
+
readonly targetMetadata?: Readonly<Partial<Record<ReleaseTarget, ReleaseTargetMetadata>>>;
|
|
14
|
+
}
|
|
15
|
+
export declare function downloadProArtifact(context: CommandContext, auth: AuthUseCaseDependencies, expected: {
|
|
16
|
+
version: string;
|
|
17
|
+
descriptorSha256: string;
|
|
18
|
+
}, deps?: {
|
|
19
|
+
readonly fetch?: typeof fetch;
|
|
20
|
+
readonly allowedHosts?: readonly string[];
|
|
21
|
+
readonly onProgress?: (event: {
|
|
22
|
+
readonly downloadedBytes: number;
|
|
23
|
+
readonly totalBytes?: number;
|
|
24
|
+
}) => void;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
readonly descriptor: ProArtifactDescriptor;
|
|
27
|
+
readonly artifactBytes: Uint8Array;
|
|
28
|
+
readonly catalogJson: string;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { CliError } from "../errors/index.js";
|
|
2
|
+
import { extractTarGz, decodeUtf8 } from "../project/tar-gz.js";
|
|
3
|
+
import { sha256Bytes } from "../project/install-metadata.js";
|
|
4
|
+
import { downloadSignedArtifact } from "./signed-artifact.js";
|
|
5
|
+
import { runAccessTokenUseCase } from "./auth.js";
|
|
6
|
+
const ENDPOINT = "https://api.moeicons.com/v1/icon-library/pro/artifact-descriptor";
|
|
7
|
+
export const PRO_DOWNLOAD_HOSTS = ["06898acc14d0b9633f259fe20145fd49.r2.cloudflarestorage.com"];
|
|
8
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
9
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-(?:alpha|beta))?$/;
|
|
10
|
+
function parseTargetMetadata(value) {
|
|
11
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
12
|
+
return undefined;
|
|
13
|
+
const record = value;
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
16
|
+
if (key !== "react" &&
|
|
17
|
+
key !== "vue" &&
|
|
18
|
+
key !== "vanilla" &&
|
|
19
|
+
key !== "assets") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
23
|
+
return undefined;
|
|
24
|
+
const item = entry;
|
|
25
|
+
if (typeof item.path !== "string" || /(^|[\\/])\.\.([\\/]|$)/.test(item.path))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof item.sha256 !== "string" || !SHA.test(item.sha256))
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof item.fileCount !== "number" || !Number.isSafeInteger(item.fileCount))
|
|
30
|
+
return undefined;
|
|
31
|
+
if (typeof item.byteCount !== "number" || !Number.isSafeInteger(item.byteCount))
|
|
32
|
+
return undefined;
|
|
33
|
+
result[key] = {
|
|
34
|
+
path: item.path,
|
|
35
|
+
sha256: item.sha256,
|
|
36
|
+
fileCount: item.fileCount,
|
|
37
|
+
byteCount: item.byteCount,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function parse(value, expected) {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
44
|
+
throw new CliError("VALIDATION_ERROR", "invalid pro artifact descriptor");
|
|
45
|
+
const item = value;
|
|
46
|
+
const allowed = ["ok", "tier", "version", "descriptorSha256", "catalogFilename", "catalogSha256", "url", "expiresAt", "size", "sha256", "targetMetadata"];
|
|
47
|
+
if (Object.keys(item).some((key) => !allowed.includes(key)) || item.ok !== true || item.tier !== "pro" || item.version !== expected.version || item.descriptorSha256 !== expected.descriptorSha256 ||
|
|
48
|
+
typeof item.version !== "string" || !VERSION.test(item.version) || typeof item.descriptorSha256 !== "string" || !SHA.test(item.descriptorSha256) ||
|
|
49
|
+
item.catalogFilename !== "catalog.json" || typeof item.catalogSha256 !== "string" || !SHA.test(item.catalogSha256) || typeof item.url !== "string" ||
|
|
50
|
+
typeof item.expiresAt !== "string" || typeof item.size !== "number" || !Number.isSafeInteger(item.size) || item.size < 1 || typeof item.sha256 !== "string" || !SHA.test(item.sha256)) {
|
|
51
|
+
throw new CliError("VALIDATION_ERROR", "invalid or changed pro artifact descriptor");
|
|
52
|
+
}
|
|
53
|
+
const targetMetadata = item.targetMetadata !== undefined ? parseTargetMetadata(item.targetMetadata) : undefined;
|
|
54
|
+
return {
|
|
55
|
+
...item,
|
|
56
|
+
...(targetMetadata ? { targetMetadata } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function requestDescriptor(token, expected, deps) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), 5_000);
|
|
62
|
+
const abort = () => controller.abort();
|
|
63
|
+
if (deps.signal.aborted)
|
|
64
|
+
controller.abort();
|
|
65
|
+
deps.signal.addEventListener("abort", abort, { once: true });
|
|
66
|
+
try {
|
|
67
|
+
const response = await deps.fetch(ENDPOINT, {
|
|
68
|
+
method: "POST", redirect: "error", signal: controller.signal,
|
|
69
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
|
|
70
|
+
body: JSON.stringify(expected),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
return { status: response.status };
|
|
74
|
+
return { status: response.status, value: parse(await response.json(), expected) };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof CliError)
|
|
78
|
+
throw error;
|
|
79
|
+
throw new CliError(deps.signal.aborted ? "CANCELLED" : "NETWORK_ERROR", deps.signal.aborted ? "pro download cancelled" : "pro descriptor request failed");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
deps.signal.removeEventListener("abort", abort);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function downloadProArtifact(context, auth, expected, deps = {}) {
|
|
87
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
88
|
+
let token = await runAccessTokenUseCase(context, auth);
|
|
89
|
+
let response = await requestDescriptor(token, expected, { fetch: fetchFn, signal: context.signal });
|
|
90
|
+
if (response.status === 401) {
|
|
91
|
+
token = await runAccessTokenUseCase(context, auth, true);
|
|
92
|
+
response = await requestDescriptor(token, expected, { fetch: fetchFn, signal: context.signal });
|
|
93
|
+
}
|
|
94
|
+
token = "";
|
|
95
|
+
if (response.status === 401)
|
|
96
|
+
throw new CliError("AUTH_ERROR", "pro download authentication failed after one refresh");
|
|
97
|
+
if (response.status === 403)
|
|
98
|
+
throw new CliError("FORBIDDEN", "active pro entitlement required");
|
|
99
|
+
if (response.status === 404)
|
|
100
|
+
throw new CliError("NOT_FOUND", "pro artifact not found");
|
|
101
|
+
if (!response.value)
|
|
102
|
+
throw new CliError("NETWORK_ERROR", `pro descriptor request failed with ${response.status}`);
|
|
103
|
+
const descriptor = response.value;
|
|
104
|
+
const artifactBytes = await downloadSignedArtifact({ url: descriptor.url, expiresAt: descriptor.expiresAt, size: descriptor.size, sha256: descriptor.sha256 }, { allowedHosts: deps.allowedHosts ?? PRO_DOWNLOAD_HOSTS, fetch: fetchFn, signal: context.signal, now: context.now().getTime(), ...(deps.onProgress ? { onProgress: deps.onProgress } : {}) });
|
|
105
|
+
const extracted = extractTarGz(artifactBytes, { maxEntries: 20_000, maxExpandedBytes: 64 * 1024 * 1024 });
|
|
106
|
+
if (extracted.errors.length > 0)
|
|
107
|
+
throw new CliError("VALIDATION_ERROR", extracted.errors[0] ?? "invalid pro archive");
|
|
108
|
+
const catalog = extracted.files[descriptor.catalogFilename] ?? extracted.files[`./${descriptor.catalogFilename}`];
|
|
109
|
+
if (!catalog || sha256Bytes(catalog) !== descriptor.catalogSha256)
|
|
110
|
+
throw new CliError("VALIDATION_ERROR", "pro catalog SHA-256 mismatch");
|
|
111
|
+
return { descriptor, artifactBytes, catalogJson: decodeUtf8(catalog) };
|
|
112
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type TransactionalFs } from "../project/install.js";
|
|
2
|
+
import type { AuthUseCaseDependencies } from "./auth.js";
|
|
3
|
+
import type { CommandContext } from "./context.js";
|
|
4
|
+
import type { Target } from "../commands/parser.js";
|
|
5
|
+
export declare function runProInstallUseCase(context: CommandContext, deps: {
|
|
6
|
+
readonly fs: TransactionalFs;
|
|
7
|
+
readonly auth: AuthUseCaseDependencies;
|
|
8
|
+
readonly fetch?: typeof fetch;
|
|
9
|
+
readonly allowedHosts?: readonly string[];
|
|
10
|
+
readonly onProgress?: (event: {
|
|
11
|
+
readonly downloadedBytes: number;
|
|
12
|
+
readonly totalBytes?: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
}, expected: {
|
|
15
|
+
readonly version: string;
|
|
16
|
+
readonly descriptorSha256: string;
|
|
17
|
+
readonly target?: Target;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
readonly projectRoot: string;
|
|
20
|
+
readonly artifactVersion: string;
|
|
21
|
+
readonly descriptorSha256: string;
|
|
22
|
+
readonly catalogSha256: string;
|
|
23
|
+
readonly artifactBytes: number;
|
|
24
|
+
}>;
|