@hiveai/cli 0.7.0 → 0.7.1

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/dist/index.js CHANGED
@@ -457,9 +457,15 @@ var FRAMEWORK_SIGNALS = {
457
457
  "Express": ["express"],
458
458
  "Fastify": ["fastify"],
459
459
  "Hono": ["hono"],
460
- "tRPC": ["@trpc/server"],
460
+ "tRPC": ["@trpc/server", "@trpc/client"],
461
461
  "Prisma": ["@prisma/client"],
462
462
  "Drizzle": ["drizzle-orm"],
463
+ "Redux Toolkit": ["@reduxjs/toolkit"],
464
+ "Zustand": ["zustand"],
465
+ "TanStack Query": ["@tanstack/react-query", "react-query"],
466
+ "Mongoose": ["mongoose"],
467
+ "Apollo": ["@apollo/client", "@apollo/server", "apollo-server"],
468
+ "GraphQL": ["graphql"],
463
469
  "Vite": ["vite"],
464
470
  "Vitest": ["vitest"],
465
471
  "Jest": ["jest"]
@@ -512,7 +518,14 @@ function detectLanguage(root) {
512
518
  if (existsSync3(path4.join(root, "package.json"))) return "JavaScript";
513
519
  return "Unknown";
514
520
  }
515
- function detectProjectType(frameworks, scripts) {
521
+ function detectProjectType(frameworks, scripts, isMonorepo) {
522
+ if (isMonorepo) {
523
+ if (frameworks.includes("NestJS")) return "Monorepo (NestJS backend)";
524
+ if (frameworks.includes("Next.js")) return "Monorepo (Next.js)";
525
+ if (frameworks.includes("React")) return "Multi-package monorepo (React)";
526
+ if (frameworks.length > 0) return `Multi-package monorepo (${frameworks.slice(0, 2).join(", ")})`;
527
+ return "Multi-package monorepo";
528
+ }
516
529
  if (frameworks.includes("NestJS")) return "Backend API (NestJS)";
517
530
  if (frameworks.includes("Next.js")) return "Full-stack web app (Next.js)";
518
531
  if (frameworks.includes("Remix")) return "Full-stack web app (Remix)";
@@ -543,7 +556,7 @@ async function scanDirs(root, maxDepth = 2) {
543
556
  await walk(root, 0);
544
557
  return results;
545
558
  }
546
- function inferModuleDescriptions(dirs) {
559
+ function inferModuleDescriptions(dirs, frameworks = []) {
547
560
  const known = {
548
561
  "src": "main source directory",
549
562
  "app": "application entrypoint / routes (Next.js App Router or similar)",
@@ -557,6 +570,12 @@ function inferModuleDescriptions(dirs) {
557
570
  "modules": "feature modules",
558
571
  "middleware": "HTTP or business middleware",
559
572
  "guards": "auth / access guards",
573
+ "decorators": "custom decorators",
574
+ "interceptors": "NestJS interceptors",
575
+ "filters": "exception filters",
576
+ "pipes": "validation / transformation pipes",
577
+ "dto": "Data Transfer Objects",
578
+ "entities": "ORM entities / database models",
560
579
  "prisma": "Prisma schema and migrations",
561
580
  "migrations": "database migrations",
562
581
  "config": "configuration files",
@@ -582,8 +601,46 @@ function inferModuleDescriptions(dirs) {
582
601
  "client": "client-side code",
583
602
  "features": "feature-based modules",
584
603
  "routes": "route definitions",
585
- "workers": "background workers / queues"
604
+ "workers": "background workers / queues",
605
+ "auth": "authentication / authorization",
606
+ "users": "user management",
607
+ "products": "product catalog",
608
+ "orders": "order management",
609
+ "common": "shared / common utilities",
610
+ "shared": "shared code across modules"
586
611
  };
612
+ const isNestJS = frameworks.includes("NestJS");
613
+ const srcSubdirs = dirs.filter((d) => d.startsWith("src/") && d.split("/").length === 2);
614
+ if (isNestJS && srcSubdirs.length >= 2) {
615
+ const result = [`- \`src/\` \u2014 main source (NestJS feature modules)`];
616
+ for (const d of srcSubdirs.slice(0, 12)) {
617
+ const name = d.split("/")[1];
618
+ const desc = known[name.toLowerCase()] ?? "feature module";
619
+ result.push(` - \`${name}/\` \u2014 ${desc}`);
620
+ }
621
+ const otherTopLevel = dirs.filter((d) => !d.includes("/") && d !== "src").slice(0, 6);
622
+ for (const d of otherTopLevel) {
623
+ const desc = known[d.toLowerCase()] ?? "module";
624
+ result.push(`- \`${d}/\` \u2014 ${desc}`);
625
+ }
626
+ return result;
627
+ }
628
+ const isMonorepo = dirs.some((d) => d === "packages") && dirs.some((d) => d.startsWith("packages/") && d.split("/").length === 2);
629
+ if (isMonorepo) {
630
+ const packageSubdirs = dirs.filter((d) => d.startsWith("packages/") && d.split("/").length === 2);
631
+ const result = [`- \`packages/\` \u2014 monorepo sub-packages`];
632
+ for (const d of packageSubdirs.slice(0, 10)) {
633
+ const name = d.split("/")[1];
634
+ const desc = known[name.toLowerCase()] ?? "sub-package";
635
+ result.push(` - \`${name}/\` \u2014 ${desc}`);
636
+ }
637
+ const otherTopLevel = dirs.filter((d) => !d.includes("/") && d !== "packages").slice(0, 5);
638
+ for (const d of otherTopLevel) {
639
+ const desc = known[d.toLowerCase()] ?? "module";
640
+ result.push(`- \`${d}/\` \u2014 ${desc}`);
641
+ }
642
+ return result;
643
+ }
587
644
  const top = dirs.filter((d) => !d.includes("/")).slice(0, 12);
588
645
  return top.map((d) => {
589
646
  const desc = known[d.toLowerCase()] ?? "module";
@@ -618,7 +675,8 @@ async function generateBootstrapContext(root) {
618
675
  const frameworks = detectFrameworks(allDeps);
619
676
  const keyDeps = detectKeyDeps(allDeps);
620
677
  const language = detectLanguage(root);
621
- const projectType = detectProjectType(frameworks, pkg.scripts ?? {});
678
+ const isMonorepo = pkg.workspaces !== void 0 && (Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : true);
679
+ const projectType = detectProjectType(frameworks, pkg.scripts ?? {}, isMonorepo);
622
680
  const projectName = pkg.name ?? path4.basename(root);
623
681
  const projectDesc = pkg.description ?? "";
624
682
  let readmeSummary = "";
@@ -634,7 +692,7 @@ async function generateBootstrapContext(root) {
634
692
  }
635
693
  }
636
694
  const dirs = await scanDirs(root, 2);
637
- const moduleLines = inferModuleDescriptions(dirs);
695
+ const moduleLines = inferModuleDescriptions(dirs, frameworks);
638
696
  const scripts = pkg.scripts ?? {};
639
697
  const scriptLines = Object.entries(scripts).filter(([k]) => ["build", "dev", "start", "test", "lint", "deploy"].includes(k)).map(([k, v]) => `- \`${k}\`: ${v}`).slice(0, 6);
640
698
  const stackParts = [language];
@@ -1079,6 +1137,324 @@ After changing schema.ts:
1079
1137
 
1080
1138
  Without this, queries silently operate on stale column definitions and may return wrong data.`
1081
1139
  }
1140
+ ],
1141
+ zustand: [
1142
+ {
1143
+ slug: "zustand-select-slices-not-whole-store",
1144
+ type: "convention",
1145
+ tags: ["zustand", "performance", "react"],
1146
+ body: `Always select specific slices \u2014 never subscribe to the whole store.
1147
+
1148
+ \`\`\`ts
1149
+ // \u274C Re-renders on any store change (even unrelated fields)
1150
+ const store = useStore();
1151
+
1152
+ // \u2705 Re-renders only when count changes
1153
+ const count = useStore((s) => s.count);
1154
+ \`\`\`
1155
+
1156
+ Subscribing to the whole store is the single most common Zustand performance mistake.`
1157
+ },
1158
+ {
1159
+ slug: "zustand-devtools-wrap-dev-only",
1160
+ type: "convention",
1161
+ tags: ["zustand", "devtools", "performance"],
1162
+ body: `Wrap Zustand devtools middleware in a dev-only condition.
1163
+
1164
+ \`\`\`ts
1165
+ import { devtools } from 'zustand/middleware';
1166
+
1167
+ const useStore = create(
1168
+ process.env.NODE_ENV === 'development'
1169
+ ? devtools(storeImpl, { name: 'AppStore' })
1170
+ : storeImpl,
1171
+ );
1172
+ \`\`\`
1173
+
1174
+ Shipping devtools to production adds overhead and exposes store internals in bundle.`
1175
+ },
1176
+ {
1177
+ slug: "zustand-persist-hydration-ssr",
1178
+ type: "gotcha",
1179
+ tags: ["zustand", "ssr", "nextjs", "hydration"],
1180
+ body: `Zustand persist middleware causes hydration mismatch in SSR (Next.js / Remix).
1181
+
1182
+ The server renders with empty state; the client rehydrates from localStorage.
1183
+ Fix: use \`skipHydration: true\` and manually call \`rehydrate()\` after mount.
1184
+
1185
+ \`\`\`ts
1186
+ // In a useEffect or useLayoutEffect on the client:
1187
+ useEffect(() => { useStore.persist.rehydrate(); }, []);
1188
+ \`\`\``
1189
+ }
1190
+ ],
1191
+ redux: [
1192
+ {
1193
+ slug: "redux-toolkit-immer-mutate-or-return",
1194
+ type: "gotcha",
1195
+ tags: ["redux", "redux-toolkit", "immer"],
1196
+ body: `In RTK createSlice reducers (Immer), you must EITHER mutate the draft OR return a new value \u2014 never both.
1197
+
1198
+ \`\`\`ts
1199
+ // \u2705 Mutate draft (Immer converts to immutable update)
1200
+ state.count += 1;
1201
+
1202
+ // \u2705 Return new value
1203
+ return { ...state, count: state.count + 1 };
1204
+
1205
+ // \u274C Both \u2014 causes undefined state
1206
+ state.count += 1;
1207
+ return state; // DON'T \u2014 Immer sees both a mutation and a return
1208
+ \`\`\``
1209
+ },
1210
+ {
1211
+ slug: "redux-toolkit-rtk-query-over-thunk",
1212
+ type: "decision",
1213
+ tags: ["redux", "redux-toolkit", "data-fetching"],
1214
+ body: `Use RTK Query for server data, not createAsyncThunk.
1215
+
1216
+ RTK Query automatically handles: caching, loading/error states, cache invalidation, polling, optimistic updates.
1217
+ createAsyncThunk is for one-off side effects that don't fit the query/mutation model (e.g. file upload with progress).`
1218
+ },
1219
+ {
1220
+ slug: "redux-toolkit-normalize-nested-data",
1221
+ type: "convention",
1222
+ tags: ["redux", "redux-toolkit", "normalization"],
1223
+ body: `Normalize nested API responses before storing in Redux \u2014 use createEntityAdapter.
1224
+
1225
+ Storing deeply nested objects causes:
1226
+ - Redundant re-renders when any deeply nested field changes
1227
+ - Difficult update logic (deep merge)
1228
+
1229
+ \`\`\`ts
1230
+ const usersAdapter = createEntityAdapter<User>();
1231
+ const usersSlice = createSlice({
1232
+ name: 'users',
1233
+ initialState: usersAdapter.getInitialState(),
1234
+ reducers: { usersReceived: usersAdapter.setAll },
1235
+ });
1236
+ \`\`\``
1237
+ }
1238
+ ],
1239
+ reactquery: [
1240
+ {
1241
+ slug: "tanstack-query-stale-time-default",
1242
+ type: "gotcha",
1243
+ tags: ["react-query", "tanstack-query", "caching"],
1244
+ body: `By default, TanStack Query marks data as stale immediately (staleTime: 0) and refetches on every window focus.
1245
+
1246
+ Set a reasonable staleTime to avoid unnecessary network requests:
1247
+
1248
+ \`\`\`ts
1249
+ useQuery({
1250
+ queryKey: ['user', id],
1251
+ queryFn: () => getUser(id),
1252
+ staleTime: 5 * 60 * 1000, // 5 minutes
1253
+ })
1254
+ \`\`\`
1255
+
1256
+ Set globally via QueryClient defaultOptions for consistency.`
1257
+ },
1258
+ {
1259
+ slug: "tanstack-query-invalidate-after-mutation",
1260
+ type: "convention",
1261
+ tags: ["react-query", "tanstack-query", "mutations"],
1262
+ body: `Always invalidate related queries after a mutation to keep the cache fresh.
1263
+
1264
+ \`\`\`ts
1265
+ useMutation({
1266
+ mutationFn: createUser,
1267
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
1268
+ })
1269
+ \`\`\`
1270
+
1271
+ Skipping invalidation causes the UI to show stale data after a write until the next background refetch.`
1272
+ },
1273
+ {
1274
+ slug: "tanstack-query-querykey-as-dependency",
1275
+ type: "convention",
1276
+ tags: ["react-query", "tanstack-query"],
1277
+ body: `Treat the queryKey array as a dependency array \u2014 include all variables the queryFn depends on.
1278
+
1279
+ \`\`\`ts
1280
+ // \u274C Won't refetch when userId changes
1281
+ useQuery({ queryKey: ['user'], queryFn: () => getUser(userId) });
1282
+
1283
+ // \u2705 Refetches automatically when userId changes
1284
+ useQuery({ queryKey: ['user', userId], queryFn: () => getUser(userId) });
1285
+ \`\`\``
1286
+ }
1287
+ ],
1288
+ trpc: [
1289
+ {
1290
+ slug: "trpc-always-validate-input-with-zod",
1291
+ type: "convention",
1292
+ tags: ["trpc", "validation", "security"],
1293
+ body: `Always validate procedure inputs with Zod \u2014 tRPC infers types but doesn't enforce them at runtime without a schema.
1294
+
1295
+ \`\`\`ts
1296
+ // \u274C No runtime validation \u2014 input is 'unknown'
1297
+ t.procedure.query(({ input }) => getUser(input as string));
1298
+
1299
+ // \u2705 Validated and typed end-to-end
1300
+ t.procedure
1301
+ .input(z.object({ id: z.string().uuid() }))
1302
+ .query(({ input }) => getUser(input.id));
1303
+ \`\`\``
1304
+ },
1305
+ {
1306
+ slug: "trpc-server-side-caller-for-ssr",
1307
+ type: "convention",
1308
+ tags: ["trpc", "nextjs", "ssr"],
1309
+ body: `Use the server-side caller in Server Components / SSR \u2014 don't call tRPC over HTTP from the server.
1310
+
1311
+ \`\`\`ts
1312
+ // In Next.js App Router server component
1313
+ const caller = appRouter.createCaller(await createContext());
1314
+ const data = await caller.users.getAll(); // Direct function call, no HTTP
1315
+ \`\`\`
1316
+
1317
+ HTTP round-trips from server \u2192 server add latency and bypass auth context.`
1318
+ },
1319
+ {
1320
+ slug: "trpc-context-for-auth",
1321
+ type: "architecture",
1322
+ tags: ["trpc", "auth"],
1323
+ body: `Put auth session on the tRPC context, not in individual procedures.
1324
+
1325
+ \`\`\`ts
1326
+ // createContext(): resolve session once, share across all procedures
1327
+ export async function createContext({ req }: CreateNextContextOptions) {
1328
+ const session = await getServerSession(req);
1329
+ return { session, db };
1330
+ }
1331
+
1332
+ // In procedure: ctx.session.user is always typed
1333
+ const protectedProcedure = t.procedure.use(({ ctx, next }) => {
1334
+ if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
1335
+ return next({ ctx: { ...ctx, user: ctx.session.user } });
1336
+ });
1337
+ \`\`\``
1338
+ }
1339
+ ],
1340
+ mongoose: [
1341
+ {
1342
+ slug: "mongoose-connection-singleton",
1343
+ type: "convention",
1344
+ tags: ["mongoose", "mongodb", "connection", "serverless"],
1345
+ body: `Create one Mongoose connection at startup \u2014 never connect inside route handlers.
1346
+
1347
+ In serverless (Next.js, Vercel), cache the connection to reuse across warm invocations:
1348
+
1349
+ \`\`\`ts
1350
+ let cached = (global as any).__mongoose ?? { conn: null, promise: null };
1351
+
1352
+ export async function dbConnect() {
1353
+ if (cached.conn) return cached.conn;
1354
+ if (!cached.promise) {
1355
+ cached.promise = mongoose.connect(process.env.MONGODB_URI!);
1356
+ }
1357
+ cached.conn = await cached.promise;
1358
+ (global as any).__mongoose = cached;
1359
+ return cached.conn;
1360
+ }
1361
+ \`\`\``
1362
+ },
1363
+ {
1364
+ slug: "mongoose-lean-for-read-only",
1365
+ type: "convention",
1366
+ tags: ["mongoose", "performance"],
1367
+ body: `Add .lean() to read-only queries to get plain JS objects instead of full Mongoose documents.
1368
+
1369
+ \`\`\`ts
1370
+ // \u274C Full Mongoose document \u2014 slow, heavy, has virtuals/methods
1371
+ const users = await User.find({});
1372
+
1373
+ // \u2705 Plain JS object \u2014 2-5x faster on large result sets
1374
+ const users = await User.find({}).lean();
1375
+ \`\`\`
1376
+
1377
+ Never use .lean() when you need to call .save() or Mongoose instance methods.`
1378
+ },
1379
+ {
1380
+ slug: "mongoose-index-frequently-queried-fields",
1381
+ type: "gotcha",
1382
+ tags: ["mongoose", "mongodb", "performance"],
1383
+ body: `Mongoose does NOT create indexes automatically unless you call syncIndexes() or ensureIndexes().
1384
+
1385
+ Declare indexes in the schema and sync them at startup:
1386
+
1387
+ \`\`\`ts
1388
+ UserSchema.index({ email: 1 }, { unique: true });
1389
+ UserSchema.index({ createdAt: -1 });
1390
+
1391
+ // At startup (not per-request):
1392
+ await User.syncIndexes();
1393
+ \`\`\`
1394
+
1395
+ Missing indexes cause full collection scans and timeouts at scale.`
1396
+ }
1397
+ ],
1398
+ graphql: [
1399
+ {
1400
+ slug: "graphql-n-plus-one-dataloader",
1401
+ type: "gotcha",
1402
+ tags: ["graphql", "performance", "n+1"],
1403
+ body: `GraphQL resolvers cause N+1 database queries without DataLoader batching.
1404
+
1405
+ Every field resolver runs independently \u2014 fetching related data naively causes N queries for N items.
1406
+
1407
+ \`\`\`ts
1408
+ // In context, create one DataLoader per request (NOT per resolver call)
1409
+ const userLoader = new DataLoader(async (ids: readonly string[]) =>
1410
+ User.findByIds(ids as string[])
1411
+ );
1412
+
1413
+ // In resolver:
1414
+ author: (post) => userLoader.load(post.authorId),
1415
+ \`\`\`
1416
+
1417
+ A list of 100 posts with authors = 101 queries without DataLoader, 2 queries with it.`
1418
+ },
1419
+ {
1420
+ slug: "graphql-mask-internal-errors-in-production",
1421
+ type: "gotcha",
1422
+ tags: ["graphql", "security", "apollo"],
1423
+ body: `Apollo Server exposes full error details (including stack traces) in development.
1424
+
1425
+ In production, mask internal errors to prevent leaking implementation details:
1426
+
1427
+ \`\`\`ts
1428
+ new ApolloServer({
1429
+ formatError: (formattedError) => {
1430
+ if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
1431
+ return { message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' } };
1432
+ }
1433
+ return formattedError;
1434
+ },
1435
+ });
1436
+ \`\`\``
1437
+ },
1438
+ {
1439
+ slug: "graphql-depth-limit-and-complexity",
1440
+ type: "convention",
1441
+ tags: ["graphql", "security", "dos"],
1442
+ body: `Add query depth and complexity limits to prevent DoS via deeply nested queries.
1443
+
1444
+ Without limits, a single query can request exponentially nested data and exhaust the server.
1445
+
1446
+ \`\`\`ts
1447
+ import depthLimit from 'graphql-depth-limit';
1448
+ import { createComplexityLimitRule } from 'graphql-validation-complexity';
1449
+
1450
+ new ApolloServer({
1451
+ validationRules: [
1452
+ depthLimit(7),
1453
+ createComplexityLimitRule(1000),
1454
+ ],
1455
+ });
1456
+ \`\`\``
1457
+ }
1082
1458
  ]
1083
1459
  };
1084
1460
  var SUPPORTED_STACKS = Object.keys(PACKS);
@@ -1095,7 +1471,13 @@ function autoDetectStacks(deps) {
1095
1471
  ["express", ["express"]],
1096
1472
  ["fastify", ["fastify"]],
1097
1473
  ["prisma", ["@prisma/client", "prisma"]],
1098
- ["drizzle", ["drizzle-orm"]]
1474
+ ["drizzle", ["drizzle-orm"]],
1475
+ ["zustand", ["zustand"]],
1476
+ ["redux", ["@reduxjs/toolkit", "redux"]],
1477
+ ["reactquery", ["@tanstack/react-query", "react-query"]],
1478
+ ["trpc", ["@trpc/server", "@trpc/client"]],
1479
+ ["mongoose", ["mongoose"]],
1480
+ ["graphql", ["@apollo/client", "@apollo/server", "apollo-server", "graphql"]]
1099
1481
  ];
1100
1482
  for (const [stack, signals] of stackDetectors) {
1101
1483
  if (signals.some((s) => s in deps)) detected.push(stack);
@@ -5183,7 +5565,7 @@ function runCommand(cmd, args, cwd) {
5183
5565
 
5184
5566
  // src/index.ts
5185
5567
  var program = new Command39();
5186
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.7.0");
5568
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.7.1");
5187
5569
  registerInit(program);
5188
5570
  registerMcp(program);
5189
5571
  registerBriefing(program);