@iqlabs-official/iq-git-cli 0.1.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/cli.js ADDED
@@ -0,0 +1,1022 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { existsSync as existsSync2 } from "fs";
8
+ import { join as join2 } from "path";
9
+
10
+ // src/core/repo.ts
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ renameSync,
17
+ rmSync,
18
+ writeFileSync
19
+ } from "fs";
20
+ import { dirname, join } from "path";
21
+ var DIR = ".iqgit";
22
+ function findRepoRoot(cwd = process.cwd()) {
23
+ let dir = cwd;
24
+ while (true) {
25
+ if (existsSync(join(dir, DIR))) return dir;
26
+ const parent = dirname(dir);
27
+ if (parent === dir) {
28
+ throw new Error("not an iqgit repo (or any parent)");
29
+ }
30
+ dir = parent;
31
+ }
32
+ }
33
+ function initRepo(cwd) {
34
+ mkdirSync(join(cwd, DIR, "pending"), { recursive: true });
35
+ }
36
+ function readConfig(repoRoot) {
37
+ try {
38
+ return JSON.parse(readFileSync(join(repoRoot, DIR, "config.json"), "utf8"));
39
+ } catch {
40
+ return { owner: "", repo: "", isPublic: true };
41
+ }
42
+ }
43
+ function writeConfig(repoRoot, cfg) {
44
+ writeFileSync(join(repoRoot, DIR, "config.json"), JSON.stringify(cfg, null, 2));
45
+ }
46
+ function readHead(repoRoot) {
47
+ try {
48
+ return readFileSync(join(repoRoot, DIR, "HEAD"), "utf8").trim() || null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ function writeHead(repoRoot, commitId) {
54
+ writeFileSync(join(repoRoot, DIR, "HEAD"), commitId);
55
+ }
56
+ function readIndex(repoRoot) {
57
+ try {
58
+ return JSON.parse(readFileSync(join(repoRoot, DIR, "index.json"), "utf8"));
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+ function writeIndex(repoRoot, paths) {
64
+ const sorted = Array.from(new Set(paths)).sort();
65
+ writeFileSync(join(repoRoot, DIR, "index.json"), JSON.stringify(sorted, null, 2));
66
+ }
67
+ function listPending(repoRoot) {
68
+ const dir = join(repoRoot, DIR, "pending");
69
+ if (!existsSync(dir)) return [];
70
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
71
+ const seqStr = e.name.split("-")[0];
72
+ if (!seqStr) throw new Error(`malformed pending dir: ${e.name}`);
73
+ const dirPath = join(dir, e.name);
74
+ return {
75
+ dir: dirPath,
76
+ seq: Number(seqStr),
77
+ meta: JSON.parse(readFileSync(join(dirPath, "meta.json"), "utf8"))
78
+ };
79
+ }).sort((a, b) => a.seq - b.seq);
80
+ }
81
+ function appendPending(repoRoot, meta, tree, blobs) {
82
+ const existing = listPending(repoRoot);
83
+ const seq = (existing[existing.length - 1]?.seq ?? 0) + 1;
84
+ const seqStr = String(seq).padStart(3, "0");
85
+ const dir = join(repoRoot, DIR, "pending", `${seqStr}-${meta.id}`);
86
+ mkdirSync(join(dir, "blobs"), { recursive: true });
87
+ writeFileSync(join(dir, "meta.json"), JSON.stringify(meta, null, 2));
88
+ writeFileSync(join(dir, "tree.json"), JSON.stringify(tree, null, 2));
89
+ for (const [hash, base64] of blobs) {
90
+ writeFileSync(join(dir, "blobs", hash), base64);
91
+ }
92
+ return { dir, seq, meta };
93
+ }
94
+ function updatePendingMeta(p, patch) {
95
+ const next = { ...p.meta, ...patch };
96
+ const target = join(p.dir, "meta.json");
97
+ const tmp = `${target}.tmp`;
98
+ writeFileSync(tmp, JSON.stringify(next, null, 2));
99
+ renameSync(tmp, target);
100
+ p.meta = next;
101
+ }
102
+ function readPendingTree(p) {
103
+ return JSON.parse(readFileSync(join(p.dir, "tree.json"), "utf8"));
104
+ }
105
+ function readPendingBlob(p, hash) {
106
+ return readFileSync(join(p.dir, "blobs", hash), "utf8");
107
+ }
108
+ function discardPending(p) {
109
+ rmSync(p.dir, { recursive: true, force: true });
110
+ }
111
+ var cacheMem = /* @__PURE__ */ new Map();
112
+ function cacheGet(repoRoot, hash) {
113
+ return loadCache(repoRoot)[hash] ?? null;
114
+ }
115
+ function cacheSet(repoRoot, hash, entry) {
116
+ const c = loadCache(repoRoot);
117
+ c[hash] = entry;
118
+ writeFileSync(cachePath(repoRoot), JSON.stringify(c, null, 2));
119
+ }
120
+ function cachePath(repoRoot) {
121
+ return join(repoRoot, DIR, "upload-cache.json");
122
+ }
123
+ function loadCache(repoRoot) {
124
+ const cached = cacheMem.get(repoRoot);
125
+ if (cached) return cached;
126
+ let parsed = {};
127
+ try {
128
+ parsed = JSON.parse(readFileSync(cachePath(repoRoot), "utf8"));
129
+ } catch {
130
+ }
131
+ cacheMem.set(repoRoot, parsed);
132
+ return parsed;
133
+ }
134
+
135
+ // src/ui.ts
136
+ import { confirm as inqConfirm, input as inqInput, select as inqSelect } from "@inquirer/prompts";
137
+ import chalk from "chalk";
138
+ import ora from "ora";
139
+ function fail(msg) {
140
+ console.error(chalk.red("\u2717"), msg);
141
+ process.exit(1);
142
+ }
143
+ var log = {
144
+ info(msg) {
145
+ console.log(msg);
146
+ },
147
+ success(msg) {
148
+ console.log(chalk.green("\u2713"), msg);
149
+ },
150
+ warn(msg) {
151
+ console.log(chalk.yellow("\u26A0"), msg);
152
+ },
153
+ dim(msg) {
154
+ console.log(chalk.gray(msg));
155
+ }
156
+ };
157
+ function spinner(label) {
158
+ return ora(label);
159
+ }
160
+ var confirm = inqConfirm;
161
+ var input = inqInput;
162
+ var select = inqSelect;
163
+ function formatSize(bytes) {
164
+ if (bytes < 1024) return `${bytes} B`;
165
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
166
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
167
+ }
168
+ function formatCommit(c) {
169
+ return `${chalk.yellow(c.id.slice(0, 7))} ${c.message} ${chalk.gray(timeAgo(c.timestamp))}`;
170
+ }
171
+ function timeAgo(ts) {
172
+ const s = Math.floor((Date.now() - ts) / 1e3);
173
+ if (s < 60) return `${s}s ago`;
174
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
175
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
176
+ return `${Math.floor(s / 86400)}d ago`;
177
+ }
178
+
179
+ // src/commands/init.ts
180
+ function register(program2) {
181
+ program2.command("init").option("--name <name>", "repository name").option("--public", "default visibility").option("--private").action((opts) => {
182
+ const cwd = process.cwd();
183
+ if (existsSync2(join2(cwd, ".iqgit"))) fail("already an iqgit repo");
184
+ initRepo(cwd);
185
+ writeConfig(cwd, {
186
+ owner: "",
187
+ repo: opts.name ?? "",
188
+ isPublic: !opts.private
189
+ });
190
+ log.success("Initialized empty iqgit repo in .iqgit/");
191
+ log.dim("Next: iqgit create <name> [--public|--private]");
192
+ });
193
+ }
194
+
195
+ // src/commands/create.ts
196
+ import { existsSync as existsSync4 } from "fs";
197
+ import { join as join4 } from "path";
198
+
199
+ // src/setup.ts
200
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
201
+ import { homedir } from "os";
202
+ import { dirname as dirname2, join as join3 } from "path";
203
+ import { Connection, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
204
+ import iqlabs from "@iqlabs-official/solana-sdk";
205
+ import { GitClient } from "@iqlabs-official/git-sdk/node";
206
+ import { config as loadEnv } from "dotenv";
207
+ var HOME_DIR = join3(homedir(), ".iq-git");
208
+ var ENV_PATH = join3(HOME_DIR, ".env");
209
+ var CONFIG_PATH = join3(HOME_DIR, "config.json");
210
+ var DEFAULT_WALLET = join3(HOME_DIR, "wallets", "default.json");
211
+ var HELIUS_URL = "https://www.helius.dev/";
212
+ async function setup() {
213
+ mkdirSync2(HOME_DIR, { recursive: true });
214
+ loadEnv({ path: ENV_PATH });
215
+ const signer = await loadOrCreateWallet();
216
+ const rpcUrl = await loadOrPromptRpc();
217
+ iqlabs.setRpcUrl(rpcUrl);
218
+ const connection = new Connection(rpcUrl, "confirmed");
219
+ await healthCheck(connection);
220
+ await maybeWarnBalance(connection, signer);
221
+ return { client: new GitClient({ connection, signer }), signer, connection };
222
+ }
223
+ async function setupReadOnly() {
224
+ mkdirSync2(HOME_DIR, { recursive: true });
225
+ loadEnv({ path: ENV_PATH });
226
+ const rpcUrl = await loadOrPromptRpc();
227
+ iqlabs.setRpcUrl(rpcUrl);
228
+ return new Connection(rpcUrl, "confirmed");
229
+ }
230
+ async function loadOrCreateWallet() {
231
+ const cfg = readGlobalConfig();
232
+ let path = cfg.walletPath;
233
+ if (!path || !existsSync3(path)) {
234
+ const choice = await select({
235
+ message: "No Solana keypair configured. What would you like to do?",
236
+ choices: [
237
+ { name: "Generate a new keypair", value: "new" },
238
+ { name: "Use an existing keypair file", value: "existing" }
239
+ ]
240
+ });
241
+ if (choice === "new") {
242
+ mkdirSync2(dirname2(DEFAULT_WALLET), { recursive: true });
243
+ const kp = Keypair.generate();
244
+ writeFileSync2(DEFAULT_WALLET, JSON.stringify(Array.from(kp.secretKey)));
245
+ log.success(`Created ${DEFAULT_WALLET}`);
246
+ log.info(`pubkey: ${kp.publicKey.toBase58()}`);
247
+ writeGlobalConfig({ ...cfg, walletPath: DEFAULT_WALLET });
248
+ return kp;
249
+ }
250
+ path = await input({ message: "Path to keypair JSON file:" });
251
+ writeGlobalConfig({ ...cfg, walletPath: path });
252
+ }
253
+ return loadKeypairFromFile(path);
254
+ }
255
+ function loadKeypairFromFile(path) {
256
+ try {
257
+ const secret = JSON.parse(readFileSync2(path, "utf8"));
258
+ return Keypair.fromSecretKey(Uint8Array.from(secret));
259
+ } catch (e) {
260
+ fail(`Invalid keypair at ${path}: ${e.message}`);
261
+ }
262
+ }
263
+ async function loadOrPromptRpc() {
264
+ const cfg = readGlobalConfig();
265
+ const url = process.env.SOLANA_RPC_ENDPOINT ?? cfg.rpcUrl;
266
+ if (url) return url;
267
+ log.warn("No Solana RPC endpoint configured.");
268
+ log.info(`Get a free RPC URL from Helius: ${HELIUS_URL}`);
269
+ const entered = await input({ message: "Paste your RPC URL:" });
270
+ appendEnv("SOLANA_RPC_ENDPOINT", entered);
271
+ writeGlobalConfig({ ...cfg, rpcUrl: entered });
272
+ return entered;
273
+ }
274
+ async function healthCheck(connection) {
275
+ try {
276
+ await connection.getLatestBlockhash();
277
+ } catch (e) {
278
+ fail(`RPC health check failed: ${e.message}`);
279
+ }
280
+ }
281
+ async function maybeWarnBalance(connection, signer) {
282
+ const lamports = await connection.getBalance(signer.publicKey);
283
+ if (lamports >= 0.01 * LAMPORTS_PER_SOL) return;
284
+ log.warn(
285
+ `Low balance: ${lamports / LAMPORTS_PER_SOL} SOL on ${signer.publicKey.toBase58()}`
286
+ );
287
+ log.dim("On-chain writes need SOL \u2014 fund the address before pushing.");
288
+ }
289
+ function readGlobalConfig() {
290
+ try {
291
+ return JSON.parse(readFileSync2(CONFIG_PATH, "utf8"));
292
+ } catch {
293
+ return {};
294
+ }
295
+ }
296
+ function writeGlobalConfig(cfg) {
297
+ mkdirSync2(HOME_DIR, { recursive: true });
298
+ writeFileSync2(CONFIG_PATH, JSON.stringify(cfg, null, 2));
299
+ }
300
+ function appendEnv(key, value) {
301
+ const line = `${key}=${value}
302
+ `;
303
+ let existing = "";
304
+ try {
305
+ existing = readFileSync2(ENV_PATH, "utf8");
306
+ } catch {
307
+ }
308
+ const stripped = existing.split("\n").filter((l) => !l.startsWith(`${key}=`)).join("\n");
309
+ writeFileSync2(ENV_PATH, stripped.endsWith("\n") || !stripped ? stripped + line : `${stripped}
310
+ ${line}`);
311
+ process.env[key] = value;
312
+ }
313
+
314
+ // src/commands/create.ts
315
+ function register2(program2) {
316
+ program2.command("create <name>").option("--public", "default visibility").option("--private").option("--description <text>", "", "").action(async (name, opts) => {
317
+ const cwd = process.cwd();
318
+ if (!existsSync4(join4(cwd, ".iqgit"))) initRepo(cwd);
319
+ const { client, signer } = await setup();
320
+ const isPublic = !opts.private;
321
+ const sp = spinner(`Creating on-chain repo ${name}...`).start();
322
+ try {
323
+ await client.createRepo({
324
+ name,
325
+ description: opts.description,
326
+ isPublic,
327
+ timestamp: Date.now()
328
+ });
329
+ sp.succeed(`Created ${signer.publicKey.toBase58()}/${name}`);
330
+ } catch (e) {
331
+ sp.fail(e.message);
332
+ process.exit(1);
333
+ }
334
+ writeConfig(cwd, {
335
+ owner: signer.publicKey.toBase58(),
336
+ repo: name,
337
+ isPublic
338
+ });
339
+ log.dim(`Next: edit files, then iqgit commit -m "..." && iqgit push`);
340
+ });
341
+ }
342
+
343
+ // src/commands/add.ts
344
+ import { existsSync as existsSync5, statSync } from "fs";
345
+ import { isAbsolute, relative as relative2, resolve, sep as sep2 } from "path";
346
+
347
+ // src/core/scan.ts
348
+ import { createHash } from "crypto";
349
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
350
+ import { join as join5, relative, sep } from "path";
351
+ import ignore from "ignore";
352
+ function readScanFile(repoRoot, repoRel) {
353
+ const buf = readFileSync3(join5(repoRoot, repoRel));
354
+ const base64 = buf.toString("base64");
355
+ return {
356
+ path: repoRel,
357
+ base64,
358
+ hash: createHash("sha256").update(base64).digest("hex"),
359
+ size: buf.byteLength
360
+ };
361
+ }
362
+ function loadIgnore(repoRoot) {
363
+ const ig = ignore().add([".git/", ".iqgit/"]);
364
+ for (const name of [".gitignore", ".iqgitignore"]) {
365
+ try {
366
+ ig.add(readFileSync3(join5(repoRoot, name), "utf8"));
367
+ } catch {
368
+ }
369
+ }
370
+ return ig;
371
+ }
372
+ function scan(repoRoot) {
373
+ const out = [];
374
+ walk(repoRoot, repoRoot, loadIgnore(repoRoot), out);
375
+ return out;
376
+ }
377
+ function toScanMap(files) {
378
+ return Object.fromEntries(files.map((f) => [f.path, f.base64]));
379
+ }
380
+ function walk(root, dir, ig, out) {
381
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
382
+ const abs = join5(dir, entry.name);
383
+ const rel = relative(root, abs).split(sep).join("/");
384
+ const relForIgnore = entry.isDirectory() ? `${rel}/` : rel;
385
+ if (ig.ignores(relForIgnore)) continue;
386
+ if (entry.isDirectory()) {
387
+ walk(root, abs, ig, out);
388
+ continue;
389
+ }
390
+ if (entry.isFile()) {
391
+ out.push(readScanFile(root, rel));
392
+ }
393
+ }
394
+ }
395
+
396
+ // src/commands/add.ts
397
+ function register3(program2) {
398
+ program2.command("add <pathspec...>").description("stage paths for the next commit").action((pathspecs) => {
399
+ const cwd = findRepoRoot();
400
+ const allPaths = scan(cwd).map((f) => f.path);
401
+ const ig = loadIgnore(cwd);
402
+ const collected = new Set(readIndex(cwd));
403
+ for (const spec of pathspecs) {
404
+ const abs = isAbsolute(spec) ? spec : resolve(process.cwd(), spec);
405
+ if (!existsSync5(abs)) fail(`path does not exist: ${spec}`);
406
+ if (statSync(abs).isDirectory()) {
407
+ const prefix = relative2(cwd, abs).split(sep2).join("/");
408
+ const matches = prefix === "" ? allPaths : allPaths.filter((p) => p === prefix || p.startsWith(`${prefix}/`));
409
+ for (const p of matches) collected.add(p);
410
+ } else {
411
+ const rel = relative2(cwd, abs).split(sep2).join("/");
412
+ if (ig.ignores(rel)) {
413
+ log.dim(`ignored: ${rel}`);
414
+ continue;
415
+ }
416
+ collected.add(rel);
417
+ }
418
+ }
419
+ writeIndex(cwd, [...collected]);
420
+ log.success(`staged ${collected.size} path(s)`);
421
+ });
422
+ }
423
+
424
+ // src/commands/reset.ts
425
+ import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
426
+ function register4(program2) {
427
+ program2.command("reset [pathspec...]").description("unstage paths (no args clears the index)").action((pathspecs) => {
428
+ const cwd = findRepoRoot();
429
+ if (pathspecs.length === 0) {
430
+ writeIndex(cwd, []);
431
+ log.success("index cleared");
432
+ return;
433
+ }
434
+ const removeRels = pathspecs.map((spec) => {
435
+ const abs = isAbsolute2(spec) ? spec : resolve2(process.cwd(), spec);
436
+ return relative3(cwd, abs).split(sep3).join("/");
437
+ });
438
+ const next = readIndex(cwd).filter(
439
+ (p) => !removeRels.some((r) => p === r || p.startsWith(`${r}/`))
440
+ );
441
+ writeIndex(cwd, next);
442
+ log.success(`index now has ${next.length} path(s)`);
443
+ });
444
+ }
445
+
446
+ // src/commands/commit.ts
447
+ import { randomUUID } from "crypto";
448
+ import { existsSync as existsSync6 } from "fs";
449
+ import { join as join6 } from "path";
450
+
451
+ // src/core/tree.ts
452
+ import {
453
+ IQGIT_ROOT_ID,
454
+ commitTableHint
455
+ } from "@iqlabs-official/git-sdk/node";
456
+ import { getDbRootPda, getTablePda } from "@iqlabs-official/solana-sdk/contract";
457
+ import { toSeedBytes } from "@iqlabs-official/solana-sdk/utils";
458
+
459
+ // src/core/gateway.ts
460
+ import iqlabs2 from "@iqlabs-official/solana-sdk";
461
+ import { loadBlob, loadTree } from "@iqlabs-official/git-sdk/node";
462
+ var DEFAULT_GATEWAYS = [
463
+ "https://gateway.solanainternet.com",
464
+ "https://gateway.iqlabs.dev",
465
+ "https://fem4pe7sthdm5f9fkhc1fnmpos.ingress.akashprovid.com",
466
+ "https://fem4pe7sthdm5f9fkhc1fnmpos.ingress.cpu.aesservices.net"
467
+ ];
468
+ function getGatewayUrls() {
469
+ const raw = process.env.GATEWAY_URL?.trim();
470
+ if (raw === void 0 || raw === "") return DEFAULT_GATEWAYS;
471
+ if (raw.toLowerCase() === "off") return [];
472
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
473
+ }
474
+ async function gwFetch(path, timeoutMs = 8e3) {
475
+ for (const base of getGatewayUrls()) {
476
+ try {
477
+ const res = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(timeoutMs) });
478
+ if (res.ok) return res;
479
+ } catch {
480
+ }
481
+ }
482
+ return null;
483
+ }
484
+ async function gwFetchRows(tablePda, limit = 50, before) {
485
+ const path = `/table/${tablePda}/rows?limit=${limit}` + (before ? `&before=${before}` : "");
486
+ const res = await gwFetch(path);
487
+ if (res !== null) {
488
+ try {
489
+ const data = await res.json();
490
+ return data.rows ?? [];
491
+ } catch {
492
+ }
493
+ }
494
+ return iqlabs2.reader.readTableRows(tablePda, { limit, before });
495
+ }
496
+ async function gwFetchAllRows(tablePda, maxRows = 200) {
497
+ const all = [];
498
+ let before;
499
+ while (all.length < maxRows) {
500
+ const limit = Math.min(50, maxRows - all.length);
501
+ const rows = await gwFetchRows(tablePda, limit, before);
502
+ all.push(...rows);
503
+ if (rows.length < limit) break;
504
+ const last = rows[rows.length - 1];
505
+ if (!last.__txSignature) break;
506
+ before = last.__txSignature;
507
+ }
508
+ return all;
509
+ }
510
+ async function gwLoadTreeJson(txSignature) {
511
+ const res = await gwFetch(`/data/${txSignature}`);
512
+ if (res !== null) {
513
+ try {
514
+ const env = await res.json();
515
+ if (typeof env.data === "string") return JSON.parse(env.data);
516
+ } catch {
517
+ }
518
+ }
519
+ return await loadTree(txSignature);
520
+ }
521
+ async function gwLoadBlobBase64(txSignature) {
522
+ const res = await gwFetch(`/data/${txSignature}`);
523
+ if (res !== null) {
524
+ try {
525
+ const env = await res.json();
526
+ if (typeof env.data === "string") return env.data;
527
+ } catch {
528
+ }
529
+ }
530
+ return await loadBlob(txSignature);
531
+ }
532
+ async function gwNotify(tablePda, txSignature, row, signer) {
533
+ const body = JSON.stringify({ txSignature, row, signer });
534
+ for (const base of getGatewayUrls()) {
535
+ try {
536
+ const res = await fetch(`${base}/table/${tablePda}/notify`, {
537
+ method: "POST",
538
+ headers: { "Content-Type": "application/json" },
539
+ body,
540
+ signal: AbortSignal.timeout(3e3)
541
+ });
542
+ if (res.ok) return;
543
+ } catch {
544
+ }
545
+ }
546
+ }
547
+
548
+ // src/core/tree.ts
549
+ async function resolveBaseTree(cwd, owner, repoName) {
550
+ const last = listPending(cwd).at(-1);
551
+ if (last) return readPendingTree(last);
552
+ const head = readHead(cwd);
553
+ if (head === null) return {};
554
+ const dbRoot = getDbRootPda(toSeedBytes(IQGIT_ROOT_ID));
555
+ const commitTable = getTablePda(dbRoot, toSeedBytes(commitTableHint(owner, repoName)));
556
+ const history = await gwFetchAllRows(commitTable.toBase58(), 200);
557
+ const headCommit = history.find((c) => c.id === head);
558
+ if (!headCommit) {
559
+ throw new Error(`HEAD ${head.slice(0, 7)} not found in on-chain history`);
560
+ }
561
+ const onChain = await gwLoadTreeJson(headCommit.treeTxId);
562
+ const out = {};
563
+ for (const [p, entry] of Object.entries(onChain)) {
564
+ out[p] = { hash: entry.hash, size: 0 };
565
+ }
566
+ return out;
567
+ }
568
+
569
+ // src/commands/commit.ts
570
+ var LARGE_FILE_THRESHOLD = 1024 * 1024;
571
+ function register5(program2) {
572
+ program2.command("commit").requiredOption("-m, --message <msg>", "commit message").option("--no-warn-large", "skip 1MB confirmation prompt").action(async (opts) => {
573
+ const cwd = findRepoRoot();
574
+ const cfg = readConfig(cwd);
575
+ if (!cfg.owner) fail("repo not yet created on-chain \u2014 run iqgit create first");
576
+ const indexPaths = readIndex(cwd);
577
+ if (indexPaths.length === 0) {
578
+ fail("nothing staged \u2014 use `iqgit add <path>` first");
579
+ }
580
+ const stagedFiles = [];
581
+ const stagedDeletes = [];
582
+ for (const p of indexPaths) {
583
+ if (existsSync6(join6(cwd, p))) {
584
+ stagedFiles.push(readScanFile(cwd, p));
585
+ } else {
586
+ stagedDeletes.push(p);
587
+ }
588
+ }
589
+ const big = stagedFiles.filter((f) => f.size > LARGE_FILE_THRESHOLD);
590
+ if (big.length > 0 && opts.warnLarge) {
591
+ log.warn("Large files (>1MB) \u2014 on-chain upload will be costly:");
592
+ for (const b of big) log.info(` ${b.path} ${formatSize(b.size)}`);
593
+ const ok = await confirm({ message: "Include them?" });
594
+ if (!ok) {
595
+ log.dim("Tip: add to .iqgitignore to skip on-chain upload only.");
596
+ return;
597
+ }
598
+ }
599
+ await setupReadOnly();
600
+ const sp = spinner("fetching base tree from chain...").start();
601
+ let baseTree;
602
+ try {
603
+ baseTree = await resolveBaseTree(cwd, cfg.owner, cfg.repo);
604
+ sp.succeed("fetched base tree");
605
+ } catch (e) {
606
+ sp.fail(e.message);
607
+ throw e;
608
+ }
609
+ const tree = { ...baseTree };
610
+ const blobs = /* @__PURE__ */ new Map();
611
+ for (const f of stagedFiles) {
612
+ tree[f.path] = { hash: f.hash, size: f.size };
613
+ blobs.set(f.hash, f.base64);
614
+ }
615
+ for (const p of stagedDeletes) {
616
+ delete tree[p];
617
+ }
618
+ const last = listPending(cwd).at(-1);
619
+ const meta = {
620
+ id: randomUUID(),
621
+ message: opts.message,
622
+ parentCommitId: last?.meta.id ?? readHead(cwd),
623
+ timestamp: Date.now(),
624
+ author: cfg.owner,
625
+ treeTxId: null,
626
+ committedSig: null
627
+ };
628
+ appendPending(cwd, meta, tree, blobs);
629
+ writeIndex(cwd, []);
630
+ const totalSize = stagedFiles.reduce((sum, f) => sum + f.size, 0);
631
+ log.success(`commit ${meta.id.slice(0, 7)} saved locally`);
632
+ log.info(
633
+ ` ${stagedFiles.length} file(s) added/modified, ${stagedDeletes.length} removed, ${formatSize(totalSize)}`
634
+ );
635
+ log.dim("Run: iqgit push to upload on-chain");
636
+ });
637
+ }
638
+
639
+ // src/commands/push.ts
640
+ import {
641
+ IQGIT_ROOT_ID as IQGIT_ROOT_ID2,
642
+ commitTableHint as commitTableHint2,
643
+ uploadBlob,
644
+ uploadTree,
645
+ writeCommit
646
+ } from "@iqlabs-official/git-sdk/node";
647
+ import { getDbRootPda as getDbRootPda2, getTablePda as getTablePda2 } from "@iqlabs-official/solana-sdk/contract";
648
+ import { toSeedBytes as toSeedBytes2 } from "@iqlabs-official/solana-sdk/utils";
649
+ function register6(program2) {
650
+ program2.command("push").action(async () => {
651
+ const cwd = findRepoRoot();
652
+ const cfg = readConfig(cwd);
653
+ const queue = listPending(cwd);
654
+ if (queue.length === 0) {
655
+ log.info("nothing to push");
656
+ return;
657
+ }
658
+ const { signer, connection } = await setup();
659
+ for (const p of queue) {
660
+ log.info(`pushing ${p.meta.id.slice(0, 7)} "${p.meta.message}"`);
661
+ try {
662
+ await pushOne(connection, signer, cwd, cfg.repo, p);
663
+ } catch (e) {
664
+ fail(
665
+ `failed at ${p.meta.id.slice(0, 7)}: ${e.message}
666
+ re-run "iqgit push" to resume.`
667
+ );
668
+ }
669
+ writeHead(cwd, p.meta.id);
670
+ discardPending(p);
671
+ log.success(` pushed ${p.meta.id.slice(0, 7)}`);
672
+ }
673
+ });
674
+ }
675
+ async function pushOne(connection, signer, cwd, repoName, p) {
676
+ const tree = readPendingTree(p);
677
+ const newTree = {};
678
+ const total = Object.keys(tree).length;
679
+ const sp = spinner(` blobs 0/${total}`).start();
680
+ let done = 0;
681
+ let reused = 0;
682
+ for (const [path, { hash }] of Object.entries(tree)) {
683
+ const cached = cacheGet(cwd, hash);
684
+ if (cached) {
685
+ newTree[path] = { txId: cached.txId, hash };
686
+ reused++;
687
+ } else {
688
+ const base64 = readPendingBlob(p, hash);
689
+ const { txId } = await uploadBlob(connection, signer, path, base64, {});
690
+ cacheSet(cwd, hash, { txId, uploadedAt: Date.now() });
691
+ newTree[path] = { txId, hash };
692
+ }
693
+ done++;
694
+ sp.text = ` blobs ${done}/${total} (${reused} reused)`;
695
+ }
696
+ sp.succeed(` blobs ${done}/${total} (${reused} reused)`);
697
+ let treeTxId = p.meta.treeTxId;
698
+ if (!treeTxId) {
699
+ treeTxId = await uploadTree(connection, signer, newTree);
700
+ updatePendingMeta(p, { treeTxId });
701
+ }
702
+ if (!p.meta.committedSig) {
703
+ const commit = {
704
+ id: p.meta.id,
705
+ message: p.meta.message,
706
+ treeTxId,
707
+ parentCommitId: p.meta.parentCommitId ?? void 0,
708
+ timestamp: p.meta.timestamp,
709
+ author: p.meta.author
710
+ };
711
+ const sig = await writeCommit(connection, signer, repoName, commit);
712
+ updatePendingMeta(p, { committedSig: sig });
713
+ const dbRoot = getDbRootPda2(toSeedBytes2(IQGIT_ROOT_ID2));
714
+ const tablePda = getTablePda2(dbRoot, toSeedBytes2(commitTableHint2(commit.author, repoName)));
715
+ void gwNotify(tablePda.toBase58(), sig, void 0, signer.publicKey.toBase58());
716
+ }
717
+ }
718
+
719
+ // src/commands/clone.ts
720
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
721
+ import { dirname as dirname3, join as join7 } from "path";
722
+ import { Buffer as Buffer2 } from "buffer";
723
+ import {
724
+ IQGIT_ROOT_ID as IQGIT_ROOT_ID3,
725
+ commitTableHint as commitTableHint3,
726
+ repoListHint
727
+ } from "@iqlabs-official/git-sdk/node";
728
+ import { getDbRootPda as getDbRootPda3, getTablePda as getTablePda3 } from "@iqlabs-official/solana-sdk/contract";
729
+ import { toSeedBytes as toSeedBytes3 } from "@iqlabs-official/solana-sdk/utils";
730
+ function register7(program2) {
731
+ program2.command("clone <slug> [dir]").action(async (slug, dir) => {
732
+ const [owner, repoName] = slug.split("/");
733
+ if (!owner || !repoName) fail("expected <owner>/<repo>");
734
+ const target = dir ?? repoName;
735
+ if (existsSync7(target)) fail(`${target} already exists`);
736
+ mkdirSync3(target, { recursive: true });
737
+ await setupReadOnly();
738
+ const dbRoot = getDbRootPda3(toSeedBytes3(IQGIT_ROOT_ID3));
739
+ const sp = spinner(`cloning ${slug}...`).start();
740
+ const commitTable = getTablePda3(dbRoot, toSeedBytes3(commitTableHint3(owner, repoName)));
741
+ const commits = await gwFetchAllRows(commitTable.toBase58(), 200);
742
+ if (commits.length === 0) {
743
+ sp.fail(`no commits in ${slug}`);
744
+ process.exit(1);
745
+ }
746
+ const latest = [...commits].sort(
747
+ (a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)
748
+ )[0];
749
+ const tree = await gwLoadTreeJson(latest.treeTxId);
750
+ const paths = Object.entries(tree);
751
+ let i = 0;
752
+ for (const [path, entry] of paths) {
753
+ i++;
754
+ sp.text = `cloning ${slug}... (${i}/${paths.length}) ${path}`;
755
+ const base64 = await gwLoadBlobBase64(entry.txId);
756
+ const abs = join7(target, path);
757
+ mkdirSync3(dirname3(abs), { recursive: true });
758
+ writeFileSync3(abs, Buffer2.from(base64, "base64"));
759
+ }
760
+ sp.succeed(`cloned ${paths.length} file(s) to ${target}/`);
761
+ const ownerListPda = getTablePda3(dbRoot, toSeedBytes3(repoListHint(owner)));
762
+ const repos = await gwFetchAllRows(ownerListPda.toBase58(), 200);
763
+ const meta = repos.find((r) => r.name === repoName);
764
+ const isPublic = meta?.isPublic ?? true;
765
+ initRepo(target);
766
+ writeConfig(target, { owner, repo: repoName, isPublic });
767
+ writeHead(target, latest.id);
768
+ });
769
+ }
770
+
771
+ // src/commands/restore.ts
772
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
773
+ import { dirname as dirname4, join as join8 } from "path";
774
+ function register8(program2) {
775
+ program2.command("restore [commitId]").option("--force", "discard uncommitted changes").action(async (commitId, opts) => {
776
+ const cwd = findRepoRoot();
777
+ const cfg = readConfig(cwd);
778
+ const { client } = await setup();
779
+ if (!opts.force) {
780
+ const { added, modified } = await client.status(
781
+ cfg.owner,
782
+ cfg.repo,
783
+ toScanMap(scan(cwd))
784
+ );
785
+ if (added.length || modified.length) {
786
+ fail("uncommitted changes \u2014 commit them or pass --force");
787
+ }
788
+ }
789
+ const target = commitId ?? "latest";
790
+ const sp = spinner(`restoring ${target}...`).start();
791
+ const c = await client.checkout(cfg.repo, target, async (path, base64) => {
792
+ const abs = join8(cwd, path);
793
+ mkdirSync4(dirname4(abs), { recursive: true });
794
+ writeFileSync4(abs, Buffer.from(base64, "base64"));
795
+ });
796
+ sp.succeed(`restored ${c.id.slice(0, 7)} "${c.message}"`);
797
+ if (target === "latest") writeHead(cwd, c.id);
798
+ });
799
+ }
800
+
801
+ // src/commands/log.ts
802
+ import {
803
+ IQGIT_ROOT_ID as IQGIT_ROOT_ID4,
804
+ commitTableHint as commitTableHint4
805
+ } from "@iqlabs-official/git-sdk/node";
806
+ import { getDbRootPda as getDbRootPda4, getTablePda as getTablePda4 } from "@iqlabs-official/solana-sdk/contract";
807
+ import { toSeedBytes as toSeedBytes4 } from "@iqlabs-official/solana-sdk/utils";
808
+ function register9(program2) {
809
+ program2.command("log").option("--limit <n>", "max entries", "20").option("--owner <pubkey>").option("--repo <name>").action(async (opts) => {
810
+ let owner = opts.owner;
811
+ let repoName = opts.repo;
812
+ if (!owner || !repoName) {
813
+ const cfg = readConfig(findRepoRoot());
814
+ owner ??= cfg.owner;
815
+ repoName ??= cfg.repo;
816
+ }
817
+ await setupReadOnly();
818
+ const dbRoot = getDbRootPda4(toSeedBytes4(IQGIT_ROOT_ID4));
819
+ const tablePda = getTablePda4(dbRoot, toSeedBytes4(commitTableHint4(owner, repoName)));
820
+ const commits = await gwFetchAllRows(tablePda.toBase58(), Number(opts.limit));
821
+ const sorted = [...commits].sort((a, b) => b.timestamp - a.timestamp);
822
+ for (const c of sorted) {
823
+ log.info(formatCommit(c));
824
+ log.dim(` author: ${c.author}`);
825
+ log.dim(` tree: ${c.treeTxId}`);
826
+ log.info("");
827
+ }
828
+ });
829
+ }
830
+
831
+ // src/commands/status.ts
832
+ function register10(program2) {
833
+ program2.command("status").action(async () => {
834
+ const cwd = findRepoRoot();
835
+ const cfg = readConfig(cwd);
836
+ const head = readHead(cwd);
837
+ const pending = listPending(cwd);
838
+ const index = new Set(readIndex(cwd));
839
+ const files = scan(cwd);
840
+ const filesByPath = new Map(files.map((f) => [f.path, f]));
841
+ if (pending.length === 0 && head !== null) await setupReadOnly();
842
+ const base = await resolveBaseTree(cwd, cfg.owner, cfg.repo);
843
+ const stagedAddedOrModified = [];
844
+ const stagedDeleted = [];
845
+ const unstagedAdded = [];
846
+ const unstagedModified = [];
847
+ const unstagedDeleted = [];
848
+ const allPaths = /* @__PURE__ */ new Set([...Object.keys(base), ...files.map((f) => f.path)]);
849
+ for (const p of allPaths) {
850
+ const inBase = base[p];
851
+ const inWorking = filesByPath.get(p);
852
+ const isStaged = index.has(p);
853
+ if (inWorking && !inBase) {
854
+ (isStaged ? stagedAddedOrModified : unstagedAdded).push(p);
855
+ } else if (!inWorking && inBase) {
856
+ (isStaged ? stagedDeleted : unstagedDeleted).push(p);
857
+ } else if (inWorking && inBase && inWorking.hash !== inBase.hash) {
858
+ (isStaged ? stagedAddedOrModified : unstagedModified).push(p);
859
+ }
860
+ }
861
+ log.info(`On HEAD: ${head ? head.slice(0, 7) : "(no commits yet)"}`);
862
+ if (pending.length) {
863
+ log.info("");
864
+ log.info(`Pending commits (not yet pushed): ${pending.length}`);
865
+ for (const p of pending) {
866
+ log.info(` ${p.meta.id.slice(0, 7)} "${p.meta.message}"`);
867
+ }
868
+ }
869
+ if (stagedAddedOrModified.length || stagedDeleted.length) {
870
+ log.info("");
871
+ log.info("Staged for next commit:");
872
+ for (const f of stagedAddedOrModified) log.info(` ${f}`);
873
+ for (const f of stagedDeleted) log.info(` deleted: ${f}`);
874
+ }
875
+ if (unstagedAdded.length || unstagedModified.length || unstagedDeleted.length) {
876
+ log.info("");
877
+ log.info("Unstaged changes:");
878
+ for (const f of unstagedAdded) log.info(` added: ${f}`);
879
+ for (const f of unstagedModified) log.info(` modified: ${f}`);
880
+ for (const f of unstagedDeleted) log.info(` deleted: ${f}`);
881
+ }
882
+ const clean = pending.length === 0 && stagedAddedOrModified.length === 0 && stagedDeleted.length === 0 && unstagedAdded.length === 0 && unstagedModified.length === 0 && unstagedDeleted.length === 0;
883
+ if (clean) log.info("clean");
884
+ });
885
+ }
886
+
887
+ // src/commands/registry.ts
888
+ import {
889
+ IQGIT_ROOT_ID as IQGIT_ROOT_ID5,
890
+ REGISTRY_HINT
891
+ } from "@iqlabs-official/git-sdk/node";
892
+ import { getDbRootPda as getDbRootPda5, getTablePda as getTablePda5 } from "@iqlabs-official/solana-sdk/contract";
893
+ import { toSeedBytes as toSeedBytes5 } from "@iqlabs-official/solana-sdk/utils";
894
+ function register11(program2) {
895
+ program2.command("registry").description("browse the public on-chain repo gallery").option("--limit <n>", "max entries", "20").action(async (opts) => {
896
+ await setupReadOnly();
897
+ const dbRoot = getDbRootPda5(toSeedBytes5(IQGIT_ROOT_ID5));
898
+ const tablePda = getTablePda5(dbRoot, toSeedBytes5(REGISTRY_HINT));
899
+ const entries = await gwFetchAllRows(tablePda.toBase58(), Number(opts.limit));
900
+ if (entries.length === 0) {
901
+ log.dim("registry empty");
902
+ return;
903
+ }
904
+ for (const e of entries) {
905
+ log.info(`${e.owner.slice(0, 8)}\u2026/${e.repo} ${e.description ?? ""}`);
906
+ }
907
+ });
908
+ }
909
+
910
+ // src/commands/config.ts
911
+ var KNOWN_KEYS = ["walletPath", "rpcUrl"];
912
+ function register12(program2) {
913
+ program2.command("config [key] [value]").option("--unset <key>").action((key, value, opts) => {
914
+ const cfg = readGlobalConfig();
915
+ if (opts.unset) {
916
+ ensureKnown(opts.unset);
917
+ delete cfg[opts.unset];
918
+ writeGlobalConfig(cfg);
919
+ log.success(`unset ${opts.unset}`);
920
+ return;
921
+ }
922
+ if (!key) {
923
+ for (const [k, v] of Object.entries(cfg)) {
924
+ if (v !== void 0) log.info(`${k}=${v}`);
925
+ }
926
+ return;
927
+ }
928
+ ensureKnown(key);
929
+ if (value === void 0) {
930
+ log.info(cfg[key] ?? "");
931
+ return;
932
+ }
933
+ cfg[key] = value;
934
+ writeGlobalConfig(cfg);
935
+ log.success(`set ${key}=${value}`);
936
+ });
937
+ }
938
+ function ensureKnown(key) {
939
+ if (!KNOWN_KEYS.includes(key)) {
940
+ fail(`unknown config key: ${key}
941
+ known: ${KNOWN_KEYS.join(", ")}`);
942
+ }
943
+ }
944
+
945
+ // src/commands/wallet.ts
946
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
947
+ import { dirname as dirname5 } from "path";
948
+ import { Keypair as Keypair2, LAMPORTS_PER_SOL as LAMPORTS_PER_SOL2 } from "@solana/web3.js";
949
+ import { IQGIT_ROOT_ID as IQGIT_ROOT_ID6, repoListHint as repoListHint2 } from "@iqlabs-official/git-sdk/node";
950
+ import { getDbRootPda as getDbRootPda6, getTablePda as getTablePda6 } from "@iqlabs-official/solana-sdk/contract";
951
+ import { toSeedBytes as toSeedBytes6 } from "@iqlabs-official/solana-sdk/utils";
952
+ function register13(program2) {
953
+ program2.command("wallet <action>").description("new | show | balance | repos").action(async (action) => {
954
+ switch (action) {
955
+ case "new":
956
+ return walletNew();
957
+ case "show":
958
+ return walletShow();
959
+ case "balance":
960
+ return walletBalance();
961
+ case "repos":
962
+ return walletRepos();
963
+ default:
964
+ fail(`unknown action: ${action}`);
965
+ }
966
+ });
967
+ }
968
+ function walletNew() {
969
+ if (existsSync8(DEFAULT_WALLET)) {
970
+ fail(`default wallet exists at ${DEFAULT_WALLET}; remove it first`);
971
+ }
972
+ mkdirSync5(dirname5(DEFAULT_WALLET), { recursive: true });
973
+ const kp = Keypair2.generate();
974
+ writeFileSync5(DEFAULT_WALLET, JSON.stringify(Array.from(kp.secretKey)));
975
+ log.success(`created ${DEFAULT_WALLET}`);
976
+ log.info(`pubkey: ${kp.publicKey.toBase58()}`);
977
+ }
978
+ function walletShow() {
979
+ const path = readGlobalConfig().walletPath ?? DEFAULT_WALLET;
980
+ const kp = loadKeypairFromFile(path);
981
+ log.info(`path: ${path}`);
982
+ log.info(`pubkey: ${kp.publicKey.toBase58()}`);
983
+ }
984
+ async function walletBalance() {
985
+ const { signer, connection } = await setup();
986
+ const lamports = await connection.getBalance(signer.publicKey);
987
+ log.info(signer.publicKey.toBase58());
988
+ log.info(`${lamports / LAMPORTS_PER_SOL2} SOL`);
989
+ }
990
+ async function walletRepos() {
991
+ const { signer } = await setup();
992
+ const owner = signer.publicKey.toBase58();
993
+ const dbRoot = getDbRootPda6(toSeedBytes6(IQGIT_ROOT_ID6));
994
+ const tablePda = getTablePda6(dbRoot, toSeedBytes6(repoListHint2(owner)));
995
+ const repos = await gwFetchAllRows(tablePda.toBase58(), 200);
996
+ if (repos.length === 0) {
997
+ log.dim("no repos yet");
998
+ return;
999
+ }
1000
+ for (const r of repos) {
1001
+ const visibility = r.isPublic ? "public " : "private";
1002
+ log.info(`${visibility} ${r.name} ${r.description}`);
1003
+ }
1004
+ }
1005
+
1006
+ // src/cli.ts
1007
+ var program = new Command("iqgit").description("On-chain Git for Solana").version("0.1.0");
1008
+ register(program);
1009
+ register2(program);
1010
+ register3(program);
1011
+ register4(program);
1012
+ register5(program);
1013
+ register6(program);
1014
+ register7(program);
1015
+ register8(program);
1016
+ register9(program);
1017
+ register10(program);
1018
+ register11(program);
1019
+ register12(program);
1020
+ register13(program);
1021
+ program.parseAsync(process.argv);
1022
+ //# sourceMappingURL=cli.js.map