gitchain-sol 0.1.0 → 0.2.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/gitchain.mjs +451 -161
  2. package/package.json +1 -1
package/dist/gitchain.mjs CHANGED
@@ -99,12 +99,12 @@ GitChain initialized for "${projectName}"
99
99
  }
100
100
 
101
101
  // src/commands/publish.ts
102
- import chalk2 from "chalk";
102
+ import chalk3 from "chalk";
103
103
  import { resolve as resolve2 } from "node:path";
104
104
  import { existsSync as existsSync2 } from "node:fs";
105
105
  import { createHash as createHash3 } from "node:crypto";
106
106
  import { execSync } from "node:child_process";
107
- import { SystemProgram } from "@solana/web3.js";
107
+ import { PublicKey as PublicKey2, SystemProgram } from "@solana/web3.js";
108
108
 
109
109
  // ../fingerprint/dist/hash.js
110
110
  import { createHash } from "node:crypto";
@@ -321,12 +321,42 @@ var IDL = {
321
321
  { name: "parent", type: "pubkey" },
322
322
  { name: "origin", type: "pubkey" }
323
323
  ]
324
+ },
325
+ {
326
+ name: "register_file_hashes",
327
+ discriminator: [13, 82, 71, 30, 119, 55, 218, 3],
328
+ accounts: [
329
+ { name: "file_hash_registry", writable: true },
330
+ { name: "project_record" },
331
+ { name: "creator", writable: true, signer: true },
332
+ { name: "system_program", address: "11111111111111111111111111111111" }
333
+ ],
334
+ args: [
335
+ { name: "file_hashes", type: { vec: { array: ["u8", 32] } } }
336
+ ]
337
+ },
338
+ {
339
+ name: "update_file_hashes",
340
+ discriminator: [201, 150, 255, 217, 36, 176, 27, 116],
341
+ accounts: [
342
+ { name: "file_hash_registry", writable: true },
343
+ { name: "project_record" },
344
+ { name: "creator", writable: true, signer: true },
345
+ { name: "system_program", address: "11111111111111111111111111111111" }
346
+ ],
347
+ args: [
348
+ { name: "file_hashes", type: { vec: { array: ["u8", 32] } } }
349
+ ]
324
350
  }
325
351
  ],
326
352
  accounts: [
327
353
  {
328
354
  name: "ProjectRecord",
329
355
  discriminator: [93, 174, 112, 203, 231, 123, 92, 56]
356
+ },
357
+ {
358
+ name: "FileHashRegistry",
359
+ discriminator: [25, 208, 115, 130, 234, 247, 70, 166]
330
360
  }
331
361
  ],
332
362
  types: [
@@ -348,6 +378,18 @@ var IDL = {
348
378
  { name: "bump", type: "u8" }
349
379
  ]
350
380
  }
381
+ },
382
+ {
383
+ name: "FileHashRegistry",
384
+ type: {
385
+ kind: "struct",
386
+ fields: [
387
+ { name: "project", type: "pubkey" },
388
+ { name: "creator", type: "pubkey" },
389
+ { name: "hash_count", type: "u16" },
390
+ { name: "hashes", type: { vec: { array: ["u8", 32] } } }
391
+ ]
392
+ }
351
393
  }
352
394
  ]
353
395
  };
@@ -372,6 +414,12 @@ function deriveProjectPda(creatorPubkey, projectName) {
372
414
  getProgramId()
373
415
  );
374
416
  }
417
+ function deriveFileHashPda(projectRecordPda) {
418
+ return PublicKey.findProgramAddressSync(
419
+ [Buffer.from("filehashes"), projectRecordPda.toBuffer()],
420
+ getProgramId()
421
+ );
422
+ }
375
423
  function getProgram(connection, keypair) {
376
424
  const wallet = new Wallet(keypair);
377
425
  const provider = new AnchorProvider(connection, wallet, {
@@ -387,6 +435,13 @@ function hexToBytes32(hex) {
387
435
  async function fetchAllProjectsViaProgram(program2) {
388
436
  return await program2.account.projectRecord.all();
389
437
  }
438
+ async function fetchAllFileHashRegistries(program2) {
439
+ try {
440
+ return await program2.account.fileHashRegistry.all();
441
+ } catch {
442
+ return [];
443
+ }
444
+ }
390
445
  var STATUS_LABELS = {
391
446
  0: "Registered",
392
447
  1: "Verified Original",
@@ -395,38 +450,138 @@ var STATUS_LABELS = {
395
450
  4: "Provenance Stripped"
396
451
  };
397
452
 
453
+ // src/utils/lineage.ts
454
+ import chalk2 from "chalk";
455
+ function scanForLineage(localFingerprint, onChainProjects) {
456
+ let bestMatch = null;
457
+ for (const project of onChainProjects) {
458
+ const onChainFp = {
459
+ merkleRoot: project.merkleRoot,
460
+ fileHashes: {},
461
+ contentHashes: [...project.fileHashes].sort(),
462
+ fileCount: project.fileCount
463
+ };
464
+ project.fileHashes.forEach((h, i) => {
465
+ onChainFp.fileHashes[`onchain_${i}`] = h;
466
+ });
467
+ const result = compareFingerprints(onChainFp, localFingerprint);
468
+ if (result.fileOverlapPercent >= 30) {
469
+ if (!bestMatch || result.fileOverlapPercent > bestMatch.overlapPercent) {
470
+ bestMatch = {
471
+ pubkey: project.pubkey,
472
+ projectName: project.projectName,
473
+ creator: project.creator,
474
+ registeredAt: project.registeredAt,
475
+ repoUrl: project.repoUrl,
476
+ overlapPercent: result.fileOverlapPercent,
477
+ matchedFiles: result.matchedFiles,
478
+ merkleMatch: result.merkleMatch,
479
+ status: result.status,
480
+ confidence: result.confidence,
481
+ parentProject: project.parentProject,
482
+ originProject: project.originProject
483
+ };
484
+ }
485
+ }
486
+ }
487
+ return bestMatch;
488
+ }
489
+ function getAutoStatus(overlapPercent) {
490
+ if (overlapPercent >= 90) return 3;
491
+ if (overlapPercent >= 60) return 2;
492
+ return 0;
493
+ }
494
+ function printLineageWarning(match) {
495
+ const tierLabel = match.overlapPercent >= 90 ? "DISPUTED ORIGIN" : match.overlapPercent >= 60 ? "VERIFIED DERIVATIVE" : "POSSIBLE DERIVATION";
496
+ const color = match.overlapPercent >= 90 ? chalk2.red : match.overlapPercent >= 60 ? chalk2.yellow : chalk2.cyan;
497
+ console.log(color(`
498
+ Lineage detected: ${tierLabel}`));
499
+ console.log(color(` ${match.overlapPercent.toFixed(1)}% file overlap with "${match.projectName}"`));
500
+ console.log(chalk2.gray(` Creator: ${match.creator.substring(0, 8)}...${match.creator.substring(match.creator.length - 4)}`));
501
+ console.log(chalk2.gray(` Registered: ${new Date(match.registeredAt * 1e3).toISOString().substring(0, 10)}`));
502
+ if (match.repoUrl) {
503
+ console.log(chalk2.gray(` Repo: ${match.repoUrl}`));
504
+ }
505
+ console.log(chalk2.gray(` Matched: ${match.matchedFiles} files`));
506
+ }
507
+
398
508
  // src/commands/publish.ts
399
509
  async function publishCommand(remote, branch, options) {
400
510
  const projectPath = resolve2(options.path || ".");
401
511
  if (!isInitialized(projectPath)) {
402
- console.error(chalk2.red("Error: Not initialized. Run 'gitchain init' first."));
512
+ console.error(chalk3.red("Error: Not initialized. Run 'gitchain init' first."));
403
513
  process.exit(1);
404
514
  }
405
515
  const config = readConfig(projectPath);
406
516
  if (!existsSync2(config.walletPath)) {
407
- console.error(chalk2.red(`Error: Wallet not found at ${config.walletPath}`));
408
- console.error(chalk2.gray("Generate one with: solana-keygen new"));
517
+ console.error(chalk3.red(`Error: Wallet not found at ${config.walletPath}`));
518
+ console.error(chalk3.gray("Generate one with: solana-keygen new"));
409
519
  process.exit(1);
410
520
  }
411
- console.log(chalk2.gray("Fingerprinting project..."));
521
+ console.log(chalk3.gray("Fingerprinting project..."));
412
522
  const fingerprint = await fingerprintDirectory(projectPath);
413
- console.log(chalk2.gray(` Files scanned: ${fingerprint.fileCount}`));
414
- console.log(chalk2.gray(` Merkle root: ${fingerprint.merkleRoot.substring(0, 16)}...`));
523
+ console.log(chalk3.gray(` Files scanned: ${fingerprint.fileCount}`));
524
+ console.log(chalk3.gray(` Merkle root: ${fingerprint.merkleRoot.substring(0, 16)}...`));
525
+ console.log(chalk3.gray("\nConnecting to Solana..."));
526
+ const keypair = loadKeypair(config.walletPath);
527
+ const connection = getConnection(config.rpcUrl);
528
+ const program2 = getProgram(connection, keypair);
529
+ const [pda] = deriveProjectPda(keypair.publicKey, config.projectName);
530
+ console.log(chalk3.gray(` Wallet: ${keypair.publicKey.toString()}`));
531
+ console.log(chalk3.gray(` PDA: ${pda.toString()}`));
532
+ console.log(chalk3.gray("\nScanning on-chain registry for overlaps..."));
533
+ const allProjects = await fetchAllProjectsViaProgram(program2);
534
+ const allFileHashRegs = await fetchAllFileHashRegistries(program2);
535
+ const fileHashMap = /* @__PURE__ */ new Map();
536
+ for (const reg of allFileHashRegs) {
537
+ const projectKey = reg.account.project.toString();
538
+ const hashes = reg.account.hashes.map(
539
+ (h) => Buffer.from(h).toString("hex")
540
+ );
541
+ fileHashMap.set(projectKey, hashes);
542
+ }
543
+ const projectsWithHashes = [];
544
+ for (const item of allProjects) {
545
+ const pubkey = item.pubkey.toString();
546
+ const account = item.account;
547
+ const hashes = fileHashMap.get(pubkey) || [];
548
+ if (pubkey === pda.toString()) continue;
549
+ if (hashes.length === 0) continue;
550
+ const registeredAt = account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt);
551
+ projectsWithHashes.push({
552
+ pubkey,
553
+ projectName: account.projectName,
554
+ creator: account.creator.toString(),
555
+ merkleRoot: Buffer.from(account.merkleRoot).toString("hex"),
556
+ fileCount: account.fileCount,
557
+ status: account.status,
558
+ parentProject: account.parentProject.toString(),
559
+ originProject: account.originProject.toString(),
560
+ registeredAt,
561
+ repoUrl: account.repoUrl || "",
562
+ fileHashes: hashes
563
+ });
564
+ }
565
+ console.log(chalk3.gray(` Scanned ${projectsWithHashes.length} registered project(s)`));
566
+ const lineageMatch = scanForLineage(fingerprint, projectsWithHashes);
567
+ if (lineageMatch && lineageMatch.overlapPercent >= 60) {
568
+ printLineageWarning(lineageMatch);
569
+ }
415
570
  const provenance = readProvenance(projectPath);
416
571
  const codeChanged = provenance.merkleRoot !== fingerprint.merkleRoot;
417
572
  if (!codeChanged && provenance.published) {
418
- console.log(chalk2.yellow("\nNo code changes since last publish. Skipping Solana registration."));
419
- console.log(chalk2.gray(`Pushing to ${remote}/${branch}...
573
+ console.log(chalk3.yellow("\nNo code changes since last publish. Skipping Solana registration."));
574
+ console.log(chalk3.gray(`Pushing to ${remote}/${branch}...
420
575
  `));
421
576
  try {
422
577
  execSync(`git push ${remote} ${branch}`, {
423
578
  cwd: projectPath,
424
579
  stdio: "inherit"
425
580
  });
426
- console.log(chalk2.green(`
581
+ console.log(chalk3.green(`
427
582
  Pushed to ${remote}/${branch}.`));
428
583
  } catch {
429
- console.error(chalk2.red(`
584
+ console.error(chalk3.red(`
430
585
  git push failed. Resolve the issue and retry.`));
431
586
  process.exit(1);
432
587
  }
@@ -438,13 +593,6 @@ git push failed. Resolve the issue and retry.`));
438
593
  )
439
594
  );
440
595
  const fileHashesHash = createHash3("sha256").update(fileHashesJson).digest("hex");
441
- console.log(chalk2.gray("\nConnecting to Solana..."));
442
- const keypair = loadKeypair(config.walletPath);
443
- const connection = getConnection(config.rpcUrl);
444
- const program2 = getProgram(connection, keypair);
445
- const [pda] = deriveProjectPda(keypair.publicKey, config.projectName);
446
- console.log(chalk2.gray(` Wallet: ${keypair.publicKey.toString()}`));
447
- console.log(chalk2.gray(` PDA: ${pda.toString()}`));
448
596
  let alreadyRegistered = false;
449
597
  try {
450
598
  const existing = await program2.account.projectRecord.fetch(pda);
@@ -453,7 +601,7 @@ git push failed. Resolve the issue and retry.`));
453
601
  }
454
602
  let txSignature = provenance.txSignature || "";
455
603
  if (!alreadyRegistered) {
456
- console.log(chalk2.gray("\nRegistering on Solana..."));
604
+ console.log(chalk3.gray("\nRegistering on Solana..."));
457
605
  try {
458
606
  txSignature = await program2.methods.registerProject(
459
607
  config.projectName,
@@ -466,15 +614,15 @@ git push failed. Resolve the issue and retry.`));
466
614
  creator: keypair.publicKey,
467
615
  systemProgram: SystemProgram.programId
468
616
  }).rpc();
469
- console.log(chalk2.green(" Solana registration successful."));
617
+ console.log(chalk3.green(" Solana registration successful."));
470
618
  } catch (err) {
471
- console.error(chalk2.red(`
619
+ console.error(chalk3.red(`
472
620
  Solana transaction failed: ${err.message}`));
473
- console.error(chalk2.red("Aborting push. Fix the issue and retry."));
621
+ console.error(chalk3.red("Aborting push. Fix the issue and retry."));
474
622
  process.exit(1);
475
623
  }
476
624
  } else {
477
- console.log(chalk2.gray("\nUpdating fingerprint on Solana..."));
625
+ console.log(chalk3.gray("\nUpdating fingerprint on Solana..."));
478
626
  try {
479
627
  txSignature = await program2.methods.updateFingerprint(
480
628
  hexToBytes32(fingerprint.merkleRoot),
@@ -484,15 +632,15 @@ Solana transaction failed: ${err.message}`));
484
632
  projectRecord: pda,
485
633
  creator: keypair.publicKey
486
634
  }).rpc();
487
- console.log(chalk2.green(" Fingerprint updated on Solana."));
635
+ console.log(chalk3.green(" Fingerprint updated on Solana."));
488
636
  } catch (err) {
489
- console.error(chalk2.red(`
637
+ console.error(chalk3.red(`
490
638
  Solana update failed: ${err.message}`));
491
- console.error(chalk2.red("Aborting push. Fix the issue and retry."));
639
+ console.error(chalk3.red("Aborting push. Fix the issue and retry."));
492
640
  process.exit(1);
493
641
  }
494
642
  }
495
- console.log(chalk2.gray(`
643
+ console.log(chalk3.gray(`
496
644
  Pushing to ${remote}/${branch}...`));
497
645
  try {
498
646
  execSync(`git push ${remote} ${branch}`, {
@@ -500,11 +648,55 @@ Pushing to ${remote}/${branch}...`));
500
648
  stdio: "inherit"
501
649
  });
502
650
  } catch {
503
- console.error(chalk2.red(`
651
+ console.error(chalk3.red(`
504
652
  git push failed. Your code IS registered on Solana.`));
505
- console.error(chalk2.gray("Resolve the git issue and run: git push " + remote + " " + branch));
653
+ console.error(chalk3.gray("Resolve the git issue and run: git push " + remote + " " + branch));
506
654
  process.exit(1);
507
655
  }
656
+ console.log(chalk3.gray("\nUploading file hashes to Solana..."));
657
+ const sortedHashes = Object.values(fingerprint.fileHashes).sort();
658
+ const hashesAsBytes = sortedHashes.map((h) => hexToBytes32(h));
659
+ const [fileHashPda] = deriveFileHashPda(pda);
660
+ let fileHashRegistryExists = false;
661
+ try {
662
+ await program2.account.fileHashRegistry.fetch(fileHashPda);
663
+ fileHashRegistryExists = true;
664
+ } catch {
665
+ }
666
+ try {
667
+ if (!fileHashRegistryExists) {
668
+ await program2.methods.registerFileHashes(hashesAsBytes).accounts({
669
+ fileHashRegistry: fileHashPda,
670
+ projectRecord: pda,
671
+ creator: keypair.publicKey,
672
+ systemProgram: SystemProgram.programId
673
+ }).rpc();
674
+ } else {
675
+ await program2.methods.updateFileHashes(hashesAsBytes).accounts({
676
+ fileHashRegistry: fileHashPda,
677
+ projectRecord: pda,
678
+ creator: keypair.publicKey,
679
+ systemProgram: SystemProgram.programId
680
+ }).rpc();
681
+ }
682
+ console.log(chalk3.green(` ${sortedHashes.length} file hashes stored on-chain.`));
683
+ } catch (err) {
684
+ console.log(chalk3.yellow(` Warning: Could not upload file hashes: ${err.message}`));
685
+ }
686
+ if (lineageMatch && lineageMatch.overlapPercent >= 60) {
687
+ const autoStatus = getAutoStatus(lineageMatch.overlapPercent);
688
+ const parentPda = new PublicKey2(lineageMatch.pubkey);
689
+ const originPda = lineageMatch.originProject !== PublicKey2.default.toString() ? new PublicKey2(lineageMatch.originProject) : parentPda;
690
+ try {
691
+ await program2.methods.updateStatus(autoStatus, parentPda, originPda).accounts({
692
+ projectRecord: pda,
693
+ creator: keypair.publicKey
694
+ }).rpc();
695
+ console.log(chalk3.gray(` Lineage status set on-chain (parent: ${lineageMatch.projectName})`));
696
+ } catch (err) {
697
+ console.log(chalk3.yellow(` Warning: Could not set lineage status: ${err.message}`));
698
+ }
699
+ }
508
700
  writeProvenance(projectPath, {
509
701
  published: true,
510
702
  merkleRoot: fingerprint.merkleRoot,
@@ -512,15 +704,20 @@ git push failed. Your code IS registered on Solana.`));
512
704
  fileCount: fingerprint.fileCount,
513
705
  txSignature,
514
706
  pdaAddress: pda.toString(),
515
- publishedAt: (/* @__PURE__ */ new Date()).toISOString()
707
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
708
+ ...lineageMatch && lineageMatch.overlapPercent >= 60 ? {
709
+ parentProject: lineageMatch.projectName,
710
+ parentPda: lineageMatch.pubkey,
711
+ overlapPercent: lineageMatch.overlapPercent
712
+ } : {}
516
713
  });
517
- console.log(chalk2.green("\nPublished & pushed!\n"));
518
- console.log(` ${chalk2.bold("Merkle Root:")} ${fingerprint.merkleRoot}`);
519
- console.log(` ${chalk2.bold("Files:")} ${fingerprint.fileCount}`);
520
- console.log(` ${chalk2.bold("Solana TX:")} ${txSignature}`);
521
- console.log(` ${chalk2.bold("Pushed to:")} ${remote}/${branch}`);
714
+ console.log(chalk3.green("\nPublished & pushed!\n"));
715
+ console.log(` ${chalk3.bold("Merkle Root:")} ${fingerprint.merkleRoot}`);
716
+ console.log(` ${chalk3.bold("Files:")} ${fingerprint.fileCount}`);
717
+ console.log(` ${chalk3.bold("Solana TX:")} ${txSignature}`);
718
+ console.log(` ${chalk3.bold("Pushed to:")} ${remote}/${branch}`);
522
719
  console.log(
523
- chalk2.gray(`
720
+ chalk3.gray(`
524
721
  Explorer: https://explorer.solana.com/tx/${txSignature}?cluster=devnet`)
525
722
  );
526
723
  }
@@ -533,7 +730,7 @@ function getRepoUrl(projectPath) {
533
730
  }
534
731
 
535
732
  // src/commands/clone.ts
536
- import chalk3 from "chalk";
733
+ import chalk4 from "chalk";
537
734
  import { resolve as resolve3, basename as basename2 } from "node:path";
538
735
  import { existsSync as existsSync3 } from "node:fs";
539
736
  import { execSync as execSync2 } from "node:child_process";
@@ -544,10 +741,10 @@ var DEFAULT_PROGRAM_ID2 = "5bHSYFLoEtoxQnqi6vuDvjFGbcuwnLJbi6wJbHmHW8R4";
544
741
  async function cloneCommand(url, options) {
545
742
  const repoName = extractRepoName(url);
546
743
  const targetDir = resolve3(options.directory || repoName);
547
- console.log(chalk3.bold(`
744
+ console.log(chalk4.bold(`
548
745
  GitChain Clone: ${repoName}
549
746
  `));
550
- console.log(chalk3.gray("Checking on-chain provenance..."));
747
+ console.log(chalk4.gray("Checking on-chain provenance..."));
551
748
  let onChainRecord = null;
552
749
  try {
553
750
  if (existsSync3(DEFAULT_WALLET_PATH2)) {
@@ -576,35 +773,35 @@ GitChain Clone: ${repoName}
576
773
  }
577
774
  }
578
775
  } catch (err) {
579
- console.log(chalk3.gray(` Could not query Solana: ${err.message}`));
776
+ console.log(chalk4.gray(` Could not query Solana: ${err.message}`));
580
777
  }
581
778
  if (onChainRecord) {
582
779
  const statusLabel = STATUS_LABELS[onChainRecord.status] || "Unknown";
583
- console.log(chalk3.green("\n On-chain provenance found:\n"));
584
- console.log(` ${chalk3.bold("Project:")} ${onChainRecord.projectName}`);
585
- console.log(` ${chalk3.bold("Creator:")} ${onChainRecord.creator.substring(0, 8)}...${onChainRecord.creator.substring(onChainRecord.creator.length - 4)}`);
586
- console.log(` ${chalk3.bold("Registered:")} ${new Date(onChainRecord.registeredAt * 1e3).toISOString().substring(0, 10)}`);
587
- console.log(` ${chalk3.bold("Status:")} ${statusLabel}`);
588
- console.log(` ${chalk3.bold("Files:")} ${onChainRecord.fileCount}`);
589
- console.log(` ${chalk3.bold("Merkle Root:")} ${onChainRecord.merkleRoot.substring(0, 16)}...`);
780
+ console.log(chalk4.green("\n On-chain provenance found:\n"));
781
+ console.log(` ${chalk4.bold("Project:")} ${onChainRecord.projectName}`);
782
+ console.log(` ${chalk4.bold("Creator:")} ${onChainRecord.creator.substring(0, 8)}...${onChainRecord.creator.substring(onChainRecord.creator.length - 4)}`);
783
+ console.log(` ${chalk4.bold("Registered:")} ${new Date(onChainRecord.registeredAt * 1e3).toISOString().substring(0, 10)}`);
784
+ console.log(` ${chalk4.bold("Status:")} ${statusLabel}`);
785
+ console.log(` ${chalk4.bold("Files:")} ${onChainRecord.fileCount}`);
786
+ console.log(` ${chalk4.bold("Merkle Root:")} ${onChainRecord.merkleRoot.substring(0, 16)}...`);
590
787
  } else {
591
- console.log(chalk3.yellow("\n Warning: No provenance record found for this repository."));
592
- console.log(chalk3.yellow(" Origin is unverified.\n"));
788
+ console.log(chalk4.yellow("\n Warning: No provenance record found for this repository."));
789
+ console.log(chalk4.yellow(" Origin is unverified.\n"));
593
790
  }
594
- console.log(chalk3.gray(`
791
+ console.log(chalk4.gray(`
595
792
  Cloning ${url}...`));
596
793
  const cloneArgs = options.directory ? `${url} ${options.directory}` : url;
597
794
  try {
598
795
  execSync2(`git clone ${cloneArgs}`, { stdio: "inherit" });
599
796
  } catch {
600
- console.error(chalk3.red("\ngit clone failed."));
797
+ console.error(chalk4.red("\ngit clone failed."));
601
798
  process.exit(1);
602
799
  }
603
800
  if (onChainRecord) {
604
- console.log(chalk3.gray("\nVerifying cloned code against on-chain fingerprint..."));
801
+ console.log(chalk4.gray("\nVerifying cloned code against on-chain fingerprint..."));
605
802
  const clonedFp = await fingerprintDirectory(targetDir);
606
803
  if (clonedFp.merkleRoot === onChainRecord.merkleRoot) {
607
- console.log(chalk3.green("\n Verified: code matches on-chain fingerprint."));
804
+ console.log(chalk4.green("\n Verified: code matches on-chain fingerprint."));
608
805
  } else {
609
806
  const fileHashesJson = JSON.stringify(
610
807
  Object.fromEntries(
@@ -613,15 +810,15 @@ Cloning ${url}...`));
613
810
  );
614
811
  const clonedHashesHash = createHash4("sha256").update(fileHashesJson).digest("hex");
615
812
  if (clonedHashesHash === onChainRecord.fileHashesHash) {
616
- console.log(chalk3.green("\n Verified: file content matches on-chain commitment."));
813
+ console.log(chalk4.green("\n Verified: file content matches on-chain commitment."));
617
814
  } else {
618
- console.log(chalk3.yellow("\n Warning: code does NOT match registered fingerprint."));
619
- console.log(chalk3.yellow(" The repository may have been modified since registration."));
620
- console.log(chalk3.gray(" Run 'gitchain verify' for detailed analysis."));
815
+ console.log(chalk4.yellow("\n Warning: code does NOT match registered fingerprint."));
816
+ console.log(chalk4.yellow(" The repository may have been modified since registration."));
817
+ console.log(chalk4.gray(" Run 'gitchain verify' for detailed analysis."));
621
818
  }
622
819
  }
623
820
  }
624
- console.log(chalk3.gray("\nInitializing GitChain in cloned repository..."));
821
+ console.log(chalk4.gray("\nInitializing GitChain in cloned repository..."));
625
822
  const cloneConfig = {
626
823
  projectName: onChainRecord?.projectName || repoName,
627
824
  walletPath: DEFAULT_WALLET_PATH2,
@@ -636,12 +833,12 @@ Cloning ${url}...`));
636
833
  publishedAt: onChainRecord ? new Date(onChainRecord.registeredAt * 1e3).toISOString() : void 0
637
834
  });
638
835
  ensureGitignore(targetDir);
639
- console.log(chalk3.green(`
836
+ console.log(chalk4.green(`
640
837
  Done! Repository cloned to ${targetDir}`));
641
838
  if (onChainRecord) {
642
- console.log(chalk3.gray(" GitChain provenance data imported from Solana."));
839
+ console.log(chalk4.gray(" GitChain provenance data imported from Solana."));
643
840
  } else {
644
- console.log(chalk3.gray(" GitChain initialized (no provenance data available)."));
841
+ console.log(chalk4.gray(" GitChain initialized (no provenance data available)."));
645
842
  }
646
843
  console.log();
647
844
  }
@@ -651,14 +848,13 @@ function extractRepoName(url) {
651
848
  }
652
849
 
653
850
  // src/commands/verify.ts
654
- import chalk4 from "chalk";
851
+ import chalk5 from "chalk";
655
852
  import { resolve as resolve4 } from "node:path";
656
853
  import { existsSync as existsSync4 } from "node:fs";
657
- import { createHash as createHash5 } from "node:crypto";
658
854
  async function verifyCommand(targetPath, options) {
659
855
  const resolvedTarget = resolve4(targetPath);
660
856
  if (!existsSync4(resolvedTarget)) {
661
- console.error(chalk4.red(`Error: Path not found: ${resolvedTarget}`));
857
+ console.error(chalk5.red(`Error: Path not found: ${resolvedTarget}`));
662
858
  process.exit(1);
663
859
  }
664
860
  const configPath = resolve4(options.path || ".");
@@ -671,11 +867,11 @@ async function verifyCommand(targetPath, options) {
671
867
  walletPath = config.walletPath;
672
868
  programId = config.programId;
673
869
  }
674
- console.log(chalk4.gray("Fingerprinting target code..."));
870
+ console.log(chalk5.gray("Fingerprinting target code..."));
675
871
  const suspectFingerprint = await fingerprintDirectory(resolvedTarget);
676
- console.log(chalk4.gray(` Files: ${suspectFingerprint.fileCount}`));
677
- console.log(chalk4.gray(` Merkle root: ${suspectFingerprint.merkleRoot.substring(0, 16)}...`));
678
- console.log(chalk4.gray("\nFetching on-chain lineage records..."));
872
+ console.log(chalk5.gray(` Files: ${suspectFingerprint.fileCount}`));
873
+ console.log(chalk5.gray(` Merkle root: ${suspectFingerprint.merkleRoot.substring(0, 16)}...`));
874
+ console.log(chalk5.gray("\nFetching on-chain lineage records..."));
679
875
  const keypair = loadKeypair(walletPath);
680
876
  const connection = getConnection(rpcUrl);
681
877
  const program2 = getProgram(connection, keypair);
@@ -683,138 +879,119 @@ async function verifyCommand(targetPath, options) {
683
879
  try {
684
880
  allProjects = await fetchAllProjectsViaProgram(program2);
685
881
  } catch (err) {
686
- console.error(chalk4.red(`Failed to fetch on-chain records: ${err.message}`));
882
+ console.error(chalk5.red(`Failed to fetch on-chain records: ${err.message}`));
687
883
  process.exit(1);
688
884
  }
689
885
  if (allProjects.length === 0) {
690
- console.log(chalk4.yellow("\nNo projects registered on-chain yet."));
691
- console.log(chalk4.gray("Register a project first with 'gitchain publish'."));
886
+ console.log(chalk5.yellow("\nNo projects registered on-chain yet."));
887
+ console.log(chalk5.gray("Register a project first with 'gitchain publish'."));
692
888
  return;
693
889
  }
694
- console.log(chalk4.gray(` Found ${allProjects.length} registered project(s)
890
+ console.log(chalk5.gray(` Found ${allProjects.length} registered project(s)
695
891
  `));
696
- let bestMatch = null;
892
+ const allFileHashRegs = await fetchAllFileHashRegistries(program2);
893
+ const fileHashMap = /* @__PURE__ */ new Map();
894
+ for (const reg of allFileHashRegs) {
895
+ const projectKey = reg.account.project.toString();
896
+ const hashes = reg.account.hashes.map(
897
+ (h) => Buffer.from(h).toString("hex")
898
+ );
899
+ fileHashMap.set(projectKey, hashes);
900
+ }
901
+ const projectsWithHashes = [];
697
902
  for (const item of allProjects) {
698
- const pubkey = item.publicKey ?? item.pubkey;
903
+ const pubkey = item.pubkey.toString();
699
904
  const account = item.account;
700
- const onChainMerkle = Buffer.from(account.merkleRoot).toString("hex");
701
- const merkleMatch = onChainMerkle === suspectFingerprint.merkleRoot;
702
- let fileOverlapPercent = 0;
703
- let confidence = 0;
704
- if (merkleMatch) {
705
- fileOverlapPercent = 100;
706
- confidence = 100;
707
- } else {
708
- const suspectHashesHash = computeFileHashesHash(suspectFingerprint);
709
- const onChainHashesHash = Buffer.from(account.fileHashesHash).toString("hex");
710
- if (suspectHashesHash === onChainHashesHash) {
711
- fileOverlapPercent = 100;
712
- confidence = 99;
713
- }
714
- }
715
- const overlapForRanking = merkleMatch ? 100 : fileOverlapPercent;
716
- if (!bestMatch || overlapForRanking > bestMatch.fileOverlapPercent) {
717
- bestMatch = {
718
- projectName: account.projectName,
719
- creator: account.creator.toString(),
720
- registeredAt: account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt),
721
- merkleMatch,
722
- fileOverlapPercent: overlapForRanking,
723
- confidence: merkleMatch ? 100 : confidence,
724
- status: merkleMatch ? "Disputed Origin" : fileOverlapPercent >= 90 ? "Disputed Origin" : "Checking...",
725
- repoUrl: account.repoUrl || "",
726
- pda: pubkey.toString()
727
- };
728
- }
905
+ const hashes = fileHashMap.get(pubkey) || [];
906
+ const registeredAt = account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt);
907
+ projectsWithHashes.push({
908
+ pubkey,
909
+ projectName: account.projectName,
910
+ creator: account.creator.toString(),
911
+ merkleRoot: Buffer.from(account.merkleRoot).toString("hex"),
912
+ fileCount: account.fileCount,
913
+ status: account.status,
914
+ parentProject: account.parentProject.toString(),
915
+ originProject: account.originProject.toString(),
916
+ registeredAt,
917
+ repoUrl: account.repoUrl || "",
918
+ fileHashes: hashes
919
+ });
729
920
  }
730
- if (bestMatch && bestMatch.fileOverlapPercent === 0) {
731
- console.log(chalk4.gray(" No Merkle or commitment match. Trying local comparison...\n"));
732
- if (isInitialized(configPath)) {
733
- const localFp = await fingerprintDirectory(configPath);
734
- const comparison = compareFingerprints(localFp, suspectFingerprint);
735
- if (comparison.fileOverlapPercent > (bestMatch?.fileOverlapPercent || 0)) {
736
- const config = readConfig(configPath);
737
- bestMatch = {
738
- projectName: config.projectName,
739
- creator: keypair.publicKey.toString(),
740
- registeredAt: Date.now() / 1e3,
741
- merkleMatch: comparison.merkleMatch,
742
- fileOverlapPercent: comparison.fileOverlapPercent,
743
- confidence: comparison.confidence,
744
- status: comparison.status,
745
- repoUrl: "",
746
- pda: bestMatch?.pda || "local"
747
- };
748
- }
749
- }
921
+ const lineageMatch = scanForLineage(suspectFingerprint, projectsWithHashes);
922
+ let bestMatch = null;
923
+ if (lineageMatch) {
924
+ bestMatch = {
925
+ projectName: lineageMatch.projectName,
926
+ creator: lineageMatch.creator,
927
+ registeredAt: lineageMatch.registeredAt,
928
+ merkleMatch: lineageMatch.merkleMatch,
929
+ fileOverlapPercent: lineageMatch.overlapPercent,
930
+ confidence: lineageMatch.confidence,
931
+ status: lineageMatch.status,
932
+ repoUrl: lineageMatch.repoUrl,
933
+ pda: lineageMatch.pubkey
934
+ };
750
935
  }
751
- console.log(chalk4.bold("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
752
- console.log(chalk4.bold("\u2551 Lineage Analysis Result \u2551"));
753
- console.log(chalk4.bold("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n"));
936
+ console.log(chalk5.bold("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
937
+ console.log(chalk5.bold("\u2551 Lineage Analysis Result \u2551"));
938
+ console.log(chalk5.bold("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n"));
754
939
  if (!bestMatch || bestMatch.fileOverlapPercent === 0) {
755
- console.log(chalk4.green(" Status: No known lineage match found."));
756
- console.log(chalk4.gray(" This code does not match any registered project."));
940
+ console.log(chalk5.green(" Status: No known lineage match found."));
941
+ console.log(chalk5.gray(" This code does not match any registered project."));
757
942
  return;
758
943
  }
759
- console.log(` ${chalk4.bold("Closest Known Origin:")} ${bestMatch.projectName}`);
760
- console.log(` ${chalk4.bold("Creator:")} ${bestMatch.creator.substring(0, 8)}...${bestMatch.creator.substring(bestMatch.creator.length - 4)}`);
761
- console.log(` ${chalk4.bold("Registered:")} ${new Date(bestMatch.registeredAt * 1e3).toISOString().substring(0, 10)}`);
944
+ console.log(` ${chalk5.bold("Closest Known Origin:")} ${bestMatch.projectName}`);
945
+ console.log(` ${chalk5.bold("Creator:")} ${bestMatch.creator.substring(0, 8)}...${bestMatch.creator.substring(bestMatch.creator.length - 4)}`);
946
+ console.log(` ${chalk5.bold("Registered:")} ${new Date(bestMatch.registeredAt * 1e3).toISOString().substring(0, 10)}`);
762
947
  if (bestMatch.repoUrl) {
763
- console.log(` ${chalk4.bold("Repo:")} ${bestMatch.repoUrl}`);
948
+ console.log(` ${chalk5.bold("Repo:")} ${bestMatch.repoUrl}`);
764
949
  }
765
- console.log(chalk4.bold("\n Signals:"));
950
+ console.log(chalk5.bold("\n Signals:"));
766
951
  console.log(` Exact File Overlap: ${formatPercent(bestMatch.fileOverlapPercent)}`);
767
- console.log(` Merkle Root Match: ${bestMatch.merkleMatch ? chalk4.red("Yes (exact copy)") : "No"}`);
768
- console.log(chalk4.bold("\n Conclusion:"));
769
- const statusColor = bestMatch.fileOverlapPercent >= 60 ? chalk4.red : chalk4.yellow;
770
- console.log(` ${chalk4.bold("Status:")} ${statusColor(bestMatch.status)}`);
771
- console.log(` ${chalk4.bold("Confidence:")} ${bestMatch.confidence}%
952
+ console.log(` Merkle Root Match: ${bestMatch.merkleMatch ? chalk5.red("Yes (exact copy)") : "No"}`);
953
+ console.log(chalk5.bold("\n Conclusion:"));
954
+ const statusColor = bestMatch.fileOverlapPercent >= 60 ? chalk5.red : chalk5.yellow;
955
+ console.log(` ${chalk5.bold("Status:")} ${statusColor(bestMatch.status)}`);
956
+ console.log(` ${chalk5.bold("Confidence:")} ${bestMatch.confidence}%
772
957
  `);
773
958
  }
774
959
  function formatPercent(value) {
775
960
  const formatted = `${value.toFixed(1)}%`;
776
- if (value >= 90) return chalk4.red(formatted);
777
- if (value >= 60) return chalk4.yellow(formatted);
778
- if (value >= 30) return chalk4.cyan(formatted);
779
- return chalk4.green(formatted);
780
- }
781
- function computeFileHashesHash(fp) {
782
- const sorted = JSON.stringify(
783
- Object.fromEntries(
784
- Object.entries(fp.fileHashes).sort(([a], [b]) => a.localeCompare(b))
785
- )
786
- );
787
- return createHash5("sha256").update(sorted).digest("hex");
961
+ if (value >= 90) return chalk5.red(formatted);
962
+ if (value >= 60) return chalk5.yellow(formatted);
963
+ if (value >= 30) return chalk5.cyan(formatted);
964
+ return chalk5.green(formatted);
788
965
  }
789
966
 
790
967
  // src/commands/status.ts
791
- import chalk5 from "chalk";
968
+ import chalk6 from "chalk";
792
969
  import { resolve as resolve5 } from "node:path";
793
970
  import { existsSync as existsSync5 } from "node:fs";
794
971
  async function statusCommand(options) {
795
972
  const projectPath = resolve5(options.path || ".");
796
973
  if (!isInitialized(projectPath)) {
797
- console.error(chalk5.red("Error: Not initialized. Run 'gitchain init' first."));
974
+ console.error(chalk6.red("Error: Not initialized. Run 'gitchain init' first."));
798
975
  process.exit(1);
799
976
  }
800
977
  const config = readConfig(projectPath);
801
978
  const provenance = readProvenance(projectPath);
802
- console.log(chalk5.bold(`
979
+ console.log(chalk6.bold(`
803
980
  GitChain Status: ${config.projectName}
804
981
  `));
805
982
  if (!provenance.published) {
806
- console.log(chalk5.yellow(" Not published."));
807
- console.log(chalk5.gray(" Run 'gitchain publish' to register on Solana.\n"));
983
+ console.log(chalk6.yellow(" Not published."));
984
+ console.log(chalk6.gray(" Run 'gitchain publish' to register on Solana.\n"));
808
985
  return;
809
986
  }
810
- console.log(chalk5.gray(" Local State:"));
987
+ console.log(chalk6.gray(" Local State:"));
811
988
  console.log(` Merkle Root: ${provenance.merkleRoot?.substring(0, 32)}...`);
812
989
  console.log(` Files: ${provenance.fileCount}`);
813
990
  console.log(` Published: ${provenance.publishedAt}`);
814
991
  console.log(` PDA: ${provenance.pdaAddress}`);
815
992
  console.log(` TX: ${provenance.txSignature}`);
816
993
  if (existsSync5(config.walletPath)) {
817
- console.log(chalk5.gray("\n On-Chain State:"));
994
+ console.log(chalk6.gray("\n On-Chain State:"));
818
995
  try {
819
996
  const keypair = loadKeypair(config.walletPath);
820
997
  const connection = getConnection(config.rpcUrl);
@@ -832,11 +1009,123 @@ GitChain Status: ${config.projectName}
832
1009
  console.log(` Repo: ${account.repoUrl}`);
833
1010
  }
834
1011
  console.log(
835
- chalk5.gray(`
1012
+ chalk6.gray(`
836
1013
  Explorer: https://explorer.solana.com/address/${pda.toString()}?cluster=devnet`)
837
1014
  );
838
1015
  } catch (err) {
839
- console.log(chalk5.yellow(` Could not fetch on-chain data: ${err.message}`));
1016
+ console.log(chalk6.yellow(` Could not fetch on-chain data: ${err.message}`));
1017
+ }
1018
+ }
1019
+ console.log();
1020
+ }
1021
+
1022
+ // src/commands/lineage.ts
1023
+ import chalk7 from "chalk";
1024
+ import { resolve as resolve6 } from "node:path";
1025
+ async function lineageCommand(options) {
1026
+ const projectPath = resolve6(options.path || ".");
1027
+ if (!isInitialized(projectPath)) {
1028
+ console.error(chalk7.red("Error: Not initialized. Run 'gitchain init' first."));
1029
+ process.exit(1);
1030
+ }
1031
+ const config = readConfig(projectPath);
1032
+ const provenance = readProvenance(projectPath);
1033
+ if (!provenance.published) {
1034
+ console.log(chalk7.yellow("\nNot published yet. Run 'gitchain publish' first.\n"));
1035
+ return;
1036
+ }
1037
+ console.log(chalk7.bold(`
1038
+ Lineage Tree: ${config.projectName}
1039
+ `));
1040
+ const keypair = loadKeypair(config.walletPath);
1041
+ const connection = getConnection(config.rpcUrl);
1042
+ const program2 = getProgram(connection, keypair);
1043
+ let allProjects;
1044
+ try {
1045
+ allProjects = await fetchAllProjectsViaProgram(program2);
1046
+ } catch (err) {
1047
+ console.error(chalk7.red(`Failed to fetch on-chain records: ${err.message}`));
1048
+ process.exit(1);
1049
+ }
1050
+ const projectMap = /* @__PURE__ */ new Map();
1051
+ for (const item of allProjects) {
1052
+ const pubkey = item.pubkey.toString();
1053
+ const account = item.account;
1054
+ const registeredAt = account.registeredAt.toNumber ? account.registeredAt.toNumber() : Number(account.registeredAt);
1055
+ projectMap.set(pubkey, {
1056
+ name: account.projectName,
1057
+ creator: account.creator.toString(),
1058
+ status: account.status,
1059
+ parent: account.parentProject.toString(),
1060
+ origin: account.originProject.toString(),
1061
+ registeredAt,
1062
+ repoUrl: account.repoUrl || ""
1063
+ });
1064
+ }
1065
+ const [myPda] = deriveProjectPda(keypair.publicKey, config.projectName);
1066
+ const myPdaStr = myPda.toString();
1067
+ const myProject = projectMap.get(myPdaStr);
1068
+ if (!myProject) {
1069
+ console.log(chalk7.yellow(" Project not found on-chain.\n"));
1070
+ return;
1071
+ }
1072
+ const NULL_KEY = "11111111111111111111111111111111";
1073
+ const chain = [];
1074
+ let currentPda = myPdaStr;
1075
+ const visited = /* @__PURE__ */ new Set();
1076
+ while (currentPda && currentPda !== NULL_KEY && !visited.has(currentPda)) {
1077
+ visited.add(currentPda);
1078
+ const proj = projectMap.get(currentPda);
1079
+ if (!proj) break;
1080
+ chain.push({
1081
+ pda: currentPda,
1082
+ name: proj.name,
1083
+ creator: proj.creator,
1084
+ status: proj.status,
1085
+ registeredAt: proj.registeredAt,
1086
+ repoUrl: proj.repoUrl
1087
+ });
1088
+ currentPda = proj.parent;
1089
+ }
1090
+ const children = [];
1091
+ for (const [pda, proj] of projectMap) {
1092
+ if (proj.parent === myPdaStr && pda !== myPdaStr) {
1093
+ children.push({
1094
+ pda,
1095
+ name: proj.name,
1096
+ creator: proj.creator,
1097
+ status: proj.status,
1098
+ registeredAt: proj.registeredAt
1099
+ });
1100
+ }
1101
+ }
1102
+ if (chain.length <= 1 && children.length === 0) {
1103
+ console.log(chalk7.green(" This project has no known lineage."));
1104
+ console.log(chalk7.gray(" It is an original, standalone project.\n"));
1105
+ return;
1106
+ }
1107
+ const ancestors = chain.slice(1).reverse();
1108
+ if (ancestors.length > 0) {
1109
+ console.log(chalk7.gray(" Ancestry (oldest first):\n"));
1110
+ for (let i = 0; i < ancestors.length; i++) {
1111
+ const a = ancestors[i];
1112
+ const prefix = i === 0 ? " " : " " + " ".repeat(i);
1113
+ const statusLabel = STATUS_LABELS[a.status] || "Unknown";
1114
+ console.log(`${prefix}${chalk7.bold(a.name)} ${chalk7.gray(`(${statusLabel})`)}`);
1115
+ console.log(`${prefix}${chalk7.gray(`by ${a.creator.substring(0, 8)}... | ${new Date(a.registeredAt * 1e3).toISOString().substring(0, 10)}`)}`);
1116
+ console.log(`${prefix}${chalk7.gray("\u2502")}`);
1117
+ }
1118
+ }
1119
+ const indent = ancestors.length > 0 ? " " + " ".repeat(ancestors.length) : " ";
1120
+ const myStatus = STATUS_LABELS[myProject.status] || "Unknown";
1121
+ console.log(`${indent}${chalk7.bold.cyan(`\u25BA ${config.projectName}`)} ${chalk7.gray(`(${myStatus})`)}`);
1122
+ console.log(`${indent}${chalk7.gray(` by ${myProject.creator.substring(0, 8)}... | ${new Date(myProject.registeredAt * 1e3).toISOString().substring(0, 10)}`)}`);
1123
+ if (children.length > 0) {
1124
+ console.log(`${indent}${chalk7.gray("\u2502")}`);
1125
+ for (const child of children) {
1126
+ const childStatus = STATUS_LABELS[child.status] || "Unknown";
1127
+ console.log(`${indent}\u251C\u2500\u2500 ${chalk7.yellow(child.name)} ${chalk7.gray(`(${childStatus})`)}`);
1128
+ console.log(`${indent}\u2502 ${chalk7.gray(`by ${child.creator.substring(0, 8)}... | ${new Date(child.registeredAt * 1e3).toISOString().substring(0, 10)}`)}`);
840
1129
  }
841
1130
  }
842
1131
  console.log();
@@ -844,10 +1133,11 @@ GitChain Status: ${config.projectName}
844
1133
 
845
1134
  // src/index.ts
846
1135
  var program = new Command();
847
- program.name("gitchain").description("Software provenance & lineage protocol on Solana").version("0.1.0");
1136
+ program.name("gitchain").description("Software provenance & lineage protocol on Solana").version("0.2.0");
848
1137
  program.command("init").description("Initialize GitChain tracking for this repository").option("-p, --path <path>", "Path to Git repository", ".").action(initCommand);
849
1138
  program.command("publish <remote> <branch>").description("Fingerprint, register on Solana, then push to remote").option("-p, --path <path>", "Path to Git repository", ".").action(publishCommand);
850
1139
  program.command("clone <url>").description("Check on-chain provenance, then clone repository").option("-d, --directory <dir>", "Target directory for clone").action(cloneCommand);
851
1140
  program.command("verify <target-path>").description("Verify code against on-chain lineage records").option("-p, --path <path>", "Path to initialized GitChain project for config", ".").action(verifyCommand);
852
1141
  program.command("status").description("Show GitChain provenance status").option("-p, --path <path>", "Path to GitChain project", ".").action(statusCommand);
1142
+ program.command("lineage").description("Show lineage tree for this project").option("-p, --path <path>", "Path to GitChain project", ".").action(lineageCommand);
853
1143
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitchain-sol",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Software provenance & lineage protocol on Solana — fingerprint, register, and verify code on-chain",
5
5
  "type": "module",
6
6
  "bin": {