@moewolf/moe-icons-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +32 -0
  3. package/bin/moeicons.js +20 -0
  4. package/dist/api/client.d.ts +42 -0
  5. package/dist/api/client.js +105 -0
  6. package/dist/auth/callback.d.ts +37 -0
  7. package/dist/auth/callback.js +72 -0
  8. package/dist/auth/device-login.d.ts +29 -0
  9. package/dist/auth/device-login.js +74 -0
  10. package/dist/auth/login.d.ts +41 -0
  11. package/dist/auth/login.js +85 -0
  12. package/dist/auth/open-browser.d.ts +2 -0
  13. package/dist/auth/open-browser.js +15 -0
  14. package/dist/auth/pkce.d.ts +17 -0
  15. package/dist/auth/pkce.js +20 -0
  16. package/dist/auth/token-store.d.ts +38 -0
  17. package/dist/auth/token-store.js +126 -0
  18. package/dist/catalog/catalog.d.ts +32 -0
  19. package/dist/catalog/catalog.js +80 -0
  20. package/dist/catalog/catalog.json +9371 -0
  21. package/dist/cli.d.ts +23 -0
  22. package/dist/cli.js +507 -0
  23. package/dist/commands/parser.d.ts +41 -0
  24. package/dist/commands/parser.js +146 -0
  25. package/dist/core/auth.d.ts +32 -0
  26. package/dist/core/auth.js +131 -0
  27. package/dist/core/bitmap-assets.d.ts +29 -0
  28. package/dist/core/bitmap-assets.js +77 -0
  29. package/dist/core/cli-update.d.ts +43 -0
  30. package/dist/core/cli-update.js +71 -0
  31. package/dist/core/context.d.ts +22 -0
  32. package/dist/core/context.js +1 -0
  33. package/dist/core/free-download.d.ts +66 -0
  34. package/dist/core/free-download.js +193 -0
  35. package/dist/core/generate.d.ts +33 -0
  36. package/dist/core/generate.js +285 -0
  37. package/dist/core/icon-names.d.ts +14 -0
  38. package/dist/core/icon-names.js +54 -0
  39. package/dist/core/init.d.ts +12 -0
  40. package/dist/core/init.js +23 -0
  41. package/dist/core/install.d.ts +67 -0
  42. package/dist/core/install.js +117 -0
  43. package/dist/core/library-update.d.ts +24 -0
  44. package/dist/core/library-update.js +161 -0
  45. package/dist/core/manage.d.ts +24 -0
  46. package/dist/core/manage.js +38 -0
  47. package/dist/core/pro-download.d.ts +29 -0
  48. package/dist/core/pro-download.js +112 -0
  49. package/dist/core/pro-install.d.ts +24 -0
  50. package/dist/core/pro-install.js +72 -0
  51. package/dist/core/release-descriptor.d.ts +40 -0
  52. package/dist/core/release-descriptor.js +114 -0
  53. package/dist/core/resource-variant.d.ts +35 -0
  54. package/dist/core/resource-variant.js +70 -0
  55. package/dist/core/signed-artifact.d.ts +29 -0
  56. package/dist/core/signed-artifact.js +81 -0
  57. package/dist/core/target-subtree.d.ts +49 -0
  58. package/dist/core/target-subtree.js +132 -0
  59. package/dist/core/update-policy.d.ts +31 -0
  60. package/dist/core/update-policy.js +27 -0
  61. package/dist/core/version-service.d.ts +26 -0
  62. package/dist/core/version-service.js +71 -0
  63. package/dist/core/wizard.d.ts +49 -0
  64. package/dist/core/wizard.js +71 -0
  65. package/dist/errors/errors.d.ts +34 -0
  66. package/dist/errors/errors.js +39 -0
  67. package/dist/errors/index.d.ts +1 -0
  68. package/dist/errors/index.js +1 -0
  69. package/dist/generator/cn.d.ts +5 -0
  70. package/dist/generator/cn.js +14 -0
  71. package/dist/generator/generate.d.ts +34 -0
  72. package/dist/generator/generate.js +562 -0
  73. package/dist/generator/theme-resolve.d.ts +26 -0
  74. package/dist/generator/theme-resolve.js +100 -0
  75. package/dist/mcp/server.d.ts +58 -0
  76. package/dist/mcp/server.js +176 -0
  77. package/dist/project/config.d.ts +74 -0
  78. package/dist/project/config.js +353 -0
  79. package/dist/project/detect.d.ts +26 -0
  80. package/dist/project/detect.js +104 -0
  81. package/dist/project/install-metadata.d.ts +29 -0
  82. package/dist/project/install-metadata.js +108 -0
  83. package/dist/project/install.d.ts +115 -0
  84. package/dist/project/install.js +537 -0
  85. package/dist/project/project-lock.d.ts +11 -0
  86. package/dist/project/project-lock.js +81 -0
  87. package/dist/project/tailwind.d.ts +51 -0
  88. package/dist/project/tailwind.js +188 -0
  89. package/dist/project/tar-gz.d.ts +12 -0
  90. package/dist/project/tar-gz.js +100 -0
  91. package/dist/tui/primitives.d.ts +36 -0
  92. package/dist/tui/primitives.js +88 -0
  93. package/dist/ui/banner.d.ts +11 -0
  94. package/dist/ui/banner.js +105 -0
  95. package/dist/ui/clack.d.ts +8 -0
  96. package/dist/ui/clack.js +53 -0
  97. package/dist/ui/create-ui.d.ts +14 -0
  98. package/dist/ui/create-ui.js +16 -0
  99. package/dist/ui/non-interactive.d.ts +5 -0
  100. package/dist/ui/non-interactive.js +30 -0
  101. package/dist/ui/stream.d.ts +11 -0
  102. package/dist/ui/stream.js +50 -0
  103. package/package.json +70 -0
@@ -0,0 +1,146 @@
1
+ import { Command as Commander, CommanderError } from "commander";
2
+ import { CliError } from "../errors/index.js";
3
+ const SIMPLE_COMMANDS = [
4
+ "login",
5
+ "logout",
6
+ "account",
7
+ "groups",
8
+ "generate",
9
+ "init",
10
+ "mcp",
11
+ "help",
12
+ ];
13
+ function addSharedOptions(command) {
14
+ return command
15
+ .allowUnknownOption(true)
16
+ .allowExcessArguments(true)
17
+ .helpOption(false)
18
+ .option("-h, --help")
19
+ .option("-v, --showVersion")
20
+ .option("--version")
21
+ .option("--json")
22
+ .option("--yes")
23
+ .option("--no-tailwind")
24
+ .option("--pro")
25
+ .option("--ent");
26
+ }
27
+ function createProgram(onSelect) {
28
+ const program = addSharedOptions(new Commander());
29
+ program
30
+ .name("moeicons")
31
+ .exitOverride()
32
+ .addHelpCommand(false)
33
+ .showHelpAfterError(false)
34
+ .enablePositionalOptions()
35
+ .configureOutput({
36
+ writeOut: () => undefined,
37
+ writeErr: () => undefined,
38
+ })
39
+ .action(() => {
40
+ onSelect({ name: "wizard" });
41
+ });
42
+ addSharedOptions(program.command("install").argument("[group]")).action((group) => {
43
+ onSelect(group ? { name: "install", group } : { name: "install" });
44
+ });
45
+ for (const name of SIMPLE_COMMANDS) {
46
+ addSharedOptions(program.command(name)).action(() => {
47
+ onSelect({ name });
48
+ });
49
+ }
50
+ return program;
51
+ }
52
+ function toCliError(error) {
53
+ if (error instanceof CommanderError && error.code === "commander.unknownCommand") {
54
+ const match = /unknown command ['`]?([^'`\s]+)/.exec(error.message);
55
+ throw new CliError("VALIDATION_ERROR", `unknown command: ${match?.[1] ?? "unknown"}`);
56
+ }
57
+ if (error instanceof CommanderError &&
58
+ (error.code === "commander.help" || error.code === "commander.helpDisplayed")) {
59
+ throw new CliError("VALIDATION_ERROR", error.message);
60
+ }
61
+ throw error;
62
+ }
63
+ const KNOWN_COMMANDS = new Set(["install", ...SIMPLE_COMMANDS]);
64
+ /**
65
+ * Parse argv into a Command via Commander. Never calls `process.exit()`.
66
+ * `moeicons --pro` / `--ent` legacy aliases map to install for the compatibility window.
67
+ */
68
+ export function parseArgs(argv) {
69
+ const json = argv.includes("--json");
70
+ const yes = argv.includes("--yes");
71
+ const noTailwind = argv.includes("--no-tailwind");
72
+ const version = argv.includes("--version") || argv.includes("-v");
73
+ const help = argv.includes("--help") || argv.includes("-h");
74
+ const pro = argv.includes("--pro");
75
+ const ent = argv.includes("--ent");
76
+ const targetIndex = argv.indexOf("--target");
77
+ const targetValue = targetIndex >= 0 ? argv[targetIndex + 1] : undefined;
78
+ if (targetIndex >= 0 && !["react", "vue", "vanilla", "assets"].includes(targetValue ?? "")) {
79
+ throw new CliError("VALIDATION_ERROR", `unknown target: ${targetValue ?? ""}`);
80
+ }
81
+ const target = targetValue;
82
+ const withTarget = (command) => target ? { ...command, target } : command;
83
+ // Preserve previous flag precedence: version/help/legacy aliases beat subcommands.
84
+ if (version)
85
+ return { command: { name: "version" }, json, yes, noTailwind, ...(target ? { target } : {}) };
86
+ if (help)
87
+ return { command: { name: "help" }, json, yes, noTailwind, ...(target ? { target } : {}) };
88
+ if (pro)
89
+ return {
90
+ command: withTarget({ name: "install", group: "pro" }),
91
+ json,
92
+ yes,
93
+ noTailwind,
94
+ ...(target ? { target } : {}),
95
+ };
96
+ if (ent)
97
+ return {
98
+ command: withTarget({ name: "install", group: "ent" }),
99
+ json,
100
+ yes,
101
+ noTailwind,
102
+ ...(target ? { target } : {}),
103
+ };
104
+ const positional = argv.filter((arg) => !arg.startsWith("-"));
105
+ const first = positional[0];
106
+ if (first !== undefined && !KNOWN_COMMANDS.has(first)) {
107
+ throw new CliError("VALIDATION_ERROR", `unknown command: ${first}`);
108
+ }
109
+ let selected;
110
+ try {
111
+ createProgram((command) => {
112
+ selected = withTarget(command);
113
+ }).parse([...argv], { from: "user" });
114
+ }
115
+ catch (error) {
116
+ toCliError(error);
117
+ }
118
+ return {
119
+ command: selected ?? { name: "wizard" },
120
+ json,
121
+ yes,
122
+ noTailwind,
123
+ ...(target ? { target } : {}),
124
+ };
125
+ }
126
+ export const HELP_TEXT = `moeicons — Moeicons icon library CLI
127
+
128
+ Usage:
129
+ moeicons interactive guided flow (free / pro / login)
130
+ moeicons install [group] install an icon group
131
+ moeicons login browser login (PKCE)
132
+ moeicons logout clear local session
133
+ moeicons account show account/tier info
134
+ moeicons groups list available icon groups
135
+ moeicons generate generate React/Vue proxy components
136
+ moeicons init create moeicons.config.json
137
+ moeicons mcp start the MCP stdio server
138
+ moeicons --version show version
139
+ moeicons --help show help
140
+
141
+ Options:
142
+ --json machine-readable JSON output
143
+ --yes skip confirmations in noninteractive mode
144
+ --target output target: react, vue, vanilla, or assets
145
+ --no-tailwind skip Tailwind config auto-integration
146
+ `;
@@ -0,0 +1,32 @@
1
+ import { type DeviceLoginDependencies } from "../auth/device-login.js";
2
+ import { redactSession, type TokenStore } from "../auth/token-store.js";
3
+ import type { CommandContext } from "./context.js";
4
+ export interface AuthUseCaseDependencies {
5
+ readonly tokenStore?: TokenStore;
6
+ readonly systemTokenStore?: () => TokenStore | undefined;
7
+ readonly fileTokenStore?: (rootDir?: string) => TokenStore;
8
+ readonly request?: DeviceLoginDependencies["request"];
9
+ readonly openBrowser?: (url: string) => Promise<void>;
10
+ readonly sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
11
+ readonly fetch?: typeof fetch;
12
+ readonly fileFallbackAllowed?: boolean;
13
+ }
14
+ export type SessionStatus = {
15
+ readonly kind: "authenticated";
16
+ readonly account: ReturnType<typeof redactSession>;
17
+ } | {
18
+ readonly kind: "signed-out";
19
+ readonly reason?: string;
20
+ } | {
21
+ readonly kind: "unknown";
22
+ readonly reason: string;
23
+ };
24
+ /** Probe existing credentials without prompting for or creating fallback storage. */
25
+ export declare function runSessionStatusUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<SessionStatus>;
26
+ export declare function runLoginUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<ReturnType<typeof redactSession>>;
27
+ /** Internal authenticated transport credential; adapters must never render it. */
28
+ export declare function runAccessTokenUseCase(context: CommandContext, deps?: AuthUseCaseDependencies, forceRefresh?: boolean): Promise<string>;
29
+ export declare function runAccountUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<ReturnType<typeof redactSession>>;
30
+ export declare function runLogoutUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<{
31
+ revoked: boolean;
32
+ }>;
@@ -0,0 +1,131 @@
1
+ import { loginWithDeviceSession, refreshAuth0Session } from "../auth/device-login.js";
2
+ import { createFileTokenStore, createSystemTokenStore, redactSession } from "../auth/token-store.js";
3
+ import { openBrowser } from "../auth/open-browser.js";
4
+ import { requestJson } from "../api/client.js";
5
+ import { CliError } from "../errors/index.js";
6
+ const API_BASE_URL = "https://api.moeicons.com";
7
+ const WEBSITE_ORIGIN = "https://moeicons.com";
8
+ function existingStore(context, deps) {
9
+ return deps.tokenStore ?? (context.env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === "1" ? undefined : (deps.systemTokenStore ?? createSystemTokenStore)()) ??
10
+ (context.env.MOEICONS_TOKEN_STORE_DIR ? (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore(rootDir ? { rootDir } : {})))(context.env.MOEICONS_TOKEN_STORE_DIR) : undefined);
11
+ }
12
+ /** Probe existing credentials without prompting for or creating fallback storage. */
13
+ export async function runSessionStatusUseCase(context, deps = {}) {
14
+ const store = existingStore(context, deps);
15
+ if (!store)
16
+ return { kind: "signed-out" };
17
+ const session = store.getActive();
18
+ if (!session)
19
+ return { kind: "signed-out" };
20
+ if (session.expiresAt > context.now().getTime())
21
+ return { kind: "authenticated", account: redactSession(session) };
22
+ const config = auth0Config(context);
23
+ if (!config.auth0Issuer || !config.auth0ClientId)
24
+ return { kind: "signed-out", reason: "stored session is expired" };
25
+ try {
26
+ const refreshed = await refreshAuth0Session(config, session, { fetch: deps.fetch ?? fetch, tokenStore: store, now: () => context.now().getTime(), signal: context.signal });
27
+ return { kind: "authenticated", account: redactSession(refreshed) };
28
+ }
29
+ catch (error) {
30
+ if (!(error instanceof CliError) || error.code === "NETWORK_ERROR" || error.code === "CANCELLED") {
31
+ return { kind: "unknown", reason: error instanceof Error ? error.message : "session validation unavailable" };
32
+ }
33
+ return { kind: "signed-out", reason: error.message };
34
+ }
35
+ }
36
+ async function selectStore(context, deps) {
37
+ if (deps.tokenStore)
38
+ return deps.tokenStore;
39
+ const system = context.env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === "1" ? undefined : (deps.systemTokenStore ?? createSystemTokenStore)();
40
+ if (system)
41
+ return system;
42
+ if (context.env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === "1" && context.env.MOEICONS_TOKEN_STORE_DIR) {
43
+ return (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore(rootDir ? { rootDir } : {})))(context.env.MOEICONS_TOKEN_STORE_DIR);
44
+ }
45
+ if (deps.fileFallbackAllowed === false)
46
+ throw new CliError("AUTH_ERROR", "system credential storage is unavailable; non-interactive file fallback is disabled");
47
+ if (!context.ui.confirm)
48
+ throw new CliError("AUTH_ERROR", "system credential storage is unavailable");
49
+ const accepted = await context.ui.confirm("System credential storage is unavailable. Use a local file protected with mode 0600?", context.signal);
50
+ if (accepted === undefined)
51
+ throw new CliError("CANCELLED", "login cancelled");
52
+ if (!accepted)
53
+ throw new CliError("AUTH_ERROR", "login requires secure credential storage");
54
+ return (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore(rootDir ? { rootDir } : {})))(context.env.MOEICONS_TOKEN_STORE_DIR);
55
+ }
56
+ function auth0Config(context) {
57
+ return {
58
+ apiBaseUrl: API_BASE_URL,
59
+ websiteOrigin: WEBSITE_ORIGIN,
60
+ auth0Issuer: context.env.MOEICONS_AUTH0_ISSUER ?? "",
61
+ auth0ClientId: context.env.MOEICONS_AUTH0_CLIENT_ID ?? "",
62
+ };
63
+ }
64
+ export async function runLoginUseCase(context, deps = {}) {
65
+ const tokenStore = await selectStore(context, deps);
66
+ const request = deps.request ?? ((path, options) => requestJson({ baseUrl: API_BASE_URL }, path, { ...options, retries: options.method === "GET" ? 3 : 0 }));
67
+ const session = await loginWithDeviceSession(auth0Config(context), {
68
+ request,
69
+ openBrowser: deps.openBrowser ?? openBrowser,
70
+ sleep: deps.sleep ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
71
+ const timer = setTimeout(resolve, milliseconds);
72
+ signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new CliError("CANCELLED", "login cancelled")); }, { once: true });
73
+ })),
74
+ tokenStore,
75
+ now: () => context.now().getTime(),
76
+ }, context.signal);
77
+ return redactSession(session);
78
+ }
79
+ async function activeSession(context, deps) {
80
+ const store = await selectStore(context, deps);
81
+ const session = store.getActive();
82
+ if (!session)
83
+ throw new CliError("AUTH_ERROR", "not logged in");
84
+ if (session.expiresAt > context.now().getTime())
85
+ return { store, session };
86
+ const config = auth0Config(context);
87
+ if (!config.auth0Issuer || !config.auth0ClientId)
88
+ throw new CliError("AUTH_ERROR", "session expired and Auth0 refresh configuration is unavailable");
89
+ const refreshed = await refreshAuth0Session(config, session, { fetch: deps.fetch ?? fetch, tokenStore: store, now: () => context.now().getTime(), signal: context.signal });
90
+ return { store, session: refreshed };
91
+ }
92
+ /** Internal authenticated transport credential; adapters must never render it. */
93
+ export async function runAccessTokenUseCase(context, deps = {}, forceRefresh = false) {
94
+ if (!forceRefresh)
95
+ return (await activeSession(context, deps)).session.accessToken;
96
+ const store = await selectStore(context, deps);
97
+ const session = store.getActive();
98
+ if (!session)
99
+ throw new CliError("AUTH_ERROR", "not logged in");
100
+ const config = auth0Config(context);
101
+ if (!config.auth0Issuer || !config.auth0ClientId)
102
+ throw new CliError("AUTH_ERROR", "Auth0 refresh configuration is unavailable");
103
+ return (await refreshAuth0Session(config, session, { fetch: deps.fetch ?? fetch, tokenStore: store, now: () => context.now().getTime(), signal: context.signal })).accessToken;
104
+ }
105
+ export async function runAccountUseCase(context, deps = {}) {
106
+ return redactSession((await activeSession(context, deps)).session);
107
+ }
108
+ export async function runLogoutUseCase(context, deps = {}) {
109
+ const store = existingStore(context, deps);
110
+ if (!store)
111
+ return { revoked: true };
112
+ const session = store.getActive();
113
+ if (!session)
114
+ return { revoked: true };
115
+ let revoked = false;
116
+ const config = auth0Config(context);
117
+ if (config.auth0Issuer && config.auth0ClientId) {
118
+ try {
119
+ const response = await (deps.fetch ?? fetch)(`${config.auth0Issuer.replace(/\/$/, "")}/oauth/revoke`, {
120
+ method: "POST", headers: { "content-type": "application/json" },
121
+ body: JSON.stringify({ client_id: config.auth0ClientId, token: session.refreshToken }), signal: context.signal,
122
+ });
123
+ revoked = response.ok;
124
+ }
125
+ catch {
126
+ revoked = false;
127
+ }
128
+ }
129
+ store.clear();
130
+ return { revoked };
131
+ }
@@ -0,0 +1,29 @@
1
+ import { type ResourceVariant } from "./resource-variant.js";
2
+ export interface SelectedBitmapAsset {
3
+ /** POSIX path relative to outputDir, e.g. assets/moe-cute-3d-webp-256/ui-search.webp */
4
+ readonly destRel: string;
5
+ readonly bytes: Uint8Array;
6
+ readonly resourceVariantId: string;
7
+ readonly iconId: string;
8
+ }
9
+ /**
10
+ * Identify a bitmap file inside a free/pro archive by parsing a directory
11
+ * segment as resourceVariantId (G2, from the right). Never scans for a mid-path `-3d`.
12
+ */
13
+ export declare function matchArchiveBitmapFile(archivePath: string): {
14
+ readonly variant: ResourceVariant;
15
+ readonly iconId: string;
16
+ } | undefined;
17
+ export type SelectBitmapAssetsResult = {
18
+ readonly ok: true;
19
+ readonly assets: readonly SelectedBitmapAsset[];
20
+ readonly skipped: readonly string[];
21
+ } | {
22
+ readonly ok: false;
23
+ readonly errors: readonly string[];
24
+ };
25
+ /**
26
+ * Keep only archive files that belong to the requested variants and selected
27
+ * icons. Other sizes/formats stay in the tarball/cache and are not written.
28
+ */
29
+ export declare function selectBitmapVariantAssets(archiveFiles: Readonly<Record<string, Uint8Array>>, variants: readonly ResourceVariant[], iconIds: readonly string[]): SelectBitmapAssetsResult;
@@ -0,0 +1,77 @@
1
+ import { parseResourceVariantId, assetRelativePath } from "./resource-variant.js";
2
+ function normalizeArchivePath(rel) {
3
+ return rel.replace(/\\/g, "/").replace(/^\.\//, "");
4
+ }
5
+ /**
6
+ * Identify a bitmap file inside a free/pro archive by parsing a directory
7
+ * segment as resourceVariantId (G2, from the right). Never scans for a mid-path `-3d`.
8
+ */
9
+ export function matchArchiveBitmapFile(archivePath) {
10
+ const cleaned = normalizeArchivePath(archivePath);
11
+ const parts = cleaned.split("/").filter(Boolean);
12
+ if (parts.length < 2)
13
+ return undefined;
14
+ const file = parts[parts.length - 1] ?? "";
15
+ const dot = file.lastIndexOf(".");
16
+ if (dot <= 0)
17
+ return undefined;
18
+ const format = file.slice(dot + 1);
19
+ const iconId = file.slice(0, dot);
20
+ if (!iconId || iconId.includes(".."))
21
+ return undefined;
22
+ for (let i = 0; i < parts.length - 1; i += 1) {
23
+ const segment = parts[i] ?? "";
24
+ try {
25
+ const variant = parseResourceVariantId(segment);
26
+ if (variant.format !== format)
27
+ continue;
28
+ return { variant, iconId };
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ /**
37
+ * Keep only archive files that belong to the requested variants and selected
38
+ * icons. Other sizes/formats stay in the tarball/cache and are not written.
39
+ */
40
+ export function selectBitmapVariantAssets(archiveFiles, variants, iconIds) {
41
+ const wanted = new Map(variants.map((variant) => [variant.resourceVariantId, variant]));
42
+ const iconSet = new Set(iconIds);
43
+ const skipped = [];
44
+ const chosen = new Map();
45
+ for (const [archivePath, bytes] of Object.entries(archiveFiles)) {
46
+ const matched = matchArchiveBitmapFile(archivePath);
47
+ if (!matched)
48
+ continue;
49
+ if (!wanted.has(matched.variant.resourceVariantId)) {
50
+ skipped.push(archivePath);
51
+ continue;
52
+ }
53
+ if (!iconSet.has(matched.iconId)) {
54
+ skipped.push(archivePath);
55
+ continue;
56
+ }
57
+ const destRel = assetRelativePath(matched.variant.resourceVariantId, matched.iconId, matched.variant.format);
58
+ chosen.set(destRel, {
59
+ destRel,
60
+ bytes,
61
+ resourceVariantId: matched.variant.resourceVariantId,
62
+ iconId: matched.iconId,
63
+ });
64
+ }
65
+ const errors = [];
66
+ for (const variant of variants) {
67
+ for (const iconId of iconIds) {
68
+ const destRel = assetRelativePath(variant.resourceVariantId, iconId, variant.format);
69
+ if (!chosen.has(destRel)) {
70
+ errors.push(`bitmap asset missing for icon "${iconId}" variant "${variant.resourceVariantId}"`);
71
+ }
72
+ }
73
+ }
74
+ if (errors.length > 0)
75
+ return { ok: false, errors };
76
+ return { ok: true, assets: [...chosen.values()], skipped };
77
+ }
@@ -0,0 +1,43 @@
1
+ export type InstallSource = {
2
+ readonly kind: "local";
3
+ readonly manager: "npm" | "pnpm" | "yarn";
4
+ } | {
5
+ readonly kind: "global";
6
+ readonly manager: "npm" | "pnpm" | "yarn";
7
+ } | {
8
+ readonly kind: "npx";
9
+ } | {
10
+ readonly kind: "unknown";
11
+ };
12
+ export declare function detectInstallSource(input: {
13
+ env: Readonly<Record<string, string | undefined>>;
14
+ localManager?: "npm" | "pnpm" | "yarn";
15
+ localDependency?: boolean;
16
+ }): InstallSource;
17
+ export declare function fixedUpdateInstruction(source: InstallSource, version: string): string;
18
+ export declare function planCliUpdate(currentVersion: string, versions: readonly string[], source: InstallSource): {
19
+ readonly status: "current" | "update" | "unavailable";
20
+ readonly currentVersion: string;
21
+ readonly latestVersion?: string;
22
+ readonly instruction?: string;
23
+ };
24
+ export interface CliUpdateFileSystem {
25
+ readonly existsSync: (path: string) => boolean;
26
+ readonly readFileSync: (path: string, encoding: "utf8") => string;
27
+ }
28
+ export declare function detectLocalInstall(cwd: string, fs: CliUpdateFileSystem): {
29
+ readonly localDependency: boolean;
30
+ readonly localManager?: "npm" | "pnpm" | "yarn";
31
+ };
32
+ /** Read-only update check. This module intentionally has no subprocess API. */
33
+ export declare function runCliUpdateCheck(input: {
34
+ readonly currentVersion: string;
35
+ readonly cwd: string;
36
+ readonly env: Readonly<Record<string, string | undefined>>;
37
+ readonly fs: CliUpdateFileSystem;
38
+ readonly signal?: AbortSignal;
39
+ readonly timeoutMs?: number;
40
+ readonly fetchVersions?: () => Promise<readonly string[]>;
41
+ }): Promise<ReturnType<typeof planCliUpdate> & {
42
+ readonly source: InstallSource;
43
+ }>;
@@ -0,0 +1,71 @@
1
+ import { fetchMoeiconsVersionsCached, latestInChannel } from "./version-service.js";
2
+ import { decideUpdate } from "./update-policy.js";
3
+ const CLI_PACKAGE = "@moewolf/moe-icons-cli";
4
+ export function detectInstallSource(input) {
5
+ if (input.localDependency && input.localManager)
6
+ return { kind: "local", manager: input.localManager };
7
+ const agent = input.env.npm_config_user_agent ?? "";
8
+ const manager = agent.startsWith("pnpm/") ? "pnpm" : agent.startsWith("yarn/") ? "yarn" : "npm";
9
+ if (input.env.npm_command === "exec" || input.env.npm_lifecycle_event === "npx")
10
+ return { kind: "npx" };
11
+ if (input.env.npm_config_global === "true")
12
+ return { kind: "global", manager };
13
+ return { kind: "unknown" };
14
+ }
15
+ export function fixedUpdateInstruction(source, version) {
16
+ const spec = `${CLI_PACKAGE}@${version}`;
17
+ if (source.kind === "npx")
18
+ return `npx --yes ${spec}`;
19
+ if (source.kind === "unknown")
20
+ return `See https://moeicons.com/docs/cli-update for ${spec}`;
21
+ if (source.kind === "local") {
22
+ if (source.manager === "npm")
23
+ return `npm install --save-exact ${spec}`;
24
+ if (source.manager === "pnpm")
25
+ return `pnpm add --save-exact ${spec}`;
26
+ return `yarn add --exact ${spec}`;
27
+ }
28
+ if (source.manager === "npm")
29
+ return `npm install --global ${spec}`;
30
+ if (source.manager === "pnpm")
31
+ return `pnpm add --global --save-exact ${spec}`;
32
+ return `yarn global add --exact ${spec}`;
33
+ }
34
+ export function planCliUpdate(currentVersion, versions, source) {
35
+ const latest = latestInChannel(currentVersion, versions);
36
+ if (!latest)
37
+ return { status: "unavailable", currentVersion };
38
+ const decision = decideUpdate({ installedTier: "free", remoteTier: "free", installedVersion: currentVersion, remoteVersion: latest });
39
+ if (decision.kind !== "update")
40
+ return { status: "current", currentVersion, latestVersion: latest };
41
+ return { status: "update", currentVersion, latestVersion: latest, instruction: fixedUpdateInstruction(source, latest) };
42
+ }
43
+ export function detectLocalInstall(cwd, fs) {
44
+ let parsed = {};
45
+ try {
46
+ parsed = JSON.parse(fs.readFileSync(`${cwd}/package.json`, "utf8"));
47
+ }
48
+ catch {
49
+ return { localDependency: false };
50
+ }
51
+ const localDependency = typeof parsed.dependencies?.[CLI_PACKAGE] === "string" || typeof parsed.devDependencies?.[CLI_PACKAGE] === "string";
52
+ if (!localDependency)
53
+ return { localDependency: false };
54
+ if (fs.existsSync(`${cwd}/pnpm-lock.yaml`))
55
+ return { localDependency, localManager: "pnpm" };
56
+ if (fs.existsSync(`${cwd}/yarn.lock`))
57
+ return { localDependency, localManager: "yarn" };
58
+ if (fs.existsSync(`${cwd}/package-lock.json`))
59
+ return { localDependency, localManager: "npm" };
60
+ return { localDependency };
61
+ }
62
+ /** Read-only update check. This module intentionally has no subprocess API. */
63
+ export async function runCliUpdateCheck(input) {
64
+ const local = detectLocalInstall(input.cwd, input.fs);
65
+ const source = detectInstallSource({ env: input.env, ...local });
66
+ const versions = await (input.fetchVersions ?? (() => fetchMoeiconsVersionsCached({
67
+ ...(input.signal ? { signal: input.signal } : {}),
68
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
69
+ })))();
70
+ return { ...planCliUpdate(input.currentVersion, versions, source), source };
71
+ }
@@ -0,0 +1,22 @@
1
+ export interface UiChoice {
2
+ readonly value: string;
3
+ readonly label: string;
4
+ }
5
+ export interface CommandUi {
6
+ readonly select: (message: string, choices: readonly UiChoice[], signal: AbortSignal) => Promise<string | undefined>;
7
+ readonly confirm: (message: string, signal: AbortSignal) => Promise<boolean | undefined>;
8
+ readonly text: (message: string, signal: AbortSignal) => Promise<string | undefined>;
9
+ readonly note: (message: string, signal: AbortSignal) => void;
10
+ readonly progress: (message: string, signal: AbortSignal) => {
11
+ readonly update?: (message: string) => void;
12
+ readonly stop: (message?: string) => void;
13
+ };
14
+ }
15
+ /** Dependencies shared by command use cases; it deliberately contains no Node globals. */
16
+ export interface CommandContext {
17
+ readonly ui: CommandUi;
18
+ readonly cwd: string;
19
+ readonly env: Readonly<Record<string, string | undefined>>;
20
+ readonly signal: AbortSignal;
21
+ readonly now: () => Date;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { type DownloadLimits } from "../project/install.js";
2
+ import { type ReleaseDescriptor } from "./release-descriptor.js";
3
+ export declare const DESCRIPTOR_MAX_BYTES: number;
4
+ export declare const ARTIFACT_MAX_BYTES: number;
5
+ export declare const DOWNLOAD_TIMEOUT_MS = 30000;
6
+ export declare const MAX_REDIRECTS = 5;
7
+ export declare const DESCRIPTOR_NAME = "release-descriptor.json";
8
+ export declare const DESCRIPTOR_SHA_NAME = "release-descriptor.json.sha256";
9
+ export type FreeDownloadFailure = {
10
+ readonly ok: false;
11
+ readonly reason: "network";
12
+ readonly message: string;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly reason: "not-found";
16
+ readonly message: string;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly reason: "validation";
20
+ readonly message: string;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly reason: "checksum-mismatch";
24
+ readonly message: string;
25
+ } | {
26
+ readonly ok: false;
27
+ readonly reason: "offline-no-cache";
28
+ readonly message: string;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly reason: "cancelled";
32
+ readonly message: string;
33
+ };
34
+ export type FreeDownloadSuccess = {
35
+ readonly ok: true;
36
+ readonly descriptor: ReleaseDescriptor;
37
+ readonly descriptorSha256: string;
38
+ readonly artifactBytes: Uint8Array;
39
+ readonly catalogJson: string;
40
+ readonly cacheHit: boolean;
41
+ readonly tag: string;
42
+ };
43
+ export type FreeDownloadResult = FreeDownloadSuccess | FreeDownloadFailure;
44
+ export interface FreeDownloadIo {
45
+ readonly fetchFn: typeof fetch;
46
+ readonly readFileSync: (path: string) => Uint8Array;
47
+ readonly writeFileSync: (path: string, data: Uint8Array) => void;
48
+ readonly mkdirSync: (path: string) => void;
49
+ readonly existsSync: (path: string) => boolean;
50
+ readonly fixtureDir?: string;
51
+ /** Test-only loopback HTTP fixture root; production callers leave unset. */
52
+ readonly fixtureBaseUrl?: string;
53
+ readonly timeoutMs?: number;
54
+ readonly cacheDir: string;
55
+ readonly cliVersion: string;
56
+ readonly signal: AbortSignal;
57
+ readonly onProgress?: DownloadLimits["onProgress"];
58
+ }
59
+ export declare function artifactCachePath(cacheDir: string, fullVersion: string, sha256: string): string;
60
+ /**
61
+ * Fetch descriptor sidecar → verify descriptor → download free.filename from
62
+ * the descriptor (never guessed) → verify archive + nested catalog checksums.
63
+ * Local `fixtureDir` stands in for a GitHub Release directory.
64
+ */
65
+ export declare function downloadFreeRelease(io: FreeDownloadIo, sourceVersion: string): Promise<FreeDownloadResult>;
66
+ export declare function bundledSourceVersion(): string;