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 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`.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.1",
3
- "sha": "55bc2c6",
4
- "builtAt": "2026-07-30T07:00:27.508Z"
2
+ "version": "0.1.3",
3
+ "sha": "c5266ce",
4
+ "builtAt": "2026-07-30T08:59:12.271Z"
5
5
  }
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
- execFileSync(process.execPath, [prismaCli, ...args], {
311
- env: { ...process.env, DATABASE_URL: dbUrl },
312
- stdio: "inherit"
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
- ctx.stderr("hanoman: `prisma migrate deploy` gagal \u2014 lihat keluaran di atas\n");
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 join3 } from "node:path";
5963
+ import { join as join4 } from "node:path";
5899
5964
  function loadConfig(repoRoot) {
5900
- const p = join3(repoRoot, "hanoman.config.json");
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 join4 } from "node:path";
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: join4(root, docsDir, "README.md") };
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 join5, relative } from "node:path";
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 readdirSync(dir)) {
6009
+ for (const name of readdirSync2(dir)) {
5945
6010
  if (name.startsWith(".")) continue;
5946
- const abs = join5(dir, name);
5947
- if (statSync(abs).isDirectory()) walk(abs);
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 join6 } from "node:path";
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 = join6(root, docsDir);
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(join6(docsRoot, rel), "utf8");
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 join7 } from "node:path";
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 = join7(root, docsDir);
6132
+ const docsRoot = join8(root, docsDir);
6068
6133
  const corpus = walkDocs(docsRoot);
6069
6134
  const read = (rel) => {
6070
6135
  try {
6071
- return readFileSync5(join7(docsRoot, rel), "utf8");
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(join7(docsRoot, p)));
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 join8 } from "node:path";
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: join8(repo, "server/dist/server.js"), to: "dist/server.js" },
6156
- { from: join8(repo, "server/dist/build-info.json"), to: "dist/build-info.json" },
6157
- { from: join8(repo, "cli/dist/hanoman.js"), to: "dist/cli.js" },
6158
- { from: join8(repo, "src/dist"), to: "web", dir: true },
6159
- { from: join8(repo, "server/prisma/schema.prisma"), to: "prisma/schema.prisma" },
6160
- { from: join8(repo, "server/prisma/migrations"), to: "prisma/migrations", dir: true },
6161
- { from: join8(repo, "internal/docs/operations/npm-readme.md"), to: "README.md" },
6162
- { from: join8(repo, "LICENSE"), to: "LICENSE" }
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 join9 } from "node:path";
6270
+ import { dirname as dirname5, join as join10 } from "node:path";
6206
6271
  function depVersions(repo) {
6207
- const read = (p) => JSON.parse(readFileSync6(join9(repo, p), "utf8"));
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(join9(repo, "server/prisma/schema.prisma"))) {
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 ? join9(repo, "dist-npm") : join9(repo, argv[outIdx + 1] ?? "dist-npm");
6233
- const version2 = JSON.parse(readFileSync6(join9(repo, "package.json"), "utf8")).version;
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(join9(out, "bin"), { recursive: true });
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 = join9(out, item.to);
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(join9(out, "bin/hanoman.mjs"), BIN_SHIM);
6259
- writeFileSync2(join9(out, "package.json"), JSON.stringify(packageJsonFor(version2, deps), null, 2) + "\n");
6260
- const missing = REQUIRED_ARTIFACTS.filter((a) => !existsSync7(join9(out, a)));
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.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.0",
28
+ "@fastify/cookie": "^9.4.0",
29
29
  "@fastify/static": "^7.0.0",
30
30
  "@fastify/websocket": "^10.0.1",
31
- "@fastify/cookie": "^9.4.0",
32
- "@prisma/client": "^6.19.0",
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
  }