gencow 0.1.137 → 0.1.138

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/bin/gencow.mjs CHANGED
@@ -12,19 +12,12 @@
12
12
  * gencow db:studio — open Drizzle Studio (visual DB browser)
13
13
  * gencow dashboard — open /_admin in browser
14
14
  * gencow backup — manage database backups (list/create/restore/delete/download)
15
+ * gencow templates — publish/list/download/clone marketplace templates
15
16
  * gencow help — show this help
16
17
  */
17
18
 
18
19
  import { execSync, spawn } from "child_process";
19
- import {
20
- existsSync,
21
- readFileSync,
22
- writeFileSync,
23
- unlinkSync,
24
- cpSync,
25
- symlinkSync,
26
- copyFileSync,
27
- } from "fs";
20
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, cpSync, symlinkSync, copyFileSync } from "fs";
28
21
  import { resolve, dirname } from "path";
29
22
  import { fileURLToPath } from "url";
30
23
  import { createAddCommand } from "../lib/add-command.mjs";
@@ -62,6 +55,7 @@ import { createJobsCommand } from "../lib/jobs-command.mjs";
62
55
  import { createLogsCommand } from "../lib/logs-command.mjs";
63
56
  import { createStaticDeployRuntime } from "../lib/static-deploy-command.mjs";
64
57
  import { createStaticCommand } from "../lib/static-command.mjs";
58
+ import { createTemplateMarketplaceCommand } from "../lib/template-marketplace-command.mjs";
65
59
  import {
66
60
  getAppUrl,
67
61
  getBaseDomain,
@@ -70,7 +64,20 @@ import {
70
64
  parseDurationToMinutes,
71
65
  validatePlatformUrl,
72
66
  } from "../lib/domain-helpers.mjs";
73
- import { BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW, error, info, log, success, warn } from "../lib/output.mjs";
67
+ import {
68
+ BOLD,
69
+ CYAN,
70
+ DIM,
71
+ GREEN,
72
+ RED,
73
+ RESET,
74
+ YELLOW,
75
+ error,
76
+ info,
77
+ log,
78
+ success,
79
+ warn,
80
+ } from "../lib/output.mjs";
74
81
  import { platformFetch, requireCreds, rpcMutation, rpcQuery } from "../lib/platform-client.mjs";
75
82
 
76
83
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -229,6 +236,7 @@ const runStaticCommand = createStaticCommand({
229
236
  requireCredsImpl: requireCreds,
230
237
  runStaticDeployRuntimeImpl: runStaticDeployRuntime,
231
238
  });
239
+ const runTemplatesCommand = createTemplateMarketplaceCommand();
232
240
 
233
241
  // ─── Commands ─────────────────────────────────────────────
234
242
 
@@ -299,6 +307,10 @@ ${BOLD}Commands (login required):${RESET}
299
307
  ${GREEN}static [dir]${RESET} Deploy static files ${DIM}(dist/, out/, build/)${RESET}
300
308
  ${DIM}--prod Deploy to production app${RESET}
301
309
  ${DIM}--force, -f Skip dependency audit${RESET}
310
+ ${GREEN}templates publish${RESET} Publish current project as a marketplace template
311
+ ${DIM}--title, --slug, --price, --private, --unlisted${RESET}
312
+ ${GREEN}templates list${RESET} Browse public templates
313
+ ${GREEN}templates clone <slug>${RESET} Clone a template into a local directory
302
314
  ${GREEN}deploy${RESET} Deploy backend to cloud ${DIM}(dev by default)${RESET}
303
315
  ${DIM}--prod Deploy to production (Pro+ only)${RESET}
304
316
  ${DIM}--rollback Rollback to previous deployment${RESET}
@@ -355,6 +367,10 @@ ${BOLD}Examples:${RESET}
355
367
 
356
368
  ${DIM}# Inspect stale workflows:${RESET}
357
369
  gencow jobs workflow cleanup --status terminal --older-than 7d
370
+
371
+ ${DIM}# Publish and clone templates:${RESET}
372
+ gencow templates publish --title "CRM Starter" --price 29
373
+ gencow templates clone crm-starter
358
374
  `);
359
375
  },
360
376
 
@@ -379,6 +395,7 @@ ${BOLD}Examples:${RESET}
379
395
  // ── backup / domain ──────────────────────────────────
380
396
  backup: runBackupCommand,
381
397
  domain: runDomainCommand,
398
+ templates: runTemplatesCommand,
382
399
 
383
400
  async mcp() {
384
401
  info(`MCP server is ${BOLD}coming soon${RESET}.`);
@@ -0,0 +1,228 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { basename, resolve } from "path";
4
+ import { BOLD, CYAN, DIM, GREEN, RESET, error, info, log, success } from "./output.mjs";
5
+ import { platformFetch, requireCreds } from "./platform-client.mjs";
6
+ import {
7
+ buildTemplateTarExcludes,
8
+ detectStaticTemplateProject,
9
+ validateStaticOutput,
10
+ } from "./template-marketplace-detection.mjs";
11
+
12
+ export function parseTemplateArgs(args) {
13
+ const [subcommand = "list", ...rest] = args;
14
+ const options = { _: [], price: "0", currency: "USD", visibility: "public" };
15
+ for (let index = 0; index < rest.length; index += 1) {
16
+ const arg = rest[index];
17
+ if (arg === "--title") options.title = rest[++index];
18
+ else if (arg === "--summary") options.summary = rest[++index];
19
+ else if (arg === "--slug") options.slug = rest[++index];
20
+ else if (arg === "--price") options.price = rest[++index];
21
+ else if (arg === "--currency") options.currency = rest[++index];
22
+ else if (arg === "--visibility") options.visibility = rest[++index];
23
+ else if (arg === "--private") options.visibility = "private";
24
+ else if (arg === "--unlisted") options.visibility = "unlisted";
25
+ else if (arg === "--build") options.build = rest[++index];
26
+ else if (arg === "--out") options.out = rest[++index];
27
+ else if (arg === "--frontend-root") options.frontendRoot = rest[++index];
28
+ else if (arg === "--backend-root") options.backendRoot = rest[++index];
29
+ else if (arg === "--version") options.version = rest[++index];
30
+ else if (arg === "--out-file") options.outFile = rest[++index];
31
+ else options._.push(arg);
32
+ }
33
+ return { subcommand, options };
34
+ }
35
+
36
+ function centsFromPrice(value) {
37
+ const numeric = Number(value || 0);
38
+ if (!Number.isFinite(numeric) || numeric < 0) throw new Error("Invalid --price");
39
+ return Math.round(numeric * 100);
40
+ }
41
+
42
+ function shellQuote(value) {
43
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
44
+ }
45
+
46
+ function createSourceBundle({ cwd, staticOutputDir, execSyncImpl = execSync, mkdirSyncImpl = mkdirSync }) {
47
+ const out = resolve(cwd, ".gencow", "template-source.tar.gz");
48
+ mkdirSyncImpl(resolve(cwd, ".gencow"), { recursive: true });
49
+ const excludes = buildTemplateTarExcludes(staticOutputDir)
50
+ .map((item) => `--exclude=${shellQuote(item)}`)
51
+ .join(" ");
52
+ execSyncImpl(`COPYFILE_DISABLE=1 tar ${excludes} -czf ${shellQuote(out)} .`, { cwd });
53
+ return out;
54
+ }
55
+
56
+ function createStaticBundle({ cwd, outputDir, execSyncImpl = execSync, mkdirSyncImpl = mkdirSync }) {
57
+ const out = resolve(cwd, ".gencow", "template-static.tar.gz");
58
+ mkdirSyncImpl(resolve(cwd, ".gencow"), { recursive: true });
59
+ execSyncImpl(`COPYFILE_DISABLE=1 tar -czf ${shellQuote(out)} -C ${shellQuote(resolve(cwd, outputDir))} .`, {
60
+ cwd,
61
+ });
62
+ return out;
63
+ }
64
+
65
+ async function rpc(creds, name, args, platformFetchImpl) {
66
+ const res = await platformFetchImpl(creds, "/api/query", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ name, args }),
70
+ });
71
+ const data = await res.json().catch(() => ({}));
72
+ if (!res.ok) throw new Error(data.error || res.statusText);
73
+ return data.result ?? data;
74
+ }
75
+
76
+ export function createTemplateMarketplaceCommand({
77
+ cwdImpl = () => process.cwd(),
78
+ errorImpl = error,
79
+ execSyncImpl = execSync,
80
+ existsSyncImpl = existsSync,
81
+ exitImpl = (code) => process.exit(code),
82
+ infoImpl = info,
83
+ logImpl = log,
84
+ platformFetchImpl = platformFetch,
85
+ readFileSyncImpl = readFileSync,
86
+ requireCredsImpl = requireCreds,
87
+ resolvePathImpl = resolve,
88
+ rmSyncImpl = rmSync,
89
+ successImpl = success,
90
+ writeFileSyncImpl = writeFileSync,
91
+ } = {}) {
92
+ async function publish(options) {
93
+ const cwd = cwdImpl();
94
+ const detection = detectStaticTemplateProject({
95
+ cwd,
96
+ explicit: {
97
+ frontendRoot: options.frontendRoot,
98
+ backendRoot: options.backendRoot,
99
+ buildCommand: options.build,
100
+ outputDir: options.out,
101
+ },
102
+ existsSyncImpl,
103
+ readFileSyncImpl,
104
+ resolvePathImpl,
105
+ });
106
+ if (!detection.ok) throw new Error(detection.error);
107
+
108
+ logImpl(`\n${BOLD}${CYAN}Gencow Template Publish${RESET}\n`);
109
+ infoImpl(`Framework: ${detection.frontendFramework}`);
110
+ infoImpl(`Kind: ${detection.projectKind}`);
111
+
112
+ if (detection.buildCommand) {
113
+ infoImpl(`Build: ${detection.buildCommand}`);
114
+ execSyncImpl(detection.buildCommand, {
115
+ cwd: resolvePathImpl(cwd, detection.buildWorkdir),
116
+ stdio: "inherit",
117
+ });
118
+ }
119
+ const outputValidation = validateStaticOutput({
120
+ cwd: resolvePathImpl(cwd, detection.buildWorkdir),
121
+ outputDir: detection.staticOutputDir,
122
+ existsSyncImpl,
123
+ resolvePathImpl,
124
+ });
125
+ if (!outputValidation.ok) throw new Error(outputValidation.error);
126
+
127
+ const sourceBundle = createSourceBundle({
128
+ cwd,
129
+ staticOutputDir: detection.staticOutputDir,
130
+ execSyncImpl,
131
+ });
132
+ const staticBundle = createStaticBundle({
133
+ cwd: resolvePathImpl(cwd, detection.buildWorkdir),
134
+ outputDir: detection.staticOutputDir,
135
+ execSyncImpl,
136
+ });
137
+
138
+ const creds = requireCredsImpl();
139
+ const priceCents = centsFromPrice(options.price);
140
+ const form = new FormData();
141
+ form.append("title", options.title || basename(cwd));
142
+ form.append("summary", options.summary || "");
143
+ form.append("slug", options.slug || "");
144
+ form.append("version", options.version || "1.0.0");
145
+ form.append("priceType", priceCents > 0 ? "paid" : "free");
146
+ form.append("priceCents", String(priceCents));
147
+ form.append("currency", options.currency || "USD");
148
+ form.append("visibilityIntent", options.visibility || "public");
149
+ form.append("projectKind", detection.projectKind);
150
+ form.append("frontendFramework", detection.frontendFramework);
151
+ form.append("packageManager", detection.packageManager);
152
+ form.append("frontendRoot", detection.frontendRoot);
153
+ form.append("backendRoot", detection.backendRoot);
154
+ form.append("buildWorkdir", detection.buildWorkdir);
155
+ form.append("buildCommand", detection.buildCommand);
156
+ form.append("staticOutputDir", detection.staticOutputDir);
157
+ form.append("bundle", new Blob([readFileSyncImpl(sourceBundle)]), "template-source.tar.gz");
158
+ form.append("staticBundle", new Blob([readFileSyncImpl(staticBundle)]), "template-static.tar.gz");
159
+
160
+ const res = await platformFetchImpl(creds, "/platform/templates/publish", { method: "POST", body: form });
161
+ const body = await res.json().catch(() => ({}));
162
+ rmSyncImpl(sourceBundle, { force: true });
163
+ rmSyncImpl(staticBundle, { force: true });
164
+ if (!res.ok) throw new Error(body.error || res.statusText);
165
+ successImpl(`Template submitted: ${body.slug}`);
166
+ infoImpl(`Status: draft review`);
167
+ if (body.audit?.errors?.length) infoImpl(`${DIM}Audit errors: ${body.audit.errors.join(", ")}${RESET}`);
168
+ }
169
+
170
+ async function list() {
171
+ const rows = await rpc(requireCredsImpl(), "templates.listPublic", {}, platformFetchImpl);
172
+ for (const row of rows)
173
+ logImpl(
174
+ `${GREEN}${row.slug}${RESET} ${row.priceType === "paid" ? `$${(row.priceCents / 100).toFixed(2)}` : "free"} ${row.title}`,
175
+ );
176
+ }
177
+
178
+ async function infoCommand(slug) {
179
+ const data = await rpc(requireCredsImpl(), "templates.getPublic", { slug }, platformFetchImpl);
180
+ if (!data) throw new Error("Template not found");
181
+ logImpl(`${BOLD}${data.listing.title}${RESET}`);
182
+ logImpl(`${DIM}${data.listing.summary || ""}${RESET}`);
183
+ logImpl(
184
+ `Price: ${data.listing.priceType === "paid" ? `$${(data.listing.priceCents / 100).toFixed(2)}` : "free"}`,
185
+ );
186
+ if (data.release?.previewFrontendUrl) logImpl(`Preview: ${data.release.previewFrontendUrl}`);
187
+ }
188
+
189
+ async function download(slug, options, cloneMode = false) {
190
+ const creds = requireCredsImpl();
191
+ const versionQuery = options.version
192
+ ? `?version=${encodeURIComponent(options.version)}${cloneMode ? "&clone=1" : ""}`
193
+ : cloneMode
194
+ ? "?clone=1"
195
+ : "";
196
+ const res = await platformFetchImpl(creds, `/platform/templates/${slug}/download${versionQuery}`);
197
+ if (!res.ok) {
198
+ const body = await res.json().catch(() => ({}));
199
+ throw new Error(body.error || res.statusText);
200
+ }
201
+ const buffer = Buffer.from(await res.arrayBuffer());
202
+ const out = options.outFile || `${slug}.tar.gz`;
203
+ writeFileSyncImpl(resolvePathImpl(cwdImpl(), out), buffer);
204
+ successImpl(`Downloaded: ${out}`);
205
+ return resolvePathImpl(cwdImpl(), out);
206
+ }
207
+
208
+ return async function runTemplatesCommand(...args) {
209
+ const { subcommand, options } = parseTemplateArgs(args);
210
+ if (subcommand === "publish") return publish(options);
211
+ if (subcommand === "list") return list();
212
+ if (subcommand === "info") return infoCommand(options._[0]);
213
+ if (subcommand === "download") return download(options._[0], options);
214
+ if (subcommand === "clone") {
215
+ const slug = options._[0];
216
+ const dir = options._[1] || slug;
217
+ const archive = await download(slug, { ...options, outFile: ".gencow-template-clone.tar.gz" }, true);
218
+ execSyncImpl(`mkdir -p ${shellQuote(dir)} && tar -xzf ${shellQuote(archive)} -C ${shellQuote(dir)}`, {
219
+ cwd: cwdImpl(),
220
+ });
221
+ rmSyncImpl(archive, { force: true });
222
+ successImpl(`Cloned into: ${dir}`);
223
+ return;
224
+ }
225
+ errorImpl("Usage: gencow templates <list|info|publish|download|clone>");
226
+ exitImpl(1);
227
+ };
228
+ }
@@ -0,0 +1,162 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
2
+ import { isAbsolute, resolve } from "path";
3
+
4
+ const CONFIG_FILES = {
5
+ vite: ["vite.config.ts", "vite.config.js", "vite.config.mjs"],
6
+ astro: ["astro.config.ts", "astro.config.mjs", "astro.config.js"],
7
+ svelte: ["svelte.config.ts", "svelte.config.js"],
8
+ next: ["next.config.ts", "next.config.js", "next.config.mjs"],
9
+ };
10
+
11
+ export function detectPackageManager({
12
+ cwd = process.cwd(),
13
+ existsSyncImpl = existsSync,
14
+ resolvePathImpl = resolve,
15
+ } = {}) {
16
+ if (existsSyncImpl(resolvePathImpl(cwd, "pnpm-lock.yaml"))) return "pnpm";
17
+ if (existsSyncImpl(resolvePathImpl(cwd, "bun.lockb"))) return "bun";
18
+ if (existsSyncImpl(resolvePathImpl(cwd, "yarn.lock"))) return "yarn";
19
+ if (existsSyncImpl(resolvePathImpl(cwd, "package-lock.json"))) return "npm";
20
+ return "unknown";
21
+ }
22
+
23
+ function readPackageJson({ cwd, readFileSyncImpl = readFileSync, resolvePathImpl = resolve }) {
24
+ try {
25
+ return JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ function hasAnyConfig(cwd, files, { existsSyncImpl = existsSync, resolvePathImpl = resolve }) {
32
+ return files.some((file) => existsSyncImpl(resolvePathImpl(cwd, file)));
33
+ }
34
+
35
+ function isSafeProjectPath(value, { allowDot = true } = {}) {
36
+ const normalized = String(value || "")
37
+ .replace(/\\/g, "/")
38
+ .replace(/\/+$/, "");
39
+ if (!normalized) return false;
40
+ if (normalized === ".") return allowDot;
41
+ if (isAbsolute(normalized) || normalized.startsWith("~")) return false;
42
+ return !normalized.split("/").some((part) => part === "" || part === "." || part === "..");
43
+ }
44
+
45
+ export function detectStaticTemplateProject({
46
+ cwd = process.cwd(),
47
+ explicit = {},
48
+ existsSyncImpl = existsSync,
49
+ readFileSyncImpl = readFileSync,
50
+ resolvePathImpl = resolve,
51
+ } = {}) {
52
+ const pkg = readPackageJson({ cwd, readFileSyncImpl, resolvePathImpl });
53
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
54
+ const metadata = pkg.gencowTemplate || {};
55
+
56
+ const frontendRoot = explicit.frontendRoot || metadata.frontend?.root || ".";
57
+ const backendRoot = explicit.backendRoot || metadata.backendRoot || "gencow";
58
+ if (!isSafeProjectPath(frontendRoot) || !isSafeProjectPath(backendRoot)) {
59
+ return { ok: false, error: "Template frontend/backend roots must be safe relative paths" };
60
+ }
61
+ const frontendCwd = resolvePathImpl(cwd, frontendRoot);
62
+ const frontendPkg =
63
+ frontendRoot === "." ? pkg : readPackageJson({ cwd: frontendCwd, readFileSyncImpl, resolvePathImpl });
64
+ const frontendDeps = { ...(frontendPkg.dependencies || {}), ...(frontendPkg.devDependencies || {}) };
65
+ const allDeps = { ...deps, ...frontendDeps };
66
+
67
+ if (hasAnyConfig(frontendCwd, CONFIG_FILES.next, { existsSyncImpl, resolvePathImpl }) || allDeps.next) {
68
+ return {
69
+ ok: false,
70
+ error: "MVP는 Vite/Astro/Svelte static frontend만 지원하며 Next.js/SSR frontend는 지원하지 않는다",
71
+ };
72
+ }
73
+
74
+ const candidates = [];
75
+ if (hasAnyConfig(frontendCwd, CONFIG_FILES.vite, { existsSyncImpl, resolvePathImpl }) || allDeps.vite) {
76
+ candidates.push("vite");
77
+ }
78
+ if (hasAnyConfig(frontendCwd, CONFIG_FILES.astro, { existsSyncImpl, resolvePathImpl }) || allDeps.astro) {
79
+ candidates.push("astro");
80
+ }
81
+ if (
82
+ hasAnyConfig(frontendCwd, CONFIG_FILES.svelte, { existsSyncImpl, resolvePathImpl }) &&
83
+ allDeps["@sveltejs/adapter-static"]
84
+ ) {
85
+ candidates.push("sveltekit_static");
86
+ }
87
+
88
+ if (!explicit.frontendRoot && candidates.length > 1) {
89
+ return {
90
+ ok: false,
91
+ error: "Multiple static frontend candidates found. Pass --frontend-root and --out explicitly.",
92
+ };
93
+ }
94
+
95
+ const packageManager = detectPackageManager({ cwd, existsSyncImpl, resolvePathImpl });
96
+ const framework = explicit.framework || metadata.frontend?.framework || candidates[0] || "unknown";
97
+ const buildCommand =
98
+ explicit.buildCommand ||
99
+ metadata.frontend?.buildCommand ||
100
+ (frontendPkg.scripts?.build ? `${packageManager === "unknown" ? "npm" : packageManager} build` : "");
101
+ const outputDir =
102
+ explicit.outputDir || metadata.frontend?.outputDir || (framework === "astro" ? "dist" : "dist");
103
+ if (!isSafeProjectPath(outputDir, { allowDot: false })) {
104
+ return { ok: false, error: "Template static output directory must be a safe relative path" };
105
+ }
106
+
107
+ const hasBackend =
108
+ existsSyncImpl(resolvePathImpl(cwd, backendRoot, "index.ts")) ||
109
+ existsSyncImpl(resolvePathImpl(cwd, backendRoot, "schema.ts")) ||
110
+ existsSyncImpl(resolvePathImpl(cwd, "gencow.config.ts"));
111
+
112
+ return {
113
+ ok: true,
114
+ projectKind: hasBackend ? "full_stack" : "static_only",
115
+ frontendFramework: framework,
116
+ packageManager,
117
+ frontendRoot,
118
+ backendRoot,
119
+ buildWorkdir: frontendRoot,
120
+ buildCommand,
121
+ staticOutputDir: outputDir,
122
+ };
123
+ }
124
+
125
+ export function validateStaticOutput({
126
+ cwd = process.cwd(),
127
+ outputDir,
128
+ existsSyncImpl = existsSync,
129
+ readdirSyncImpl = readdirSync,
130
+ statSyncImpl = statSync,
131
+ resolvePathImpl = resolve,
132
+ }) {
133
+ if (!isSafeProjectPath(outputDir, { allowDot: false })) {
134
+ return { ok: false, error: "Static output directory must be a safe relative path" };
135
+ }
136
+ const absolute = resolvePathImpl(cwd, outputDir);
137
+ if (!existsSyncImpl(resolvePathImpl(absolute, "index.html"))) {
138
+ return { ok: false, error: `Static output missing index.html: ${outputDir}` };
139
+ }
140
+ const entries = readdirSyncImpl(absolute);
141
+ const hasAsset = entries.some((entry) => {
142
+ const entryPath = resolvePathImpl(absolute, entry);
143
+ return entry !== "index.html" && statSyncImpl(entryPath).size >= 0;
144
+ });
145
+ return hasAsset ? { ok: true } : { ok: false, error: `Static output has no assets: ${outputDir}` };
146
+ }
147
+
148
+ export function buildTemplateTarExcludes(staticOutputDir) {
149
+ const excludes = [
150
+ ".git",
151
+ ".gencow",
152
+ ".env",
153
+ ".env.*",
154
+ "node_modules",
155
+ "coverage",
156
+ ".next",
157
+ ".svelte-kit",
158
+ "gencow.json",
159
+ ];
160
+ if (staticOutputDir) excludes.push(staticOutputDir.replace(/\/$/, ""));
161
+ return excludes;
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.137",
3
+ "version": "0.1.138",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {