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.
- package/dist/gitchain.mjs +451 -161
- 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
|
|
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(
|
|
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(
|
|
408
|
-
console.error(
|
|
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(
|
|
521
|
+
console.log(chalk3.gray("Fingerprinting project..."));
|
|
412
522
|
const fingerprint = await fingerprintDirectory(projectPath);
|
|
413
|
-
console.log(
|
|
414
|
-
console.log(
|
|
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(
|
|
419
|
-
console.log(
|
|
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(
|
|
581
|
+
console.log(chalk3.green(`
|
|
427
582
|
Pushed to ${remote}/${branch}.`));
|
|
428
583
|
} catch {
|
|
429
|
-
console.error(
|
|
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(
|
|
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(
|
|
617
|
+
console.log(chalk3.green(" Solana registration successful."));
|
|
470
618
|
} catch (err) {
|
|
471
|
-
console.error(
|
|
619
|
+
console.error(chalk3.red(`
|
|
472
620
|
Solana transaction failed: ${err.message}`));
|
|
473
|
-
console.error(
|
|
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(
|
|
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(
|
|
635
|
+
console.log(chalk3.green(" Fingerprint updated on Solana."));
|
|
488
636
|
} catch (err) {
|
|
489
|
-
console.error(
|
|
637
|
+
console.error(chalk3.red(`
|
|
490
638
|
Solana update failed: ${err.message}`));
|
|
491
|
-
console.error(
|
|
639
|
+
console.error(chalk3.red("Aborting push. Fix the issue and retry."));
|
|
492
640
|
process.exit(1);
|
|
493
641
|
}
|
|
494
642
|
}
|
|
495
|
-
console.log(
|
|
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(
|
|
651
|
+
console.error(chalk3.red(`
|
|
504
652
|
git push failed. Your code IS registered on Solana.`));
|
|
505
|
-
console.error(
|
|
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(
|
|
518
|
-
console.log(` ${
|
|
519
|
-
console.log(` ${
|
|
520
|
-
console.log(` ${
|
|
521
|
-
console.log(` ${
|
|
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
|
-
|
|
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
|
|
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(
|
|
744
|
+
console.log(chalk4.bold(`
|
|
548
745
|
GitChain Clone: ${repoName}
|
|
549
746
|
`));
|
|
550
|
-
console.log(
|
|
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(
|
|
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(
|
|
584
|
-
console.log(` ${
|
|
585
|
-
console.log(` ${
|
|
586
|
-
console.log(` ${
|
|
587
|
-
console.log(` ${
|
|
588
|
-
console.log(` ${
|
|
589
|
-
console.log(` ${
|
|
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(
|
|
592
|
-
console.log(
|
|
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(
|
|
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(
|
|
797
|
+
console.error(chalk4.red("\ngit clone failed."));
|
|
601
798
|
process.exit(1);
|
|
602
799
|
}
|
|
603
800
|
if (onChainRecord) {
|
|
604
|
-
console.log(
|
|
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(
|
|
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(
|
|
813
|
+
console.log(chalk4.green("\n Verified: file content matches on-chain commitment."));
|
|
617
814
|
} else {
|
|
618
|
-
console.log(
|
|
619
|
-
console.log(
|
|
620
|
-
console.log(
|
|
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(
|
|
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(
|
|
836
|
+
console.log(chalk4.green(`
|
|
640
837
|
Done! Repository cloned to ${targetDir}`));
|
|
641
838
|
if (onChainRecord) {
|
|
642
|
-
console.log(
|
|
839
|
+
console.log(chalk4.gray(" GitChain provenance data imported from Solana."));
|
|
643
840
|
} else {
|
|
644
|
-
console.log(
|
|
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
|
|
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(
|
|
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(
|
|
870
|
+
console.log(chalk5.gray("Fingerprinting target code..."));
|
|
675
871
|
const suspectFingerprint = await fingerprintDirectory(resolvedTarget);
|
|
676
|
-
console.log(
|
|
677
|
-
console.log(
|
|
678
|
-
console.log(
|
|
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(
|
|
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(
|
|
691
|
-
console.log(
|
|
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(
|
|
890
|
+
console.log(chalk5.gray(` Found ${allProjects.length} registered project(s)
|
|
695
891
|
`));
|
|
696
|
-
|
|
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.
|
|
903
|
+
const pubkey = item.pubkey.toString();
|
|
699
904
|
const account = item.account;
|
|
700
|
-
const
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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(
|
|
752
|
-
console.log(
|
|
753
|
-
console.log(
|
|
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(
|
|
756
|
-
console.log(
|
|
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(` ${
|
|
760
|
-
console.log(` ${
|
|
761
|
-
console.log(` ${
|
|
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(` ${
|
|
948
|
+
console.log(` ${chalk5.bold("Repo:")} ${bestMatch.repoUrl}`);
|
|
764
949
|
}
|
|
765
|
-
console.log(
|
|
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 ?
|
|
768
|
-
console.log(
|
|
769
|
-
const statusColor = bestMatch.fileOverlapPercent >= 60 ?
|
|
770
|
-
console.log(` ${
|
|
771
|
-
console.log(` ${
|
|
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
|
|
777
|
-
if (value >= 60) return
|
|
778
|
-
if (value >= 30) return
|
|
779
|
-
return
|
|
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
|
|
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(
|
|
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(
|
|
979
|
+
console.log(chalk6.bold(`
|
|
803
980
|
GitChain Status: ${config.projectName}
|
|
804
981
|
`));
|
|
805
982
|
if (!provenance.published) {
|
|
806
|
-
console.log(
|
|
807
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1012
|
+
chalk6.gray(`
|
|
836
1013
|
Explorer: https://explorer.solana.com/address/${pda.toString()}?cluster=devnet`)
|
|
837
1014
|
);
|
|
838
1015
|
} catch (err) {
|
|
839
|
-
console.log(
|
|
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.
|
|
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();
|