makefx 0.2.4 → 0.2.6

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 (3) hide show
  1. package/README.md +1 -1
  2. package/makefx.mjs +313 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -176,4 +176,4 @@ Run `makefx --help` or `makefx help <command>` for command-specific options.
176
176
  - Media generation guide: https://makefx.app/docs/media-playbooks
177
177
  - Source: https://github.com/krasnoperov/inventory
178
178
 
179
- Version: 0.2.4
179
+ Version: 0.2.6
package/makefx.mjs CHANGED
@@ -4,9 +4,10 @@ import process$1 from "node:process";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import http from "node:http";
6
6
  import { spawn } from "node:child_process";
7
- import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
7
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import os from "node:os";
9
9
  import path from "node:path";
10
+ import { createReadStream } from "node:fs";
10
11
  import https from "node:https";
11
12
  //#region \0rolldown/runtime.js
12
13
  var __create = Object.create;
@@ -5160,6 +5161,167 @@ function resolveMediaType(ext, requestedMediaKind) {
5160
5161
  return mediaType;
5161
5162
  }
5162
5163
  //#endregion
5164
+ //#region src/shared/websocket-types.ts
5165
+ var DEFAULT_MEDIA_KIND = "image";
5166
+ //#endregion
5167
+ //#region src/cli/lib/mirror-store.ts
5168
+ var MIRROR_REGISTRY_VERSION = 1;
5169
+ var MIRROR_REGISTRY_FILE = "mirrors.json";
5170
+ function getMirrorRegistryPath(projectRoot = process.cwd()) {
5171
+ return path.join(projectRoot, ".inventory", MIRROR_REGISTRY_FILE);
5172
+ }
5173
+ async function readMirrorRegistry(projectRoot = process.cwd()) {
5174
+ const registryPath = getMirrorRegistryPath(projectRoot);
5175
+ let raw;
5176
+ try {
5177
+ raw = await readFile(registryPath, "utf8");
5178
+ } catch (error) {
5179
+ if (error.code === "ENOENT") return emptyRegistry();
5180
+ throw error;
5181
+ }
5182
+ let parsed;
5183
+ try {
5184
+ parsed = JSON.parse(raw);
5185
+ } catch {
5186
+ throw new Error(`Mirror registry is not valid JSON: ${registryPath}. Move it aside or delete it so the CLI can recreate it.`);
5187
+ }
5188
+ if (!isMirrorRegistry(parsed)) throw new Error(`Mirror registry has an unsupported format: ${registryPath}`);
5189
+ return parsed;
5190
+ }
5191
+ async function writeMirrorRegistry(registry, projectRoot = process.cwd()) {
5192
+ const registryPath = getMirrorRegistryPath(projectRoot);
5193
+ await mkdir(path.dirname(registryPath), { recursive: true });
5194
+ const tempPath = `${registryPath}.${process.pid}.${Date.now()}.tmp`;
5195
+ await writeFile(tempPath, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
5196
+ await rename(tempPath, registryPath);
5197
+ return registryPath;
5198
+ }
5199
+ async function fingerprintFile(filePath) {
5200
+ const fileStat = await stat(filePath);
5201
+ if (!fileStat.isFile()) throw new Error(`Mirror source is not a file: ${filePath}`);
5202
+ const hash = createHash("sha256");
5203
+ await new Promise((resolve, reject) => {
5204
+ const stream = createReadStream(filePath);
5205
+ stream.on("data", (chunk) => hash.update(chunk));
5206
+ stream.on("error", reject);
5207
+ stream.on("end", resolve);
5208
+ });
5209
+ return {
5210
+ sha256: hash.digest("hex"),
5211
+ sizeBytes: fileStat.size
5212
+ };
5213
+ }
5214
+ async function resolveMirrorForFile(input) {
5215
+ const projectRoot = input.projectRoot ?? process.cwd();
5216
+ const registry = await readMirrorRegistry(projectRoot);
5217
+ const fingerprint = await fingerprintFile(input.filePath);
5218
+ const pathAlias = toMirrorPathAlias(input.filePath, projectRoot);
5219
+ const mediaKind = input.mediaKind ?? "image";
5220
+ return {
5221
+ fingerprint,
5222
+ digestEntry: findMirrorByDigest(registry, {
5223
+ ...input,
5224
+ mediaKind,
5225
+ ...fingerprint
5226
+ }),
5227
+ pathEntry: findMirrorByPath(registry, {
5228
+ ...input,
5229
+ mediaKind,
5230
+ pathAlias
5231
+ }),
5232
+ pathAlias
5233
+ };
5234
+ }
5235
+ async function recordMirrorForFile(input) {
5236
+ const projectRoot = input.projectRoot ?? process.cwd();
5237
+ const registry = await readMirrorRegistry(projectRoot);
5238
+ const fingerprint = await fingerprintFile(input.filePath);
5239
+ const pathAlias = toMirrorPathAlias(input.filePath, projectRoot);
5240
+ const mediaKind = input.mediaKind ?? "image";
5241
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5242
+ movePathAliasToCurrentMirror(registry, {
5243
+ ...input,
5244
+ mediaKind,
5245
+ pathAlias
5246
+ });
5247
+ const existing = findMirrorByDigest(registry, {
5248
+ ...input,
5249
+ mediaKind,
5250
+ ...fingerprint
5251
+ });
5252
+ const entry = {
5253
+ version: MIRROR_REGISTRY_VERSION,
5254
+ baseUrl: input.baseUrl,
5255
+ ...input.environment ? { environment: input.environment } : {},
5256
+ spaceId: input.spaceId,
5257
+ sha256: fingerprint.sha256,
5258
+ sizeBytes: fingerprint.sizeBytes,
5259
+ paths: mergePathAliases(existing?.paths ?? [], pathAlias),
5260
+ assetId: input.assetId,
5261
+ variantId: input.variantId,
5262
+ mediaKind,
5263
+ mediaKey: input.mediaKey ?? null,
5264
+ updatedAt: now
5265
+ };
5266
+ if (existing) {
5267
+ const index = registry.entries.indexOf(existing);
5268
+ registry.entries[index] = entry;
5269
+ } else registry.entries.push(entry);
5270
+ await writeMirrorRegistry(registry, projectRoot);
5271
+ return entry;
5272
+ }
5273
+ function describeMirrorMismatch(filePath, entry) {
5274
+ return [
5275
+ `Local reference changed since it was mirrored: ${filePath}`,
5276
+ `Previously mirrored to variant ${entry.variantId} in space ${entry.spaceId}.`,
5277
+ "Use the existing variant ID explicitly, restore the original file, or upload/register a new reference intentionally."
5278
+ ].join(" ");
5279
+ }
5280
+ function findMirrorByDigest(registry, input) {
5281
+ return registry.entries.find((entry) => entry.baseUrl === input.baseUrl && entry.spaceId === input.spaceId && entry.sha256 === input.sha256 && entry.sizeBytes === input.sizeBytes && entry.mediaKind === input.mediaKind && environmentMatches(entry.environment, input.environment));
5282
+ }
5283
+ function findMirrorByPath(registry, input) {
5284
+ return registry.entries.find((entry) => entry.baseUrl === input.baseUrl && entry.spaceId === input.spaceId && entry.paths.includes(input.pathAlias) && entry.mediaKind === input.mediaKind && environmentMatches(entry.environment, input.environment));
5285
+ }
5286
+ function environmentMatches(entryEnvironment, inputEnvironment) {
5287
+ return (entryEnvironment ?? "") === (inputEnvironment ?? "");
5288
+ }
5289
+ function movePathAliasToCurrentMirror(registry, input) {
5290
+ for (const entry of registry.entries) {
5291
+ if (entry.baseUrl !== input.baseUrl || entry.spaceId !== input.spaceId || entry.mediaKind !== input.mediaKind || !environmentMatches(entry.environment, input.environment)) continue;
5292
+ entry.paths = entry.paths.filter((pathAlias) => pathAlias !== input.pathAlias);
5293
+ }
5294
+ }
5295
+ function toMirrorPathAlias(filePath, projectRoot) {
5296
+ const absolutePath = path.resolve(filePath);
5297
+ const relativePath = path.relative(projectRoot, absolutePath);
5298
+ if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) return normalizePath(relativePath);
5299
+ return normalizePath(absolutePath);
5300
+ }
5301
+ function normalizePath(value) {
5302
+ return value.split(path.sep).join("/");
5303
+ }
5304
+ function mergePathAliases(paths, pathAlias) {
5305
+ return Array.from(new Set([...paths, pathAlias])).sort();
5306
+ }
5307
+ function emptyRegistry() {
5308
+ return {
5309
+ version: MIRROR_REGISTRY_VERSION,
5310
+ entries: []
5311
+ };
5312
+ }
5313
+ function isMirrorRegistry(value) {
5314
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5315
+ const registry = value;
5316
+ if (registry.version !== MIRROR_REGISTRY_VERSION || !Array.isArray(registry.entries)) return false;
5317
+ return registry.entries.every(isMirrorEntry);
5318
+ }
5319
+ function isMirrorEntry(value) {
5320
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5321
+ const entry = value;
5322
+ return entry.version === MIRROR_REGISTRY_VERSION && typeof entry.baseUrl === "string" && (entry.environment === void 0 || typeof entry.environment === "string") && typeof entry.spaceId === "string" && typeof entry.sha256 === "string" && typeof entry.sizeBytes === "number" && Array.isArray(entry.paths) && entry.paths.every((item) => typeof item === "string") && typeof entry.assetId === "string" && typeof entry.variantId === "string" && typeof entry.mediaKind === "string" && (entry.mediaKey === void 0 || entry.mediaKey === null || typeof entry.mediaKey === "string") && typeof entry.updatedAt === "string";
5323
+ }
5324
+ //#endregion
5163
5325
  //#region src/cli/commands/upload.ts
5164
5326
  /**
5165
5327
  * Upload Command - Upload one media file to create variants
@@ -5176,6 +5338,7 @@ var defaultDeps$12 = {
5176
5338
  fetch,
5177
5339
  readFile,
5178
5340
  stat,
5341
+ recordMirrorForFile,
5179
5342
  print: console.log
5180
5343
  };
5181
5344
  var UploadUsageError = class extends Error {};
@@ -5318,6 +5481,18 @@ async function executeUpload(parsed, deps = defaultDeps$12) {
5318
5481
  lineage: upload.lineage,
5319
5482
  organization
5320
5483
  };
5484
+ await recordUploadMirror({
5485
+ deps,
5486
+ projectRoot: projectConfig?.projectRoot,
5487
+ env,
5488
+ baseUrl,
5489
+ spaceId,
5490
+ filePath,
5491
+ assetId: upload.asset?.id ?? upload.variant.asset_id,
5492
+ variantId: upload.variant.id,
5493
+ mediaKind: upload.variant.media_kind || mediaType.mediaKind,
5494
+ mediaKey: upload.variant.media_key || upload.variant.image_key
5495
+ });
5321
5496
  if (jsonOutput) {
5322
5497
  deps.print(JSON.stringify(result, null, 2));
5323
5498
  return result;
@@ -5359,6 +5534,24 @@ async function executeUpload(parsed, deps = defaultDeps$12) {
5359
5534
  throw error;
5360
5535
  }
5361
5536
  }
5537
+ async function recordUploadMirror(input) {
5538
+ if (!input.deps.recordMirrorForFile) return;
5539
+ try {
5540
+ await input.deps.recordMirrorForFile({
5541
+ projectRoot: input.projectRoot,
5542
+ baseUrl: input.baseUrl,
5543
+ environment: input.env,
5544
+ spaceId: input.spaceId,
5545
+ filePath: input.filePath,
5546
+ assetId: input.assetId,
5547
+ variantId: input.variantId,
5548
+ mediaKind: input.mediaKind,
5549
+ mediaKey: input.mediaKey
5550
+ });
5551
+ } catch (error) {
5552
+ console.warn(`Warning: upload succeeded but mirror registry was not updated: ${error instanceof Error ? error.message : String(error)}`);
5553
+ }
5554
+ }
5362
5555
  function parseJsonObjectOption(value, optionName) {
5363
5556
  if (value === void 0) return void 0;
5364
5557
  let parsed;
@@ -5631,9 +5824,6 @@ Examples:
5631
5824
  `);
5632
5825
  }
5633
5826
  //#endregion
5634
- //#region src/shared/websocket-types.ts
5635
- var DEFAULT_MEDIA_KIND = "image";
5636
- //#endregion
5637
5827
  //#region src/cli/lib/image-transfer.ts
5638
5828
  var MAX_FILE_SIZE_MB = 10;
5639
5829
  var MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
@@ -21455,6 +21645,19 @@ var VIDEO_GENERATION_REQUEST_TIMEOUT_MS = 72e4;
21455
21645
  function getGenerationRequestTimeoutMs(mediaKind) {
21456
21646
  return mediaKind === "video" ? VIDEO_GENERATION_REQUEST_TIMEOUT_MS : GENERATION_REQUEST_TIMEOUT_MS;
21457
21647
  }
21648
+ /**
21649
+ * Thrown when a mutation's confirmation broadcast does not arrive within the
21650
+ * timeout. The server write may still have landed — callers can re-read state
21651
+ * to confirm. Distinct from server-rejection errors (PERMISSION_DENIED,
21652
+ * VALIDATION_ERROR, …), which reject with their own message and must not be
21653
+ * treated as a missing confirmation.
21654
+ */
21655
+ var ServerConfirmationTimeoutError = class extends Error {
21656
+ constructor(message = "Timed out waiting for server confirmation") {
21657
+ super(message);
21658
+ this.name = "ServerConfirmationTimeoutError";
21659
+ }
21660
+ };
21458
21661
  var WebSocketClient = class WebSocketClient {
21459
21662
  ws = null;
21460
21663
  baseUrl;
@@ -22031,7 +22234,7 @@ var WebSocketClient = class WebSocketClient {
22031
22234
  reject,
22032
22235
  timeout: setTimeout(() => {
22033
22236
  this.removeWaiter(waiter);
22034
- reject(/* @__PURE__ */ new Error("Timed out waiting for server confirmation"));
22237
+ reject(new ServerConfirmationTimeoutError());
22035
22238
  }, timeoutMs)
22036
22239
  };
22037
22240
  this.mutationWaiters.push(waiter);
@@ -22885,6 +23088,8 @@ var defaultDeps$10 = {
22885
23088
  resolveBaseUrl,
22886
23089
  createClient: WebSocketClient.create,
22887
23090
  uploadLocalReference: uploadLocalImageAsReference,
23091
+ resolveMirrorForFile,
23092
+ recordMirrorForFile,
22888
23093
  downloadImage,
22889
23094
  downloadFile,
22890
23095
  saveRunManifest,
@@ -23602,12 +23807,41 @@ async function resolveReferenceVariantIds(refs, ctx, deps, variants = [], mediaK
23602
23807
  const variantIds = [];
23603
23808
  for (const ref of refs) {
23604
23809
  if (await deps.fileExists(ref)) {
23810
+ const localReferenceMediaKind = mediaKind === "video" ? CLI_GENERATION_MEDIA_KIND : mediaKind;
23811
+ const mirror = deps.resolveMirrorForFile ? await deps.resolveMirrorForFile({
23812
+ projectRoot: ctx.projectRoot,
23813
+ baseUrl: ctx.baseUrl,
23814
+ environment: ctx.env,
23815
+ spaceId: ctx.spaceId,
23816
+ filePath: ref,
23817
+ mediaKind: localReferenceMediaKind
23818
+ }) : void 0;
23819
+ if (mirror?.pathEntry && (mirror.pathEntry.sha256 !== mirror.fingerprint.sha256 || mirror.pathEntry.sizeBytes !== mirror.fingerprint.sizeBytes)) throw new Error(describeMirrorMismatch(ref, mirror.pathEntry));
23820
+ if (mirror?.digestEntry) {
23821
+ validateReferenceVariant(mirror.digestEntry.variantId, variants, mediaKind);
23822
+ await recordReusedReferenceMirror({
23823
+ ref,
23824
+ ctx,
23825
+ deps,
23826
+ entry: mirror.digestEntry
23827
+ });
23828
+ variantIds.push(mirror.digestEntry.variantId);
23829
+ continue;
23830
+ }
23831
+ if (mirror?.pathEntry) throw new Error(describeMirrorMismatch(ref, mirror.pathEntry));
23605
23832
  const uploaded = await deps.uploadLocalReference({
23606
23833
  baseUrl: ctx.baseUrl,
23607
23834
  accessToken: ctx.accessToken,
23608
23835
  spaceId: ctx.spaceId,
23609
23836
  filePath: ref
23610
23837
  });
23838
+ if (deps.recordMirrorForFile) await recordUploadedReferenceMirror({
23839
+ ref,
23840
+ ctx,
23841
+ deps,
23842
+ uploaded,
23843
+ mediaKind: localReferenceMediaKind
23844
+ });
23611
23845
  variantIds.push(uploaded.variant.id);
23612
23846
  continue;
23613
23847
  }
@@ -23617,6 +23851,40 @@ async function resolveReferenceVariantIds(refs, ctx, deps, variants = [], mediaK
23617
23851
  }
23618
23852
  return variantIds;
23619
23853
  }
23854
+ async function recordReusedReferenceMirror(input) {
23855
+ try {
23856
+ await input.deps.recordMirrorForFile?.({
23857
+ projectRoot: input.ctx.projectRoot,
23858
+ baseUrl: input.ctx.baseUrl,
23859
+ environment: input.ctx.env,
23860
+ spaceId: input.ctx.spaceId,
23861
+ filePath: input.ref,
23862
+ assetId: input.entry.assetId,
23863
+ variantId: input.entry.variantId,
23864
+ mediaKind: input.entry.mediaKind,
23865
+ mediaKey: input.entry.mediaKey
23866
+ });
23867
+ } catch (error) {
23868
+ console.warn(`Warning: reused mirrored reference but mirror registry alias was not updated: ${error instanceof Error ? error.message : String(error)}`);
23869
+ }
23870
+ }
23871
+ async function recordUploadedReferenceMirror(input) {
23872
+ try {
23873
+ await input.deps.recordMirrorForFile?.({
23874
+ projectRoot: input.ctx.projectRoot,
23875
+ baseUrl: input.ctx.baseUrl,
23876
+ environment: input.ctx.env,
23877
+ spaceId: input.ctx.spaceId,
23878
+ filePath: input.ref,
23879
+ assetId: input.uploaded.asset?.id ?? input.uploaded.variant.asset_id,
23880
+ variantId: input.uploaded.variant.id,
23881
+ mediaKind: input.uploaded.variant.media_kind ?? input.mediaKind,
23882
+ mediaKey: input.uploaded.variant.media_key ?? input.uploaded.variant.image_key
23883
+ });
23884
+ } catch (error) {
23885
+ console.warn(`Warning: reference upload succeeded but mirror registry was not updated: ${error instanceof Error ? error.message : String(error)}`);
23886
+ }
23887
+ }
23620
23888
  function validateReferenceVariant(ref, variants, mediaKind) {
23621
23889
  const variant = variants.find((candidate) => candidate.id === ref);
23622
23890
  if (!variant) throw new Error(`Reference variant not found in space sync state: ${ref}`);
@@ -24387,6 +24655,7 @@ var defaultDeps$6 = {
24387
24655
  resolveBaseUrl,
24388
24656
  fetch,
24389
24657
  downloadFile,
24658
+ recordMirrorForFile,
24390
24659
  createMutationClient: (env, spaceId) => WebSocketClient.create(env, spaceId),
24391
24660
  print: console.log
24392
24661
  };
@@ -24443,6 +24712,12 @@ async function executeAssets(parsed, deps = defaultDeps$6) {
24443
24712
  outputPath,
24444
24713
  force: ctx.force
24445
24714
  });
24715
+ await recordDownloadedMirror({
24716
+ ctx,
24717
+ deps,
24718
+ outputPath,
24719
+ variant: resolved.variant
24720
+ });
24446
24721
  deps.print(`Downloaded ${resolved.mediaKey} to ${outputPath}`);
24447
24722
  return {
24448
24723
  type: "download",
@@ -24476,7 +24751,7 @@ async function executeAssets(parsed, deps = defaultDeps$6) {
24476
24751
  const assetId = parsed.positionals[1];
24477
24752
  const variantId = parsed.positionals[2] || normalizeOption(parsed.options.variant);
24478
24753
  if (!assetId || !variantId) throw new Error("Usage: makefx assets set-active <asset-id> <variant-id>");
24479
- const asset = await withAssetClient(ctx, deps, (client) => client.setActiveVariant(assetId, variantId));
24754
+ const asset = await setActiveVariant(ctx, deps, assetId, variantId);
24480
24755
  deps.print(`Set active variant of asset ${assetId} to ${asset.active_variant_id}`);
24481
24756
  return {
24482
24757
  type: "set-active",
@@ -24502,15 +24777,45 @@ async function buildContext$6(parsed, deps) {
24502
24777
  spaceId,
24503
24778
  baseUrl: deps.resolveBaseUrl(env),
24504
24779
  accessToken: config.token.accessToken,
24505
- force: parsed.options.force === "true"
24780
+ force: parsed.options.force === "true",
24781
+ projectRoot: projectConfig?.projectRoot
24506
24782
  };
24507
24783
  }
24784
+ async function recordDownloadedMirror(input) {
24785
+ const variant = input.variant;
24786
+ if (!variant || !input.deps.recordMirrorForFile) return;
24787
+ try {
24788
+ await input.deps.recordMirrorForFile({
24789
+ projectRoot: input.ctx.projectRoot,
24790
+ baseUrl: input.ctx.baseUrl,
24791
+ environment: input.ctx.env,
24792
+ spaceId: input.ctx.spaceId,
24793
+ filePath: input.outputPath,
24794
+ assetId: variant.asset_id,
24795
+ variantId: variant.id,
24796
+ mediaKind: variant.media_kind || "image",
24797
+ mediaKey: variant.media_key || variant.image_key
24798
+ });
24799
+ } catch (error) {
24800
+ console.warn(`Warning: download succeeded but mirror registry was not updated: ${error instanceof Error ? error.message : String(error)}`);
24801
+ }
24802
+ }
24508
24803
  async function listAssets$1(ctx, deps) {
24509
24804
  return (await fetchJson(ctx, deps, `/api/spaces/${ctx.spaceId}/assets`)).assets || [];
24510
24805
  }
24511
24806
  async function getAssetDetails(ctx, deps, assetId) {
24512
24807
  return fetchJson(ctx, deps, `/api/spaces/${ctx.spaceId}/assets/${assetId}`);
24513
24808
  }
24809
+ async function setActiveVariant(ctx, deps, assetId, variantId) {
24810
+ try {
24811
+ return await withAssetClient(ctx, deps, (client) => client.setActiveVariant(assetId, variantId));
24812
+ } catch (error) {
24813
+ if (!(error instanceof ServerConfirmationTimeoutError)) throw error;
24814
+ const { asset } = await getAssetDetails(ctx, deps, assetId);
24815
+ if (asset.active_variant_id === variantId) return asset;
24816
+ throw error;
24817
+ }
24818
+ }
24514
24819
  async function fetchJson(ctx, deps, apiPath) {
24515
24820
  const response = await deps.fetch(`${ctx.baseUrl}${apiPath}`, { headers: {
24516
24821
  "Authorization": `Bearer ${ctx.accessToken}`,
@@ -26247,7 +26552,7 @@ Options:
26247
26552
  }
26248
26553
  //#endregion
26249
26554
  //#region src/cli/index.ts
26250
- var CLI_VERSION = "0.2.4+f4148f7b2b61";
26555
+ var CLI_VERSION = "0.2.6+be63b9dd565a";
26251
26556
  async function main() {
26252
26557
  const argv = process$1.argv.slice(2);
26253
26558
  if (argv[0] === "--") argv.shift();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makefx",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Command-line interface for AI-assisted game asset production with Make Effects.",
5
5
  "license": "MIT",
6
6
  "type": "module",