@kaddo/cli 3.47.0 → 3.48.0

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 (2) hide show
  1. package/dist/index.js +602 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -239,6 +239,552 @@ async function runInit() {
239
239
  outro2("Kaddo initialized.");
240
240
  }
241
241
 
242
+ // src/core/scan-signals.ts
243
+ function emptySignals() {
244
+ return {
245
+ auth: [],
246
+ payments: [],
247
+ webhooks: [],
248
+ storage: [],
249
+ background_jobs: [],
250
+ email: [],
251
+ database: [],
252
+ migrations: [],
253
+ api_routes: [],
254
+ tests: [],
255
+ security: [],
256
+ infrastructure: [],
257
+ external_integrations: [],
258
+ environment: []
259
+ };
260
+ }
261
+ function sig(type, label, confidence, evidence, extra) {
262
+ return { type, label, confidence, evidence, ...extra };
263
+ }
264
+ function readDeps(dir) {
265
+ const pkgPath = join(dir, "package.json");
266
+ if (!exists(pkgPath)) return {};
267
+ try {
268
+ const pkg = JSON.parse(readFile(pkgPath));
269
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
270
+ } catch {
271
+ return {};
272
+ }
273
+ }
274
+ function findFilesMatching(dir, codeDirs, pattern, maxDepth = 3) {
275
+ const found = [];
276
+ const walk = (base, rel, depth) => {
277
+ if (depth > maxDepth) return;
278
+ try {
279
+ const entries = readDir(join(base, rel));
280
+ for (const e of entries) {
281
+ if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build" || e === ".next") continue;
282
+ const fullRel = rel ? `${rel}/${e}` : e;
283
+ if (isDir(join(base, fullRel))) {
284
+ walk(base, fullRel, depth + 1);
285
+ } else if (pattern.test(e)) {
286
+ found.push(fullRel);
287
+ }
288
+ }
289
+ } catch {
290
+ }
291
+ };
292
+ for (const cd of codeDirs) {
293
+ walk(dir, cd, 0);
294
+ }
295
+ return found;
296
+ }
297
+ function findPathsContaining(dir, codeDirs, keywords, maxDepth = 3) {
298
+ const found = [];
299
+ const walk = (base, rel, depth) => {
300
+ if (depth > maxDepth) return;
301
+ try {
302
+ const entries = readDir(join(base, rel));
303
+ for (const e of entries) {
304
+ if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build" || e === ".next") continue;
305
+ const fullRel = rel ? `${rel}/${e}` : e;
306
+ const lower = e.toLowerCase();
307
+ if (keywords.some((k) => lower.includes(k))) {
308
+ found.push(fullRel);
309
+ }
310
+ if (isDir(join(base, fullRel))) {
311
+ walk(base, fullRel, depth + 1);
312
+ }
313
+ }
314
+ } catch {
315
+ }
316
+ };
317
+ for (const cd of codeDirs) {
318
+ walk(dir, cd, 0);
319
+ }
320
+ return found;
321
+ }
322
+ function detectAuth(dir, deps, codeDirs, signals) {
323
+ const authDeps = [
324
+ ["next-auth", "NextAuth"],
325
+ ["@auth/core", "Auth.js"],
326
+ ["@supabase/supabase-js", "Supabase Auth"],
327
+ ["@supabase/auth-helpers-nextjs", "Supabase Auth"],
328
+ ["@supabase/ssr", "Supabase Auth"],
329
+ ["@clerk/nextjs", "Clerk"],
330
+ ["@clerk/clerk-sdk-node", "Clerk"],
331
+ ["firebase", "Firebase Auth"],
332
+ ["firebase-admin", "Firebase Auth"],
333
+ ["passport", "Passport.js"],
334
+ ["jsonwebtoken", "JWT"],
335
+ ["jose", "JWT (jose)"],
336
+ ["bcrypt", "Password hashing (bcrypt)"],
337
+ ["bcryptjs", "Password hashing (bcryptjs)"],
338
+ ["@aws-amplify/auth", "AWS Amplify Auth"]
339
+ ];
340
+ const seen = /* @__PURE__ */ new Set();
341
+ for (const [dep, label] of authDeps) {
342
+ if (dep in deps && !seen.has(label)) {
343
+ seen.add(label);
344
+ signals.auth.push(sig("auth", label, "high", ["package.json"]));
345
+ }
346
+ }
347
+ const authPaths = findPathsContaining(dir, codeDirs, ["auth", "login", "register", "signin", "signup", "session"], 3);
348
+ if (authPaths.length > 0 && signals.auth.length === 0) {
349
+ signals.auth.push(sig("auth", "Auth-related paths detected", "medium", authPaths.slice(0, 5)));
350
+ }
351
+ }
352
+ function detectPayments(dir, deps, codeDirs, signals) {
353
+ const payDeps = [
354
+ ["stripe", "Stripe"],
355
+ ["@stripe/stripe-js", "Stripe"],
356
+ ["mercadopago", "Mercado Pago"],
357
+ ["@paypal/checkout-server-sdk", "PayPal"],
358
+ ["paypal-rest-sdk", "PayPal"],
359
+ ["wompi", "Wompi"],
360
+ ["@adyen/api-library", "Adyen"],
361
+ ["braintree", "Braintree"],
362
+ ["square", "Square"]
363
+ ];
364
+ const seen = /* @__PURE__ */ new Set();
365
+ for (const [dep, label] of payDeps) {
366
+ if (dep in deps && !seen.has(label)) {
367
+ seen.add(label);
368
+ signals.payments.push(sig("payments", label, "high", ["package.json"]));
369
+ }
370
+ }
371
+ const payPaths = findPathsContaining(dir, codeDirs, ["payment", "checkout", "billing", "invoice", "subscription", "pricing"], 3);
372
+ if (payPaths.length > 0 && signals.payments.length === 0) {
373
+ signals.payments.push(sig("payments", "Payment-related paths detected", "medium", payPaths.slice(0, 5)));
374
+ }
375
+ }
376
+ function detectWebhooks(dir, codeDirs, signals) {
377
+ const whPaths = findPathsContaining(dir, codeDirs, ["webhook", "callback"], 3);
378
+ if (whPaths.length > 0) {
379
+ signals.webhooks.push(sig("webhooks", "Webhook routes detected", "high", whPaths.slice(0, 5), {
380
+ recommended_review: "Verify idempotency and signature validation."
381
+ }));
382
+ }
383
+ }
384
+ function detectStorage(dir, deps, codeDirs, signals) {
385
+ const storageDeps = [
386
+ ["@aws-sdk/client-s3", "AWS S3"],
387
+ ["aws-sdk", "AWS SDK (includes S3)"],
388
+ ["@supabase/storage-js", "Supabase Storage"],
389
+ ["cloudinary", "Cloudinary"],
390
+ ["multer", "File upload (Multer)"],
391
+ ["formidable", "File upload (Formidable)"],
392
+ ["@google-cloud/storage", "Google Cloud Storage"],
393
+ ["@azure/storage-blob", "Azure Blob Storage"]
394
+ ];
395
+ const seen = /* @__PURE__ */ new Set();
396
+ for (const [dep, label] of storageDeps) {
397
+ if (dep in deps && !seen.has(label)) {
398
+ seen.add(label);
399
+ signals.storage.push(sig("storage", label, "high", ["package.json"]));
400
+ }
401
+ }
402
+ const storagePaths = findPathsContaining(dir, codeDirs, ["upload", "storage", "bucket"], 3);
403
+ if (storagePaths.length > 0 && signals.storage.length === 0) {
404
+ signals.storage.push(sig("storage", "Storage-related paths detected", "medium", storagePaths.slice(0, 5)));
405
+ }
406
+ }
407
+ function detectBackgroundJobs(dir, deps, codeDirs, signals) {
408
+ const jobDeps = [
409
+ ["bullmq", "BullMQ"],
410
+ ["bull", "Bull"],
411
+ ["agenda", "Agenda"],
412
+ ["@aws-sdk/client-sqs", "AWS SQS"],
413
+ ["amqplib", "RabbitMQ (AMQP)"],
414
+ ["bee-queue", "Bee-Queue"],
415
+ ["node-cron", "Node-cron"],
416
+ ["cron", "Cron"]
417
+ ];
418
+ for (const [dep, label] of jobDeps) {
419
+ if (dep in deps) {
420
+ signals.background_jobs.push(sig("background_jobs", label, "high", ["package.json"]));
421
+ }
422
+ }
423
+ if (isDir(join(dir, "supabase", "functions"))) {
424
+ signals.background_jobs.push(sig("background_jobs", "Supabase Edge Functions", "high", ["supabase/functions/"]));
425
+ }
426
+ const cronMigrations = findFilesMatching(dir, ["supabase/migrations"], /pg_cron|cron/i, 2);
427
+ if (cronMigrations.length > 0) {
428
+ signals.background_jobs.push(sig("background_jobs", "pg_cron scheduled jobs", "high", cronMigrations.slice(0, 3)));
429
+ }
430
+ if (exists(join(dir, "vercel.json"))) {
431
+ try {
432
+ const vc = JSON.parse(readFile(join(dir, "vercel.json")));
433
+ if (vc.crons && Array.isArray(vc.crons) && vc.crons.length > 0) {
434
+ signals.background_jobs.push(sig("background_jobs", "Vercel Cron Jobs", "high", ["vercel.json"]));
435
+ }
436
+ } catch {
437
+ }
438
+ }
439
+ const jobPaths = findPathsContaining(dir, codeDirs, ["cron", "queue", "worker", "job"], 3);
440
+ if (jobPaths.length > 0 && signals.background_jobs.length === 0) {
441
+ signals.background_jobs.push(sig("background_jobs", "Job-related paths detected", "medium", jobPaths.slice(0, 5)));
442
+ }
443
+ }
444
+ function detectEmail(dir, deps, signals) {
445
+ const emailDeps = [
446
+ ["resend", "Resend"],
447
+ ["@sendgrid/mail", "SendGrid"],
448
+ ["@sendinblue/client", "Brevo (Sendinblue)"],
449
+ ["@getbrevo/brevo", "Brevo"],
450
+ ["nodemailer", "Nodemailer"],
451
+ ["@aws-sdk/client-ses", "AWS SES"],
452
+ ["postmark", "Postmark"],
453
+ ["mailgun-js", "Mailgun"],
454
+ ["@mailchimp/mailchimp_transactional", "Mailchimp Transactional"]
455
+ ];
456
+ for (const [dep, label] of emailDeps) {
457
+ if (dep in deps) {
458
+ signals.email.push(sig("email", label, "high", ["package.json"]));
459
+ }
460
+ }
461
+ }
462
+ function detectDatabase(dir, deps, signals) {
463
+ const dbDeps = [
464
+ ["prisma", "Prisma"],
465
+ ["@prisma/client", "Prisma"],
466
+ ["drizzle-orm", "Drizzle ORM"],
467
+ ["typeorm", "TypeORM"],
468
+ ["sequelize", "Sequelize"],
469
+ ["knex", "Knex.js"],
470
+ ["@supabase/supabase-js", "Supabase (Postgres)"],
471
+ ["pg", "PostgreSQL (pg)"],
472
+ ["mysql2", "MySQL"],
473
+ ["mongodb", "MongoDB"],
474
+ ["mongoose", "Mongoose (MongoDB)"],
475
+ ["better-sqlite3", "SQLite"],
476
+ ["@neondatabase/serverless", "Neon Postgres"],
477
+ ["@planetscale/database", "PlanetScale"],
478
+ ["@libsql/client", "Turso (LibSQL)"]
479
+ ];
480
+ const seen = /* @__PURE__ */ new Set();
481
+ for (const [dep, label] of dbDeps) {
482
+ if (dep in deps && !seen.has(label)) {
483
+ seen.add(label);
484
+ signals.database.push(sig("database", label, "high", ["package.json"]));
485
+ }
486
+ }
487
+ }
488
+ function detectMigrations(dir, migrationDirs, signals) {
489
+ for (const md of migrationDirs) {
490
+ const fullPath = join(dir, md);
491
+ if (!isDir(fullPath)) continue;
492
+ let count = 0;
493
+ try {
494
+ count = readDir(fullPath).filter((f) => !f.startsWith(".")).length;
495
+ } catch {
496
+ }
497
+ signals.migrations.push(sig("migrations", `${md} (${count} file${count === 1 ? "" : "s"})`, "high", [md]));
498
+ }
499
+ }
500
+ function detectApiRoutes(dir, codeDirs, signals) {
501
+ for (const cd of codeDirs) {
502
+ const apiDir = join(dir, cd, "app", "api");
503
+ if (isDir(apiDir)) {
504
+ signals.api_routes.push(sig("api_routes", "Next.js API routes", "high", [`${cd}/app/api`]));
505
+ }
506
+ const pagesApi = join(dir, cd, "pages", "api");
507
+ if (isDir(pagesApi)) {
508
+ signals.api_routes.push(sig("api_routes", "Next.js Pages API routes", "high", [`${cd}/pages/api`]));
509
+ }
510
+ }
511
+ if (isDir(join(dir, "app", "api"))) {
512
+ signals.api_routes.push(sig("api_routes", "Next.js API routes", "high", ["app/api"]));
513
+ }
514
+ if (isDir(join(dir, "pages", "api"))) {
515
+ signals.api_routes.push(sig("api_routes", "Next.js Pages API routes", "high", ["pages/api"]));
516
+ }
517
+ if (isDir(join(dir, "routes"))) {
518
+ signals.api_routes.push(sig("api_routes", "Routes directory", "medium", ["routes/"]));
519
+ }
520
+ }
521
+ function detectTests(dir, testDirs, deps, signals) {
522
+ const testFrameworks = [
523
+ ["vitest", "Vitest"],
524
+ ["jest", "Jest"],
525
+ ["@playwright/test", "Playwright"],
526
+ ["cypress", "Cypress"],
527
+ ["@testing-library/react", "Testing Library"],
528
+ ["mocha", "Mocha"],
529
+ ["ava", "AVA"]
530
+ ];
531
+ for (const [dep, label] of testFrameworks) {
532
+ if (dep in deps) {
533
+ signals.tests.push(sig("tests", label, "high", ["package.json"]));
534
+ }
535
+ }
536
+ if (testDirs.length > 0) {
537
+ signals.tests.push(sig("tests", `Test directories: ${testDirs.join(", ")}`, "high", testDirs));
538
+ }
539
+ if (testDirs.length === 0 && signals.tests.length === 0) {
540
+ signals.tests.push(sig("tests", "No test directory detected", "high", [], {
541
+ recommended_review: "Confirm how this project is validated before production changes."
542
+ }));
543
+ }
544
+ }
545
+ function detectSecurity(dir, deps, codeDirs, envVarNames, signals) {
546
+ const secDeps = [
547
+ ["helmet", "Helmet (security headers)"],
548
+ ["cors", "CORS middleware"],
549
+ ["csurf", "CSRF protection"],
550
+ ["express-rate-limit", "Rate limiting"],
551
+ ["@upstash/ratelimit", "Upstash Rate Limiting"]
552
+ ];
553
+ for (const [dep, label] of secDeps) {
554
+ if (dep in deps) {
555
+ signals.security.push(sig("security", label, "high", ["package.json"]));
556
+ }
557
+ }
558
+ const middlewarePaths = findPathsContaining(dir, codeDirs, ["middleware"], 2);
559
+ if (middlewarePaths.length > 0) {
560
+ signals.security.push(sig("security", "Middleware detected", "medium", middlewarePaths.slice(0, 5)));
561
+ }
562
+ if (isFile(join(dir, "middleware.ts")) || isFile(join(dir, "middleware.js")) || isFile(join(dir, "src", "middleware.ts")) || isFile(join(dir, "src", "middleware.js"))) {
563
+ const evidence = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"].filter((f) => isFile(join(dir, f)));
564
+ if (evidence.length > 0) {
565
+ signals.security.push(sig("security", "Next.js middleware", "high", evidence));
566
+ }
567
+ }
568
+ const rlsMigrations = findFilesMatching(dir, ["supabase/migrations"], /rls|policies|policy/i, 2);
569
+ if (rlsMigrations.length > 0) {
570
+ signals.security.push(sig("security", "RLS / row-level security policies", "high", rlsMigrations.slice(0, 3)));
571
+ }
572
+ const secretVars = envVarNames.filter((v) => /secret|token|key|password|credential/i.test(v));
573
+ if (secretVars.length > 0) {
574
+ signals.security.push(sig("security", "Secret-bearing environment variables", "medium", secretVars.slice(0, 10), {
575
+ recommended_review: "Confirm secret rotation and endpoint protection strategy."
576
+ }));
577
+ }
578
+ }
579
+ function detectInfrastructure(dir, infraFiles, signals) {
580
+ for (const f of infraFiles) {
581
+ const label = inferInfraLabel(f);
582
+ signals.infrastructure.push(sig("infrastructure", label, "high", [f]));
583
+ }
584
+ }
585
+ function inferInfraLabel(f) {
586
+ const lower = f.toLowerCase();
587
+ if (lower.includes("docker-compose")) return "Docker Compose";
588
+ if (lower === "dockerfile") return "Dockerfile";
589
+ if (lower.includes("serverless")) return "Serverless Framework";
590
+ if (lower.includes("amplify")) return "AWS Amplify";
591
+ if (lower.includes("terraform")) return "Terraform";
592
+ if (lower.includes("k8s") || lower.includes("kubernetes")) return "Kubernetes";
593
+ if (lower.includes("helm")) return "Helm";
594
+ if (lower.includes(".github/workflows")) return "GitHub Actions";
595
+ if (lower.includes("vercel")) return "Vercel";
596
+ if (lower.includes("netlify")) return "Netlify";
597
+ return f;
598
+ }
599
+ function detectExternalIntegrations(deps, signals) {
600
+ const integrations = [
601
+ ["@supabase/supabase-js", "Supabase"],
602
+ ["@aws-sdk/client-s3", "AWS"],
603
+ ["aws-sdk", "AWS"],
604
+ ["@google-cloud/storage", "Google Cloud"],
605
+ ["@azure/storage-blob", "Azure"],
606
+ ["twilio", "Twilio"],
607
+ ["@slack/web-api", "Slack"],
608
+ ["@slack/bolt", "Slack"],
609
+ ["@notionhq/client", "Notion"],
610
+ ["@octokit/rest", "GitHub API"],
611
+ ["@sentry/node", "Sentry"],
612
+ ["@sentry/nextjs", "Sentry"],
613
+ ["@datadog/datadog-api-client", "Datadog"],
614
+ ["newrelic", "New Relic"],
615
+ ["@segment/analytics-node", "Segment"],
616
+ ["posthog-node", "PostHog"],
617
+ ["mixpanel", "Mixpanel"],
618
+ ["@amplitude/node", "Amplitude"],
619
+ ["firebase-admin", "Firebase"],
620
+ ["@google/generative-ai", "Google AI"],
621
+ ["openai", "OpenAI"],
622
+ ["@anthropic-ai/sdk", "Anthropic"],
623
+ ["@vercel/analytics", "Vercel Analytics"],
624
+ ["@vercel/kv", "Vercel KV"],
625
+ ["@upstash/redis", "Upstash Redis"],
626
+ ["ioredis", "Redis"]
627
+ ];
628
+ const seen = /* @__PURE__ */ new Set();
629
+ for (const [dep, label] of integrations) {
630
+ if (dep in deps && !seen.has(label)) {
631
+ seen.add(label);
632
+ signals.external_integrations.push(sig("external_integrations", label, "high", ["package.json"]));
633
+ }
634
+ }
635
+ }
636
+ function detectEnvironment(dir) {
637
+ const envFiles = [".env.example", ".env.local.example", ".env.template", ".env.sample"];
638
+ const varNames = /* @__PURE__ */ new Set();
639
+ for (const ef of envFiles) {
640
+ const p2 = join(dir, ef);
641
+ if (!exists(p2)) continue;
642
+ try {
643
+ const lines = readFile(p2).split(/\r?\n/);
644
+ for (const line of lines) {
645
+ const m = line.match(/^([A-Z][A-Z0-9_]+)\s*=/);
646
+ if (m) varNames.add(m[1]);
647
+ }
648
+ } catch {
649
+ }
650
+ }
651
+ const envPath = join(dir, ".env");
652
+ if (exists(envPath)) {
653
+ try {
654
+ const lines = readFile(envPath).split(/\r?\n/);
655
+ for (const line of lines) {
656
+ const m = line.match(/^([A-Z][A-Z0-9_]+)\s*=/);
657
+ if (m) varNames.add(m[1]);
658
+ }
659
+ } catch {
660
+ }
661
+ }
662
+ return [...varNames].sort();
663
+ }
664
+ function buildScanSignals(dir, codeDirs, migrationDirs, testDirs, infraFiles) {
665
+ const signals = emptySignals();
666
+ const deps = readDeps(dir);
667
+ const envVarNames = detectEnvironment(dir);
668
+ detectAuth(dir, deps, codeDirs, signals);
669
+ detectPayments(dir, deps, codeDirs, signals);
670
+ detectWebhooks(dir, codeDirs, signals);
671
+ detectStorage(dir, deps, codeDirs, signals);
672
+ detectBackgroundJobs(dir, deps, codeDirs, signals);
673
+ detectEmail(dir, deps, signals);
674
+ detectDatabase(dir, deps, signals);
675
+ detectMigrations(dir, migrationDirs, signals);
676
+ detectApiRoutes(dir, codeDirs, signals);
677
+ detectTests(dir, testDirs, deps, signals);
678
+ detectSecurity(dir, deps, codeDirs, envVarNames, signals);
679
+ detectInfrastructure(dir, infraFiles, signals);
680
+ detectExternalIntegrations(deps, signals);
681
+ if (envVarNames.length > 0) {
682
+ signals.environment.push(sig("environment", `${envVarNames.length} environment variable(s) detected`, "high", envVarNames.slice(0, 20)));
683
+ }
684
+ return signals;
685
+ }
686
+ function signalCount(signals) {
687
+ return Object.values(signals).reduce((sum, arr) => sum + arr.length, 0);
688
+ }
689
+ function hasWarnings(signals) {
690
+ return signals.tests.some((s) => s.label.includes("No test directory"));
691
+ }
692
+ function renderSignalsCompact(signals) {
693
+ const lines = [];
694
+ const CATEGORIES = [
695
+ ["auth", "Auth"],
696
+ ["payments", "Payments"],
697
+ ["webhooks", "Webhooks"],
698
+ ["storage", "Storage"],
699
+ ["background_jobs", "Background jobs"],
700
+ ["email", "Email"],
701
+ ["database", "Database"],
702
+ ["migrations", "Migrations"],
703
+ ["api_routes", "API routes"],
704
+ ["tests", "Tests"],
705
+ ["security", "Security"],
706
+ ["infrastructure", "Infrastructure"],
707
+ ["external_integrations", "External integrations"],
708
+ ["environment", "Environment"]
709
+ ];
710
+ for (const [key, label] of CATEGORIES) {
711
+ const arr = signals[key];
712
+ if (arr.length === 0) continue;
713
+ if (arr.length === 1) {
714
+ lines.push(`- ${label}: ${arr[0].label}`);
715
+ } else {
716
+ lines.push(`- ${label}: ${arr.length}`);
717
+ }
718
+ }
719
+ return lines.length > 0 ? lines.join("\n") + "\n" : "No signals detected.\n";
720
+ }
721
+ function renderSignalsInventory(signals) {
722
+ const parts = [];
723
+ const CATEGORIES = [
724
+ ["auth", "Auth"],
725
+ ["payments", "Payments"],
726
+ ["webhooks", "Webhooks"],
727
+ ["storage", "Storage"],
728
+ ["background_jobs", "Background Jobs"],
729
+ ["email", "Email / Notifications"],
730
+ ["database", "Database"],
731
+ ["migrations", "Migrations"],
732
+ ["api_routes", "API Routes"],
733
+ ["tests", "Tests"],
734
+ ["security", "Security"],
735
+ ["infrastructure", "Infrastructure"],
736
+ ["external_integrations", "External Integrations"],
737
+ ["environment", "Environment"]
738
+ ];
739
+ for (const [key, label] of CATEGORIES) {
740
+ const arr = signals[key];
741
+ if (arr.length === 0) continue;
742
+ parts.push(`### ${label}
743
+ `);
744
+ for (const s of arr) {
745
+ parts.push(`- ${s.label} \u2014 confidence: ${s.confidence}`);
746
+ if (s.evidence.length > 0) {
747
+ parts.push(` - Evidence: ${s.evidence.map((e) => "`" + e + "`").join(", ")}`);
748
+ }
749
+ if (s.recommended_review) {
750
+ parts.push(` - Review: ${s.recommended_review}`);
751
+ }
752
+ }
753
+ parts.push("");
754
+ }
755
+ return parts.join("\n");
756
+ }
757
+ function renderSignalsConsole(signals) {
758
+ const lines = [];
759
+ const CATEGORIES = [
760
+ ["auth", "Auth"],
761
+ ["payments", "Payments"],
762
+ ["webhooks", "Webhooks"],
763
+ ["storage", "Storage"],
764
+ ["background_jobs", "Background jobs"],
765
+ ["email", "Email"],
766
+ ["database", "Database"],
767
+ ["migrations", "Migrations"],
768
+ ["api_routes", "API routes"],
769
+ ["tests", "Tests"],
770
+ ["security", "Security"],
771
+ ["infrastructure", "Infrastructure"],
772
+ ["external_integrations", "Integrations"],
773
+ ["environment", "Environment"]
774
+ ];
775
+ for (const [key, label] of CATEGORIES) {
776
+ const arr = signals[key];
777
+ if (arr.length === 0) continue;
778
+ if (arr.length === 1) {
779
+ const warning = arr[0].label.toLowerCase().includes("no test") ? " \u26A0" : "";
780
+ lines.push(` ${label}: ${arr[0].label}${warning}`);
781
+ } else {
782
+ lines.push(` ${label}: ${arr.map((s) => s.label).join(", ")}`);
783
+ }
784
+ }
785
+ return lines;
786
+ }
787
+
242
788
  // src/services/scanner.ts
243
789
  function detectLanguage(dir) {
244
790
  if (exists(join(dir, "tsconfig.json"))) return "typescript";
@@ -391,6 +937,7 @@ function scan(dir) {
391
937
  const testDirs = detectTestDirs(dir);
392
938
  const hasGit2 = isDir(join(dir, ".git"));
393
939
  const suggestedDomains = suggestDomains(dir, codeDirs);
940
+ const signals = buildScanSignals(dir, codeDirs, migrationDirs, testDirs, infraFiles);
394
941
  return {
395
942
  language,
396
943
  framework,
@@ -401,7 +948,8 @@ function scan(dir) {
401
948
  infraFiles,
402
949
  testDirs,
403
950
  hasGit: hasGit2,
404
- suggestedDomains
951
+ suggestedDomains,
952
+ signals
405
953
  };
406
954
  }
407
955
 
@@ -465,6 +1013,7 @@ function buildBaseline(result, context, now = /* @__PURE__ */ new Date()) {
465
1013
  infrastructureFiles: result.infraFiles,
466
1014
  testDirectories: result.testDirs
467
1015
  },
1016
+ signals: result.signals,
468
1017
  suggestions: {
469
1018
  possibleDomains: result.suggestedDomains.map((d) => d.replace(/^#\s*/, "").trim()).filter(Boolean),
470
1019
  openQuestions: buildOpenQuestions(result, context)
@@ -509,6 +1058,10 @@ function renderInventory(baseline) {
509
1058
  parts.push(section("Contracts", detected.contractFiles));
510
1059
  parts.push(section("Infrastructure", detected.infrastructureFiles));
511
1060
  parts.push(section("Tests", detected.testDirectories.map((d) => `${d}/`)));
1061
+ if (signalCount(baseline.signals) > 0) {
1062
+ parts.push("## Detected Signals\n");
1063
+ parts.push(renderSignalsInventory(baseline.signals));
1064
+ }
512
1065
  parts.push(section("Possible Domains", suggestions.possibleDomains));
513
1066
  parts.push(section("Open Questions", suggestions.openQuestions));
514
1067
  return parts.join("\n");
@@ -766,7 +1319,15 @@ function printResult(result) {
766
1319
  console.log("No domains detected automatically.");
767
1320
  console.log("You can add them manually in .kaddo/config.yml under project.domains.");
768
1321
  }
769
- console.log("");
1322
+ const signalLines = renderSignalsConsole(result.signals);
1323
+ if (signalLines.length > 0) {
1324
+ console.log("Signals:");
1325
+ for (const line of signalLines) console.log(line);
1326
+ console.log("");
1327
+ } else {
1328
+ console.log("Signals: none detected");
1329
+ console.log("");
1330
+ }
770
1331
  }
771
1332
  function updateConfig(dir, result) {
772
1333
  const configPath = join(dir, ".kaddo", "config.yml");
@@ -4641,8 +5202,8 @@ function parseBlock(block) {
4641
5202
  }
4642
5203
  function decisionCandidatesFromSignals(signals) {
4643
5204
  const out = [];
4644
- for (const sig of signals) {
4645
- const m = sig.match(/decision candidate\s*:?\s*(.+)$/i);
5205
+ for (const sig2 of signals) {
5206
+ const m = sig2.match(/decision candidate\s*:?\s*(.+)$/i);
4646
5207
  if (!m) continue;
4647
5208
  const rest = m[1].trim();
4648
5209
  const token = rest.match(/\b[A-Z][A-Z0-9_]{2,}\b/);
@@ -8696,11 +9257,11 @@ var enableKaddo = {
8696
9257
  var scanRepository = {
8697
9258
  id: "scan-repository",
8698
9259
  label: "Scan repository",
8699
- evaluate: (ctx) => ({
8700
- status: ctx.hasScan ? "done" : ctx.hasConfig ? "pending" : "pending",
8701
- evidence: ctx.hasScan ? [".kaddo/scan.json"] : void 0,
8702
- command: ctx.hasScan ? void 0 : "kaddo scan"
8703
- })
9260
+ evaluate: (ctx) => {
9261
+ if (!ctx.hasScan) return { status: "pending", command: "kaddo scan" };
9262
+ if (ctx.hasScanWarnings) return { status: "warning", evidence: [".kaddo/scan.json"], reason: "Scan has warnings (e.g. no test directory detected).", command: "kaddo scan" };
9263
+ return { status: "done", evidence: [".kaddo/scan.json"] };
9264
+ }
8704
9265
  };
8705
9266
  var defineBusiness = {
8706
9267
  id: "define-business",
@@ -8987,9 +9548,18 @@ function buildRouteContext(dir) {
8987
9548
  totalAdrs: td.adrs,
8988
9549
  adaptersInstalled: adapters.length,
8989
9550
  hasGuardHistory: exists(join(dir, ".kaddo", "history", "guard-runs.jsonl")),
9551
+ hasScanWarnings: hasScan ? loadScanWarnings(dir) : false,
8990
9552
  nextStepId: mapNextStepId(nextStep.id)
8991
9553
  };
8992
9554
  }
9555
+ function loadScanWarnings(dir) {
9556
+ try {
9557
+ const parsed = JSON.parse(readFile(join(dir, ".kaddo", "scan.json")));
9558
+ return parsed.signals ? hasWarnings(parsed.signals) : false;
9559
+ } catch {
9560
+ return false;
9561
+ }
9562
+ }
8993
9563
  function mapNextStepId(id) {
8994
9564
  const MAP = {
8995
9565
  init: "enable-kaddo",
@@ -9136,6 +9706,16 @@ function loadScan(dir) {
9136
9706
  return null;
9137
9707
  }
9138
9708
  }
9709
+ function loadScanSignals(dir) {
9710
+ const scanPath = join(dir, ".kaddo", "scan.json");
9711
+ if (!exists(scanPath)) return null;
9712
+ try {
9713
+ const parsed = JSON.parse(readFile(scanPath));
9714
+ return parsed.signals ?? null;
9715
+ } catch {
9716
+ return null;
9717
+ }
9718
+ }
9139
9719
  function hasAgents(dir) {
9140
9720
  const agentsDir = join(dir, ARCH_DIR4, "agents");
9141
9721
  if (!exists(agentsDir)) return false;
@@ -9307,7 +9887,8 @@ function buildProjectExplanation(dir) {
9307
9887
  },
9308
9888
  installedAssets: installedAssetsSummary(dir),
9309
9889
  roadmapQuality: buildRoadmapQuality(dir),
9310
- projectRoute: buildProjectRoute(dir)
9890
+ projectRoute: buildProjectRoute(dir),
9891
+ scanSignals: loadScanSignals(dir)
9311
9892
  };
9312
9893
  }
9313
9894
  function stateLabel(state) {
@@ -9352,6 +9933,10 @@ function renderExplanationHuman(exp) {
9352
9933
  lines.push(`- Infrastructure: ${exp.stack.infrastructureFiles.join(", ")}`);
9353
9934
  lines.push("");
9354
9935
  }
9936
+ if (exp.scanSignals && signalCount(exp.scanSignals) > 0) {
9937
+ lines.push("## Scan Signals");
9938
+ lines.push(renderSignalsCompact(exp.scanSignals));
9939
+ }
9355
9940
  const ls = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
9356
9941
  lines.push("## Knowledge Status");
9357
9942
  lines.push(`- Inventory: ${exp.knowledge.hasInventory ? "available" : "missing"}`);
@@ -10017,6 +10602,7 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
10017
10602
  graph: loadGraphSummary(dir),
10018
10603
  graphHints: loadGraphHints(dir),
10019
10604
  skills: discoverInstalledSkills(dir).map((s) => s.id),
10605
+ scanSignals: scanJson?.signals ?? null,
10020
10606
  projectRoute: buildProjectRoute(dir),
10021
10607
  mappedModules,
10022
10608
  missing,
@@ -10131,6 +10717,10 @@ function renderContextPack(pack) {
10131
10717
  } else {
10132
10718
  parts.push("Scan baseline missing. Run `kaddo scan` for better context.\n");
10133
10719
  }
10720
+ if (pack.scanSignals && signalCount(pack.scanSignals) > 0) {
10721
+ parts.push("## Scan Signals\n");
10722
+ parts.push(renderSignalsCompact(pack.scanSignals));
10723
+ }
10134
10724
  parts.push("## Current Knowledge\n");
10135
10725
  parts.push((knowledge.summary || "No project knowledge summary found yet.") + "\n");
10136
10726
  parts.push("## Roadmap Status\n");
@@ -11502,7 +12092,7 @@ function findWorkItemArtifacts(dir) {
11502
12092
  function artifactsMissingOwnership(artifacts) {
11503
12093
  return artifacts.filter((a) => !a.hasOwnership);
11504
12094
  }
11505
- function loadScanSignals(dir) {
12095
+ function loadScanSignals2(dir) {
11506
12096
  const scanPath = join(dir, SCAN_PATH);
11507
12097
  if (!exists(scanPath)) return null;
11508
12098
  try {
@@ -11680,7 +12270,7 @@ async function runOwnersSuggest() {
11680
12270
  }))
11681
12271
  });
11682
12272
  const artifact = missing.find((a) => a.relPath === relPath);
11683
- const signals = loadScanSignals(dir);
12273
+ const signals = loadScanSignals2(dir);
11684
12274
  if (!signals) {
11685
12275
  log2.warn("No .kaddo/scan.json found \u2014 run `kaddo scan` for suggestions. You can enter globs manually.");
11686
12276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.47.0",
3
+ "version": "3.48.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {