hanoman 0.1.1 → 0.1.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 +18 -0
- package/dist/build-info.json +3 -3
- package/dist/cli.js +109 -44
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -78,6 +78,24 @@ ditolak kecuali `--force`.
|
|
|
78
78
|
> berkas DB tertentu tanpa menyentuh var itu, pakai `HANOMAN_DATABASE_URL=file:/path/hanoman.db`
|
|
79
79
|
> atau `hanoman --db /path/hanoman.db`.
|
|
80
80
|
|
|
81
|
+
## Kalau `hanoman` gagal menerapkan migrasi
|
|
82
|
+
|
|
83
|
+
**`P3005 — The database schema is not empty`** berarti berkas DB itu sudah punya tabel tapi tak
|
|
84
|
+
punya riwayat migrasi hanoman — biasanya bukan DB hanoman versi ini (sisa prototipe lama, atau
|
|
85
|
+
berkas tool lain yang kebetulan bernama sama). hanoman **tidak** mengubah isinya. Pindahkan berkas
|
|
86
|
+
itu lalu jalankan ulang, atau tunjuk berkas lain dengan `hanoman --db /path/baru.db`.
|
|
87
|
+
|
|
88
|
+
## Kalau terminal sesi terbuka tapi kosong
|
|
89
|
+
|
|
90
|
+
Sesi hanoman hidup di dalam **tmux**; layarnya dialirkan ke browser lewat node-pty. Dua sebab:
|
|
91
|
+
|
|
92
|
+
- **tmux belum terpasang** — `brew install tmux` (macOS) atau paket distro Anda.
|
|
93
|
+
- **`spawn-helper` node-pty tak executable** — node-pty menerbitkan biner pendampingnya dengan mode
|
|
94
|
+
`0644`, jadi `posix_spawnp` gagal dan tak satu byte pun mengalir ke terminal walau sesinya hidup.
|
|
95
|
+
Sejak `0.1.3` `hanoman` memperbaikinya sendiri saat start (sekali per instalasi). Bila instalasi
|
|
96
|
+
global itu milik root dan hanoman dijalankan sebagai pengguna biasa, chmod-nya ditolak — perbaiki
|
|
97
|
+
manual: `sudo chmod +x "$(npm root -g)"/hanoman/node_modules/node-pty/prebuilds/*/spawn-helper`.
|
|
98
|
+
|
|
81
99
|
## Lisensi
|
|
82
100
|
|
|
83
101
|
MIT — lihat `LICENSE`.
|
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -256,13 +256,17 @@ __export(start_exports, {
|
|
|
256
256
|
default: () => start,
|
|
257
257
|
distDir: () => distDir,
|
|
258
258
|
ensurePrismaClient: () => ensurePrismaClient,
|
|
259
|
+
ensureSpawnHelpersExecutable: () => ensureSpawnHelpersExecutable,
|
|
260
|
+
migrateFailureHint: () => migrateFailureHint,
|
|
259
261
|
parseStartArgs: () => parseStartArgs,
|
|
260
|
-
prismaClientUsable: () => prismaClientUsable
|
|
262
|
+
prismaClientUsable: () => prismaClientUsable,
|
|
263
|
+
repairSpawnHelper: () => repairSpawnHelper,
|
|
264
|
+
spawnHelperPaths: () => spawnHelperPaths
|
|
261
265
|
});
|
|
262
266
|
import { spawn, execFileSync } from "node:child_process";
|
|
263
267
|
import { createRequire } from "node:module";
|
|
264
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
265
|
-
import { dirname as dirname2, resolve as resolvePath } from "node:path";
|
|
268
|
+
import { existsSync, mkdirSync, readdirSync, statSync, chmodSync } from "node:fs";
|
|
269
|
+
import { dirname as dirname2, join as join3, resolve as resolvePath } from "node:path";
|
|
266
270
|
import { fileURLToPath } from "node:url";
|
|
267
271
|
function parseStartArgs(argv) {
|
|
268
272
|
const out = { port: null, host: null, db: null, migrate: true };
|
|
@@ -307,10 +311,29 @@ function distDir() {
|
|
|
307
311
|
}
|
|
308
312
|
function runPrisma(args, dbUrl) {
|
|
309
313
|
const prismaCli = prismaCliPath(createRequire(import.meta.url).resolve);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
try {
|
|
315
|
+
const out = execFileSync(process.execPath, [prismaCli, ...args], {
|
|
316
|
+
env: { ...process.env, DATABASE_URL: dbUrl },
|
|
317
|
+
encoding: "utf8",
|
|
318
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
319
|
+
});
|
|
320
|
+
process.stdout.write(out);
|
|
321
|
+
return out;
|
|
322
|
+
} catch (e) {
|
|
323
|
+
const err = e;
|
|
324
|
+
const output = `${err.stdout ?? ""}${err.stderr ?? ""}`;
|
|
325
|
+
process.stderr.write(output);
|
|
326
|
+
throw Object.assign(new Error("prisma gagal"), { output });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function migrateFailureHint(output, dbFile) {
|
|
330
|
+
if (!output.includes("P3005")) return null;
|
|
331
|
+
return `hanoman: berkas DB ${dbFile} sudah berisi tabel, tapi tak punya riwayat migrasi hanoman.
|
|
332
|
+
Biasanya ia bukan DB hanoman versi ini \u2014 mis. sisa prototipe lama atau berkas tool lain.
|
|
333
|
+
Pilih salah satu:
|
|
334
|
+
\u2022 Pindahkan berkas itu, lalu jalankan ulang: mv "${dbFile}" "${dbFile}.lama"
|
|
335
|
+
\u2022 Pakai berkas lain: hanoman --db /path/baru.db (atau HANOMAN_DATABASE_URL=file:/path/baru.db)
|
|
336
|
+
Isi berkas lama tidak diubah oleh hanoman \u2014 periksa dulu sebelum menghapusnya.`;
|
|
314
337
|
}
|
|
315
338
|
function applyMigrations(schema, dbUrl) {
|
|
316
339
|
runPrisma(["migrate", "deploy", "--schema", schema], dbUrl);
|
|
@@ -335,6 +358,44 @@ async function ensurePrismaClient(schema, dbUrl, ctx) {
|
|
|
335
358
|
return false;
|
|
336
359
|
}
|
|
337
360
|
}
|
|
361
|
+
function spawnHelperPaths(ptyDir, listDir, exists) {
|
|
362
|
+
const dirs = [join3(ptyDir, "build", "Release")];
|
|
363
|
+
for (const name of listDir(join3(ptyDir, "prebuilds"))) dirs.push(join3(ptyDir, "prebuilds", name));
|
|
364
|
+
return dirs.map((d) => join3(d, "spawn-helper")).filter(exists);
|
|
365
|
+
}
|
|
366
|
+
function ensureSpawnHelpersExecutable(paths2, ops) {
|
|
367
|
+
const fixed = [];
|
|
368
|
+
for (const p of paths2) {
|
|
369
|
+
try {
|
|
370
|
+
const mode = ops.mode(p) & 4095;
|
|
371
|
+
if ((mode & 73) === 73) continue;
|
|
372
|
+
ops.chmod(p, mode | 73);
|
|
373
|
+
fixed.push(p);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return fixed;
|
|
378
|
+
}
|
|
379
|
+
function repairSpawnHelper(ctx) {
|
|
380
|
+
let ptyDir;
|
|
381
|
+
try {
|
|
382
|
+
ptyDir = dirname2(createRequire(import.meta.url).resolve("node-pty/package.json"));
|
|
383
|
+
} catch {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const paths2 = spawnHelperPaths(ptyDir, (d) => {
|
|
387
|
+
try {
|
|
388
|
+
return readdirSync(d);
|
|
389
|
+
} catch {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
}, existsSync);
|
|
393
|
+
const fixed = ensureSpawnHelpersExecutable(paths2, {
|
|
394
|
+
mode: (p) => statSync(p).mode,
|
|
395
|
+
chmod: (p, m) => chmodSync(p, m)
|
|
396
|
+
});
|
|
397
|
+
if (fixed.length) ctx.stdout("hanoman \xB7 memperbaiki izin `spawn-helper` node-pty (sekali per instalasi)\n");
|
|
398
|
+
}
|
|
338
399
|
async function start(argv, ctx) {
|
|
339
400
|
let opts;
|
|
340
401
|
try {
|
|
@@ -366,13 +427,17 @@ async function start(argv, ctx) {
|
|
|
366
427
|
return 1;
|
|
367
428
|
}
|
|
368
429
|
if (!await ensurePrismaClient(layout.schema, dbUrl, ctx)) return 1;
|
|
430
|
+
repairSpawnHelper(ctx);
|
|
369
431
|
if (opts.migrate) {
|
|
370
432
|
ctx.stdout(`hanoman \xB7 menerapkan migrasi ke ${dbFilePath(dbUrl)}
|
|
371
433
|
`);
|
|
372
434
|
try {
|
|
373
435
|
applyMigrations(layout.schema, dbUrl);
|
|
374
|
-
} catch {
|
|
375
|
-
|
|
436
|
+
} catch (e) {
|
|
437
|
+
const hint = migrateFailureHint(String(e.output ?? ""), dbFilePath(dbUrl));
|
|
438
|
+
ctx.stderr(hint ? `
|
|
439
|
+
${hint}
|
|
440
|
+
` : "hanoman: `prisma migrate deploy` gagal \u2014 lihat keluaran di atas\n");
|
|
376
441
|
return 1;
|
|
377
442
|
}
|
|
378
443
|
}
|
|
@@ -5895,9 +5960,9 @@ var init_migrate_pg = __esm({
|
|
|
5895
5960
|
|
|
5896
5961
|
// src/config.ts
|
|
5897
5962
|
import { readFileSync, existsSync as existsSync4 } from "node:fs";
|
|
5898
|
-
import { join as
|
|
5963
|
+
import { join as join4 } from "node:path";
|
|
5899
5964
|
function loadConfig(repoRoot) {
|
|
5900
|
-
const p =
|
|
5965
|
+
const p = join4(repoRoot, "hanoman.config.json");
|
|
5901
5966
|
const raw = existsSync4(p) ? JSON.parse(readFileSync(p, "utf8")) : {};
|
|
5902
5967
|
return zHanomanConfig.parse(raw);
|
|
5903
5968
|
}
|
|
@@ -5910,12 +5975,12 @@ var init_config2 = __esm({
|
|
|
5910
5975
|
|
|
5911
5976
|
// src/repo.ts
|
|
5912
5977
|
import { spawnSync } from "node:child_process";
|
|
5913
|
-
import { join as
|
|
5978
|
+
import { join as join5 } from "node:path";
|
|
5914
5979
|
function resolveRepo(cwd) {
|
|
5915
5980
|
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
5916
5981
|
const root = r.status === 0 ? r.stdout.trim() : cwd;
|
|
5917
5982
|
const { docsDir } = loadConfig(root);
|
|
5918
|
-
return { root, docsDir, indexPath:
|
|
5983
|
+
return { root, docsDir, indexPath: join5(root, docsDir, "README.md") };
|
|
5919
5984
|
}
|
|
5920
5985
|
var init_repo = __esm({
|
|
5921
5986
|
"src/repo.ts"() {
|
|
@@ -5925,8 +5990,8 @@ var init_repo = __esm({
|
|
|
5925
5990
|
});
|
|
5926
5991
|
|
|
5927
5992
|
// src/docs-model.ts
|
|
5928
|
-
import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
5929
|
-
import { join as
|
|
5993
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
5994
|
+
import { join as join6, relative } from "node:path";
|
|
5930
5995
|
function parseIndex(indexPath) {
|
|
5931
5996
|
const md = readFileSync2(indexPath, "utf8");
|
|
5932
5997
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -5941,10 +6006,10 @@ function parseIndex(indexPath) {
|
|
|
5941
6006
|
function walkDocs(docsRoot) {
|
|
5942
6007
|
const out = [];
|
|
5943
6008
|
const walk = (dir) => {
|
|
5944
|
-
for (const name of
|
|
6009
|
+
for (const name of readdirSync2(dir)) {
|
|
5945
6010
|
if (name.startsWith(".")) continue;
|
|
5946
|
-
const abs =
|
|
5947
|
-
if (
|
|
6011
|
+
const abs = join6(dir, name);
|
|
6012
|
+
if (statSync2(abs).isDirectory()) walk(abs);
|
|
5948
6013
|
else out.push(relative(docsRoot, abs).split("\\").join("/"));
|
|
5949
6014
|
}
|
|
5950
6015
|
};
|
|
@@ -5975,17 +6040,17 @@ var init_docs_model = __esm({
|
|
|
5975
6040
|
});
|
|
5976
6041
|
|
|
5977
6042
|
// src/verify.ts
|
|
5978
|
-
import { join as
|
|
6043
|
+
import { join as join7 } from "node:path";
|
|
5979
6044
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
5980
6045
|
function scanCoverage(cwd) {
|
|
5981
6046
|
const { root, docsDir, indexPath } = resolveRepo(cwd);
|
|
5982
|
-
const docsRoot =
|
|
6047
|
+
const docsRoot = join7(root, docsDir);
|
|
5983
6048
|
if (!existsSync5(docsRoot)) return { coverage: 100, cats: [] };
|
|
5984
6049
|
if (!existsSync5(indexPath)) throw new Error(`index Source of Truth tidak ada: ${indexPath}`);
|
|
5985
6050
|
const corpus = walkDocs(docsRoot);
|
|
5986
6051
|
const read = (rel) => {
|
|
5987
6052
|
try {
|
|
5988
|
-
return readFileSync3(
|
|
6053
|
+
return readFileSync3(join7(docsRoot, rel), "utf8");
|
|
5989
6054
|
} catch {
|
|
5990
6055
|
return null;
|
|
5991
6056
|
}
|
|
@@ -6059,23 +6124,23 @@ __export(docs_index_exports, {
|
|
|
6059
6124
|
default: () => docs_index_default
|
|
6060
6125
|
});
|
|
6061
6126
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
6062
|
-
import { join as
|
|
6127
|
+
import { join as join8 } from "node:path";
|
|
6063
6128
|
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
6064
6129
|
async function docs_index_default(args, ctx) {
|
|
6065
6130
|
const { values } = parseArgs2({ args, options: { check: { type: "boolean" }, fix: { type: "boolean" } }, allowPositionals: true });
|
|
6066
6131
|
const { root, docsDir, indexPath } = resolveRepo(ctx.cwd);
|
|
6067
|
-
const docsRoot =
|
|
6132
|
+
const docsRoot = join8(root, docsDir);
|
|
6068
6133
|
const corpus = walkDocs(docsRoot);
|
|
6069
6134
|
const read = (rel) => {
|
|
6070
6135
|
try {
|
|
6071
|
-
return readFileSync5(
|
|
6136
|
+
return readFileSync5(join8(docsRoot, rel), "utf8");
|
|
6072
6137
|
} catch {
|
|
6073
6138
|
return null;
|
|
6074
6139
|
}
|
|
6075
6140
|
};
|
|
6076
6141
|
const linked = linkedSetFrom(INDEX_NAME, corpus, read);
|
|
6077
6142
|
const unlinked = corpus.filter((f) => f !== INDEX_NAME && !linked.has(f));
|
|
6078
|
-
const dangling = [...parseIndex(indexPath)].filter((p) => !existsSync6(
|
|
6143
|
+
const dangling = [...parseIndex(indexPath)].filter((p) => !existsSync6(join8(docsRoot, p)));
|
|
6079
6144
|
if (values.fix) {
|
|
6080
6145
|
for (const f of unlinked) addLink(indexPath, f, f.split("/")[0]);
|
|
6081
6146
|
ctx.stdout(`linked ${unlinked.length} doc(s)
|
|
@@ -6128,7 +6193,7 @@ var init_docs_link = __esm({
|
|
|
6128
6193
|
});
|
|
6129
6194
|
|
|
6130
6195
|
// src/release/pack.ts
|
|
6131
|
-
import { join as
|
|
6196
|
+
import { join as join9 } from "node:path";
|
|
6132
6197
|
function packageJsonFor(version2, deps) {
|
|
6133
6198
|
return {
|
|
6134
6199
|
name: PKG_NAME,
|
|
@@ -6152,14 +6217,14 @@ function packageJsonFor(version2, deps) {
|
|
|
6152
6217
|
}
|
|
6153
6218
|
function copyPlan(repo) {
|
|
6154
6219
|
return [
|
|
6155
|
-
{ from:
|
|
6156
|
-
{ from:
|
|
6157
|
-
{ from:
|
|
6158
|
-
{ from:
|
|
6159
|
-
{ from:
|
|
6160
|
-
{ from:
|
|
6161
|
-
{ from:
|
|
6162
|
-
{ from:
|
|
6220
|
+
{ from: join9(repo, "server/dist/server.js"), to: "dist/server.js" },
|
|
6221
|
+
{ from: join9(repo, "server/dist/build-info.json"), to: "dist/build-info.json" },
|
|
6222
|
+
{ from: join9(repo, "cli/dist/hanoman.js"), to: "dist/cli.js" },
|
|
6223
|
+
{ from: join9(repo, "src/dist"), to: "web", dir: true },
|
|
6224
|
+
{ from: join9(repo, "server/prisma/schema.prisma"), to: "prisma/schema.prisma" },
|
|
6225
|
+
{ from: join9(repo, "server/prisma/migrations"), to: "prisma/migrations", dir: true },
|
|
6226
|
+
{ from: join9(repo, "internal/docs/operations/npm-readme.md"), to: "README.md" },
|
|
6227
|
+
{ from: join9(repo, "LICENSE"), to: "LICENSE" }
|
|
6163
6228
|
];
|
|
6164
6229
|
}
|
|
6165
6230
|
var PKG_NAME, REPO_URL, RUNTIME_DEPS, REQUIRED_ARTIFACTS, BIN_SHIM;
|
|
@@ -6202,9 +6267,9 @@ __export(pack_exports, {
|
|
|
6202
6267
|
default: () => pack
|
|
6203
6268
|
});
|
|
6204
6269
|
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6205
|
-
import { dirname as dirname5, join as
|
|
6270
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
6206
6271
|
function depVersions(repo) {
|
|
6207
|
-
const read = (p) => JSON.parse(readFileSync6(
|
|
6272
|
+
const read = (p) => JSON.parse(readFileSync6(join10(repo, p), "utf8"));
|
|
6208
6273
|
const server = read("server/package.json"), cli = read("cli/package.json");
|
|
6209
6274
|
const pools = [server.dependencies, server.devDependencies, cli.dependencies, cli.devDependencies];
|
|
6210
6275
|
const out = {};
|
|
@@ -6224,13 +6289,13 @@ async function pack(argv, ctx) {
|
|
|
6224
6289
|
`);
|
|
6225
6290
|
return 1;
|
|
6226
6291
|
}
|
|
6227
|
-
if (!existsSync7(
|
|
6292
|
+
if (!existsSync7(join10(repo, "server/prisma/schema.prisma"))) {
|
|
6228
6293
|
ctx.stderr("pack: hanya bisa dijalankan dari checkout repo hanoman\n");
|
|
6229
6294
|
return 1;
|
|
6230
6295
|
}
|
|
6231
6296
|
const outIdx = argv.indexOf("--out");
|
|
6232
|
-
const out = outIdx === -1 ?
|
|
6233
|
-
const version2 = JSON.parse(readFileSync6(
|
|
6297
|
+
const out = outIdx === -1 ? join10(repo, "dist-npm") : join10(repo, argv[outIdx + 1] ?? "dist-npm");
|
|
6298
|
+
const version2 = JSON.parse(readFileSync6(join10(repo, "package.json"), "utf8")).version;
|
|
6234
6299
|
if (!version2) {
|
|
6235
6300
|
ctx.stderr("pack: root package.json tanpa field version\n");
|
|
6236
6301
|
return 1;
|
|
@@ -6244,20 +6309,20 @@ async function pack(argv, ctx) {
|
|
|
6244
6309
|
return 1;
|
|
6245
6310
|
}
|
|
6246
6311
|
rmSync(out, { recursive: true, force: true });
|
|
6247
|
-
mkdirSync2(
|
|
6312
|
+
mkdirSync2(join10(out, "bin"), { recursive: true });
|
|
6248
6313
|
for (const item of copyPlan(repo)) {
|
|
6249
6314
|
if (!existsSync7(item.from)) {
|
|
6250
6315
|
ctx.stderr(`pack: artefak hilang \u2014 ${item.from} (sudah \`pnpm build\`?)
|
|
6251
6316
|
`);
|
|
6252
6317
|
return 1;
|
|
6253
6318
|
}
|
|
6254
|
-
const dest =
|
|
6319
|
+
const dest = join10(out, item.to);
|
|
6255
6320
|
mkdirSync2(dirname5(dest), { recursive: true });
|
|
6256
6321
|
cpSync(item.from, dest, item.dir ? { recursive: true } : {});
|
|
6257
6322
|
}
|
|
6258
|
-
writeFileSync2(
|
|
6259
|
-
writeFileSync2(
|
|
6260
|
-
const missing = REQUIRED_ARTIFACTS.filter((a) => !existsSync7(
|
|
6323
|
+
writeFileSync2(join10(out, "bin/hanoman.mjs"), BIN_SHIM);
|
|
6324
|
+
writeFileSync2(join10(out, "package.json"), JSON.stringify(packageJsonFor(version2, deps), null, 2) + "\n");
|
|
6325
|
+
const missing = REQUIRED_ARTIFACTS.filter((a) => !existsSync7(join10(out, a)));
|
|
6261
6326
|
if (missing.length) {
|
|
6262
6327
|
ctx.stderr(`pack: artefak wajib hilang: ${missing.join(", ")}
|
|
6263
6328
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanoman",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Orchestrator + dashboard workflow docs-driven untuk sesi Claude Code / Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -25,15 +25,17 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"fastify": "^4.
|
|
28
|
+
"@fastify/cookie": "^9.4.0",
|
|
29
29
|
"@fastify/static": "^7.0.0",
|
|
30
30
|
"@fastify/websocket": "^10.0.1",
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
31
|
+
"@prisma/client": "^6.19.3",
|
|
32
|
+
"fastify": "^4.28.0",
|
|
33
33
|
"node-pty": "^1.1.0",
|
|
34
34
|
"pdfkit": "^0.19.1",
|
|
35
|
-
"prisma": "^6.19.0",
|
|
36
35
|
"pg": "^8.13.1"
|
|
37
36
|
},
|
|
38
|
-
"license": "MIT"
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"prisma": "^6.19.3"
|
|
40
|
+
}
|
|
39
41
|
}
|