@moewolf/moe-icons-cli 0.0.1 → 0.0.3

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 +249 -10
  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 +12768 -3965
  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,34 @@
1
+ /**
2
+ * Minimal filesystem surface needed for atomic, concurrency-safe cache
3
+ * persistence. Matches the project install transaction contract: verified
4
+ * bytes are written to a writer-unique sibling staging file and `renameSync`'d
5
+ * over the final path, so readers never observe a truncated cache and a crash
6
+ * never leaves a partial cache at the final path.
7
+ */
8
+ export interface CacheIo {
9
+ readonly mkdirSync: (path: string) => void;
10
+ readonly writeFileSync: (path: string, bytes: Uint8Array) => void;
11
+ readonly renameSync: (from: string, to: string) => void;
12
+ readonly existsSync: (path: string) => boolean;
13
+ readonly rmSync: (path: string, options?: {
14
+ readonly force?: boolean;
15
+ }) => void;
16
+ readonly readFileSync?: (path: string) => Uint8Array;
17
+ readonly readdirSync?: (path: string) => readonly string[];
18
+ }
19
+ /**
20
+ * Atomically persist verified bytes to a cache path.
21
+ *
22
+ * Concurrency contract (MC.5): every writer uses a PID + random suffixed
23
+ * staging file, so concurrent processes never share a staging file; the final
24
+ * rename is atomic on POSIX and Node uses `MOVEFILE_REPLACE_EXISTING` on
25
+ * Windows. When `expectedSha256` is provided and the final path already holds
26
+ * exactly those bytes, the write is skipped (idempotent fast path), so a second
27
+ * writer that lost the rename race reports success without rewriting.
28
+ *
29
+ * Staging files left behind by a crashed writer are inert (never read) and are
30
+ * swept on the next successful write only when their owner PID is proven
31
+ * dead. Live or unknown writers are preserved, including writers already
32
+ * staging when this call begins.
33
+ */
34
+ export declare function cacheArtifact(io: CacheIo, path: string, bytes: Uint8Array, expectedSha256?: string): void;
@@ -0,0 +1,96 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { basename, join } from "node:path";
3
+ function sha256Hex(bytes) {
4
+ return createHash("sha256").update(bytes).digest("hex");
5
+ }
6
+ /**
7
+ * Atomically persist verified bytes to a cache path.
8
+ *
9
+ * Concurrency contract (MC.5): every writer uses a PID + random suffixed
10
+ * staging file, so concurrent processes never share a staging file; the final
11
+ * rename is atomic on POSIX and Node uses `MOVEFILE_REPLACE_EXISTING` on
12
+ * Windows. When `expectedSha256` is provided and the final path already holds
13
+ * exactly those bytes, the write is skipped (idempotent fast path), so a second
14
+ * writer that lost the rename race reports success without rewriting.
15
+ *
16
+ * Staging files left behind by a crashed writer are inert (never read) and are
17
+ * swept on the next successful write only when their owner PID is proven
18
+ * dead. Live or unknown writers are preserved, including writers already
19
+ * staging when this call begins.
20
+ */
21
+ export function cacheArtifact(io, path, bytes, expectedSha256) {
22
+ io.mkdirSync(join(path, ".."));
23
+ const orphans = new Set(io.readdirSync ? io.readdirSync(join(path, "..")) : []);
24
+ const stagingPattern = `${basename(path)}.staging-`;
25
+ if (expectedSha256 && io.existsSync(path) && io.readFileSync) {
26
+ const existing = io.readFileSync(path);
27
+ if (existing.byteLength === bytes.byteLength && sha256Hex(existing) === expectedSha256) {
28
+ return;
29
+ }
30
+ }
31
+ const staging = `${path}.staging-${process.pid}-${randomBytes(4).toString("hex")}`;
32
+ try {
33
+ io.writeFileSync(staging, bytes);
34
+ try {
35
+ io.renameSync(staging, path);
36
+ }
37
+ catch (error) {
38
+ // Cross-process race: another writer may have replaced `path` first. Only
39
+ // treat that as success when the surviving bytes actually match what we
40
+ // set out to persist; otherwise rethrow so a corrupt cache is not kept.
41
+ if (expectedSha256 && io.existsSync(path) && io.readFileSync) {
42
+ const existing = io.readFileSync(path);
43
+ if (existing.byteLength === bytes.byteLength && sha256Hex(existing) === expectedSha256) {
44
+ try {
45
+ io.rmSync(staging, { force: true });
46
+ }
47
+ catch {
48
+ /* Best effort own staging cleanup. */
49
+ }
50
+ return;
51
+ }
52
+ }
53
+ throw error;
54
+ }
55
+ if (io.rmSync && io.readdirSync) {
56
+ for (const name of orphans) {
57
+ if (!name.startsWith(stagingPattern))
58
+ continue;
59
+ const owner = /^(\d+)-[a-f0-9]{8}$/.exec(name.slice(stagingPattern.length));
60
+ if (!owner)
61
+ continue;
62
+ const pid = Number(owner[1]);
63
+ if (!Number.isSafeInteger(pid) || pid < 1 || pid > 2147483647)
64
+ continue;
65
+ let dead = false;
66
+ try {
67
+ process.kill(pid, 0);
68
+ }
69
+ catch (error) {
70
+ dead =
71
+ typeof error === "object" &&
72
+ error !== null &&
73
+ "code" in error &&
74
+ error.code === "ESRCH";
75
+ }
76
+ if (!dead)
77
+ continue;
78
+ try {
79
+ io.rmSync(join(join(path, ".."), name), { force: true });
80
+ }
81
+ catch {
82
+ // Best-effort orphan sweep.
83
+ }
84
+ }
85
+ }
86
+ }
87
+ catch (error) {
88
+ try {
89
+ io.rmSync(staging, { force: true });
90
+ }
91
+ catch {
92
+ // Best-effort cleanup; the writer-unique staging file is never read.
93
+ }
94
+ throw error;
95
+ }
96
+ }
@@ -1,6 +1,25 @@
1
1
  export interface UiChoice {
2
2
  readonly value: string;
3
3
  readonly label: string;
4
+ /** When true, renderer inserts a blank line before this option (e.g. Back). */
5
+ readonly separatorBefore?: boolean;
6
+ }
7
+ /** W3-A: a structured progress tick (bytes or item counts). */
8
+ export interface ProgressBarEvent {
9
+ readonly done?: number;
10
+ readonly total?: number;
11
+ readonly label?: string;
12
+ readonly detail?: string;
13
+ }
14
+ export interface ProgressBarHandle {
15
+ readonly update: (event?: ProgressBarEvent) => void;
16
+ readonly stop: (message?: string) => void;
17
+ }
18
+ /** W3-C: strict task lifecycle for work without a progress bar. */
19
+ export interface TaskHandle {
20
+ readonly succeed: (message?: string) => void;
21
+ readonly fail: (message?: string) => void;
22
+ readonly timeout: (message?: string) => void;
4
23
  }
5
24
  export interface CommandUi {
6
25
  readonly select: (message: string, choices: readonly UiChoice[], signal: AbortSignal) => Promise<string | undefined>;
@@ -11,6 +30,13 @@ export interface CommandUi {
11
30
  readonly update?: (message: string) => void;
12
31
  readonly stop: (message?: string) => void;
13
32
  };
33
+ /**
34
+ * W3-A: optional richer progress surface. Adapters that cannot render one
35
+ * (JSON / non-TTY) omit it; callers fall back to `progress`.
36
+ */
37
+ readonly progressBar?: (label: string, signal: AbortSignal) => ProgressBarHandle;
38
+ /** W3-C: optional strict task status surface. */
39
+ readonly task?: (label: string, signal: AbortSignal) => TaskHandle;
14
40
  }
15
41
  /** Dependencies shared by command use cases; it deliberately contains no Node globals. */
16
42
  export interface CommandContext {
@@ -0,0 +1,48 @@
1
+ import { type DiagnoseOutcome } from "../project/anchors/diagnose.js";
2
+ import type { DiagnosticReport, PlannedFileChange } from "../project/anchors/types.js";
3
+ import type { IconCatalog } from "../catalog/catalog.js";
4
+ import type { Target } from "../commands/parser.js";
5
+ export type DoctorMode = "diagnose" | "check" | "dry-run" | "apply";
6
+ export interface DoctorResult {
7
+ readonly mode: DoctorMode;
8
+ readonly ok: true;
9
+ readonly report: DiagnosticReport;
10
+ readonly written?: readonly string[];
11
+ readonly alreadyConfigured?: boolean;
12
+ }
13
+ /** Read-only four-anchor diagnosis. Never writes. */
14
+ export declare function runDoctorDiagnose(cwd: string, sourceCatalog?: IconCatalog): DiagnoseOutcome;
15
+ /** Human-readable table (manifest + config always shown). */
16
+ export declare function formatDoctorReport(report: DiagnosticReport): string;
17
+ /** JSON-stable machine report. Paths are POSIX-relative to projectRoot (never absolute home). */
18
+ export declare function doctorJson(report: DiagnosticReport): Record<string, unknown>;
19
+ /**
20
+ * `moeicons doctor --check`: exit non-zero when any required anchor is not OK.
21
+ * Required anchors are manifest/config/application; style is optional.
22
+ */
23
+ export declare function checkRequiresFix(report: DiagnosticReport): boolean;
24
+ /** Build the complete init plan (config create + application + style fixes). */
25
+ export declare function buildInitPlan(cwd: string, target?: Target, confirmed?: {
26
+ adapter?: string;
27
+ entry?: string;
28
+ style?: string;
29
+ }, sourceCatalog?: IconCatalog): {
30
+ readonly outcome: DiagnoseOutcome;
31
+ };
32
+ /** Collect safe planned fixes from a diagnose outcome (already deduped). */
33
+ export declare function collectSafeFixes(outcome: DiagnoseOutcome): readonly PlannedFileChange[];
34
+ /** Human-readable unified-style diffs for a planned change set. */
35
+ export declare function formatPlannedDiffs(changes: readonly PlannedFileChange[]): string;
36
+ /** Machine-readable diffs for `--json --dry-run`. */
37
+ export declare function plannedDiffsJson(changes: readonly PlannedFileChange[]): readonly Record<string, unknown>[];
38
+ export interface DoctorApplyFailure {
39
+ readonly ok: false;
40
+ readonly message: string;
41
+ readonly written: readonly string[];
42
+ }
43
+ export type DoctorApplyOutcome = DoctorResult | DoctorApplyFailure;
44
+ /**
45
+ * Apply a confirmed safe plan (all anchors' fixes). Transactional; a second run
46
+ * is a no-op (`alreadyConfigured`).
47
+ */
48
+ export declare function runDoctorApply(cwd: string, fixes: readonly PlannedFileChange[]): DoctorApplyOutcome;
@@ -0,0 +1,190 @@
1
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync } from "node:fs";
3
+ import { diagnoseProject } from "../project/anchors/diagnose.js";
4
+ import { applyPlannedChanges } from "../project/anchors/apply.js";
5
+ import { withProjectLockSync } from "../project/project-lock.js";
6
+ import { loadInstalledCatalogState } from "./generate.js";
7
+ const STATUS_LABEL = {
8
+ ok: "OK",
9
+ missing: "MISSING",
10
+ invalid: "INVALID",
11
+ ambiguous: "AMBIGUOUS",
12
+ unsupported: "UNSUPPORTED",
13
+ "not-required": "N/A",
14
+ };
15
+ /** Read-only four-anchor diagnosis. Never writes. */
16
+ export function runDoctorDiagnose(cwd, sourceCatalog) {
17
+ return diagnoseProject({ cwd, sourceCatalog: sourceCatalog ?? diagnosticCatalog(cwd) });
18
+ }
19
+ /** Human-readable table (manifest + config always shown). */
20
+ export function formatDoctorReport(report) {
21
+ const lines = [];
22
+ for (const anchor of report.anchors) {
23
+ const label = STATUS_LABEL[anchor.status] ?? anchor.status.toUpperCase();
24
+ const path = anchor.path
25
+ ? report.projectRoot
26
+ ? toPosixRelative(report.projectRoot, anchor.path)
27
+ : anchor.path
28
+ : "-";
29
+ const name = anchorName(anchor.kind);
30
+ lines.push(`${name.padEnd(22)} ${label.padEnd(10)} ${path}`);
31
+ for (const evidence of anchor.evidence)
32
+ lines.push(` - ${evidence}`);
33
+ if (anchor.fixes.length) {
34
+ lines.push(` fixes: ${anchor.fixes.map((f) => `${f.kind} ${f.path}`).join(", ")}`);
35
+ }
36
+ }
37
+ return lines.join("\n");
38
+ }
39
+ function anchorName(kind) {
40
+ switch (kind) {
41
+ case "manifest":
42
+ return "Project manifest";
43
+ case "config":
44
+ return "Moeicons config";
45
+ case "application":
46
+ return "Application integration";
47
+ case "style":
48
+ return "Styling integration";
49
+ }
50
+ }
51
+ function toPosixRelative(root, path) {
52
+ if (!isAbsolute(path))
53
+ return path.split(sep).join("/");
54
+ const rel = relative(root, path);
55
+ if (!rel || rel === "")
56
+ return ".";
57
+ return rel.split(sep).join("/");
58
+ }
59
+ /** JSON-stable machine report. Paths are POSIX-relative to projectRoot (never absolute home). */
60
+ export function doctorJson(report) {
61
+ const root = report.projectRoot;
62
+ return {
63
+ ok: !checkRequiresFix(report),
64
+ ...(root ? { projectRoot: "." } : {}),
65
+ anchors: report.anchors.map((a) => ({
66
+ kind: a.kind,
67
+ status: a.status,
68
+ ...(a.path && root ? { path: toPosixRelative(root, a.path) } : a.path ? { path: a.path } : {}),
69
+ candidates: root
70
+ ? a.candidates.map((c) => toPosixRelative(root, c))
71
+ : [...a.candidates],
72
+ evidence: a.evidence,
73
+ fixes: a.fixes.map((f) => ({
74
+ kind: f.kind,
75
+ path: f.path,
76
+ ...(f.before !== undefined ? { before: f.before } : {}),
77
+ after: f.after,
78
+ })),
79
+ })),
80
+ };
81
+ }
82
+ /**
83
+ * `moeicons doctor --check`: exit non-zero when any required anchor is not OK.
84
+ * Required anchors are manifest/config/application; style is optional.
85
+ */
86
+ export function checkRequiresFix(report) {
87
+ const required = [
88
+ "manifest",
89
+ "config",
90
+ "application",
91
+ ];
92
+ return report.anchors.some((a) => required.includes(a.kind) && a.status !== "ok" && a.status !== "not-required");
93
+ }
94
+ function diagnosticCatalog(cwd) {
95
+ const state = loadInstalledCatalogState(cwd, { readFileSync, existsSync });
96
+ if (state.status === "invalid")
97
+ throw new Error(state.message);
98
+ return state.status === "ok" ? state.catalog : undefined;
99
+ }
100
+ /** Build the complete init plan (config create + application + style fixes). */
101
+ export function buildInitPlan(cwd, target, confirmed, sourceCatalog) {
102
+ const adapter = confirmed?.adapter ?? (target === "vue" ? "vite-vue" : target === "react" ? "vite-react" : target === "vanilla" ? "vanilla" : target === "assets" ? "assets-only" : undefined);
103
+ return { outcome: diagnoseProject({ cwd, sourceCatalog: sourceCatalog ?? diagnosticCatalog(cwd), ...(adapter ? { confirmed: { ...confirmed, adapter } } : {}) }) };
104
+ }
105
+ /** Collect safe planned fixes from a diagnose outcome (already deduped). */
106
+ export function collectSafeFixes(outcome) {
107
+ return outcome.fixes;
108
+ }
109
+ /** Human-readable unified-style diffs for a planned change set. */
110
+ export function formatPlannedDiffs(changes) {
111
+ if (changes.length === 0)
112
+ return "";
113
+ const blocks = [];
114
+ for (const change of changes) {
115
+ blocks.push(formatOneDiff(change));
116
+ }
117
+ return blocks.join("\n\n");
118
+ }
119
+ function formatOneDiff(change) {
120
+ const path = change.path;
121
+ if (change.kind === "create" || change.before === undefined) {
122
+ const added = change.after.split("\n").map((line) => `+${line}`).join("\n");
123
+ return `*** create ${path}\n--- /dev/null\n+++ b/${path}\n${added}`;
124
+ }
125
+ const beforeLines = change.before.split("\n");
126
+ const afterLines = change.after.split("\n");
127
+ const removed = beforeLines.map((line) => `-${line}`).join("\n");
128
+ const added = afterLines.map((line) => `+${line}`).join("\n");
129
+ return `*** replace ${path}\n--- a/${path}\n+++ b/${path}\n${removed}\n${added}`;
130
+ }
131
+ /** Machine-readable diffs for `--json --dry-run`. */
132
+ export function plannedDiffsJson(changes) {
133
+ return changes.map((change) => ({
134
+ kind: change.kind,
135
+ path: change.path,
136
+ diff: formatOneDiff(change),
137
+ }));
138
+ }
139
+ const FS = {
140
+ existsSync: (path) => existsSync(path),
141
+ readTextFileSync: (path) => readFileSync(path, "utf8"),
142
+ mkdirSync: (path) => {
143
+ mkdirSync(path, { recursive: true });
144
+ },
145
+ writeTextFileSync: (path, content) => {
146
+ writeFileSync(path, content);
147
+ },
148
+ renameSync: (from, to) => renameSync(from, to),
149
+ rmSync: (path) => {
150
+ rmSync(path, { recursive: true, force: true });
151
+ },
152
+ };
153
+ function projectRootOf(cwd) {
154
+ let dir = cwd;
155
+ for (;;) {
156
+ if (existsSync(join(dir, "package.json")))
157
+ return dir;
158
+ const parent = dirname(dir);
159
+ if (parent === dir)
160
+ throw new Error("no project found");
161
+ dir = parent;
162
+ }
163
+ }
164
+ function emptyReport(projectRoot) {
165
+ return { projectRoot, anchors: [] };
166
+ }
167
+ /**
168
+ * Apply a confirmed safe plan (all anchors' fixes). Transactional; a second run
169
+ * is a no-op (`alreadyConfigured`).
170
+ */
171
+ export function runDoctorApply(cwd, fixes) {
172
+ const root = projectRootOf(cwd);
173
+ let outcome;
174
+ try {
175
+ outcome = withProjectLockSync(root, "doctor", () => applyPlannedChanges(root, fixes, FS));
176
+ }
177
+ catch (error) {
178
+ return { ok: false, message: error instanceof Error ? error.message : String(error), written: [] };
179
+ }
180
+ if (!outcome.ok) {
181
+ return { ok: false, message: outcome.message, written: [] };
182
+ }
183
+ return {
184
+ mode: "apply",
185
+ ok: true,
186
+ alreadyConfigured: outcome.alreadyConfigured,
187
+ written: outcome.written,
188
+ report: emptyReport(root),
189
+ };
190
+ }
@@ -1,11 +1,18 @@
1
+ import { type ResourceRefs } from "./selected-resources.js";
2
+ import { type ConfigDocument, type MoeiconsConfigFile } from "../project/config.js";
1
3
  import { type DownloadLimits } from "../project/install.js";
2
- import { type ReleaseDescriptor } from "./release-descriptor.js";
4
+ import { type CacheIo } from "./cache.js";
5
+ import { type MetadataArchiveFiles } from "../metadata/archive.js";
6
+ import { type ReleaseDescriptor, type ReleaseMetadataRef } from "./release-descriptor.js";
3
7
  export declare const DESCRIPTOR_MAX_BYTES: number;
4
8
  export declare const ARTIFACT_MAX_BYTES: number;
5
- export declare const DOWNLOAD_TIMEOUT_MS = 30000;
9
+ export declare const METADATA_MAX_BYTES: number;
10
+ export declare const DOWNLOAD_TIMEOUT_MS = 120000;
6
11
  export declare const MAX_REDIRECTS = 5;
7
12
  export declare const DESCRIPTOR_NAME = "release-descriptor.json";
8
13
  export declare const DESCRIPTOR_SHA_NAME = "release-descriptor.json.sha256";
14
+ export declare const RELEASE_LATEST_NAME = "release-latest.json";
15
+ export declare const MAX_DOWNLOAD_ATTEMPTS = 3;
9
16
  export type FreeDownloadFailure = {
10
17
  readonly ok: false;
11
18
  readonly reason: "network";
@@ -30,18 +37,36 @@ export type FreeDownloadFailure = {
30
37
  readonly ok: false;
31
38
  readonly reason: "cancelled";
32
39
  readonly message: string;
40
+ } | {
41
+ readonly ok: false;
42
+ readonly reason: "disk-full";
43
+ readonly message: string;
33
44
  };
45
+ export interface SelectedResourceDownload {
46
+ readonly files: Readonly<Record<string, Uint8Array>>;
47
+ readonly refs: ResourceRefs;
48
+ readonly indexBytes: Uint8Array;
49
+ readonly payloadBytes: number;
50
+ readonly networkBytes: number;
51
+ readonly cacheHits: number;
52
+ readonly fallbacks: readonly string[];
53
+ }
34
54
  export type FreeDownloadSuccess = {
35
55
  readonly ok: true;
36
56
  readonly descriptor: ReleaseDescriptor;
37
57
  readonly descriptorSha256: string;
38
58
  readonly artifactBytes: Uint8Array;
59
+ readonly selected?: SelectedResourceDownload;
60
+ readonly downloadNote?: string;
39
61
  readonly catalogJson: string;
40
62
  readonly cacheHit: boolean;
41
63
  readonly tag: string;
64
+ readonly metadataSha256: string;
65
+ readonly manifestJson: string;
66
+ readonly manualMd: string;
42
67
  };
43
68
  export type FreeDownloadResult = FreeDownloadSuccess | FreeDownloadFailure;
44
- export interface FreeDownloadIo {
69
+ export interface FreeDownloadIo extends CacheIo {
45
70
  readonly fetchFn: typeof fetch;
46
71
  readonly readFileSync: (path: string) => Uint8Array;
47
72
  readonly writeFileSync: (path: string, data: Uint8Array) => void;
@@ -55,12 +80,45 @@ export interface FreeDownloadIo {
55
80
  readonly cliVersion: string;
56
81
  readonly signal: AbortSignal;
57
82
  readonly onProgress?: DownloadLimits["onProgress"];
83
+ readonly statfs?: (dir: string) => {
84
+ readonly availableBytes: number;
85
+ } | undefined;
58
86
  }
59
87
  export declare function artifactCachePath(cacheDir: string, fullVersion: string, sha256: string): string;
88
+ export declare function metadataCachePath(cacheDir: string, fullVersion: string, sha256: string): string;
89
+ /** Best-effort disk-space preflight; missing statfs support is treated as "enough". */
90
+ export declare function hasEnoughFreeSpace(io: FreeDownloadIo, dir: string, requiredBytes: number): boolean;
91
+ export interface ExtractedMetadata extends MetadataArchiveFiles {
92
+ metadataSha256: string;
93
+ }
94
+ /**
95
+ * Download the small metadata archive, verify the whole-archive SHA-256 plus
96
+ * size and every nested file digest, then cache it atomically.
97
+ */
98
+ export declare function downloadMetadataArchive(io: FreeDownloadIo, metadataRef: ReleaseMetadataRef, expectedCatalogSha: string, expectedTier: "free" | "pro", expectedVersion: string, tag: string, allowLocalTest?: boolean): Promise<{
99
+ ok: true;
100
+ value: ExtractedMetadata;
101
+ cacheHit: boolean;
102
+ } | FreeDownloadFailure>;
103
+ /**
104
+ * Fetch the published release index → verify descriptor. Older releases that
105
+ * predate release-latest.json fall back to the descriptor SHA sidecar. Does not
106
+ * download the code artifact; used by metadata-only sync and by the full download.
107
+ */
108
+ export declare function fetchFreeDescriptor(io: FreeDownloadIo, sourceVersion: string): Promise<{
109
+ ok: true;
110
+ descriptor: ReleaseDescriptor;
111
+ descriptorSha256: string;
112
+ tag: string;
113
+ } | FreeDownloadFailure>;
60
114
  /**
61
115
  * 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.
116
+ * the descriptor (never guessed) → verify archive + nested catalog checksums →
117
+ * download/verify/cache the matching metadata archive.
64
118
  */
65
- export declare function downloadFreeRelease(io: FreeDownloadIo, sourceVersion: string): Promise<FreeDownloadResult>;
119
+ export declare function downloadFreeRelease(io: FreeDownloadIo, sourceVersion: string, selection?: {
120
+ readonly config: MoeiconsConfigFile;
121
+ readonly document: ConfigDocument;
122
+ readonly onPlan?: (message: string) => void;
123
+ }): Promise<FreeDownloadResult>;
66
124
  export declare function bundledSourceVersion(): string;