@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.
- package/README.md +249 -10
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +12768 -3965
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
package/dist/core/install.js
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
+
import { EMPTY_DOWNLOAD_SELECTION_MESSAGE } from "./selected-resources.js";
|
|
1
2
|
import { createInstallPlan, executeInstallPlan } from "../project/install.js";
|
|
2
3
|
import { detectProject } from "../project/detect.js";
|
|
3
|
-
import { readMoeiconsConfig } from "../project/config.js";
|
|
4
|
+
import { readMoeiconsConfig, loadConfigDocument } from "../project/config.js";
|
|
5
|
+
import { CliError, isCliError } from "../errors/index.js";
|
|
4
6
|
import { bundledSourceVersion, downloadFreeRelease } from "./free-download.js";
|
|
5
7
|
import { serializeInstallMetadata, sha256Bytes } from "../project/install-metadata.js";
|
|
6
8
|
import { withProjectLock } from "../project/project-lock.js";
|
|
7
|
-
import { selectTargetSubtree } from "./target-subtree.js";
|
|
9
|
+
import { configuredComponentFiles, selectTargetSubtree, computeSubtreeHash, } from "./target-subtree.js";
|
|
10
|
+
import { parseCatalog } from "../catalog/catalog.js";
|
|
11
|
+
import { posix } from "node:path";
|
|
12
|
+
/**
|
|
13
|
+
* P0-3: the project `types.ts` must re-export a type that actually exists in the
|
|
14
|
+
* installed `moe-icons` package. Assets have no component type, so it is empty.
|
|
15
|
+
*/
|
|
16
|
+
export function typesReexport(tier, target, outputDir = "src/moeicons") {
|
|
17
|
+
void tier;
|
|
18
|
+
const from = posix.relative(outputDir.replace(/\\/g, "/"), `.moeicons/artifact/${target}/types`);
|
|
19
|
+
const specifier = from.startsWith(".") ? from : `./${from}`;
|
|
20
|
+
if (target === "react")
|
|
21
|
+
return `export type { ReactIconProps } from "${specifier}";\n`;
|
|
22
|
+
if (target === "vue")
|
|
23
|
+
return `export type { VueIconProps } from "${specifier}";\n`;
|
|
24
|
+
if (target === "vanilla")
|
|
25
|
+
return `export type { VanillaIconOptions } from "${specifier}";\n`;
|
|
26
|
+
return "export {};\n";
|
|
27
|
+
}
|
|
8
28
|
function normalizeGroup(group) {
|
|
9
29
|
if (group === undefined || group === "free")
|
|
10
30
|
return "free";
|
|
@@ -25,7 +45,14 @@ export async function runInstallUseCase(context, deps, options) {
|
|
|
25
45
|
return { ok: false, reason: "no-project" };
|
|
26
46
|
const groupArg = options.group;
|
|
27
47
|
if (groupArg !== undefined && groupArg !== "free" && groupArg !== "pro" && groupArg !== "ent") {
|
|
28
|
-
|
|
48
|
+
// AUD-CL-01: single style-group install (`moeicons install <styleGroupId>`)
|
|
49
|
+
// is not supported. Fail closed with explicit guidance instead of silently
|
|
50
|
+
// treating an unknown group as Free.
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: "validation",
|
|
54
|
+
message: `unknown install group "${groupArg}"; single style-group install is not supported — run "moeicons install free" or "moeicons install pro"`,
|
|
55
|
+
};
|
|
29
56
|
}
|
|
30
57
|
const group = normalizeGroup(groupArg);
|
|
31
58
|
if (group === "pro") {
|
|
@@ -35,16 +62,41 @@ export async function runInstallUseCase(context, deps, options) {
|
|
|
35
62
|
message: "pro install requires the authenticated pro flow",
|
|
36
63
|
};
|
|
37
64
|
}
|
|
38
|
-
const
|
|
65
|
+
const document = loadConfigDocument(project.root);
|
|
66
|
+
const plannedConfigDocument = JSON.stringify(document);
|
|
67
|
+
const config = readMoeiconsConfig(project.root, undefined, { lenientCatalog: true });
|
|
39
68
|
if (config.kind === "invalid" || config.kind === "unsupported") {
|
|
40
69
|
return {
|
|
41
70
|
ok: false,
|
|
42
71
|
reason: "validation",
|
|
43
|
-
message: config.kind === "invalid"
|
|
72
|
+
message: config.kind === "invalid"
|
|
73
|
+
? config.message
|
|
74
|
+
: `unsupported config schema version ${config.version}`,
|
|
44
75
|
};
|
|
45
76
|
}
|
|
77
|
+
if (config.kind === "ok" && config.config.tier !== "free")
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: "validation",
|
|
81
|
+
message: 'config.tier is pro; run "moeicons install pro" or change tier to free before downloading',
|
|
82
|
+
};
|
|
83
|
+
if (config.kind === "ok" && config.config.icons.length === 0)
|
|
84
|
+
return { ok: false, reason: "validation", message: EMPTY_DOWNLOAD_SELECTION_MESSAGE };
|
|
46
85
|
const target = options.target ?? (config.kind === "ok" ? config.config.target : "react");
|
|
47
|
-
|
|
86
|
+
if (config.kind === "ok" && config.config.target !== target) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
reason: "validation",
|
|
90
|
+
message: `config target is "${config.config.target}" but install target is "${target}"; set target to "${target}" in moeicons.config before installing`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const downloaded = await downloadFreeRelease({ ...deps.download, signal: context.signal }, options.sourceVersion ?? bundledSourceVersion(), config.kind === "ok"
|
|
94
|
+
? {
|
|
95
|
+
config: config.config,
|
|
96
|
+
document,
|
|
97
|
+
onPlan: (message) => context.ui.note(message, context.signal),
|
|
98
|
+
}
|
|
99
|
+
: undefined);
|
|
48
100
|
if (!downloaded.ok) {
|
|
49
101
|
return downloaded.reason === "cancelled"
|
|
50
102
|
? { ok: false, reason: "cancelled", message: downloaded.message }
|
|
@@ -59,21 +111,52 @@ export async function runInstallUseCase(context, deps, options) {
|
|
|
59
111
|
message: "downloaded release identity changed after version check; retry the update",
|
|
60
112
|
};
|
|
61
113
|
}
|
|
62
|
-
const
|
|
114
|
+
const selectedTarget = downloaded.selected
|
|
115
|
+
? Object.fromEntries(Object.entries(downloaded.selected.files)
|
|
116
|
+
.filter(([path]) => path.startsWith(`${target}/`))
|
|
117
|
+
.map(([path, bytes]) => [path.slice(target.length + 1), bytes]))
|
|
118
|
+
: undefined;
|
|
119
|
+
const subtree = selectedTarget
|
|
120
|
+
? { ok: true, target, files: selectedTarget, ...computeSubtreeHash(selectedTarget) }
|
|
121
|
+
: selectTargetSubtree(downloaded.artifactBytes, downloaded.descriptor.free, target);
|
|
63
122
|
if (!subtree.ok) {
|
|
64
123
|
return subtree.reason === "checksum-mismatch"
|
|
65
124
|
? { ok: false, reason: "checksum-mismatch", message: subtree.message }
|
|
66
125
|
: { ok: false, reason: "validation", message: subtree.message };
|
|
67
126
|
}
|
|
127
|
+
let installedFiles;
|
|
128
|
+
try {
|
|
129
|
+
installedFiles = downloaded.selected
|
|
130
|
+
? subtree.files
|
|
131
|
+
: configuredComponentFiles(subtree.files, target, config.kind === "ok" ? config.config : undefined, parseCatalog(JSON.parse(downloaded.catalogJson)));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "validation",
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
68
140
|
const catalogJson = downloaded.catalogJson;
|
|
141
|
+
const outputDir = config.kind === "ok"
|
|
142
|
+
? config.config.outputDir.replace(/\\/g, "/").replace(/\/$/, "")
|
|
143
|
+
: "src/moeicons";
|
|
69
144
|
const files = {
|
|
70
145
|
".moeicons/catalog.json": catalogJson,
|
|
71
|
-
"
|
|
72
|
-
"
|
|
146
|
+
".moeicons/manifest.json": downloaded.manifestJson,
|
|
147
|
+
".moeicons/MANUAL.md": downloaded.manualMd,
|
|
148
|
+
".moeicons/artifact/package.json": '{"private":true,"type":"module","sideEffects":false}\n',
|
|
149
|
+
[`${outputDir}/types.ts`]: typesReexport("free", target, outputDir),
|
|
150
|
+
[`${outputDir}/.moeicons-free.marker`]: "free\n",
|
|
73
151
|
};
|
|
74
|
-
for (const [rel, bytes] of Object.entries(
|
|
152
|
+
for (const [rel, bytes] of Object.entries(installedFiles)) {
|
|
75
153
|
files[`.moeicons/artifact/${target}/${rel}`] = bytes;
|
|
76
154
|
}
|
|
155
|
+
if (downloaded.selected) {
|
|
156
|
+
files[".moeicons/resource-index.json.gz"] = downloaded.selected.indexBytes;
|
|
157
|
+
for (const [rel, bytes] of Object.entries(downloaded.selected.files))
|
|
158
|
+
files[`.moeicons/artifact/${rel}`] = bytes;
|
|
159
|
+
}
|
|
77
160
|
const managedFiles = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, sha256Bytes(content)]));
|
|
78
161
|
files[".moeicons/install-metadata.json"] = serializeInstallMetadata({
|
|
79
162
|
schemaVersion: 1,
|
|
@@ -85,18 +168,36 @@ export async function runInstallUseCase(context, deps, options) {
|
|
|
85
168
|
artifactSha256: downloaded.descriptor.free.sha256,
|
|
86
169
|
installedAt: context.now().toISOString(),
|
|
87
170
|
managedFiles,
|
|
171
|
+
...(downloaded.selected
|
|
172
|
+
? {
|
|
173
|
+
delivery: {
|
|
174
|
+
mode: "icons",
|
|
175
|
+
indexSha256: downloaded.selected.refs.index.sha256,
|
|
176
|
+
bundleSha256: downloaded.selected.refs.bundle.sha256,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
88
180
|
targetSha256: subtree.sha256,
|
|
89
181
|
targetFileCount: subtree.fileCount,
|
|
90
182
|
targetByteCount: subtree.byteCount,
|
|
183
|
+
// Local-test candidates record the declared model so a later generate can
|
|
184
|
+
// verify the install state (mirrors the Pro install path).
|
|
185
|
+
...(downloaded.descriptor.channel === "local-test"
|
|
186
|
+
? { channel: "local-test", publishable: false }
|
|
187
|
+
: {}),
|
|
91
188
|
});
|
|
92
189
|
const plan = createInstallPlan(project.root, files);
|
|
93
190
|
try {
|
|
94
|
-
await withProjectLock(project.root, "install", () =>
|
|
191
|
+
await withProjectLock(project.root, "install", () => {
|
|
192
|
+
if (JSON.stringify(loadConfigDocument(project.root)) !== plannedConfigDocument)
|
|
193
|
+
throw new CliError("VALIDATION_ERROR", "config changed while preparing install; retry");
|
|
194
|
+
executeInstallPlan(plan, deps.fs);
|
|
195
|
+
});
|
|
95
196
|
}
|
|
96
197
|
catch (error) {
|
|
97
198
|
return {
|
|
98
199
|
ok: false,
|
|
99
|
-
reason: "write-failed",
|
|
200
|
+
reason: isCliError(error) && error.code === "VALIDATION_ERROR" ? "validation" : "write-failed",
|
|
100
201
|
message: error instanceof Error ? error.message : String(error),
|
|
101
202
|
};
|
|
102
203
|
}
|
|
@@ -106,12 +207,30 @@ export async function runInstallUseCase(context, deps, options) {
|
|
|
106
207
|
packageManager: project.packageManager,
|
|
107
208
|
group: "free",
|
|
108
209
|
target,
|
|
109
|
-
artifactBytes: downloaded.artifactBytes.byteLength,
|
|
210
|
+
artifactBytes: downloaded.selected?.payloadBytes ?? downloaded.artifactBytes.byteLength,
|
|
211
|
+
downloadMode: downloaded.selected ? "icons" : "full",
|
|
212
|
+
...(downloaded.selected
|
|
213
|
+
? {
|
|
214
|
+
networkBytes: downloaded.selected.networkBytes,
|
|
215
|
+
selectedFiles: Object.keys(downloaded.selected.files).length,
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
downloadNotes: downloaded.selected?.fallbacks ??
|
|
219
|
+
(config.kind === "ok" && config.config.downloadMode !== "full"
|
|
220
|
+
? [
|
|
221
|
+
"This release does not support selected downloads; used the verified full archive. Set downloadMode=icons to require selected downloads.",
|
|
222
|
+
]
|
|
223
|
+
: config.kind === "ok"
|
|
224
|
+
? []
|
|
225
|
+
: [
|
|
226
|
+
"No config was found; installed the full archive. Run moeicons init, select icons/themes, then reinstall for selected downloads.",
|
|
227
|
+
]),
|
|
110
228
|
planItems: plan.items.length,
|
|
111
229
|
config: config.kind,
|
|
112
230
|
artifactVersion: downloaded.descriptor.fullVersion,
|
|
113
231
|
descriptorSha256: downloaded.descriptorSha256,
|
|
114
232
|
catalogSha256: downloaded.descriptor.catalog.sha256,
|
|
233
|
+
metadataSha256: downloaded.metadataSha256,
|
|
115
234
|
cacheHit: downloaded.cacheHit,
|
|
116
235
|
};
|
|
117
236
|
}
|
|
@@ -4,7 +4,7 @@ import type { CommandContext } from "./context.js";
|
|
|
4
4
|
import { type FreeDownloadIo } from "./free-download.js";
|
|
5
5
|
export interface LibraryUpdateDeps {
|
|
6
6
|
readonly fs: TransactionalFsWithCopy;
|
|
7
|
-
readonly free
|
|
7
|
+
readonly free?: Omit<FreeDownloadIo, "signal">;
|
|
8
8
|
readonly auth: AuthUseCaseDependencies;
|
|
9
9
|
readonly fetch?: typeof fetch;
|
|
10
10
|
readonly allowedProHosts?: readonly string[];
|
|
@@ -21,4 +21,8 @@ export declare function runLibraryUpdateUseCase(context: CommandContext, deps: L
|
|
|
21
21
|
readonly projectRoot: string;
|
|
22
22
|
readonly artifactVersion: string;
|
|
23
23
|
readonly files: readonly string[];
|
|
24
|
+
readonly downloadMode: "icons" | "full";
|
|
25
|
+
readonly downloadNotes: readonly string[];
|
|
26
|
+
readonly networkBytes?: number;
|
|
27
|
+
readonly selectedFiles?: number;
|
|
24
28
|
}>;
|
|
@@ -1,62 +1,97 @@
|
|
|
1
|
+
import { assertDownloadSelection } from "./selected-resources.js";
|
|
1
2
|
import { join, relative, resolve } from "node:path";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { parseCatalog } from "../catalog/catalog.js";
|
|
4
5
|
import { CliError } from "../errors/index.js";
|
|
5
6
|
import { planGeneratedFiles } from "../generator/generate.js";
|
|
6
|
-
import { readMoeiconsConfig } from "../project/config.js";
|
|
7
|
+
import { readMoeiconsConfig, loadConfigDocument, validateConfigDocument, } from "../project/config.js";
|
|
7
8
|
import { detectProject } from "../project/detect.js";
|
|
8
9
|
import { executeManagedReconcile } from "../project/install.js";
|
|
9
10
|
import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
|
|
10
11
|
import { withProjectLock } from "../project/project-lock.js";
|
|
11
12
|
import { ensureClassMergeDependencies, planTailwindIntegration } from "../project/tailwind.js";
|
|
12
|
-
import { extractTarGz } from "../project/tar-gz.js";
|
|
13
|
-
import { artifactCachePath, downloadFreeRelease } from "./free-download.js";
|
|
13
|
+
import { extractTarGz, ICON_ARCHIVE_MAX_ENTRIES, ICON_ARCHIVE_MAX_EXPANDED_BYTES, } from "../project/tar-gz.js";
|
|
14
|
+
import { artifactCachePath, downloadFreeRelease, metadataCachePath, } from "./free-download.js";
|
|
14
15
|
import { downloadProArtifact } from "./pro-download.js";
|
|
15
|
-
import {
|
|
16
|
+
import { PRO_DOWNLOAD_HOSTS, resolveProDescriptorEndpoint } from "./pro-download.js";
|
|
17
|
+
import { runAccessTokenUseCase } from "./auth.js";
|
|
18
|
+
import { resolveBitmapTuples, resolveConfiguredBitmapShards } from "./bitmap-shard-resolver.js";
|
|
19
|
+
import { mergeBitmapShardCacheManifest, readBitmapShardCacheManifest, writeBitmapShardCacheManifest, } from "./bitmap-shard-cache.js";
|
|
20
|
+
import { configuredComponentFiles, selectTargetSubtree, computeSubtreeHash, } from "./target-subtree.js";
|
|
16
21
|
export async function runLibraryUpdateUseCase(context, deps, expected) {
|
|
17
22
|
const project = detectProject(context.cwd);
|
|
18
23
|
if (!project)
|
|
19
24
|
throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents");
|
|
25
|
+
const document = loadConfigDocument(project.root);
|
|
26
|
+
const bootstrap = validateConfigDocument(document, undefined, { lenientCatalog: true });
|
|
27
|
+
if (bootstrap.kind !== "ok" || bootstrap.config.tier !== expected.tier)
|
|
28
|
+
throw new CliError("VALIDATION_ERROR", bootstrap.kind === "invalid"
|
|
29
|
+
? bootstrap.message
|
|
30
|
+
: `config.tier must match ${expected.tier}; edit config before updating`);
|
|
31
|
+
assertDownloadSelection(bootstrap.config);
|
|
32
|
+
let selected;
|
|
20
33
|
let catalogJson;
|
|
21
34
|
let archiveBytes;
|
|
22
35
|
let artifactSha256;
|
|
23
36
|
let catalogSha256;
|
|
37
|
+
let manifestJson;
|
|
38
|
+
let manualMd;
|
|
24
39
|
let tierSource;
|
|
25
40
|
if (expected.tier === "free") {
|
|
41
|
+
if (!deps.free)
|
|
42
|
+
throw new CliError("VALIDATION_ERROR", "free release download dependencies are missing");
|
|
26
43
|
const downloaded = await downloadFreeRelease({
|
|
27
44
|
...deps.free,
|
|
28
45
|
signal: context.signal,
|
|
29
46
|
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
30
|
-
}, expected.version
|
|
47
|
+
}, expected.version, {
|
|
48
|
+
config: bootstrap.config,
|
|
49
|
+
document,
|
|
50
|
+
onPlan: (message) => context.ui.note(message, context.signal),
|
|
51
|
+
});
|
|
31
52
|
if (!downloaded.ok)
|
|
32
53
|
throw new CliError(downloaded.reason === "cancelled" ? "CANCELLED" : "VALIDATION_ERROR", downloaded.message);
|
|
33
54
|
if (downloaded.descriptorSha256 !== expected.descriptorSha256 ||
|
|
34
55
|
downloaded.descriptor.fullVersion !== expected.version) {
|
|
35
56
|
throw new CliError("VALIDATION_ERROR", "downloaded release identity changed after version check; retry the update");
|
|
36
57
|
}
|
|
58
|
+
selected = downloaded.selected;
|
|
37
59
|
catalogJson = downloaded.catalogJson;
|
|
38
60
|
archiveBytes = downloaded.artifactBytes;
|
|
39
61
|
artifactSha256 = downloaded.descriptor.free.sha256;
|
|
40
62
|
catalogSha256 = downloaded.descriptor.catalog.sha256;
|
|
63
|
+
manifestJson = downloaded.manifestJson;
|
|
64
|
+
manualMd = downloaded.manualMd;
|
|
41
65
|
tierSource = downloaded.descriptor.free;
|
|
42
66
|
}
|
|
43
67
|
else {
|
|
44
68
|
const downloaded = await downloadProArtifact(context, deps.auth, expected, {
|
|
69
|
+
selection: { config: bootstrap.config, document },
|
|
45
70
|
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
46
71
|
...(deps.allowedProHosts ? { allowedHosts: deps.allowedProHosts } : {}),
|
|
47
72
|
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
48
73
|
});
|
|
74
|
+
selected = downloaded.selected;
|
|
49
75
|
catalogJson = downloaded.catalogJson;
|
|
50
76
|
archiveBytes = downloaded.artifactBytes;
|
|
51
77
|
artifactSha256 = downloaded.descriptor.sha256;
|
|
52
78
|
catalogSha256 = downloaded.descriptor.catalogSha256;
|
|
79
|
+
manifestJson = downloaded.manifestJson;
|
|
80
|
+
manualMd = downloaded.manualMd;
|
|
53
81
|
tierSource = downloaded.descriptor;
|
|
54
82
|
const cacheDir = context.env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
55
83
|
const cached = artifactCachePath(cacheDir, downloaded.descriptor.version, downloaded.descriptor.sha256);
|
|
56
|
-
if (!deps.fs.existsSync(cached)) {
|
|
84
|
+
if (!selected && !deps.fs.existsSync(cached)) {
|
|
57
85
|
deps.fs.mkdirSync(join(cached, ".."), { recursive: true });
|
|
58
86
|
deps.fs.writeFileSync(cached, downloaded.artifactBytes);
|
|
59
87
|
}
|
|
88
|
+
if (downloaded.metadataBytes && downloaded.descriptor.metadata) {
|
|
89
|
+
const metaCached = metadataCachePath(cacheDir, downloaded.descriptor.version, downloaded.descriptor.metadata.sha256);
|
|
90
|
+
if (!deps.fs.existsSync(metaCached)) {
|
|
91
|
+
deps.fs.mkdirSync(join(metaCached, ".."), { recursive: true });
|
|
92
|
+
deps.fs.writeFileSync(metaCached, downloaded.metadataBytes);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
60
95
|
}
|
|
61
96
|
let candidateCatalog;
|
|
62
97
|
try {
|
|
@@ -69,49 +104,127 @@ export async function runLibraryUpdateUseCase(context, deps, expected) {
|
|
|
69
104
|
sha256Bytes(catalogJson) !== catalogSha256) {
|
|
70
105
|
throw new CliError("VALIDATION_ERROR", "candidate catalog identity does not match the selected release");
|
|
71
106
|
}
|
|
72
|
-
const loaded =
|
|
107
|
+
const loaded = validateConfigDocument(document, candidateCatalog);
|
|
73
108
|
if (loaded.kind !== "ok" || loaded.config.tier !== expected.tier)
|
|
74
109
|
throw new CliError("VALIDATION_ERROR", `config is invalid for the ${expected.tier} candidate`);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
const plannedConfig = JSON.stringify(loaded.config);
|
|
111
|
+
const installMetadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
112
|
+
const plannedMetadata = deps.fs.existsSync(installMetadataPath)
|
|
113
|
+
? deps.fs.readFileSync(installMetadataPath, "utf8")
|
|
114
|
+
: undefined;
|
|
115
|
+
const unpacked = selected
|
|
116
|
+
? { files: selected.files, errors: [] }
|
|
117
|
+
: extractTarGz(archiveBytes, {
|
|
118
|
+
maxEntries: ICON_ARCHIVE_MAX_ENTRIES,
|
|
119
|
+
maxExpandedBytes: ICON_ARCHIVE_MAX_EXPANDED_BYTES,
|
|
120
|
+
});
|
|
79
121
|
if (unpacked.errors.length)
|
|
80
122
|
throw new CliError("VALIDATION_ERROR", unpacked.errors[0] ?? "invalid artifact");
|
|
81
123
|
const target = loaded.config.target;
|
|
82
|
-
const
|
|
124
|
+
const selectedTarget = selected
|
|
125
|
+
? Object.fromEntries(Object.entries(selected.files)
|
|
126
|
+
.filter(([path]) => path.startsWith(`${target}/`))
|
|
127
|
+
.map(([path, bytes]) => [path.slice(target.length + 1), bytes]))
|
|
128
|
+
: undefined;
|
|
129
|
+
const subtree = selectedTarget
|
|
130
|
+
? { ok: true, target, files: selectedTarget, ...computeSubtreeHash(selectedTarget) }
|
|
131
|
+
: selectTargetSubtree(archiveBytes, tierSource, target);
|
|
83
132
|
if (!subtree.ok)
|
|
84
133
|
throw new CliError("VALIDATION_ERROR", subtree.message);
|
|
134
|
+
const archiveFiles = { ...unpacked.files };
|
|
135
|
+
let bitmapPins;
|
|
136
|
+
let bitmapShardSetSha256;
|
|
137
|
+
const tuples = resolveBitmapTuples(loaded.config, candidateCatalog);
|
|
138
|
+
if (!tuples.ok)
|
|
139
|
+
throw new CliError("VALIDATION_ERROR", tuples.errors.join("; "));
|
|
140
|
+
if (!selected && tuples.tuples.length > 0 && expected.tier === "pro") {
|
|
141
|
+
const cacheDir = context.env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
142
|
+
const cacheIo = {
|
|
143
|
+
mkdirSync: (path) => deps.fs.mkdirSync(path, { recursive: true }),
|
|
144
|
+
writeFileSync: deps.fs.writeFileSync,
|
|
145
|
+
renameSync: deps.fs.renameSync,
|
|
146
|
+
existsSync: deps.fs.existsSync,
|
|
147
|
+
rmSync: deps.fs.rmSync,
|
|
148
|
+
readFileSync: deps.fs.readFileSync,
|
|
149
|
+
readdirSync: deps.fs.readdirSync,
|
|
150
|
+
};
|
|
151
|
+
const cacheManifest = readBitmapShardCacheManifest(cacheDir, cacheIo);
|
|
152
|
+
const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
153
|
+
const old = deps.fs.existsSync(metadataPath)
|
|
154
|
+
? parseInstallMetadata(deps.fs.readFileSync(metadataPath, "utf8"))
|
|
155
|
+
: undefined;
|
|
156
|
+
const existingPins = mergeBitmapShardCacheManifest(cacheManifest, old?.bitmapShards ?? []).filter((pin) => pin.resourceVersion === expected.version);
|
|
157
|
+
const accessToken = await runAccessTokenUseCase(context, deps.auth);
|
|
158
|
+
const { loopbackHost } = resolveProDescriptorEndpoint(context.env);
|
|
159
|
+
const allowedHosts = deps.allowedProHosts ??
|
|
160
|
+
(loopbackHost ? [...PRO_DOWNLOAD_HOSTS, loopbackHost] : PRO_DOWNLOAD_HOSTS);
|
|
161
|
+
const shards = await resolveConfiguredBitmapShards({
|
|
162
|
+
config: loaded.config,
|
|
163
|
+
catalog: candidateCatalog,
|
|
164
|
+
version: expected.version,
|
|
165
|
+
descriptorSha256: expected.descriptorSha256,
|
|
166
|
+
cacheDir,
|
|
167
|
+
io: cacheIo,
|
|
168
|
+
accessToken,
|
|
169
|
+
allowedHosts,
|
|
170
|
+
existingPins,
|
|
171
|
+
env: context.env,
|
|
172
|
+
now: context.now().getTime(),
|
|
173
|
+
signal: context.signal,
|
|
174
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
175
|
+
});
|
|
176
|
+
Object.assign(archiveFiles, shards.files);
|
|
177
|
+
bitmapPins = shards.pins;
|
|
178
|
+
bitmapShardSetSha256 = shards.bitmapShardSetSha256;
|
|
179
|
+
writeBitmapShardCacheManifest(cacheDir, cacheIo, mergeBitmapShardCacheManifest(cacheManifest, shards.pins), context.now().getTime());
|
|
180
|
+
}
|
|
85
181
|
const generated = planGeneratedFiles(loaded.config, loaded.config.outputDir, {
|
|
86
|
-
archiveFiles
|
|
182
|
+
archiveFiles,
|
|
87
183
|
catalog: candidateCatalog,
|
|
88
184
|
});
|
|
89
185
|
if (!generated.ok)
|
|
90
186
|
throw new CliError("VALIDATION_ERROR", generated.errors.join("; "));
|
|
187
|
+
const installedFiles = selected
|
|
188
|
+
? subtree.files
|
|
189
|
+
: configuredComponentFiles(subtree.files, target, loaded.config, candidateCatalog);
|
|
91
190
|
const writes = {
|
|
92
191
|
".moeicons/catalog.json": catalogJson,
|
|
192
|
+
".moeicons/manifest.json": manifestJson,
|
|
193
|
+
".moeicons/MANUAL.md": manualMd,
|
|
194
|
+
".moeicons/artifact/package.json": '{"private":true,"type":"module","sideEffects":false}\n',
|
|
93
195
|
[`${loaded.config.outputDir.replace(/\\/g, "/").replace(/\/$/, "")}/.moeicons-${expected.tier}.marker`]: `${expected.tier}\n`,
|
|
94
196
|
};
|
|
95
197
|
for (const file of generated.files)
|
|
96
198
|
writes[file.path.replace(/\\/g, "/")] = file.content;
|
|
97
|
-
for (const [rel, bytes] of Object.entries(
|
|
199
|
+
for (const [rel, bytes] of Object.entries(installedFiles)) {
|
|
98
200
|
writes[`.moeicons/artifact/${target}/${rel}`] = bytes;
|
|
99
201
|
}
|
|
202
|
+
if (selected) {
|
|
203
|
+
writes[".moeicons/resource-index.json.gz"] = selected.indexBytes;
|
|
204
|
+
for (const [rel, bytes] of Object.entries(selected.files))
|
|
205
|
+
writes[`.moeicons/artifact/${rel}`] = bytes;
|
|
206
|
+
}
|
|
100
207
|
const tailwindPlan = planTailwindIntegration(project.root, loaded.config.outputDir, {
|
|
101
208
|
noTailwind: false,
|
|
102
209
|
target,
|
|
103
210
|
});
|
|
211
|
+
const expectedSideFiles = {};
|
|
104
212
|
for (const side of tailwindPlan.files) {
|
|
105
213
|
const rel = relative(project.root, resolve(side.path)).replace(/\\/g, "/");
|
|
106
214
|
if (!rel || rel === ".." || rel.startsWith("../"))
|
|
107
215
|
throw new CliError("VALIDATION_ERROR", `side file escapes project: ${side.path}`);
|
|
108
216
|
writes[rel] = side.content;
|
|
217
|
+
expectedSideFiles[rel] = deps.fs.existsSync(side.path)
|
|
218
|
+
? sha256Bytes(deps.fs.readFileSync(side.path))
|
|
219
|
+
: undefined;
|
|
109
220
|
}
|
|
110
221
|
const pkgPath = join(project.root, "package.json");
|
|
111
222
|
if (deps.fs.existsSync(pkgPath)) {
|
|
223
|
+
const pkgSource = deps.fs.readFileSync(pkgPath, "utf8");
|
|
224
|
+
expectedSideFiles["package.json"] = sha256Bytes(pkgSource);
|
|
112
225
|
const dependencyPlan = target === "react" || target === "vue"
|
|
113
|
-
? ensureClassMergeDependencies(
|
|
114
|
-
: { nextSource:
|
|
226
|
+
? ensureClassMergeDependencies(pkgSource)
|
|
227
|
+
: { nextSource: pkgSource, changed: false, notes: [] };
|
|
115
228
|
if (dependencyPlan.changed)
|
|
116
229
|
writes["package.json"] = dependencyPlan.nextSource;
|
|
117
230
|
}
|
|
@@ -132,13 +245,33 @@ export async function runLibraryUpdateUseCase(context, deps, expected) {
|
|
|
132
245
|
catalogSha256,
|
|
133
246
|
installedAt: context.now().toISOString(),
|
|
134
247
|
managedFiles,
|
|
248
|
+
...(selected
|
|
249
|
+
? {
|
|
250
|
+
delivery: {
|
|
251
|
+
mode: "icons",
|
|
252
|
+
indexSha256: selected.refs.index.sha256,
|
|
253
|
+
bundleSha256: selected.refs.bundle.sha256,
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
: {}),
|
|
257
|
+
generatedOutputDir: loaded.config.outputDir,
|
|
135
258
|
targetSha256: subtree.sha256,
|
|
136
259
|
targetFileCount: subtree.fileCount,
|
|
137
260
|
targetByteCount: subtree.byteCount,
|
|
261
|
+
...(bitmapPins && bitmapShardSetSha256
|
|
262
|
+
? { bitmapShards: bitmapPins, bitmapShardSetSha256 }
|
|
263
|
+
: {}),
|
|
138
264
|
};
|
|
139
265
|
writes[".moeicons/install-metadata.json"] = serializeInstallMetadata(nextMetadata);
|
|
140
266
|
await withProjectLock(project.root, "update", () => {
|
|
267
|
+
const currentConfig = readMoeiconsConfig(project.root, candidateCatalog);
|
|
268
|
+
if (currentConfig.kind !== "ok" || JSON.stringify(currentConfig.config) !== plannedConfig)
|
|
269
|
+
throw new CliError("VALIDATION_ERROR", "config changed while preparing update; retry");
|
|
141
270
|
const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
271
|
+
if (!plannedMetadata ||
|
|
272
|
+
!deps.fs.existsSync(metadataPath) ||
|
|
273
|
+
deps.fs.readFileSync(metadataPath, "utf8") !== plannedMetadata)
|
|
274
|
+
throw new CliError("VALIDATION_ERROR", "installation changed while preparing update; retry");
|
|
142
275
|
if (!deps.fs.existsSync(metadataPath))
|
|
143
276
|
throw new CliError("VALIDATION_ERROR", "managed install metadata is missing");
|
|
144
277
|
const old = parseInstallMetadata(deps.fs.readFileSync(metadataPath, "utf8"));
|
|
@@ -151,11 +284,37 @@ export async function runLibraryUpdateUseCase(context, deps, expected) {
|
|
|
151
284
|
throw new CliError("VALIDATION_ERROR", `managed file was modified or removed: ${path}`);
|
|
152
285
|
}
|
|
153
286
|
}
|
|
154
|
-
|
|
287
|
+
for (const path of Object.keys(managedFiles)) {
|
|
288
|
+
if (deps.fs.existsSync(join(project.root, path)) && !(path in old.managedFiles)) {
|
|
289
|
+
throw new CliError("VALIDATION_ERROR", `update path collides with an unowned user file: ${path}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
executeManagedReconcile(project.root, writes, Object.keys(old.managedFiles).filter((path) => !(path in managedFiles)), deps.fs, {
|
|
293
|
+
expectedSha256: {
|
|
294
|
+
...Object.fromEntries([...Object.keys(writes), ...Object.keys(old.managedFiles)].map((path) => [
|
|
295
|
+
path,
|
|
296
|
+
old.managedFiles[path] ??
|
|
297
|
+
(path === ".moeicons/install-metadata.json"
|
|
298
|
+
? sha256Bytes(plannedMetadata)
|
|
299
|
+
: undefined),
|
|
300
|
+
])),
|
|
301
|
+
...expectedSideFiles,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
155
304
|
});
|
|
156
305
|
return {
|
|
157
306
|
projectRoot: project.root,
|
|
158
307
|
artifactVersion: expected.version,
|
|
159
308
|
files: generated.files.map((file) => file.path),
|
|
309
|
+
downloadMode: selected ? "icons" : "full",
|
|
310
|
+
downloadNotes: selected?.fallbacks ??
|
|
311
|
+
(bootstrap.config.downloadMode === "full"
|
|
312
|
+
? []
|
|
313
|
+
: [
|
|
314
|
+
"Release does not advertise selected downloads; auto used the verified full archive. Set downloadMode=icons to require selected downloads.",
|
|
315
|
+
]),
|
|
316
|
+
...(selected
|
|
317
|
+
? { networkBytes: selected.networkBytes, selectedFiles: Object.keys(selected.files).length }
|
|
318
|
+
: {}),
|
|
160
319
|
};
|
|
161
320
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-1b (DEC-A1): a strictly gated seam that lets the packed CLI consume a
|
|
3
|
+
* declared local-test candidate (`X.Y.Z-test`, channel `local-test`,
|
|
4
|
+
* `publishable: false`) through the Pro flow, without touching the production
|
|
5
|
+
* version parsers. `allowLocalTest` is true only when ALL hold:
|
|
6
|
+
* - MOEICONS_ENV=local
|
|
7
|
+
* - the resolved versions endpoint is loopback HTTP
|
|
8
|
+
* - the resolved Pro descriptor endpoint is loopback HTTP
|
|
9
|
+
* Any other combination fails closed, so a production endpoint returning
|
|
10
|
+
* `X.Y.Z-test` is still rejected.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LOCAL_TEST_ENV_VALUE = "local";
|
|
13
|
+
export declare function isLoopbackHttpUrl(value: string): boolean;
|
|
14
|
+
export declare function resolveLocalTestContext(env: Readonly<Record<string, string | undefined>>): {
|
|
15
|
+
readonly allowLocalTest: boolean;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Read-side local-test context for an already-installed project. The Pro network
|
|
19
|
+
* seam above is strict; parsing a *local* install metadata file may also be a
|
|
20
|
+
* Free fixture install driven by `MOEICONS_FREE_RELEASE_DIR` (the existing local
|
|
21
|
+
* release-directory seam). Both are local-only and never set on a production
|
|
22
|
+
* endpoint, so they share the read-side allowance.
|
|
23
|
+
*/
|
|
24
|
+
export declare function allowLocalTestFromEnv(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveProDescriptorEndpoint } from "./pro-download.js";
|
|
2
|
+
import { resolveVersionsEndpoint } from "./version-service.js";
|
|
3
|
+
/**
|
|
4
|
+
* A-1b (DEC-A1): a strictly gated seam that lets the packed CLI consume a
|
|
5
|
+
* declared local-test candidate (`X.Y.Z-test`, channel `local-test`,
|
|
6
|
+
* `publishable: false`) through the Pro flow, without touching the production
|
|
7
|
+
* version parsers. `allowLocalTest` is true only when ALL hold:
|
|
8
|
+
* - MOEICONS_ENV=local
|
|
9
|
+
* - the resolved versions endpoint is loopback HTTP
|
|
10
|
+
* - the resolved Pro descriptor endpoint is loopback HTTP
|
|
11
|
+
* Any other combination fails closed, so a production endpoint returning
|
|
12
|
+
* `X.Y.Z-test` is still rejected.
|
|
13
|
+
*/
|
|
14
|
+
export const LOCAL_TEST_ENV_VALUE = "local";
|
|
15
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
16
|
+
export function isLoopbackHttpUrl(value) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function resolveLocalTestContext(env) {
|
|
26
|
+
if (env.MOEICONS_ENV !== LOCAL_TEST_ENV_VALUE)
|
|
27
|
+
return { allowLocalTest: false };
|
|
28
|
+
if (!isLoopbackHttpUrl(resolveVersionsEndpoint(env)))
|
|
29
|
+
return { allowLocalTest: false };
|
|
30
|
+
if (!resolveProDescriptorEndpoint(env).allowLoopback)
|
|
31
|
+
return { allowLocalTest: false };
|
|
32
|
+
return { allowLocalTest: true };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read-side local-test context for an already-installed project. The Pro network
|
|
36
|
+
* seam above is strict; parsing a *local* install metadata file may also be a
|
|
37
|
+
* Free fixture install driven by `MOEICONS_FREE_RELEASE_DIR` (the existing local
|
|
38
|
+
* release-directory seam). Both are local-only and never set on a production
|
|
39
|
+
* endpoint, so they share the read-side allowance.
|
|
40
|
+
*/
|
|
41
|
+
export function allowLocalTestFromEnv(env) {
|
|
42
|
+
return resolveLocalTestContext(env).allowLocalTest || env.MOEICONS_FREE_RELEASE_DIR !== undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type TransactionalFsWithCopy } from "../project/install.js";
|
|
2
|
+
import type { CommandContext } from "./context.js";
|
|
3
|
+
import type { AuthUseCaseDependencies } from "./auth.js";
|
|
4
|
+
import { type FreeDownloadIo } from "./free-download.js";
|
|
5
|
+
export interface MetadataSyncDeps {
|
|
6
|
+
readonly fs: TransactionalFsWithCopy;
|
|
7
|
+
readonly free: Omit<FreeDownloadIo, "signal">;
|
|
8
|
+
readonly auth: AuthUseCaseDependencies;
|
|
9
|
+
readonly fetch?: typeof fetch;
|
|
10
|
+
readonly allowedProHosts?: readonly string[];
|
|
11
|
+
readonly onProgress?: (event: {
|
|
12
|
+
readonly downloadedBytes: number;
|
|
13
|
+
readonly totalBytes?: number;
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Metadata-only sync: downloads only the small metadata archive for the
|
|
18
|
+
* already-installed code version and reconciles `.moeicons/{MANUAL.md,
|
|
19
|
+
* catalog.json, manifest.json}`. Never downloads the code archive and never
|
|
20
|
+
* changes the installed code version.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runMetadataSyncUseCase(context: CommandContext, deps: MetadataSyncDeps, options?: {
|
|
23
|
+
readonly tier?: "free" | "pro";
|
|
24
|
+
}): Promise<{
|
|
25
|
+
readonly projectRoot: string;
|
|
26
|
+
readonly tier: "free" | "pro";
|
|
27
|
+
readonly artifactVersion: string;
|
|
28
|
+
readonly metadataSha256: string;
|
|
29
|
+
readonly files: readonly string[];
|
|
30
|
+
}>;
|