@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.
Files changed (151) hide show
  1. package/README.md +243 -9
  2. package/bin/check-node.cjs +36 -0
  3. package/bin/moeicons.js +12 -1
  4. package/dist/api/client.d.ts +13 -2
  5. package/dist/api/client.js +145 -21
  6. package/dist/auth/device-login.d.ts +10 -7
  7. package/dist/auth/device-login.js +60 -37
  8. package/dist/auth/login-schemas.d.ts +24 -0
  9. package/dist/auth/login-schemas.js +67 -0
  10. package/dist/catalog/catalog.d.ts +11 -0
  11. package/dist/catalog/catalog.js +43 -0
  12. package/dist/catalog/catalog.json +538 -3395
  13. package/dist/cli.d.ts +11 -2
  14. package/dist/cli.js +773 -147
  15. package/dist/commands/parser.d.ts +13 -2
  16. package/dist/commands/parser.js +138 -50
  17. package/dist/config-package/generated/SOURCE.json +25 -0
  18. package/dist/config-package/generated/moeicons-config.schema.json +191 -0
  19. package/dist/config-package/generated/moeicons.config.jsonc +42 -0
  20. package/dist/config-package/generated/render-config.cjs +145 -0
  21. package/dist/config-package/generated/validate-config.cjs +293 -0
  22. package/dist/config-package/generated-config.d.ts +32 -0
  23. package/dist/config-package/generated-config.js +19 -0
  24. package/dist/core/auth.d.ts +24 -0
  25. package/dist/core/auth.js +127 -8
  26. package/dist/core/bitmap-assets.d.ts +19 -5
  27. package/dist/core/bitmap-assets.js +40 -31
  28. package/dist/core/bitmap-shard-budget.d.ts +43 -0
  29. package/dist/core/bitmap-shard-budget.js +16 -0
  30. package/dist/core/bitmap-shard-cache.d.ts +68 -0
  31. package/dist/core/bitmap-shard-cache.js +115 -0
  32. package/dist/core/bitmap-shard-download.d.ts +130 -0
  33. package/dist/core/bitmap-shard-download.js +359 -0
  34. package/dist/core/bitmap-shard-resolver.d.ts +129 -0
  35. package/dist/core/bitmap-shard-resolver.js +262 -0
  36. package/dist/core/bitmap-shards.d.ts +70 -0
  37. package/dist/core/bitmap-shards.js +249 -0
  38. package/dist/core/bootstrap.d.ts +37 -0
  39. package/dist/core/bootstrap.js +54 -0
  40. package/dist/core/cache.d.ts +34 -0
  41. package/dist/core/cache.js +96 -0
  42. package/dist/core/context.d.ts +26 -0
  43. package/dist/core/doctor.d.ts +48 -0
  44. package/dist/core/doctor.js +190 -0
  45. package/dist/core/free-download.d.ts +64 -6
  46. package/dist/core/free-download.js +402 -30
  47. package/dist/core/generate.d.ts +29 -0
  48. package/dist/core/generate.js +341 -65
  49. package/dist/core/icon-selection.d.ts +8 -0
  50. package/dist/core/icon-selection.js +19 -0
  51. package/dist/core/install.d.ts +14 -0
  52. package/dist/core/install.js +132 -13
  53. package/dist/core/library-update.d.ts +5 -1
  54. package/dist/core/library-update.js +176 -17
  55. package/dist/core/local-test-env.d.ts +24 -0
  56. package/dist/core/local-test-env.js +43 -0
  57. package/dist/core/metadata-sync.d.ts +30 -0
  58. package/dist/core/metadata-sync.js +97 -0
  59. package/dist/core/pro-download.d.ts +63 -0
  60. package/dist/core/pro-download.js +247 -27
  61. package/dist/core/pro-install.d.ts +11 -3
  62. package/dist/core/pro-install.js +181 -19
  63. package/dist/core/pro-resources.d.ts +79 -0
  64. package/dist/core/pro-resources.js +204 -0
  65. package/dist/core/pro-selected-download.d.ts +18 -0
  66. package/dist/core/pro-selected-download.js +269 -0
  67. package/dist/core/release-descriptor.d.ts +42 -1
  68. package/dist/core/release-descriptor.js +117 -9
  69. package/dist/core/resource-variant.d.ts +27 -10
  70. package/dist/core/resource-variant.js +75 -34
  71. package/dist/core/selected-resources.d.ts +59 -0
  72. package/dist/core/selected-resources.js +350 -0
  73. package/dist/core/signed-artifact.d.ts +8 -1
  74. package/dist/core/signed-artifact.js +35 -11
  75. package/dist/core/target-subtree.d.ts +8 -0
  76. package/dist/core/target-subtree.js +54 -3
  77. package/dist/core/version-service.d.ts +11 -0
  78. package/dist/core/version-service.js +129 -24
  79. package/dist/core/wizard.d.ts +20 -17
  80. package/dist/core/wizard.js +66 -28
  81. package/dist/errors/errors.d.ts +2 -1
  82. package/dist/errors/errors.js +1 -0
  83. package/dist/generator/generate.d.ts +1 -1
  84. package/dist/generator/generate.js +420 -213
  85. package/dist/generator/shared/SOURCE.json +7 -0
  86. package/dist/generator/shared/svg-model.cjs +156 -0
  87. package/dist/generator/svg-model.d.ts +16 -0
  88. package/dist/generator/svg-model.js +4 -0
  89. package/dist/generator/theme-resolve.d.ts +2 -0
  90. package/dist/generator/theme-resolve.js +57 -26
  91. package/dist/mcp/server.d.ts +23 -36
  92. package/dist/mcp/server.js +74 -152
  93. package/dist/mcp/services.d.ts +4 -0
  94. package/dist/mcp/services.js +230 -0
  95. package/dist/metadata/archive.d.ts +38 -0
  96. package/dist/metadata/archive.js +94 -0
  97. package/dist/metadata/manifest.d.ts +33 -0
  98. package/dist/metadata/manifest.js +136 -0
  99. package/dist/metadata/version.d.ts +22 -0
  100. package/dist/metadata/version.js +53 -0
  101. package/dist/project/anchors/application.d.ts +13 -0
  102. package/dist/project/anchors/application.js +377 -0
  103. package/dist/project/anchors/apply.d.ts +28 -0
  104. package/dist/project/anchors/apply.js +65 -0
  105. package/dist/project/anchors/diagnose.d.ts +26 -0
  106. package/dist/project/anchors/diagnose.js +90 -0
  107. package/dist/project/anchors/helpers.d.ts +20 -0
  108. package/dist/project/anchors/helpers.js +55 -0
  109. package/dist/project/anchors/moeicons-config.d.ts +21 -0
  110. package/dist/project/anchors/moeicons-config.js +83 -0
  111. package/dist/project/anchors/parse.d.ts +14 -0
  112. package/dist/project/anchors/parse.js +36 -0
  113. package/dist/project/anchors/project-manifest.d.ts +19 -0
  114. package/dist/project/anchors/project-manifest.js +109 -0
  115. package/dist/project/anchors/style.d.ts +12 -0
  116. package/dist/project/anchors/style.js +87 -0
  117. package/dist/project/anchors/types.d.ts +32 -0
  118. package/dist/project/anchors/types.js +9 -0
  119. package/dist/project/config.d.ts +59 -5
  120. package/dist/project/config.js +234 -102
  121. package/dist/project/install-metadata.d.ts +23 -2
  122. package/dist/project/install-metadata.js +58 -7
  123. package/dist/project/install.d.ts +24 -4
  124. package/dist/project/install.js +402 -190
  125. package/dist/project/project-lock.d.ts +2 -2
  126. package/dist/project/project-lock.js +4 -4
  127. package/dist/project/tailwind.d.ts +2 -2
  128. package/dist/project/tailwind.js +3 -4
  129. package/dist/project/tar-gz.d.ts +18 -1
  130. package/dist/project/tar-gz.js +40 -3
  131. package/dist/tui/components.d.ts +62 -0
  132. package/dist/tui/components.js +100 -0
  133. package/dist/tui/primitives.d.ts +1 -0
  134. package/dist/tui/primitives.js +8 -1
  135. package/dist/ui/banner.d.ts +31 -3
  136. package/dist/ui/banner.js +120 -25
  137. package/dist/ui/branded-prompts.d.ts +41 -0
  138. package/dist/ui/branded-prompts.js +80 -0
  139. package/dist/ui/clack.d.ts +6 -3
  140. package/dist/ui/clack.js +56 -5
  141. package/dist/ui/create-ui.d.ts +1 -0
  142. package/dist/ui/create-ui.js +5 -1
  143. package/dist/ui/generated/wordmark-large.d.ts +17 -0
  144. package/dist/ui/generated/wordmark-large.js +28 -0
  145. package/dist/ui/generated/wordmark.d.ts +2 -0
  146. package/dist/ui/generated/wordmark.js +2 -0
  147. package/dist/ui/non-interactive.js +8 -0
  148. package/dist/ui/stream.js +30 -0
  149. package/dist/ui/theme.d.ts +42 -0
  150. package/dist/ui/theme.js +59 -0
  151. package/package.json +14 -6
@@ -1,5 +1,7 @@
1
1
  import type { ReleaseTarget, ReleaseTargetMetadata } from "./release-descriptor.js";
2
2
  import type { Target } from "../commands/parser.js";
3
+ import type { MoeiconsConfigFile } from "../project/config.js";
4
+ import { type IconCatalog } from "../catalog/catalog.js";
3
5
  /**
4
6
  * Select the target subtree the CLI must install. Mirrors the archive layout
5
7
  * frozen in the code-library contract: each tier archive contains
@@ -27,6 +29,12 @@ export type TargetSubtreeFailure = {
27
29
  readonly message: string;
28
30
  };
29
31
  export type TargetSubtreeResult = TargetSubtreeSelection | TargetSubtreeFailure;
32
+ /** Keep only the component modules reachable from configured icon proxies.
33
+ * The full target subtree must be verified against its release descriptor
34
+ * before this projection is called; this is a local installation filter, not
35
+ * a replacement for the immutable archive digest.
36
+ */
37
+ export declare function configuredComponentFiles(files: Readonly<Record<string, Uint8Array>>, target: Target, config: MoeiconsConfigFile | undefined, catalog: IconCatalog): Readonly<Record<string, Uint8Array>>;
30
38
  /**
31
39
  * Deterministic subtree hash over subtree-relative files. Files and directories
32
40
  * are visited depth-first sorted by `localeCompare`, and each file contributes
@@ -1,5 +1,56 @@
1
1
  import { createHash } from "node:crypto";
2
- import { extractTarGz } from "../project/tar-gz.js";
2
+ import { extractTarGz, ICON_ARCHIVE_MAX_ENTRIES, ICON_ARCHIVE_MAX_EXPANDED_BYTES } from "../project/tar-gz.js";
3
+ import { posix } from "node:path";
4
+ import { toProxyName } from "./icon-names.js";
5
+ import { findCatalogStyleGroup } from "../catalog/catalog.js";
6
+ import { resolveIconTheme } from "./icon-selection.js";
7
+ /** Keep only the component modules reachable from configured icon proxies.
8
+ * The full target subtree must be verified against its release descriptor
9
+ * before this projection is called; this is a local installation filter, not
10
+ * a replacement for the immutable archive digest.
11
+ */
12
+ export function configuredComponentFiles(files, target, config, catalog) {
13
+ if (!config || (target !== "react" && target !== "vue"))
14
+ return files;
15
+ const selected = new Set();
16
+ const visit = (file) => {
17
+ if (selected.has(file))
18
+ return;
19
+ const bytes = files[file];
20
+ if (!bytes)
21
+ throw new Error(`verified ${target} archive is missing configured module: ${file}`);
22
+ selected.add(file);
23
+ const source = Buffer.from(bytes).toString("utf8");
24
+ const imports = /(?:\b(?:import|export)\s+(?:[^'";]*?\s+from\s*)?|\bimport\s*\()\s*["'](\.[^"']+)["']/g;
25
+ for (const match of source.matchAll(imports)) {
26
+ const specifier = match[1];
27
+ if (!specifier)
28
+ continue;
29
+ const resolved = posix.normalize(posix.join(posix.dirname(file), specifier));
30
+ if (resolved.startsWith("../") || resolved === "..")
31
+ throw new Error(`component import escapes target: ${file} → ${specifier}`);
32
+ const dependency = files[resolved] ? resolved : files[`${resolved}.d.ts`] ? `${resolved}.d.ts` : undefined;
33
+ if (!dependency)
34
+ throw new Error(`verified ${target} archive is missing dependency: ${file} → ${specifier}`);
35
+ visit(dependency);
36
+ }
37
+ };
38
+ visit("types.d.ts");
39
+ for (const iconId of config.icons) {
40
+ for (const theme of Object.keys(config.themes)) {
41
+ const actual = resolveIconTheme(config, catalog, theme, iconId);
42
+ if (!actual)
43
+ throw new Error(`icon "${iconId}" has no selected variant for theme "${theme}"`);
44
+ const group = config.themes[actual].styleGroup;
45
+ if (findCatalogStyleGroup(group, catalog)?.type === "bitmap")
46
+ continue;
47
+ const basename = `${group}/${toProxyName(iconId)}${target === "vue" ? ".vue" : ""}`;
48
+ visit(`${basename}.js`);
49
+ visit(`${basename}.d.ts`);
50
+ }
51
+ }
52
+ return Object.fromEntries([...selected].sort().map((file) => [file, files[file]]));
53
+ }
3
54
  /**
4
55
  * Deterministic subtree hash over subtree-relative files. Files and directories
5
56
  * are visited depth-first sorted by `localeCompare`, and each file contributes
@@ -65,8 +116,8 @@ function hasModernTargetContract(source) {
65
116
  */
66
117
  export function selectTargetSubtree(archiveBytes, source, target) {
67
118
  const extracted = extractTarGz(archiveBytes, {
68
- maxEntries: 20_000,
69
- maxExpandedBytes: 64 * 1024 * 1024,
119
+ maxEntries: ICON_ARCHIVE_MAX_ENTRIES,
120
+ maxExpandedBytes: ICON_ARCHIVE_MAX_EXPANDED_BYTES,
70
121
  });
71
122
  if (extracted.errors.length > 0) {
72
123
  return { ok: false, reason: "validation", message: extracted.errors[0] ?? "invalid archive" };
@@ -1,7 +1,16 @@
1
+ /**
2
+ * Test seam (candidate acceptance): `MOEICONS_LIBRARY_VERSIONS_URL` overrides
3
+ * the public versions endpoint. Only an https URL or a loopback http URL is
4
+ * accepted, so production behavior is unchanged.
5
+ */
6
+ export declare function resolveVersionsEndpoint(env: Readonly<Record<string, string | undefined>>): string;
1
7
  export interface PublicTierVersion {
2
8
  readonly version: string;
3
9
  readonly releasedAt: string;
4
10
  readonly descriptorSha256: string;
11
+ /** A-1b: present only for an accepted local-test candidate. */
12
+ readonly channel?: "local-test";
13
+ readonly publishable?: boolean;
5
14
  }
6
15
  export interface PublicLibraryVersions {
7
16
  readonly schemaVersion: 1;
@@ -12,6 +21,8 @@ export declare function fetchLibraryVersions(deps?: {
12
21
  fetch?: typeof fetch;
13
22
  signal?: AbortSignal;
14
23
  timeoutMs?: number;
24
+ env?: Readonly<Record<string, string | undefined>>;
25
+ allowLocalTest?: boolean;
15
26
  }): Promise<PublicLibraryVersions>;
16
27
  export declare function fetchMoeiconsVersions(deps?: {
17
28
  fetch?: typeof fetch;
@@ -3,19 +3,93 @@ import { parseVersion } from "./update-policy.js";
3
3
  const LIBRARY_VERSIONS_URL = "https://api.moeicons.com/v1/icon-library/versions";
4
4
  const NPM_PACKAGE_URL = "https://registry.npmjs.org/@moewolf%2fmoe-icons-cli";
5
5
  const SHA256 = /^[a-f0-9]{64}$/;
6
+ /** A-1b: local-test candidate versions; only accepted with an explicit local context. */
7
+ const LOCAL_TEST_VERSION = /^\d+\.\d+\.\d+-test$/;
6
8
  const CLI_VERSION_CACHE_TTL_MS = 5 * 60 * 1_000;
7
9
  let cliVersionCache;
8
- async function fixedFetch(url, fetchFn, signal, timeoutMs = 5_000) {
10
+ /**
11
+ * Test seam (candidate acceptance): `MOEICONS_LIBRARY_VERSIONS_URL` overrides
12
+ * the public versions endpoint. Only an https URL or a loopback http URL is
13
+ * accepted, so production behavior is unchanged.
14
+ */
15
+ export function resolveVersionsEndpoint(env) {
16
+ const override = env.MOEICONS_LIBRARY_VERSIONS_URL;
17
+ if (!override)
18
+ return LIBRARY_VERSIONS_URL;
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(override);
22
+ }
23
+ catch {
24
+ throw new CliError("VALIDATION_ERROR", "MOEICONS_LIBRARY_VERSIONS_URL is not a valid URL");
25
+ }
26
+ const loopback = parsed.protocol === "http:" && ["127.0.0.1", "localhost", "::1"].includes(parsed.hostname);
27
+ if (parsed.protocol !== "https:" && !loopback) {
28
+ throw new CliError("VALIDATION_ERROR", "MOEICONS_LIBRARY_VERSIONS_URL must be https or loopback http");
29
+ }
30
+ return override;
31
+ }
32
+ async function fixedJson(url, fetchFn, maxBytes, signal, timeoutMs = 5_000) {
9
33
  const controller = new AbortController();
10
- const timer = setTimeout(() => controller.abort(), timeoutMs);
11
- const abort = () => controller.abort();
12
- if (signal?.aborted)
34
+ let reader;
35
+ let rejectAbort = () => { };
36
+ const interrupted = new Promise((_resolve, reject) => {
37
+ rejectAbort = reject;
38
+ });
39
+ const abort = () => {
13
40
  controller.abort();
41
+ void reader?.cancel().catch(() => { });
42
+ rejectAbort(new CliError(signal?.aborted ? "CANCELLED" : "NETWORK_ERROR", signal?.aborted ? "version check cancelled" : "version check timed out"));
43
+ };
44
+ const timer = setTimeout(abort, timeoutMs);
14
45
  signal?.addEventListener("abort", abort, { once: true });
46
+ const operation = async () => {
47
+ if (signal?.aborted)
48
+ throw new CliError("CANCELLED", "version check cancelled");
49
+ const response = await fetchFn(url, {
50
+ method: "GET",
51
+ headers: { accept: "application/json" },
52
+ signal: controller.signal,
53
+ redirect: "error",
54
+ });
55
+ if (!response.ok) {
56
+ void response.body?.cancel().catch(() => { });
57
+ throw new CliError("NETWORK_ERROR", `version check failed with ${response.status}`);
58
+ }
59
+ const declared = response.headers.get("content-length");
60
+ if (declared && (!/^\d+$/.test(declared) || Number(declared) > maxBytes)) {
61
+ void response.body?.cancel().catch(() => { });
62
+ throw new CliError("VALIDATION_ERROR", "version response exceeds size limit");
63
+ }
64
+ if (!response.body)
65
+ throw new CliError("VALIDATION_ERROR", "empty version response");
66
+ reader = response.body.getReader();
67
+ const chunks = [];
68
+ let total = 0;
69
+ for (;;) {
70
+ const part = await reader.read();
71
+ if (part.done)
72
+ break;
73
+ total += part.value.byteLength;
74
+ if (total > maxBytes) {
75
+ void reader.cancel().catch(() => { });
76
+ throw new CliError("VALIDATION_ERROR", "version response exceeds size limit");
77
+ }
78
+ chunks.push(part.value);
79
+ }
80
+ try {
81
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
82
+ }
83
+ catch {
84
+ throw new CliError("VALIDATION_ERROR", "invalid version response JSON");
85
+ }
86
+ };
15
87
  try {
16
- return await fetchFn(url, { method: "GET", headers: { accept: "application/json" }, signal: controller.signal, redirect: "error" });
88
+ return await Promise.race([operation(), interrupted]);
17
89
  }
18
- catch {
90
+ catch (error) {
91
+ if (error instanceof CliError)
92
+ throw error;
19
93
  throw new CliError(signal?.aborted ? "CANCELLED" : "NETWORK_ERROR", signal?.aborted ? "version check cancelled" : "version check failed");
20
94
  }
21
95
  finally {
@@ -23,34 +97,61 @@ async function fixedFetch(url, fetchFn, signal, timeoutMs = 5_000) {
23
97
  signal?.removeEventListener("abort", abort);
24
98
  }
25
99
  }
26
- function tier(value) {
100
+ function tier(value, allowLocalTest) {
27
101
  if (value === null)
28
102
  return null;
29
103
  if (typeof value !== "object" || value === null || Array.isArray(value))
30
104
  return undefined;
31
105
  const item = value;
32
- return typeof item.version === "string" && parseVersion(item.version) && typeof item.releasedAt === "string" &&
33
- /^\d{4}-\d{2}-\d{2}T.*Z$/.test(item.releasedAt) && typeof item.descriptorSha256 === "string" && SHA256.test(item.descriptorSha256)
34
- ? item : undefined;
106
+ const baseValid = typeof item.version === "string" &&
107
+ typeof item.releasedAt === "string" &&
108
+ /^\d{4}-\d{2}-\d{2}T.*Z$/.test(item.releasedAt) &&
109
+ typeof item.descriptorSha256 === "string" &&
110
+ SHA256.test(item.descriptorSha256);
111
+ if (!baseValid)
112
+ return undefined;
113
+ const version = item.version;
114
+ if (parseVersion(version)) {
115
+ // A formal (stable/alpha/beta) version must never carry the local-test marker.
116
+ if (item.channel !== undefined || item.publishable !== undefined)
117
+ return undefined;
118
+ return item;
119
+ }
120
+ // A-1b: an `X.Y.Z-test` version is only accepted when the caller proved a
121
+ // local-only context and the release explicitly declares the local-test model.
122
+ if (allowLocalTest &&
123
+ LOCAL_TEST_VERSION.test(version) &&
124
+ item.channel === "local-test" &&
125
+ item.publishable === false) {
126
+ return item;
127
+ }
128
+ return undefined;
35
129
  }
36
130
  export async function fetchLibraryVersions(deps = {}) {
37
- const response = await fixedFetch(LIBRARY_VERSIONS_URL, deps.fetch ?? fetch, deps.signal, deps.timeoutMs);
38
- if (!response.ok)
39
- throw new CliError("NETWORK_ERROR", `version check failed with ${response.status}`);
40
- const value = await response.json();
41
- const free = tier(value.free);
42
- const pro = tier(value.pro);
43
- if (value.schemaVersion !== 1 || free === undefined || pro === undefined || Object.keys(value).some((key) => !["schemaVersion", "free", "pro"].includes(key))) {
131
+ const url = resolveVersionsEndpoint(deps.env ?? {});
132
+ const allowLocalTest = deps.allowLocalTest === true;
133
+ const raw = await fixedJson(url, deps.fetch ?? fetch, 32 * 1024, deps.signal, deps.timeoutMs);
134
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
135
+ throw new CliError("VALIDATION_ERROR", "invalid icon library versions response");
136
+ const value = raw;
137
+ const free = tier(value.free, allowLocalTest);
138
+ const pro = tier(value.pro, allowLocalTest);
139
+ if (value.schemaVersion !== 1 ||
140
+ free === undefined ||
141
+ pro === undefined ||
142
+ Object.keys(value).some((key) => !["schemaVersion", "free", "pro"].includes(key))) {
44
143
  throw new CliError("VALIDATION_ERROR", "invalid icon library versions response");
45
144
  }
46
145
  return { schemaVersion: 1, free, pro };
47
146
  }
48
147
  export async function fetchMoeiconsVersions(deps = {}) {
49
- const response = await fixedFetch(NPM_PACKAGE_URL, deps.fetch ?? fetch, deps.signal, deps.timeoutMs);
50
- if (!response.ok)
51
- throw new CliError("NETWORK_ERROR", `CLI version check failed with ${response.status}`);
52
- const value = await response.json();
53
- if (typeof value.versions !== "object" || value.versions === null || Array.isArray(value.versions))
148
+ const raw = await fixedJson(NPM_PACKAGE_URL, deps.fetch ?? fetch, 8 * 1024 * 1024, deps.signal, deps.timeoutMs);
149
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
150
+ throw new CliError("VALIDATION_ERROR", "invalid npm package metadata");
151
+ const value = raw;
152
+ if (typeof value.versions !== "object" ||
153
+ value.versions === null ||
154
+ Array.isArray(value.versions))
54
155
  throw new CliError("VALIDATION_ERROR", "invalid npm package metadata");
55
156
  return Object.keys(value.versions).filter((version) => parseVersion(version));
56
157
  }
@@ -66,6 +167,10 @@ export function latestInChannel(current, versions) {
66
167
  const parsed = parseVersion(current);
67
168
  if (!parsed)
68
169
  return undefined;
69
- return versions.map((version) => ({ version, parsed: parseVersion(version) })).filter((item) => item.parsed?.channel === parsed.channel)
70
- .sort((a, b) => (b.parsed.major - a.parsed.major) || (b.parsed.minor - a.parsed.minor) || (b.parsed.patch - a.parsed.patch))[0]?.version;
170
+ return versions
171
+ .map((version) => ({ version, parsed: parseVersion(version) }))
172
+ .filter((item) => item.parsed?.channel === parsed.channel)
173
+ .sort((a, b) => b.parsed.major - a.parsed.major ||
174
+ b.parsed.minor - a.parsed.minor ||
175
+ b.parsed.patch - a.parsed.patch)[0]?.version;
71
176
  }
@@ -1,4 +1,4 @@
1
- import type { CommandContext } from "./context.js";
1
+ import type { CommandContext, UiChoice } from "./context.js";
2
2
  export type WizardResult = {
3
3
  readonly ok: true;
4
4
  readonly action: "json-hint";
@@ -12,6 +12,9 @@ export type WizardResult = {
12
12
  readonly ok: true;
13
13
  readonly action: "pending";
14
14
  readonly flow: "login";
15
+ } | {
16
+ readonly ok: true;
17
+ readonly action: "pro-resources";
15
18
  } | {
16
19
  readonly ok: true;
17
20
  readonly action: "manage";
@@ -20,30 +23,30 @@ export type WizardResult = {
20
23
  readonly ok: true;
21
24
  readonly action: "settings";
22
25
  readonly flow: "logout" | "cli-update";
26
+ } | {
27
+ readonly ok: true;
28
+ readonly action: "back";
29
+ } | {
30
+ readonly ok: true;
31
+ readonly action: "exit";
32
+ readonly via: "menu" | "cancel";
23
33
  } | {
24
34
  readonly ok: false;
25
35
  readonly reason: "cancelled";
26
36
  };
27
37
  export type WizardSessionState = "authenticated" | "signed-out" | "unknown";
28
- export declare function homeChoices(session: WizardSessionState): readonly [{
29
- readonly value: "pro";
30
- readonly label: "Install moeicons pro";
31
- }, {
32
- readonly value: "free";
33
- readonly label: "Install moeicons free";
34
- }, {
35
- readonly value: "manage";
36
- readonly label: "Manage project icons";
37
- }, ...{
38
- value: string;
39
- label: string;
40
- }[], {
41
- readonly value: "settings";
42
- readonly label: "Settings";
43
- }];
38
+ export declare function homeChoices(session: WizardSessionState, proResourcesLabel?: string): UiChoice[];
39
+ export declare function settingsChoices(session: WizardSessionState): UiChoice[];
40
+ export declare function manageChoices(status?: string): UiChoice[];
41
+ export declare function targetChoices(): UiChoice[];
42
+ export declare function loginRecoveryChoices(): UiChoice[];
44
43
  /** Wizard state machine. No Clack/Commander/process imports. */
44
+ export declare const HOME_MESSAGE = "Choose an option";
45
45
  export declare function runWizardUseCase(context: CommandContext, options: {
46
46
  readonly json: boolean;
47
47
  readonly session?: WizardSessionState;
48
48
  readonly getLibraryStatus?: () => Promise<string>;
49
+ readonly getProResourceLabel?: () => Promise<string | undefined>;
50
+ /** W3-E2: optional single-line recent-task status shown above the menu. */
51
+ readonly historyLine?: string;
49
52
  }): Promise<WizardResult>;
@@ -1,10 +1,15 @@
1
1
  import { detectProject } from "../project/detect.js";
2
2
  import { CliError } from "../errors/index.js";
3
3
  const JSON_HINT = "interactive wizard unavailable in JSON mode; use install/login/account/groups/generate";
4
- export function homeChoices(session) {
4
+ const BACK = { value: "back", label: "Back", separatorBefore: true };
5
+ const EXIT = { value: "exit", label: "Exit" };
6
+ export function homeChoices(session, proResourcesLabel) {
5
7
  return [
6
8
  { value: "pro", label: "Install moeicons pro" },
7
9
  { value: "free", label: "Install moeicons free" },
10
+ ...(session === "authenticated" && proResourcesLabel
11
+ ? [{ value: "pro-resources", label: proResourcesLabel }]
12
+ : []),
8
13
  { value: "manage", label: "Manage project icons" },
9
14
  ...(session === "authenticated"
10
15
  ? []
@@ -15,56 +20,89 @@ export function homeChoices(session) {
15
20
  },
16
21
  ]),
17
22
  { value: "settings", label: "Settings" },
23
+ EXIT,
24
+ ];
25
+ }
26
+ export function settingsChoices(session) {
27
+ return [
28
+ ...(session === "authenticated" ? [{ value: "logout", label: "Log out" }] : []),
29
+ { value: "cli-update", label: "Check for CLI updates" },
30
+ BACK,
31
+ ];
32
+ }
33
+ export function manageChoices(status) {
34
+ return [
35
+ { value: "reload", label: "Update project resources" },
36
+ {
37
+ value: "library-update",
38
+ label: `Update icon library version${status ? ` — ${status}` : ""}`,
39
+ },
40
+ BACK,
41
+ ];
42
+ }
43
+ export function targetChoices() {
44
+ return [
45
+ { value: "react", label: "React" },
46
+ { value: "vue", label: "Vue" },
47
+ { value: "vanilla", label: "Vanilla" },
48
+ { value: "assets", label: "Static assets" },
49
+ BACK,
50
+ ];
51
+ }
52
+ export function loginRecoveryChoices() {
53
+ return [
54
+ { value: "retry", label: "Retry login" },
55
+ BACK,
18
56
  ];
19
57
  }
20
58
  /** Wizard state machine. No Clack/Commander/process imports. */
59
+ export const HOME_MESSAGE = "Choose an option";
21
60
  export async function runWizardUseCase(context, options) {
22
61
  if (options.json) {
23
62
  return { ok: true, action: "json-hint", message: JSON_HINT };
24
63
  }
25
- const choice = await context.ui.select("Choose an option", homeChoices(options.session ?? "signed-out"), context.signal);
64
+ const session = options.session ?? "signed-out";
65
+ const proLabel = session === "authenticated" ? await options.getProResourceLabel?.().catch(() => undefined) : undefined;
66
+ const message = options.historyLine ? `${options.historyLine}\n\n${HOME_MESSAGE}` : HOME_MESSAGE;
67
+ const choice = await context.ui.select(message, homeChoices(session, proLabel), context.signal);
68
+ // Esc on home == Exit (cancel path); explicit Exit menu item uses via=menu.
26
69
  if (choice === undefined)
27
- return { ok: false, reason: "cancelled" };
70
+ return { ok: true, action: "exit", via: "cancel" };
71
+ if (choice === "exit")
72
+ return { ok: true, action: "exit", via: "menu" };
28
73
  if (choice === "login")
29
74
  return { ok: true, action: "pending", flow: "login" };
75
+ if (choice === "pro-resources")
76
+ return { ok: true, action: "pro-resources" };
30
77
  if (choice === "settings") {
31
- const setting = await context.ui.select("Settings", [
32
- ...(options.session === "authenticated" ? [{ value: "logout", label: "Log out" }] : []),
33
- { value: "cli-update", label: "Check for CLI updates" },
34
- ], context.signal);
35
- return setting === undefined
36
- ? { ok: false, reason: "cancelled" }
37
- : { ok: true, action: "settings", flow: setting };
78
+ const setting = await context.ui.select("Settings", settingsChoices(session), context.signal);
79
+ if (setting === undefined || setting === "back")
80
+ return { ok: true, action: "back" };
81
+ return { ok: true, action: "settings", flow: setting };
38
82
  }
39
83
  const project = detectProject(context.cwd);
40
84
  if (!project)
41
85
  throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents; run inside a project");
42
86
  if (choice === "manage") {
43
87
  const status = await options.getLibraryStatus?.();
44
- const management = await context.ui.select("Manage project icons", [
45
- { value: "reload", label: "Update project resources" },
46
- {
47
- value: "library-update",
48
- label: `Update icon library version${status ? ` — ${status}` : ""}`,
49
- },
50
- ], context.signal);
51
- return management === undefined
52
- ? { ok: false, reason: "cancelled" }
53
- : { ok: true, action: "manage", flow: management };
88
+ const management = await context.ui.select("Manage project icons", manageChoices(status), context.signal);
89
+ if (management === undefined || management === "back")
90
+ return { ok: true, action: "back" };
91
+ return { ok: true, action: "manage", flow: management };
54
92
  }
55
93
  if (choice === "free" || choice === "pro") {
56
- const target = await context.ui.select("Choose an output target", [
57
- { value: "react", label: "React" },
58
- { value: "vue", label: "Vue" },
59
- { value: "vanilla", label: "Vanilla" },
60
- { value: "assets", label: "Static assets" },
61
- ], context.signal);
94
+ const target = await context.ui.select("Choose an output target", targetChoices(), context.signal);
95
+ if (target === undefined || target === "back")
96
+ return { ok: true, action: "back" };
62
97
  if (target !== "react" && target !== "vue" && target !== "vanilla" && target !== "assets") {
63
- return { ok: false, reason: "cancelled" };
98
+ return { ok: true, action: "back" };
64
99
  }
65
100
  const confirmed = await context.ui.confirm(`Install ${choice} ${target} into ${project.root}?`, context.signal);
66
- if (confirmed !== true)
101
+ // Esc / Ctrl+C on confirm exits; No returns to home (back)
102
+ if (confirmed === undefined)
67
103
  return { ok: false, reason: "cancelled" };
104
+ if (confirmed !== true)
105
+ return { ok: true, action: "back" };
68
106
  return { ok: true, action: "install", group: choice, target };
69
107
  }
70
108
  throw new CliError("VALIDATION_ERROR", `unknown wizard choice: ${choice}`);
@@ -3,7 +3,7 @@
3
3
  * 0 success/cancel, 1 validation error, 2 auth error, 3 network error,
4
4
  * 4 not found, 5 unexpected.
5
5
  */
6
- export type CliErrorCode = "HELP" | "VERSION" | "CANCELLED" | "VALIDATION_ERROR" | "TAILWIND_VERSION_UNSUPPORTED" | "AUTH_ERROR" | "NETWORK_ERROR" | "NOT_FOUND" | "FORBIDDEN" | "NOT_TTY" | "NOT_IMPLEMENTED" | "UNEXPECTED";
6
+ export type CliErrorCode = "HELP" | "VERSION" | "CANCELLED" | "VALIDATION_ERROR" | "TAILWIND_VERSION_UNSUPPORTED" | "AUTH_ERROR" | "NETWORK_ERROR" | "NOT_FOUND" | "FORBIDDEN" | "NOT_TTY" | "NOT_IMPLEMENTED" | "DISK_FULL" | "UNEXPECTED";
7
7
  /** Frozen CliErrorCode → process exit code. Register new codes here before use. */
8
8
  export declare const CLI_ERROR_EXIT_MAP: {
9
9
  readonly HELP: 0;
@@ -17,6 +17,7 @@ export declare const CLI_ERROR_EXIT_MAP: {
17
17
  readonly FORBIDDEN: 2;
18
18
  readonly NETWORK_ERROR: 3;
19
19
  readonly NOT_FOUND: 4;
20
+ readonly DISK_FULL: 5;
20
21
  readonly UNEXPECTED: 5;
21
22
  };
22
23
  export type JsonErrorBody = {
@@ -16,6 +16,7 @@ export const CLI_ERROR_EXIT_MAP = {
16
16
  FORBIDDEN: 2,
17
17
  NETWORK_ERROR: 3,
18
18
  NOT_FOUND: 4,
19
+ DISK_FULL: 5,
19
20
  UNEXPECTED: 5,
20
21
  };
21
22
  export class CliError extends Error {
@@ -6,7 +6,7 @@ export { toLibraryExportName, toProxyName };
6
6
  export declare const toPascalCase: typeof toProxyName;
7
7
  /**
8
8
  * React/Vue proxy generation. Purely returns owned file paths/content for
9
- * types, registry, provider/plugin, proxies and barrel exports. Rejects
9
+ * types, provider/plugin, per-icon proxies and barrel exports. Rejects
10
10
  * duplicate PascalCase names and missing fallback. Bitmap themes use local
11
11
  * img wrappers + static asset imports (G1/G4); SVG themes import moe-icons.
12
12
  */