@stage5/lumine 0.1.3 → 0.1.4

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 +14 -0
  2. package/bin/lumine.js +323 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,9 @@ Launch Twinkle Lumine builds from any terminal.
6
6
  npx @stage5/lumine@latest
7
7
  npx @stage5/lumine@latest login
8
8
  npx @stage5/lumine@latest projects
9
+ npx @stage5/lumine@latest explore --sort forks
10
+ npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
11
+ npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
9
12
  npx @stage5/lumine@latest pull
10
13
  npx @stage5/lumine@latest save
11
14
  npx @stage5/lumine@latest save --publish
@@ -21,6 +24,13 @@ pulling the owner's main project creates or reuses your contribution branch and
21
24
  checks out that branch locally. Saves go to your branch, so the project owner
22
25
  can merge or replace main from Twinkle.
23
26
 
27
+ Use `lumine explore` to list public open-source Build apps that can be used as
28
+ examples or starting points. It supports `--search` and `--sort forks`,
29
+ `--sort popular`, or `--sort recent`. Use `lumine reference <build-url-or-id>`
30
+ to pull source files into a read-only reference folder, or
31
+ `lumine fork <build-url-or-id>` to create your own editable fork and pull it
32
+ locally.
33
+
24
34
  After editing pulled files, run `lumine save` from that folder. The CLI saves
25
35
  through Twinkle's normal workspace project-file route, creates a project artifact
26
36
  version, records the same save metadata, and marks public builds as having
@@ -35,6 +45,10 @@ uploaded by `lumine save`. Build apps run in sandboxed iframes without native
35
45
  form submission, so use JavaScript-handled inputs and buttons instead of
36
46
  `<form>` elements.
37
47
 
48
+ Reference folders are marked `readOnly` in `.twinkle/lumine-project.json`.
49
+ Running `lumine save` from a reference folder is blocked; fork the source Build
50
+ first if you want an editable workspace.
51
+
38
52
  After pulling a project, run an agent from the pulled folder:
39
53
 
40
54
  ```bash
package/bin/lumine.js CHANGED
@@ -29,6 +29,8 @@ const EXCLUDED_UPLOAD_FILES = new Set([
29
29
  const LUMINE_AGENT_INSTRUCTIONS_MARKER =
30
30
  "<!-- Lumine CLI Agent Instructions -->";
31
31
  const LUMINE_SDK_REFERENCE_MARKER = "<!-- Lumine CLI SDK Reference -->";
32
+ const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
33
+ "<!-- Lumine CLI Reference Instructions -->";
32
34
  const BUNDLED_SDK_REFERENCE_URL = new URL(
33
35
  "../sdk/BUILD_SDK_INDEX.md",
34
36
  import.meta.url,
@@ -90,6 +92,20 @@ lumine save --summary "Describe the change"
90
92
  Report the changed files, any SDK methods used, the lumine save result, the
91
93
  build or branch id, and whether the result is published or unpublished changes.
92
94
  `;
95
+ const LUMINE_REFERENCE_INSTRUCTIONS = `${LUMINE_REFERENCE_INSTRUCTIONS_MARKER}
96
+ # Lumine Reference Guide
97
+
98
+ This directory contains read-only reference files pulled from a public
99
+ open-source Twinkle Build. Use it for inspection and borrowing patterns, not as
100
+ the workspace to save.
101
+
102
+ ## Source Of Truth
103
+
104
+ - Read .twinkle/lumine-project.json before using these files.
105
+ - If metadata.readOnly is true or build.role is "reference", do not run lumine save from this directory.
106
+ - To start from this Build, run lumine fork with the source build id and edit the forked workspace.
107
+ - Do not edit another local checkout to bypass reference read-only semantics.
108
+ `;
93
109
  const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
94
110
  const COMMANDS = new Set([
95
111
  "workspace",
@@ -97,8 +113,11 @@ const COMMANDS = new Set([
97
113
  "logout",
98
114
  "whoami",
99
115
  "projects",
116
+ "explore",
100
117
  "select",
101
118
  "pull",
119
+ "reference",
120
+ "fork",
102
121
  "save",
103
122
  "push",
104
123
  "check",
@@ -138,6 +157,10 @@ async function main() {
138
157
  await projects(options);
139
158
  return;
140
159
  }
160
+ if (options.command === "explore") {
161
+ await explore(options);
162
+ return;
163
+ }
141
164
  if (options.command === "select") {
142
165
  await selectProject(options);
143
166
  return;
@@ -146,6 +169,14 @@ async function main() {
146
169
  await pull(options);
147
170
  return;
148
171
  }
172
+ if (options.command === "reference") {
173
+ await reference(options);
174
+ return;
175
+ }
176
+ if (options.command === "fork") {
177
+ await fork(options);
178
+ return;
179
+ }
149
180
  if (options.command === "save" || options.command === "push") {
150
181
  await save(options);
151
182
  return;
@@ -286,6 +317,12 @@ async function projects(options) {
286
317
  printBuildList(builds);
287
318
  }
288
319
 
320
+ async function explore(options) {
321
+ const auth = await resolveAuth(options);
322
+ const builds = await listOpenSourceBuilds({ options, auth });
323
+ printOpenSourceBuildList(builds, options);
324
+ }
325
+
289
326
  async function selectProject(options) {
290
327
  const auth = await resolveAuth(options);
291
328
  const selectedBuild = options.target
@@ -326,12 +363,38 @@ async function pull(options) {
326
363
  printPullResult(result);
327
364
  }
328
365
 
366
+ async function reference(options) {
367
+ const auth = await resolveAuth(options);
368
+ const buildId = resolveRequiredBuildId(options.target);
369
+ const result = await pullReferenceFiles({ options, auth, buildId });
370
+ printReferenceResult(result);
371
+ }
372
+
373
+ async function fork(options) {
374
+ const auth = await resolveAuth(options);
375
+ await assertAuthScope({ options, auth, scope: "build:write" });
376
+ const buildId = resolveRequiredBuildId(options.target);
377
+ const forkResult = await forkBuild({ options, auth, buildId });
378
+ const forkedBuildId = Number(forkResult.build?.id || 0);
379
+ if (!forkedBuildId) {
380
+ throw new Error("Twinkle did not return a forked Build.");
381
+ }
382
+ const result = await pullBuildFiles({
383
+ options,
384
+ auth,
385
+ buildId: forkedBuildId,
386
+ });
387
+ await saveSelectedBuild({ options, auth, build: result.build });
388
+ printForkResult({ forkResult, pullResult: result });
389
+ }
390
+
329
391
  async function save(options) {
330
392
  const auth = await resolveAuth(options);
331
393
  await assertAuthScope({ options, auth, scope: "build:write" });
332
394
  const localProject = await findLocalProjectMetadata(
333
395
  path.resolve(options.dir || process.cwd()),
334
396
  );
397
+ assertLocalProjectCanBeSaved(localProject);
335
398
  let buildId = await resolveRequiredBuildIdOrSelected(options, auth, {
336
399
  localProject,
337
400
  });
@@ -543,6 +606,21 @@ async function listBuilds({ options, auth }) {
543
606
  return Array.isArray(result.builds) ? result.builds : [];
544
607
  }
545
608
 
609
+ async function listOpenSourceBuilds({ options, auth }) {
610
+ const url = new URL(`${options.apiUrl}/cli/open-source-builds`);
611
+ url.searchParams.set("limit", String(options.limit));
612
+ url.searchParams.set("sort", options.sort);
613
+ if (options.searchQuery) {
614
+ url.searchParams.set("search", options.searchQuery);
615
+ }
616
+ const result = await requestJson({
617
+ url: url.toString(),
618
+ authToken: auth.token,
619
+ timeoutMs: options.timeoutMs,
620
+ });
621
+ return Array.isArray(result.builds) ? result.builds : [];
622
+ }
623
+
546
624
  async function loadBuildMetadata({ options, auth, buildId }) {
547
625
  const result = await loadBuildFiles({
548
626
  options,
@@ -556,6 +634,21 @@ async function loadBuildMetadata({ options, auth, buildId }) {
556
634
  return result.build;
557
635
  }
558
636
 
637
+ async function loadOpenSourceBuildFiles({
638
+ options,
639
+ auth,
640
+ buildId,
641
+ includeContent,
642
+ }) {
643
+ const url = new URL(`${options.apiUrl}/cli/build/${buildId}/open-source-files`);
644
+ if (!includeContent) url.searchParams.set("includeContent", "0");
645
+ return await requestJson({
646
+ url: url.toString(),
647
+ authToken: auth.token,
648
+ timeoutMs: options.timeoutMs,
649
+ });
650
+ }
651
+
559
652
  async function loadBuildFiles({ options, auth, buildId, includeContent }) {
560
653
  const url = new URL(`${options.apiUrl}/cli/build/${buildId}/files`);
561
654
  if (!includeContent) url.searchParams.set("includeContent", "0");
@@ -566,6 +659,16 @@ async function loadBuildFiles({ options, auth, buildId, includeContent }) {
566
659
  });
567
660
  }
568
661
 
662
+ async function forkBuild({ options, auth, buildId }) {
663
+ return await requestJson({
664
+ method: "POST",
665
+ url: `${options.apiUrl}/build/${buildId}/fork`,
666
+ authToken: auth.token,
667
+ body: {},
668
+ timeoutMs: options.timeoutMs,
669
+ });
670
+ }
671
+
569
672
  async function saveProjectFiles({ options, auth, buildId, files, summary }) {
570
673
  return await requestJson({
571
674
  method: "PUT",
@@ -754,6 +857,40 @@ async function pullBuildFiles({ options, auth, buildId }) {
754
857
  };
755
858
  }
756
859
 
860
+ async function pullReferenceFiles({ options, auth, buildId }) {
861
+ const result = await loadOpenSourceBuildFiles({
862
+ options,
863
+ auth,
864
+ buildId,
865
+ includeContent: true,
866
+ });
867
+ const build = result.build || { id: buildId, title: `Build ${buildId}` };
868
+ const files = Array.isArray(result.projectFiles) ? result.projectFiles : [];
869
+ const dir = path.resolve(options.dir || defaultReferenceDir(build));
870
+ await writeProjectFiles({ dir, files });
871
+ await writeReferenceInstructions({ dir });
872
+ await writeSdkReference({ dir });
873
+ await writeReferenceMetadata({
874
+ dir,
875
+ options,
876
+ build,
877
+ manifest: result.projectManifest || null,
878
+ reference: result.reference || {
879
+ readOnly: true,
880
+ forkable: true,
881
+ sourceBuildId: Number(build.id || buildId),
882
+ },
883
+ pulledAt: new Date().toISOString(),
884
+ });
885
+ return {
886
+ build,
887
+ dir,
888
+ fileCount: files.length,
889
+ manifest: result.projectManifest || null,
890
+ reference: result.reference || null,
891
+ };
892
+ }
893
+
757
894
  async function writeAgentInstructions({ dir }) {
758
895
  for (const fileName of AGENT_INSTRUCTION_FILES) {
759
896
  const filePath = path.join(dir, fileName);
@@ -769,6 +906,21 @@ async function writeAgentInstructions({ dir }) {
769
906
  }
770
907
  }
771
908
 
909
+ async function writeReferenceInstructions({ dir }) {
910
+ for (const fileName of AGENT_INSTRUCTION_FILES) {
911
+ const filePath = path.join(dir, fileName);
912
+ try {
913
+ const existing = await fs.readFile(filePath, "utf8");
914
+ if (!existing.includes(LUMINE_REFERENCE_INSTRUCTIONS_MARKER)) {
915
+ continue;
916
+ }
917
+ } catch (error) {
918
+ if (error.code !== "ENOENT") throw error;
919
+ }
920
+ await fs.writeFile(filePath, LUMINE_REFERENCE_INSTRUCTIONS, "utf8");
921
+ }
922
+ }
923
+
772
924
  async function writeSdkReference({ dir }) {
773
925
  const filePath = path.join(dir, SDK_REFERENCE_FILE);
774
926
  try {
@@ -976,6 +1128,52 @@ async function writeProjectMetadata({
976
1128
  );
977
1129
  }
978
1130
 
1131
+ async function writeReferenceMetadata({
1132
+ dir,
1133
+ options,
1134
+ build,
1135
+ manifest,
1136
+ reference,
1137
+ pulledAt,
1138
+ }) {
1139
+ const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
1140
+ await fs.mkdir(metadataDir, { recursive: true });
1141
+ const sourceBuildId =
1142
+ Number(reference?.sourceBuildId || 0) || Number(build?.id || 0) || null;
1143
+ await fs.writeFile(
1144
+ path.join(metadataDir, PROJECT_METADATA_FILE),
1145
+ JSON.stringify(
1146
+ {
1147
+ schemaVersion: 1,
1148
+ buildId: sourceBuildId,
1149
+ readOnly: true,
1150
+ reference: {
1151
+ readOnly: true,
1152
+ forkable: true,
1153
+ sourceBuildId,
1154
+ sourceAppUrl: sourceBuildId ? `${options.siteUrl}/app/${sourceBuildId}` : null,
1155
+ },
1156
+ build: {
1157
+ id: sourceBuildId,
1158
+ title: build?.title || (sourceBuildId ? `Build ${sourceBuildId}` : ""),
1159
+ role: "reference",
1160
+ ownerUsername: build?.ownerUsername || null,
1161
+ collaborationMode: build?.collaborationMode || "open_source",
1162
+ canWrite: false,
1163
+ canPublish: false,
1164
+ },
1165
+ apiUrl: options.apiUrl,
1166
+ siteUrl: options.siteUrl,
1167
+ manifest,
1168
+ pulledAt,
1169
+ },
1170
+ null,
1171
+ 2,
1172
+ ),
1173
+ "utf8",
1174
+ );
1175
+ }
1176
+
979
1177
  async function findLocalProjectMetadata(startDir) {
980
1178
  let current = path.resolve(startDir || process.cwd());
981
1179
  while (true) {
@@ -1002,6 +1200,29 @@ function resolveProjectDirForSave({ options, localProject }) {
1002
1200
  return process.cwd();
1003
1201
  }
1004
1202
 
1203
+ function assertLocalProjectCanBeSaved(localProject) {
1204
+ const metadata = localProject?.metadata;
1205
+ if (!metadata) return;
1206
+ if (isReadOnlyReferenceMetadata(metadata)) {
1207
+ const sourceBuildId =
1208
+ Number(metadata.reference?.sourceBuildId || 0) ||
1209
+ Number(metadata.buildId || 0) ||
1210
+ Number(metadata.build?.id || 0) ||
1211
+ 0;
1212
+ throw new Error(
1213
+ `This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace.`,
1214
+ );
1215
+ }
1216
+ }
1217
+
1218
+ function isReadOnlyReferenceMetadata(metadata) {
1219
+ return (
1220
+ metadata?.readOnly === true ||
1221
+ metadata?.reference?.readOnly === true ||
1222
+ metadata?.build?.role === "reference"
1223
+ );
1224
+ }
1225
+
1005
1226
  function resolveLocalProjectFilePath({ rootDir, projectPath }) {
1006
1227
  const relativePath = projectPathToRelativePath(projectPath);
1007
1228
  const root = path.resolve(rootDir);
@@ -1048,6 +1269,23 @@ function printBuildList(builds) {
1048
1269
  });
1049
1270
  }
1050
1271
 
1272
+ function printOpenSourceBuildList(builds, options) {
1273
+ if (!builds.length) {
1274
+ const searchText = options.searchQuery
1275
+ ? ` matching "${options.searchQuery}"`
1276
+ : "";
1277
+ console.log(`No public open-source Twinkle builds found${searchText}.`);
1278
+ return;
1279
+ }
1280
+ const searchText = options.searchQuery ? ` for "${options.searchQuery}"` : "";
1281
+ console.log(`Public open-source Twinkle builds${searchText}:`);
1282
+ builds.forEach((build, index) => {
1283
+ console.log(`${index + 1}. ${formatOpenSourceBuildListItem(build)}`);
1284
+ });
1285
+ console.log("Reference: lumine reference <build-id>");
1286
+ console.log("Fork: lumine fork <build-id>");
1287
+ }
1288
+
1051
1289
  function formatBuildListItem(build) {
1052
1290
  const role =
1053
1291
  build.role === "owner"
@@ -1057,6 +1295,16 @@ function formatBuildListItem(build) {
1057
1295
  return `${formatBuildTitle(build)} - ${role}, ${published}`;
1058
1296
  }
1059
1297
 
1298
+ function formatOpenSourceBuildListItem(build) {
1299
+ const owner = build.ownerUsername ? ` by ${build.ownerUsername}` : "";
1300
+ const stats = [
1301
+ `${Math.max(0, Number(build.forkCount || 0))} forks`,
1302
+ `${Math.max(0, Number(build.viewCount || 0))} views`,
1303
+ ].join(", ");
1304
+ const appUrl = build.appUrl ? ` - ${build.appUrl}` : "";
1305
+ return `${formatBuildTitle(build)}${owner} - ${stats}${appUrl}`;
1306
+ }
1307
+
1060
1308
  function formatBuildTitle(build) {
1061
1309
  return `${build.title || `Build ${build.id}`} (#${build.id})`;
1062
1310
  }
@@ -1095,6 +1343,39 @@ function printPullResult(result) {
1095
1343
  }
1096
1344
  }
1097
1345
 
1346
+ function printReferenceResult(result) {
1347
+ const build = result.build || {};
1348
+ const sourceBuildId =
1349
+ Number(result.reference?.sourceBuildId || 0) || Number(build.id || 0);
1350
+ const entryPath = result.manifest?.entryPath || "unknown";
1351
+ console.log(`Referenced ${formatBuildTitle(build)}.`);
1352
+ console.log(
1353
+ `Pulled ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} to ${result.dir}`,
1354
+ );
1355
+ console.log(`Entry: ${entryPath}`);
1356
+ console.log("Mode: read-only reference");
1357
+ console.log(`Next: cd ${shellQuote(result.dir)}`);
1358
+ if (sourceBuildId) {
1359
+ console.log(`Start from this app: lumine fork ${sourceBuildId}`);
1360
+ }
1361
+ }
1362
+
1363
+ function printForkResult({ forkResult, pullResult }) {
1364
+ const build = pullResult.build || forkResult.build || {};
1365
+ const sourceBuildId =
1366
+ Number(forkResult.sourceBuild?.id || 0) ||
1367
+ Number(forkResult.sourceBuild?.contentId || 0) ||
1368
+ 0;
1369
+ console.log(
1370
+ forkResult.alreadyExists
1371
+ ? `Using your existing fork ${formatBuildTitle(build)}.`
1372
+ : `Forked Build ${sourceBuildId || "source"} into ${formatBuildTitle(
1373
+ build,
1374
+ )}.`,
1375
+ );
1376
+ printPullResult(pullResult);
1377
+ }
1378
+
1098
1379
  function printSaveResult({ result, build, dir, files }) {
1099
1380
  const entryPath = result.projectManifest?.entryPath || "unknown";
1100
1381
  const version = result.artifactVersion?.versionNumber
@@ -1263,6 +1544,13 @@ function parseArgs(args) {
1263
1544
  return {
1264
1545
  command,
1265
1546
  target: raw.url || raw.target || positional[0] || "",
1547
+ searchQuery:
1548
+ String(
1549
+ raw.search ||
1550
+ raw.query ||
1551
+ (command === "explore" ? positional.join(" ") : ""),
1552
+ ).trim() || "",
1553
+ sort: normalizeOpenSourceSort(raw.sort),
1266
1554
  apiUrl: trimTrailingSlash(
1267
1555
  String(raw.apiUrl || process.env.TWINKLE_API_URL || DEFAULT_API_URL),
1268
1556
  ),
@@ -1312,6 +1600,19 @@ async function resolveRequiredBuildIdOrSelected(
1312
1600
  (await findLocalProjectMetadata(
1313
1601
  path.resolve(options.dir || process.cwd()),
1314
1602
  ));
1603
+ if (
1604
+ resolvedLocalProject?.metadata &&
1605
+ isReadOnlyReferenceMetadata(resolvedLocalProject.metadata)
1606
+ ) {
1607
+ const sourceBuildId =
1608
+ Number(resolvedLocalProject.metadata.reference?.sourceBuildId || 0) ||
1609
+ Number(resolvedLocalProject.metadata.buildId || 0) ||
1610
+ Number(resolvedLocalProject.metadata.build?.id || 0) ||
1611
+ 0;
1612
+ throw new Error(
1613
+ `This is a read-only Lumine reference${sourceBuildId ? ` for Build ${sourceBuildId}` : ""}. Run \`lumine fork${sourceBuildId ? ` ${sourceBuildId}` : ""}\` to create an editable workspace, or pass an explicit Build URL.`,
1614
+ );
1615
+ }
1315
1616
  const localBuildId = Number(resolvedLocalProject?.metadata?.buildId || 0);
1316
1617
  if (localBuildId > 0) return localBuildId;
1317
1618
  const selectedBuildId = Number(auth?.selectedBuildId || 0);
@@ -1403,6 +1704,14 @@ function parseBoolean(value, fallback) {
1403
1704
  return fallback;
1404
1705
  }
1405
1706
 
1707
+ function normalizeOpenSourceSort(value) {
1708
+ const normalized = String(value || "")
1709
+ .trim()
1710
+ .toLowerCase();
1711
+ if (["recent", "popular", "forks"].includes(normalized)) return normalized;
1712
+ return "forks";
1713
+ }
1714
+
1406
1715
  async function openBrowser(url) {
1407
1716
  const command =
1408
1717
  process.platform === "darwin"
@@ -1435,6 +1744,12 @@ function defaultWorkspaceDir(build) {
1435
1744
  return `twinkle-${titleSlug || "build"}-${buildId}`;
1436
1745
  }
1437
1746
 
1747
+ function defaultReferenceDir(build) {
1748
+ const titleSlug = slugify(build?.title || "");
1749
+ const buildId = Number(build?.id || 0) || "build";
1750
+ return `twinkle-reference-${titleSlug || "build"}-${buildId}`;
1751
+ }
1752
+
1438
1753
  function slugify(value) {
1439
1754
  return String(value || "")
1440
1755
  .toLowerCase()
@@ -1454,8 +1769,11 @@ function printHelp() {
1454
1769
  lumine whoami
1455
1770
  lumine logout
1456
1771
  lumine projects
1772
+ lumine explore [search terms]
1457
1773
  lumine select [twinkle-build-url]
1458
1774
  lumine pull [twinkle-build-url]
1775
+ lumine reference <twinkle-build-url>
1776
+ lumine fork <twinkle-build-url>
1459
1777
  lumine save
1460
1778
  lumine check [twinkle-build-url]
1461
1779
  lumine launch [twinkle-build-url]
@@ -1463,6 +1781,9 @@ function printHelp() {
1463
1781
  Examples:
1464
1782
  npx @stage5/lumine@latest
1465
1783
  npx @stage5/lumine@latest login
1784
+ npx @stage5/lumine@latest explore --sort forks
1785
+ npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
1786
+ npx @stage5/lumine@latest fork https://www.twin-kle.com/app/123
1466
1787
  npx @stage5/lumine@latest pull
1467
1788
  npx @stage5/lumine@latest save
1468
1789
  npx @stage5/lumine@latest save --publish
@@ -1476,6 +1797,8 @@ Options:
1476
1797
  --auth-token <token> Override saved login
1477
1798
  --dir <path> Directory for pulled project files
1478
1799
  --summary <text> Save summary
1800
+ --search <text> Search public open-source Builds
1801
+ --sort <sort> Sort open-source Builds: forks, popular, recent
1479
1802
  --publish Publish after saving
1480
1803
  --save Save local files before launch
1481
1804
  --limit <number> Number of projects to show
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {