@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
@@ -0,0 +1,65 @@
1
+ import { realpathSync, readdirSync, copyFileSync } from "node:fs";
2
+ import { executeManagedReconcile, safeManagedPath } from "../install.js";
3
+ export function applyPlannedChanges(projectRoot, changes, fs_) {
4
+ const seen = new Map();
5
+ const foldedPaths = new Map();
6
+ for (const change of changes) {
7
+ if (seen.has(change.path))
8
+ return { ok: false, message: `duplicate planned path: ${change.path}` };
9
+ const folded = change.path.toLowerCase();
10
+ const collision = foldedPaths.get(folded);
11
+ if (collision)
12
+ return { ok: false, message: `case-insensitive planned path collision: ${collision}, ${change.path}` };
13
+ foldedPaths.set(folded, change.path);
14
+ seen.set(change.path, change);
15
+ }
16
+ for (const [folded, path] of foldedPaths) {
17
+ let parent = folded;
18
+ while (parent.includes("/")) {
19
+ parent = parent.slice(0, parent.lastIndexOf("/"));
20
+ const collision = foldedPaths.get(parent);
21
+ if (collision)
22
+ return { ok: false, message: `planned file and directory paths collide: ${collision}, ${path}` };
23
+ }
24
+ }
25
+ const entries = [...seen.values()];
26
+ const root = realpathSync(projectRoot);
27
+ const targetFor = (path) => safeManagedPath(root, path).target;
28
+ let pending;
29
+ try {
30
+ pending = entries.filter((entry) => {
31
+ const full = targetFor(entry.path);
32
+ const current = fs_.existsSync(full) ? fs_.readTextFileSync(full) : undefined;
33
+ if (current === entry.after)
34
+ return false;
35
+ if (current !== entry.before)
36
+ throw new Error(`file changed since planning: ${entry.path}`);
37
+ return true;
38
+ });
39
+ if (!pending.length)
40
+ return { ok: true, alreadyConfigured: true, written: [] };
41
+ }
42
+ catch (error) {
43
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
44
+ }
45
+ const writes = Object.fromEntries(pending.map((entry) => [entry.path, entry.after]));
46
+ const expectedText = Object.fromEntries(pending.map((entry) => [entry.path, entry.before]));
47
+ const transactionFs = {
48
+ existsSync: fs_.existsSync,
49
+ readFileSync: ((path) => fs_.readTextFileSync(path)),
50
+ mkdirSync: fs_.mkdirSync,
51
+ writeFileSync: ((path, content) => fs_.writeTextFileSync(path, String(content))),
52
+ renameSync: fs_.renameSync,
53
+ rmSync: fs_.rmSync,
54
+ readdirSync,
55
+ copyFileSync,
56
+ };
57
+ try {
58
+ executeManagedReconcile(root, writes, [], transactionFs, { expectedText });
59
+ }
60
+ catch (error) {
61
+ const reason = error instanceof Error ? error.message : String(error);
62
+ return { ok: false, message: reason.includes("original files retained at") ? `recovery incomplete; ${reason}` : reason };
63
+ }
64
+ return { ok: true, alreadyConfigured: false, written: pending.map((e) => e.path) };
65
+ }
@@ -0,0 +1,26 @@
1
+ import type { IconCatalog } from "../../catalog/catalog.js";
2
+ import type { DiagnosticReport, PlannedFileChange } from "./types.js";
3
+ import { type DetectorIo } from "./helpers.js";
4
+ /**
5
+ * Four-anchor orchestrator (E2E-B2..B5). Runs the fixed probe order and returns
6
+ * a stable DiagnosticReport. This function is read-only: it never writes. Callers
7
+ * decide how to render or apply `fixes`.
8
+ */
9
+ export interface DiagnoseOptions {
10
+ readonly cwd: string;
11
+ readonly sourceCatalog?: IconCatalog | undefined;
12
+ readonly io?: DetectorIo;
13
+ /** Confirmed config integration paths, when previously stored in config. */
14
+ readonly confirmed?: {
15
+ readonly adapter?: string;
16
+ readonly entry?: string;
17
+ readonly style?: string;
18
+ };
19
+ }
20
+ export interface DiagnoseOutcome {
21
+ readonly ok: boolean;
22
+ readonly report: DiagnosticReport;
23
+ /** Combined, deduplicated, ordered fixes across anchors. */
24
+ readonly fixes: readonly PlannedFileChange[];
25
+ }
26
+ export declare function diagnoseProject(options: DiagnoseOptions): DiagnoseOutcome;
@@ -0,0 +1,90 @@
1
+ import { dirname } from "node:path";
2
+ import { realDetectorIo } from "./helpers.js";
3
+ import { inspectProjectManifest } from "./project-manifest.js";
4
+ import { inspectMoeiconsConfig } from "./moeicons-config.js";
5
+ import { inspectApplicationAnchor } from "./application.js";
6
+ import { inspectStyleAnchor } from "./style.js";
7
+ import { readMoeiconsConfig } from "../config.js";
8
+ export function diagnoseProject(options) {
9
+ const io = options.io ?? realDetectorIo;
10
+ const manifest = inspectProjectManifest({ cwd: options.cwd, io });
11
+ const adapterLine = manifest.evidence.find((line) => line.startsWith("adapter: "));
12
+ const rawAdapter = adapterLine ? adapterLine.slice("adapter: ".length) : undefined;
13
+ const adapter = rawAdapter && rawAdapter !== "unknown" && !rawAdapter.startsWith("ambiguous")
14
+ ? rawAdapter
15
+ : undefined;
16
+ let config;
17
+ let application;
18
+ let style;
19
+ if (manifest.status === "missing" || manifest.status === "invalid") {
20
+ config = {
21
+ kind: "config",
22
+ status: "not-required",
23
+ candidates: [],
24
+ evidence: ["no writable project manifest; config anchor not evaluated"],
25
+ fixes: [],
26
+ };
27
+ application = {
28
+ kind: "application",
29
+ status: "not-required",
30
+ candidates: [],
31
+ evidence: ["no writable project manifest; application anchor not evaluated"],
32
+ fixes: [],
33
+ };
34
+ style = {
35
+ kind: "style",
36
+ status: "not-required",
37
+ candidates: [],
38
+ evidence: ["no writable project manifest; style anchor not evaluated"],
39
+ fixes: [],
40
+ };
41
+ }
42
+ else {
43
+ const root = manifest.path ? dirname(manifest.path) : options.cwd;
44
+ const loaded = readMoeiconsConfig(root, options.sourceCatalog);
45
+ const confirmed = loaded.kind === "ok" ? loaded.config.integration : undefined;
46
+ const effectiveAdapter = options.confirmed?.adapter ?? confirmed?.adapter ?? adapter;
47
+ const confirmedEntry = options.confirmed?.entry ?? confirmed?.entry;
48
+ const confirmedStyle = options.confirmed?.style ?? confirmed?.style;
49
+ const assetsOnly = effectiveAdapter === "assets-only" || loaded.kind === "ok" && loaded.config.target === "assets";
50
+ config = inspectMoeiconsConfig({
51
+ root,
52
+ io,
53
+ sourceCatalog: options.sourceCatalog,
54
+ ...(effectiveAdapter && effectiveAdapter !== "assets-only" ? { adapter: effectiveAdapter } : {}),
55
+ assetsOnly,
56
+ ambiguous: manifest.evidence.some((line) => line.startsWith("adapter ambiguous:")),
57
+ });
58
+ application = inspectApplicationAnchor({
59
+ root,
60
+ io,
61
+ ...(effectiveAdapter ? { adapter: effectiveAdapter } : {}),
62
+ assetsOnly,
63
+ ...(loaded.kind === "ok" ? { outputDir: loaded.config.outputDir } : {}),
64
+ ...(confirmedEntry ? { confirmedEntry } : {}),
65
+ });
66
+ style = inspectStyleAnchor({
67
+ root,
68
+ io,
69
+ assetsOnly,
70
+ ...(confirmedStyle ? { confirmedStyle } : {}),
71
+ });
72
+ }
73
+ const report = {
74
+ ...(manifest.status === "ok" && manifest.path ? { projectRoot: dirname(manifest.path) } : {}),
75
+ anchors: [manifest, config, application, style],
76
+ };
77
+ const seen = new Set();
78
+ const fixes = [];
79
+ for (const anchor of report.anchors) {
80
+ for (const fix of anchor.fixes) {
81
+ const key = `${fix.kind}:${fix.path}`;
82
+ if (seen.has(key))
83
+ continue;
84
+ seen.add(key);
85
+ fixes.push(fix);
86
+ }
87
+ }
88
+ const ok = report.anchors.every((anchor) => anchor.kind === "style" || anchor.status === "ok" || anchor.status === "not-required");
89
+ return { ok, report, fixes };
90
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { AnchorResult, PlannedFileChange } from "./types.js";
3
+ /**
4
+ * Read-only detector io. Detectors never write: they accept only read primitives
5
+ * so a detector cannot be coerced into mutating a project.
6
+ */
7
+ export interface DetectorIo {
8
+ readonly existsSync: typeof existsSync;
9
+ readonly readFileSync: (path: string) => string;
10
+ }
11
+ export declare const realDetectorIo: DetectorIo;
12
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
13
+ /** Read a JSON file safely; returns undefined on parse/io error. */
14
+ export declare function readJsonSafe(io: DetectorIo, file: string): Record<string, unknown> | undefined;
15
+ /** Walk upward from startDir to the nearest directory containing package.json. */
16
+ export declare function findPackageJsonDir(io: DetectorIo, startDir: string): string | undefined;
17
+ /** Package manager precedence with ambiguity -> unknown (never guesses). */
18
+ export declare function detectPackageManager(io: DetectorIo, root: string): string;
19
+ export declare function okResult(kind: AnchorResult["kind"], patch: Partial<AnchorResult>): AnchorResult;
20
+ export declare function createFileFix(path: string, content: string): PlannedFileChange;
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ export const realDetectorIo = {
4
+ existsSync,
5
+ readFileSync: (path) => readFileSync(path, "utf8"),
6
+ };
7
+ export function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ /** Read a JSON file safely; returns undefined on parse/io error. */
11
+ export function readJsonSafe(io, file) {
12
+ if (!io.existsSync(file))
13
+ return undefined;
14
+ try {
15
+ const raw = JSON.parse(io.readFileSync(file));
16
+ return isRecord(raw) ? raw : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Walk upward from startDir to the nearest directory containing package.json. */
23
+ export function findPackageJsonDir(io, startDir) {
24
+ let dir = startDir;
25
+ for (;;) {
26
+ if (io.existsSync(join(dir, "package.json")))
27
+ return dir;
28
+ const parent = dirname(dir);
29
+ if (parent === dir)
30
+ return undefined;
31
+ dir = parent;
32
+ }
33
+ }
34
+ /** Package manager precedence with ambiguity -> unknown (never guesses). */
35
+ export function detectPackageManager(io, root) {
36
+ const pnpm = io.existsSync(join(root, "pnpm-lock.yaml"));
37
+ const yarn = io.existsSync(join(root, "yarn.lock"));
38
+ const npm = io.existsSync(join(root, "package-lock.json"));
39
+ const present = [pnpm, yarn, npm].filter(Boolean).length;
40
+ if (present > 1)
41
+ return "unknown";
42
+ if (pnpm)
43
+ return "pnpm";
44
+ if (yarn)
45
+ return "yarn";
46
+ if (npm)
47
+ return "npm";
48
+ return "unknown";
49
+ }
50
+ export function okResult(kind, patch) {
51
+ return { kind, status: "ok", candidates: [], evidence: [], fixes: [], ...patch };
52
+ }
53
+ export function createFileFix(path, content) {
54
+ return { kind: "create", path, before: undefined, after: content };
55
+ }
@@ -0,0 +1,21 @@
1
+ import type { IconCatalog } from "../../catalog/catalog.js";
2
+ import type { AnchorResult } from "./types.js";
3
+ import type { DetectorIo } from "./helpers.js";
4
+ import type { Target } from "../../commands/parser.js";
5
+ /** Supported target for a freshly created config from the manifest adapter. */
6
+ export declare function targetFromAdapter(adapter: string | undefined): Target;
7
+ export interface ConfigAnchorOptions {
8
+ readonly root: string;
9
+ readonly sourceCatalog?: IconCatalog | undefined;
10
+ readonly io?: DetectorIo;
11
+ readonly adapter?: string;
12
+ /** true when only assets are consumed and app entry is not required. */
13
+ readonly assetsOnly?: boolean;
14
+ readonly ambiguous?: boolean;
15
+ }
16
+ /**
17
+ * Moeicons config anchor (E2E-B2). Never writes. If the config is missing and
18
+ * this is a writable project it builds a create-file plan using the canonical
19
+ * renderer; an existing invalid file is reported and never overwritten.
20
+ */
21
+ export declare function inspectMoeiconsConfig(options: ConfigAnchorOptions): AnchorResult;
@@ -0,0 +1,83 @@
1
+ import { join } from "node:path";
2
+ import { findConfigFile, readMoeiconsConfig } from "../config.js";
3
+ import { renderMoeiconsConfigJsonc } from "../config.js";
4
+ /** Supported target for a freshly created config from the manifest adapter. */
5
+ export function targetFromAdapter(adapter) {
6
+ if (adapter === "vite-vue" || adapter === "nuxt")
7
+ return "vue";
8
+ if (adapter === "vite-react" || adapter === "next-app" || adapter === "next-pages")
9
+ return "react";
10
+ if (adapter === "vanilla")
11
+ return "vanilla";
12
+ return "react";
13
+ }
14
+ /**
15
+ * Moeicons config anchor (E2E-B2). Never writes. If the config is missing and
16
+ * this is a writable project it builds a create-file plan using the canonical
17
+ * renderer; an existing invalid file is reported and never overwritten.
18
+ */
19
+ export function inspectMoeiconsConfig(options) {
20
+ const configPath = findConfigFile(options.root);
21
+ if (!configPath) {
22
+ if (options.ambiguous && !options.adapter && !options.assetsOnly) {
23
+ return { kind: "config", status: "ambiguous", candidates: [], evidence: ["select a target with --target before initializing this project"], fixes: [] };
24
+ }
25
+ const target = options.assetsOnly ? "assets" : targetFromAdapter(options.adapter);
26
+ const content = renderMoeiconsConfigJsonc({ target });
27
+ return {
28
+ kind: "config",
29
+ status: "missing",
30
+ path: join(options.root, "moeicons.config.jsonc"),
31
+ candidates: [],
32
+ evidence: ["no moeicons config found; a create plan is available"],
33
+ fixes: [
34
+ {
35
+ kind: "create",
36
+ path: "moeicons.config.jsonc",
37
+ before: undefined,
38
+ after: content,
39
+ },
40
+ ],
41
+ };
42
+ }
43
+ const loaded = readMoeiconsConfig(options.root, options.sourceCatalog);
44
+ if (loaded.kind === "ok") {
45
+ return {
46
+ kind: "config",
47
+ status: "ok",
48
+ path: configPath,
49
+ candidates: [],
50
+ evidence: [`config schema v${loaded.config.schemaVersion} valid`, ...loaded.warnings],
51
+ fixes: [],
52
+ };
53
+ }
54
+ if (loaded.kind === "unsupported") {
55
+ return {
56
+ kind: "config",
57
+ status: "unsupported",
58
+ path: configPath,
59
+ candidates: [],
60
+ evidence: [`config schema version ${loaded.version} is not supported`],
61
+ fixes: [],
62
+ };
63
+ }
64
+ if (loaded.kind === "invalid") {
65
+ return {
66
+ kind: "config",
67
+ status: "invalid",
68
+ path: configPath,
69
+ candidates: [],
70
+ evidence: [loaded.message],
71
+ fixes: [],
72
+ };
73
+ }
74
+ // loaded.kind === "missing" is impossible because findConfigFile found one.
75
+ return {
76
+ kind: "config",
77
+ status: "invalid",
78
+ path: configPath,
79
+ candidates: [],
80
+ evidence: ["config exists but could not be loaded"],
81
+ fixes: [],
82
+ };
83
+ }
@@ -0,0 +1,14 @@
1
+ import type { File } from "@babel/types";
2
+ /**
3
+ * Parse TS/TSX/JS/JSX/Vue-main source with @babel/parser. Returns `undefined`
4
+ * when the file cannot be parsed (syntax error / unknown syntax), never throws.
5
+ * Recorded before adding as runtime dep: @babel/parser 7.29.8, MIT license,
6
+ * ~1.9 MB unpacked, `npm audit --omit=dev` clean.
7
+ */
8
+ export declare function parseSource(source: string, filePath: string): {
9
+ readonly ok: true;
10
+ readonly ast: File;
11
+ } | {
12
+ readonly ok: false;
13
+ readonly error: string;
14
+ };
@@ -0,0 +1,36 @@
1
+ import { parse } from "@babel/parser";
2
+ /**
3
+ * Parse TS/TSX/JS/JSX/Vue-main source with @babel/parser. Returns `undefined`
4
+ * when the file cannot be parsed (syntax error / unknown syntax), never throws.
5
+ * Recorded before adding as runtime dep: @babel/parser 7.29.8, MIT license,
6
+ * ~1.9 MB unpacked, `npm audit --omit=dev` clean.
7
+ */
8
+ export function parseSource(source, filePath) {
9
+ const isTs = /\.tsx?$/i.test(filePath);
10
+ const plugins = [];
11
+ if (isTs) {
12
+ plugins.push("typescript");
13
+ if (/\.tsx$/i.test(filePath))
14
+ plugins.push("jsx");
15
+ }
16
+ else {
17
+ plugins.push("jsx");
18
+ }
19
+ plugins.push("decorators-legacy", "importAttributes", "explicitResourceManagement", "classProperties", "classPrivateProperties", "classPrivateMethods", "topLevelAwait");
20
+ try {
21
+ const ast = parse(source, {
22
+ sourceType: "module",
23
+ allowReturnOutsideFunction: true,
24
+ allowImportExportEverywhere: true,
25
+ errorRecovery: false,
26
+ plugins,
27
+ });
28
+ return { ok: true, ast };
29
+ }
30
+ catch (error) {
31
+ return {
32
+ ok: false,
33
+ error: error instanceof Error ? error.message : String(error),
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,19 @@
1
+ import type { AnchorResult } from "./types.js";
2
+ import { type DetectorIo } from "./helpers.js";
3
+ export interface AdapterEvidence {
4
+ readonly adapter: string | undefined;
5
+ readonly candidateAdapters: readonly string[];
6
+ readonly framework: "react" | "vue" | "vanilla" | "unknown";
7
+ }
8
+ /** Detect adapter/framework from package.json deps + key config files. */
9
+ export declare function detectAdapter(io: DetectorIo, root: string, pkg: Record<string, unknown>): AdapterEvidence;
10
+ export interface ManifestAnchorOptions {
11
+ readonly cwd: string;
12
+ readonly io: DetectorIo;
13
+ }
14
+ /**
15
+ * Project manifest anchor (E2E-B2). Never writes. Locates the nearest
16
+ * package.json, classifies package manager + workspace membership + adapter,
17
+ * and returns candidates when the layout is ambiguous.
18
+ */
19
+ export declare function inspectProjectManifest(options: ManifestAnchorOptions): AnchorResult;
@@ -0,0 +1,109 @@
1
+ import { join, relative } from "node:path";
2
+ import { findPackageJsonDir, detectPackageManager, readJsonSafe, isRecord, } from "./helpers.js";
3
+ /** Detect adapter/framework from package.json deps + key config files. */
4
+ export function detectAdapter(io, root, pkg) {
5
+ const deps = new Set([
6
+ ...Object.keys(isRecord(pkg.dependencies) ? pkg.dependencies : {}),
7
+ ...Object.keys(isRecord(pkg.devDependencies) ? pkg.devDependencies : {}),
8
+ ]);
9
+ const has = (name) => deps.has(name);
10
+ const candidates = [];
11
+ const hasNext = has("next");
12
+ const hasNuxt = has("nuxt");
13
+ const hasVue = has("vue");
14
+ const hasReact = has("react") || has("react-dom");
15
+ const hasVite = has("vite") || has("@vitejs/plugin-react") || has("@vitejs/plugin-vue");
16
+ if (hasNext) {
17
+ const app = io.existsSync(join(root, "app")) || io.existsSync(join(root, "src", "app"));
18
+ candidates.push(app ? "next-app" : "next-pages");
19
+ }
20
+ if (hasNuxt) {
21
+ candidates.push("nuxt");
22
+ }
23
+ if (hasVite && hasVue)
24
+ candidates.push("vite-vue");
25
+ if (hasVite && hasReact)
26
+ candidates.push("vite-react");
27
+ if (hasVue && !hasVite && !hasNuxt)
28
+ candidates.push("nuxt"); // heuristic via plugin file below
29
+ if (hasReact && !hasVite && !hasNext)
30
+ candidates.push("vite-react");
31
+ if (candidates.length === 0) {
32
+ const hasVanillaEntry = io.existsSync(join(root, "src", "main.ts")) ||
33
+ io.existsSync(join(root, "src", "main.js")) ||
34
+ io.existsSync(join(root, "main.ts")) ||
35
+ io.existsSync(join(root, "main.js"));
36
+ if (hasVanillaEntry) {
37
+ candidates.push("vanilla");
38
+ return { adapter: "vanilla", candidateAdapters: ["vanilla"], framework: "vanilla" };
39
+ }
40
+ return { adapter: undefined, candidateAdapters: [], framework: "unknown" };
41
+ }
42
+ const unique = [...new Set(candidates)];
43
+ const framework = hasVue
44
+ ? "vue"
45
+ : hasReact
46
+ ? "react"
47
+ : "unknown";
48
+ return {
49
+ adapter: unique.length === 1 ? unique[0] : undefined,
50
+ candidateAdapters: unique,
51
+ framework: unique.length === 1 ? framework : "unknown",
52
+ };
53
+ }
54
+ /**
55
+ * Project manifest anchor (E2E-B2). Never writes. Locates the nearest
56
+ * package.json, classifies package manager + workspace membership + adapter,
57
+ * and returns candidates when the layout is ambiguous.
58
+ */
59
+ export function inspectProjectManifest(options) {
60
+ const { io } = options;
61
+ const root = findPackageJsonDir(io, options.cwd);
62
+ if (!root) {
63
+ return {
64
+ kind: "manifest",
65
+ status: "missing",
66
+ candidates: [],
67
+ evidence: ["no package.json found in cwd or parents"],
68
+ fixes: [],
69
+ };
70
+ }
71
+ const pkg = readJsonSafe(io, join(root, "package.json"));
72
+ if (!pkg) {
73
+ return {
74
+ kind: "manifest",
75
+ status: "invalid",
76
+ path: join(root, "package.json"),
77
+ candidates: [],
78
+ evidence: ["package.json exists but is not valid JSON"],
79
+ fixes: [],
80
+ };
81
+ }
82
+ const packageManager = detectPackageManager(io, root);
83
+ const workspace = isRecord(pkg.workspaces)
84
+ ? []
85
+ : typeof pkg.workspaces === "string"
86
+ ? [pkg.workspaces]
87
+ : Array.isArray(pkg.workspaces)
88
+ ? pkg.workspaces.filter((w) => typeof w === "string")
89
+ : [];
90
+ const memberHint = workspace.length ? ` (workspace root, members: ${workspace.join(", ")})` : "";
91
+ const adapterInfo = detectAdapter(io, root, pkg);
92
+ const evidence = [
93
+ `package.json: ${relative(options.cwd, join(root, "package.json")) || "package.json"}`,
94
+ `packageManager: ${packageManager}${memberHint}`,
95
+ adapterInfo.adapter
96
+ ? `adapter: ${adapterInfo.adapter}`
97
+ : adapterInfo.candidateAdapters.length > 0
98
+ ? `adapter ambiguous: ${adapterInfo.candidateAdapters.join(", ")}`
99
+ : "adapter: unknown",
100
+ ];
101
+ return {
102
+ kind: "manifest",
103
+ status: packageManager === "unknown" ? "ambiguous" : "ok",
104
+ path: join(root, "package.json"),
105
+ candidates: [],
106
+ evidence,
107
+ fixes: [],
108
+ };
109
+ }
@@ -0,0 +1,12 @@
1
+ import type { AnchorResult } from "./types.js";
2
+ import type { DetectorIo } from "./helpers.js";
3
+ export interface StyleAnchorOptions {
4
+ readonly root: string;
5
+ readonly io: DetectorIo;
6
+ /** Confirmed style path stored in config (preferred source). */
7
+ readonly confirmedStyle?: string;
8
+ /** true when the target is assets-only. */
9
+ readonly assetsOnly?: boolean;
10
+ }
11
+ export declare function findCssCandidates(io: DetectorIo, root: string): readonly string[];
12
+ export declare function inspectStyleAnchor(options: StyleAnchorOptions): AnchorResult;
@@ -0,0 +1,87 @@
1
+ import { join } from "node:path";
2
+ /**
3
+ * Styling anchor (E2E-B4). This anchor is OPTIONAL: absence of Tailwind or CSS
4
+ * is a warning/`missing` result, never a hard failure. The detector is
5
+ * read-only: it resolves the CSS entry, checks whether moeicons styles are
6
+ * imported, and reports Tailwind status as evidence. It only offers a safe fix
7
+ * only when a real generated stylesheet exists. Core icon dimensions are
8
+ * expressed as element attributes, so no stylesheet is required by default.
9
+ */
10
+ const CSS_CANDIDATES = [
11
+ "src/index.css",
12
+ "src/style.css",
13
+ "src/styles.css",
14
+ "app/globals.css",
15
+ "src/app/globals.css",
16
+ ];
17
+ export function findCssCandidates(io, root) {
18
+ return CSS_CANDIDATES.filter((rel) => io.existsSync(join(root, rel)));
19
+ }
20
+ function cssImportsMoeicons(source) {
21
+ // CSS entry may import a stylesheet that carries the moeicons class base, or a
22
+ // JS/TS entry imports "moeicons/styles.css". Keep both cheap checks.
23
+ return /moeicons/.test(source);
24
+ }
25
+ export function inspectStyleAnchor(options) {
26
+ const { io, root, assetsOnly } = options;
27
+ if (assetsOnly) {
28
+ return {
29
+ kind: "style",
30
+ status: "not-required",
31
+ candidates: [],
32
+ evidence: ["assets-only target requires no styling integration"],
33
+ fixes: [],
34
+ };
35
+ }
36
+ const candidates = options.confirmedStyle && io.existsSync(join(root, options.confirmedStyle))
37
+ ? [options.confirmedStyle]
38
+ : findCssCandidates(io, root);
39
+ if (candidates.length === 0) {
40
+ return {
41
+ kind: "style",
42
+ status: "missing",
43
+ candidates: CSS_CANDIDATES,
44
+ evidence: ["no CSS entry found; styling integration is optional (warning only)"],
45
+ fixes: [],
46
+ };
47
+ }
48
+ if (candidates.length > 1) {
49
+ return {
50
+ kind: "style",
51
+ status: "ambiguous",
52
+ candidates,
53
+ evidence: [`multiple CSS entry candidates: ${candidates.join(", ")}`],
54
+ fixes: [],
55
+ };
56
+ }
57
+ const rel = candidates[0] ?? "";
58
+ const full = join(root, rel);
59
+ let source = "";
60
+ try {
61
+ source = io.readFileSync(full);
62
+ }
63
+ catch {
64
+ source = "";
65
+ }
66
+ if (cssImportsMoeicons(source)) {
67
+ return {
68
+ kind: "style",
69
+ status: "ok",
70
+ path: full,
71
+ candidates: [],
72
+ evidence: [`moeicons styles referenced in ${rel}`],
73
+ fixes: [],
74
+ };
75
+ }
76
+ return {
77
+ kind: "style",
78
+ status: "missing",
79
+ path: full,
80
+ candidates: [],
81
+ evidence: [
82
+ `CSS entry: ${rel}`,
83
+ "no moeicons stylesheet is generated; add optional project styles manually if needed",
84
+ ],
85
+ fixes: [],
86
+ };
87
+ }