withub-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.
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCommands = registerCommands;
4
+ const init_1 = require("./init");
5
+ const commit_1 = require("./commit");
6
+ const checkout_1 = require("./checkout");
7
+ const diff_1 = require("./diff");
8
+ const workspace_1 = require("./workspace");
9
+ const push_1 = require("./push");
10
+ const clone_1 = require("./clone");
11
+ const fetch_1 = require("./fetch");
12
+ const pull_1 = require("./pull");
13
+ const invite_1 = require("./invite");
14
+ const ui_1 = require("../lib/ui");
15
+ const account_1 = require("./account");
16
+ const walrusBlob_1 = require("./walrusBlob");
17
+ const walrusQuilt_1 = require("./walrusQuilt");
18
+ const list_1 = require("./list");
19
+ const transfer_1 = require("./transfer");
20
+ const removeUser_1 = require("./removeUser");
21
+ function registerCommands(program) {
22
+ // Global options (propagate to subcommands)
23
+ program.option('--color', 'force color output').option('--no-color', 'disable color output');
24
+ program
25
+ .command('init [name]')
26
+ .description('Initialize a wit repository (creates .wit, config, ignores)')
27
+ .option('--private', 'Initialize as private repository (generate seal policy + secret)')
28
+ .option('--seal-policy <id>', 'Use existing seal policy id for private repo')
29
+ .option('--seal-secret <secret>', 'Explicit seal secret (otherwise auto-generated/stored)')
30
+ .action((name, opts) => (0, init_1.initAction)(name, { private: opts.private, sealPolicy: opts.sealPolicy, sealSecret: opts.sealSecret }));
31
+ program
32
+ .command('status')
33
+ .description('Show workspace vs index status')
34
+ .action(workspace_1.statusAction);
35
+ program
36
+ .command('add [paths...]')
37
+ .option('-A, --all', 'add all changes (equivalent to add .)')
38
+ .description('Add file(s) to the wit index')
39
+ .action(workspace_1.addAction);
40
+ program
41
+ .command('reset [paths...]')
42
+ .option('-A, --all', 'unstage all entries from the wit index')
43
+ .description('Reset index entries for paths (like git reset -- <paths>)')
44
+ .action(workspace_1.resetAction);
45
+ program
46
+ .command('restore')
47
+ .option('--staged', 'unstage paths from the index (alias of reset)')
48
+ .argument('[paths...]')
49
+ .description('Restore worktree files from index or unstage when using --staged')
50
+ .action((paths, opts) => {
51
+ if (opts.staged) {
52
+ return (0, workspace_1.resetAction)(paths, { staged: true });
53
+ }
54
+ return (0, workspace_1.resetAction)(paths, { staged: false });
55
+ });
56
+ program
57
+ .command('commit')
58
+ .option('-m, --message <message>', 'commit message')
59
+ .description('Create a local commit (single-branch)')
60
+ .action(commit_1.commitAction);
61
+ program
62
+ .command('checkout <commit_id>')
63
+ .description('Checkout a commit snapshot to the worktree (updates index and HEAD ref)')
64
+ .action(async (commitId) => {
65
+ await (0, checkout_1.checkoutAction)(commitId);
66
+ });
67
+ program
68
+ .command('push')
69
+ .description('Upload manifest/quilt/commit to Walrus and update Sui head')
70
+ .action(push_1.pushAction);
71
+ program
72
+ .command('clone <repo_id>')
73
+ .description('Clone a wit repository from Sui/Walrus')
74
+ .action(clone_1.cloneAction);
75
+ program
76
+ .command('diff')
77
+ .option('--cached', 'compare against index instead of worktree')
78
+ .description('Show diffs between worktree/index/commit')
79
+ .action(diff_1.diffAction);
80
+ program
81
+ .command('log')
82
+ .description('Show commit history (local, single-branch)')
83
+ .action(commit_1.logAction);
84
+ program
85
+ .command('fetch')
86
+ .description('Update remote mirror (head/manifest/commit) without changing worktree')
87
+ .action(fetch_1.fetchAction);
88
+ program
89
+ .command('pull')
90
+ .description('Fetch and fast-forward to remote head when possible')
91
+ .action(pull_1.pullAction);
92
+ program
93
+ .command('invite <address>')
94
+ .description('Add a collaborator to the repository')
95
+ .option('--seal-policy <id>', 'Seal policy id to apply (defaults to repo config)')
96
+ .option('--seal-secret <secret>', 'Seal secret to save locally when setting policy')
97
+ .action((address) => (0, invite_1.inviteAction)(address));
98
+ program
99
+ .command('transfer <new_owner>')
100
+ .description('Transfer repository ownership to a new address')
101
+ .action(transfer_1.transferAction);
102
+ program
103
+ .command('remove-user <address>')
104
+ .description('Remove a collaborator from the repository')
105
+ .action(removeUser_1.removeUserAction);
106
+ program
107
+ .command('push-blob <path>')
108
+ .description('Upload a single blob to Walrus (hash-verified)')
109
+ .option('--epochs <n>', 'epochs to store blob for (default 1)', (v) => parseInt(v, 10), 1)
110
+ .option('--deletable', 'mark blob deletable (default true)', true)
111
+ .action((pathArg, opts) => (0, walrusBlob_1.pushBlobAction)(pathArg, opts));
112
+ program
113
+ .command('pull-blob <blob_id> <out_path>')
114
+ .description('Download a Walrus blob and verify hash')
115
+ .action((blobId, outPath) => (0, walrusBlob_1.pullBlobAction)(blobId, outPath));
116
+ program
117
+ .command('push-quilt <dir>')
118
+ .description('Upload a directory as Walrus files (tags + hash), emits local manifest')
119
+ .option('--epochs <n>', 'epochs to store quilt for (default 1)', (v) => parseInt(v, 10), 1)
120
+ .option('--deletable', 'mark quilt deletable (default true)', true)
121
+ .option('--manifest-out <path>', 'where to write manifest (default ./quilt-manifest.json)')
122
+ .action((dir, opts) => (0, walrusQuilt_1.pushQuiltAction)(dir, opts));
123
+ program
124
+ .command('pull-quilt <manifest_path> <out_dir>')
125
+ .description('Download files from Walrus using manifest produced by push-quilt (hash/root_hash verified)')
126
+ .action((manifestPath, outDir) => (0, walrusQuilt_1.pullQuiltAction)(manifestPath, outDir));
127
+ program
128
+ .command('quilt-cat <manifest_path> <identifier>')
129
+ .description('Fetch a single file from a quilt (by identifier) and print to stdout')
130
+ .action(async (manifestPath, identifier) => {
131
+ const { fetchQuiltFile } = await import('./walrusQuilt.js');
132
+ const { bytes } = await fetchQuiltFile(manifestPath, identifier);
133
+ process.stdout.write(Buffer.from(bytes));
134
+ });
135
+ program
136
+ .command('quilt-ls <quilt_id>')
137
+ .description('List identifiers inside a quilt (no manifest needed)')
138
+ .action((quiltId) => (0, walrusQuilt_1.listQuiltIdentifiersCommand)(quiltId));
139
+ program
140
+ .command('quilt-cat-id <quilt_id> <identifier>')
141
+ .description('Fetch a single file from a quilt by quilt_id + identifier (no manifest needed)')
142
+ .action((quiltId, identifier) => (0, walrusQuilt_1.catQuiltFileById)(quiltId, identifier));
143
+ program
144
+ .command('push-quilt-legacy <dir>')
145
+ .description('Upload directory as legacy archive (single blob with embedded manifest)')
146
+ .option('--epochs <n>', 'epochs to store archive for (default 1)', (v) => parseInt(v, 10), 1)
147
+ .option('--deletable', 'mark archive deletable (default true)', true)
148
+ .action((dir, opts) => (0, walrusQuilt_1.pushQuiltLegacyAction)(dir, opts));
149
+ program
150
+ .command('pull-quilt-legacy <blob_id> <out_dir>')
151
+ .description('Download legacy archive and restore files (hash/root_hash verified)')
152
+ .action((blobId, outDir) => (0, walrusQuilt_1.pullQuiltLegacyAction)(blobId, outDir));
153
+ program
154
+ .command('list')
155
+ .description('List repositories you own or collaborate on')
156
+ .option('--owned', 'Show only owned repositories')
157
+ .option('--collaborated', 'Show only collaborated repositories')
158
+ .action(list_1.listAction);
159
+ const account = program.command('account').description('Manage wit accounts (keys, active address)');
160
+ account.command('list').description('List locally stored accounts (keys) and show active').action(account_1.accountListAction);
161
+ account.command('use <address>').description('Set active account address (updates ~/.witconfig, author if unknown)').action(account_1.accountUseAction);
162
+ account
163
+ .command('generate')
164
+ .option('--alias <name>', 'alias to record in key file (defaults to "default")')
165
+ .description('Generate a new account (keypair), set as active, and update author if unknown')
166
+ .action(account_1.accountGenerateAction);
167
+ account
168
+ .command('balance')
169
+ .argument('[address]', 'Address to query (defaults to active)')
170
+ .description('Show SUI/WAL balance for the address (defaults to active)')
171
+ .action((address) => (0, account_1.accountBalanceAction)(address));
172
+ program.hook('preAction', (cmd) => {
173
+ const opts = cmd.optsWithGlobals ? cmd.optsWithGlobals() : program.opts();
174
+ const envDefault = process.env.WIT_NO_COLOR === undefined &&
175
+ process.env.NO_COLOR === undefined &&
176
+ (process.env.FORCE_COLOR === undefined || process.env.FORCE_COLOR !== '0');
177
+ const desired = opts.color ? true : opts.noColor ? false : envDefault;
178
+ (0, ui_1.setColorsEnabled)(desired);
179
+ if (!desired && (0, ui_1.colorsEnabled)()) {
180
+ (0, ui_1.setColorsEnabled)(false);
181
+ }
182
+ });
183
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeUserAction = removeUserAction;
4
+ const client_1 = require("@mysten/sui/client");
5
+ const keys_1 = require("../lib/keys");
6
+ const walrus_1 = require("../lib/walrus");
7
+ const suiRepo_1 = require("../lib/suiRepo");
8
+ const repo_1 = require("../lib/repo");
9
+ const ui_1 = require("../lib/ui");
10
+ async function removeUserAction(addressToRemove) {
11
+ const witPath = await (0, repo_1.requireWitDir)();
12
+ const repoCfg = await (0, repo_1.readRepoConfig)(witPath);
13
+ if (!repoCfg.repo_id) {
14
+ throw new Error('Repository not initialized on chain. Run `wit push` first.');
15
+ }
16
+ const signer = await (0, keys_1.loadSigner)();
17
+ const address = signer.address;
18
+ // Basic validation
19
+ if (!addressToRemove.startsWith('0x')) {
20
+ throw new Error('Invalid address format. Must start with 0x.');
21
+ }
22
+ console.log(ui_1.colors.header(`Removing collaborator ${addressToRemove} from ${repoCfg.repo_id}...`));
23
+ const res = await (0, keys_1.checkResources)(address);
24
+ if (res.hasMinSui === false) {
25
+ throw new Error(`Insufficient SUI balance. Need at least ${res.minSui} MIST.`);
26
+ }
27
+ const config = await (0, walrus_1.resolveWalrusConfig)();
28
+ const client = new client_1.SuiClient({ url: config.suiRpcUrl });
29
+ let whitelistId;
30
+ try {
31
+ const state = await (0, suiRepo_1.fetchRepositoryState)(client, repoCfg.repo_id);
32
+ if (state.sealPolicyId) {
33
+ whitelistId = state.sealPolicyId;
34
+ }
35
+ }
36
+ catch (err) {
37
+ // ignore
38
+ }
39
+ try {
40
+ await (0, suiRepo_1.removeCollaborator)(client, signer.signer, {
41
+ repoId: repoCfg.repo_id,
42
+ collaborator: addressToRemove,
43
+ whitelistId
44
+ });
45
+ console.log(ui_1.colors.green(`Collaborator ${addressToRemove} removed successfully.`));
46
+ if (whitelistId) {
47
+ console.log(ui_1.colors.yellow('IMPORTANT: Key Rotation Triggered.'));
48
+ console.log(ui_1.colors.yellow('The next `wit push` will automatically generate a new session key.'));
49
+ console.log(ui_1.colors.yellow('This ensures the removed user cannot decrypt future commits.'));
50
+ // TODO: We could force a re-key commit here, but for MVP we rely on next push.
51
+ // To ensure rotation, we can delete any cached session key if we were caching it (we aren't currently).
52
+ // Since `wit push` generates a fresh key every time, rotation is implicit!
53
+ // We just need to inform the user.
54
+ }
55
+ }
56
+ catch (err) {
57
+ console.error(ui_1.colors.red(`Removal failed: ${err.message}`));
58
+ if (err.message.includes('ENotAuthorized')) {
59
+ console.error(ui_1.colors.yellow('Hint: Only the owner can remove collaborators.'));
60
+ }
61
+ process.exit(1);
62
+ }
63
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeStubAction = makeStubAction;
4
+ function makeStubAction(commandName, detail) {
5
+ return async () => {
6
+ const React = await import('react');
7
+ const ink = await import('ink');
8
+ const { render, Box, Text } = ink;
9
+ render(React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { color: 'cyan' }, `wit ${commandName}`), detail ? React.createElement(Text, { dimColor: true }, detail) : null, React.createElement(Text, { dimColor: true }, 'Scaffold placeholder (Stage 1).')));
10
+ };
11
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transferAction = transferAction;
4
+ const client_1 = require("@mysten/sui/client");
5
+ const keys_1 = require("../lib/keys");
6
+ const walrus_1 = require("../lib/walrus");
7
+ const suiRepo_1 = require("../lib/suiRepo");
8
+ const repo_1 = require("../lib/repo");
9
+ const ui_1 = require("../lib/ui");
10
+ async function transferAction(newOwner) {
11
+ const witPath = await (0, repo_1.requireWitDir)();
12
+ const repoCfg = await (0, repo_1.readRepoConfig)(witPath);
13
+ if (!repoCfg.repo_id) {
14
+ throw new Error('Repository not initialized on chain. Run `wit push` first.');
15
+ }
16
+ const signer = await (0, keys_1.loadSigner)();
17
+ const address = signer.address;
18
+ // Basic validation
19
+ if (!newOwner.startsWith('0x')) {
20
+ throw new Error('Invalid address format. Must start with 0x.');
21
+ }
22
+ console.log(ui_1.colors.header(`Transferring ownership of ${repoCfg.repo_id} to ${newOwner}...`));
23
+ const res = await (0, keys_1.checkResources)(address);
24
+ if (res.hasMinSui === false) {
25
+ throw new Error(`Insufficient SUI balance. Need at least ${res.minSui} MIST.`);
26
+ }
27
+ const config = await (0, walrus_1.resolveWalrusConfig)();
28
+ const client = new client_1.SuiClient({ url: config.suiRpcUrl });
29
+ try {
30
+ await (0, suiRepo_1.transferOwnership)(client, signer.signer, {
31
+ repoId: repoCfg.repo_id,
32
+ newOwner,
33
+ });
34
+ console.log(ui_1.colors.green('Ownership transferred successfully!'));
35
+ console.log(ui_1.colors.cyan(`You are now a collaborator. ${newOwner} is the new owner.`));
36
+ // Update local config author just in case, though strictly not required as author != owner
37
+ // But we might want to warn user if they try to do owner-only things later
38
+ }
39
+ catch (err) {
40
+ console.error(ui_1.colors.red(`Transfer failed: ${err.message}`));
41
+ if (err.message.includes('ENotAuthorized')) {
42
+ console.error(ui_1.colors.yellow('Hint: Only the current owner can transfer ownership.'));
43
+ }
44
+ process.exit(1);
45
+ }
46
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pushBlobAction = pushBlobAction;
7
+ exports.pullBlobAction = pullBlobAction;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const walrus_1 = require("../lib/walrus");
11
+ const serialize_1 = require("../lib/serialize");
12
+ const ui_1 = require("../lib/ui");
13
+ async function pushBlobAction(filePath, opts) {
14
+ const absPath = path_1.default.resolve(filePath);
15
+ const data = await promises_1.default.readFile(absPath);
16
+ const hash = (0, serialize_1.sha256Base64)(data);
17
+ const svc = await walrus_1.WalrusService.fromRepo();
18
+ const signerInfo = await maybeLoadSigner();
19
+ const epochs = opts.epochs && opts.epochs > 0 ? opts.epochs : 1;
20
+ const res = await svc.writeBlob({
21
+ blob: data,
22
+ signer: signerInfo.signer,
23
+ epochs,
24
+ deletable: opts.deletable !== false,
25
+ attributes: { hash },
26
+ });
27
+ // eslint-disable-next-line no-console
28
+ console.log(`${ui_1.colors.green('Uploaded')} ${absPath}`);
29
+ // eslint-disable-next-line no-console
30
+ console.log(` blobId: ${ui_1.colors.hash(res.blobId)}`);
31
+ // eslint-disable-next-line no-console
32
+ console.log(` hash: ${ui_1.colors.hash(hash)}`);
33
+ }
34
+ async function pullBlobAction(blobId, outPath) {
35
+ const svc = await walrus_1.WalrusService.fromRepo();
36
+ const bytes = await svc.readBlob(blobId);
37
+ const hash = (0, serialize_1.sha256Base64)(Buffer.from(bytes));
38
+ await promises_1.default.mkdir(path_1.default.dirname(path_1.default.resolve(outPath)), { recursive: true });
39
+ await promises_1.default.writeFile(outPath, bytes);
40
+ // eslint-disable-next-line no-console
41
+ console.log(`${ui_1.colors.green('Downloaded')} -> ${outPath}`);
42
+ // eslint-disable-next-line no-console
43
+ console.log(` blobId: ${ui_1.colors.hash(blobId)}`);
44
+ // eslint-disable-next-line no-console
45
+ console.log(` hash: ${ui_1.colors.hash(hash)}`);
46
+ }
47
+ async function maybeLoadSigner() {
48
+ const { loadSigner } = await import('../lib/keys.js');
49
+ return loadSigner();
50
+ }
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pushQuiltAction = pushQuiltAction;
7
+ exports.pullQuiltAction = pullQuiltAction;
8
+ exports.fetchQuiltFile = fetchQuiltFile;
9
+ exports.listQuiltIdentifiersCommand = listQuiltIdentifiersCommand;
10
+ exports.catQuiltFileById = catQuiltFileById;
11
+ exports.pushQuiltLegacyAction = pushQuiltLegacyAction;
12
+ exports.pullQuiltLegacyAction = pullQuiltLegacyAction;
13
+ const promises_1 = __importDefault(require("fs/promises"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const walrus_1 = require("../lib/walrus");
16
+ const ui_1 = require("../lib/ui");
17
+ const fs_1 = require("../lib/fs");
18
+ const manifest_1 = require("../lib/manifest");
19
+ const schema_1 = require("../lib/schema");
20
+ const serialize_1 = require("../lib/serialize");
21
+ const quilt_1 = require("../lib/quilt");
22
+ const walrus_2 = require("@mysten/walrus");
23
+ // Step 1: Native quilt upload using writeQuilt (identifier + tags)
24
+ async function pushQuiltAction(dir, opts) {
25
+ const cwd = process.cwd();
26
+ const absDir = path_1.default.resolve(dir);
27
+ const entries = await collectFiles(absDir);
28
+ if (!entries.length) {
29
+ throw new Error(`Directory is empty: ${absDir}`);
30
+ }
31
+ const files = await Promise.all(entries.map(async (rel) => {
32
+ const abs = path_1.default.join(absDir, rel);
33
+ const meta = await (0, fs_1.computeFileMeta)(abs);
34
+ const data = await promises_1.default.readFile(abs);
35
+ return { rel: (0, fs_1.pathToPosix)(rel), meta, data };
36
+ }));
37
+ const walrusBlobs = files.map(({ rel, data, meta }) => ({
38
+ contents: data,
39
+ identifier: rel,
40
+ tags: {
41
+ hash: meta.hash,
42
+ size: String(meta.size),
43
+ mode: meta.mode,
44
+ mtime: String(meta.mtime),
45
+ },
46
+ }));
47
+ const svc = await walrus_1.WalrusService.fromRepo(cwd);
48
+ const signerInfo = await maybeLoadSigner();
49
+ const epochs = opts.epochs && opts.epochs > 0 ? opts.epochs : 1;
50
+ // Native quilt upload
51
+ const quiltRes = await svc.writeQuilt({ blobs: walrusBlobs, signer: signerInfo.signer, epochs, deletable: opts.deletable !== false });
52
+ const quiltId = quiltRes.quiltId;
53
+ // Per-file ids for pull: store via writeFiles
54
+ const walrusFiles = walrusBlobs.map((b) => walrus_2.WalrusFile.from({
55
+ contents: b.contents,
56
+ identifier: b.identifier,
57
+ tags: b.tags,
58
+ }));
59
+ const filesRes = await svc.getClient().writeFiles({ files: walrusFiles, signer: signerInfo.signer, epochs, deletable: opts.deletable !== false });
60
+ const manifest = schema_1.ManifestSchema.parse({
61
+ version: 1,
62
+ quilt_id: quiltId,
63
+ root_hash: (0, manifest_1.computeRootHash)(Object.fromEntries(files.map(({ rel, meta }) => [rel, meta]))),
64
+ files: Object.fromEntries(files.map(({ rel, meta }, idx) => [
65
+ rel,
66
+ {
67
+ ...meta,
68
+ id: filesRes[idx]?.id || '',
69
+ },
70
+ ])),
71
+ });
72
+ const manifestPath = opts.manifestOut ? path_1.default.resolve(opts.manifestOut) : path_1.default.resolve(cwd, 'quilt-manifest.json');
73
+ await promises_1.default.writeFile(manifestPath, (0, serialize_1.canonicalStringify)(manifest), 'utf8');
74
+ // eslint-disable-next-line no-console
75
+ console.log(ui_1.colors.green(`Uploaded quilt from ${absDir}`));
76
+ // eslint-disable-next-line no-console
77
+ console.log(` quiltId: ${ui_1.colors.hash(quiltId)}`);
78
+ // eslint-disable-next-line no-console
79
+ console.log(` manifest: ${manifestPath}`);
80
+ // eslint-disable-next-line no-console
81
+ console.log(` root_hash: ${ui_1.colors.hash(manifest.root_hash)}`);
82
+ }
83
+ // Step 2 will implement pull-quilt using Quilt index (pending)
84
+ async function pullQuiltAction(manifestPath, outDir) {
85
+ const manifestRaw = await promises_1.default.readFile(manifestPath, 'utf8');
86
+ const manifest = schema_1.ManifestSchema.parse(JSON.parse(manifestRaw));
87
+ if (!manifest.files || !Object.keys(manifest.files).length) {
88
+ throw new Error('Manifest has no files');
89
+ }
90
+ const entries = Object.entries(manifest.files);
91
+ const svc = await walrus_1.WalrusService.fromRepo();
92
+ const fetched = {};
93
+ for (const [identifier, expected] of entries) {
94
+ let content = null;
95
+ if (manifest.quilt_id) {
96
+ try {
97
+ content = Buffer.from(await svc.readQuiltFile(manifest.quilt_id, identifier));
98
+ }
99
+ catch {
100
+ // fallback to file id if available
101
+ }
102
+ }
103
+ if (!content) {
104
+ if (!expected.id) {
105
+ throw new Error(`Manifest missing Walrus file id for ${identifier}`);
106
+ }
107
+ const files = await svc.getClient().getFiles({ ids: [expected.id] });
108
+ content = Buffer.from(await files[0].bytes());
109
+ const tags = await files[0].getTags();
110
+ if (tags?.hash && tags.hash !== expected.hash) {
111
+ throw new Error(`Tag hash mismatch for ${identifier}`);
112
+ }
113
+ }
114
+ const computed = { hash: (0, serialize_1.sha256Base64)(content), size: content.length };
115
+ if (expected.hash !== computed.hash || expected.size !== computed.size) {
116
+ throw new Error(`Hash/size mismatch for ${identifier}`);
117
+ }
118
+ fetched[identifier] = content;
119
+ }
120
+ const computedRoot = (0, manifest_1.computeRootHash)(Object.fromEntries(Object.entries(manifest.files).map(([rel, meta]) => [
121
+ rel,
122
+ { hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime },
123
+ ])));
124
+ if (computedRoot !== manifest.root_hash) {
125
+ throw new Error(`root_hash mismatch: manifest=${manifest.root_hash}, computed=${computedRoot}`);
126
+ }
127
+ for (const [rel, data] of Object.entries(fetched)) {
128
+ const outPath = path_1.default.join(outDir, rel);
129
+ await promises_1.default.mkdir(path_1.default.dirname(outPath), { recursive: true });
130
+ await promises_1.default.writeFile(outPath, data);
131
+ const meta = manifest.files[rel];
132
+ const mode = parseInt(meta.mode, 10);
133
+ if (!Number.isNaN(mode)) {
134
+ await promises_1.default.chmod(outPath, mode & 0o777);
135
+ }
136
+ }
137
+ // eslint-disable-next-line no-console
138
+ console.log(ui_1.colors.green(`Downloaded quilt to ${outDir}`));
139
+ // eslint-disable-next-line no-console
140
+ console.log(` manifest root_hash: ${ui_1.colors.hash(manifest.root_hash)}`);
141
+ }
142
+ // On-demand fetch for a single identifier (web/explorer reuse)
143
+ async function fetchQuiltFile(manifestPath, identifier) {
144
+ const manifestRaw = await promises_1.default.readFile(manifestPath, 'utf8');
145
+ const manifest = schema_1.ManifestSchema.parse(JSON.parse(manifestRaw));
146
+ const idNormalized = (0, fs_1.pathToPosix)(identifier);
147
+ const entry = findEntry(manifest, idNormalized);
148
+ if (!entry || !entry.id) {
149
+ throw new Error(`Identifier not found in manifest: ${identifier}`);
150
+ }
151
+ return (0, quilt_1.fetchQuiltFileById)(entry.id, idNormalized);
152
+ }
153
+ function findEntry(manifest, identifier) {
154
+ if (manifest.files[identifier])
155
+ return manifest.files[identifier];
156
+ // If caller passed a prefixed path (e.g., dir/a.txt) but manifest stored a.txt, try stripping leading segments.
157
+ const parts = identifier.split('/');
158
+ while (parts.length > 1) {
159
+ parts.shift();
160
+ const candidate = parts.join('/');
161
+ if (manifest.files[candidate])
162
+ return manifest.files[candidate];
163
+ }
164
+ return null;
165
+ }
166
+ // Direct quilt access (no manifest): list identifiers and fetch a single file
167
+ async function listQuiltIdentifiersCommand(quiltId) {
168
+ const ids = await (await import('../lib/quilt.js')).listQuiltIdentifiers(quiltId);
169
+ if (!ids.length) {
170
+ // eslint-disable-next-line no-console
171
+ console.log('No identifiers found in quilt.');
172
+ return;
173
+ }
174
+ for (const id of ids) {
175
+ // eslint-disable-next-line no-console
176
+ console.log(id);
177
+ }
178
+ }
179
+ async function catQuiltFileById(quiltId, identifier) {
180
+ const { fetchQuiltFileById } = await import('../lib/quilt.js');
181
+ const { bytes } = await fetchQuiltFileById(quiltId, (0, fs_1.pathToPosix)(identifier));
182
+ process.stdout.write(Buffer.from(bytes));
183
+ }
184
+ async function collectFiles(baseDir) {
185
+ const result = [];
186
+ async function walk(cur, relPrefix = '') {
187
+ const entries = await promises_1.default.readdir(cur, { withFileTypes: true });
188
+ for (const e of entries) {
189
+ const rel = path_1.default.join(relPrefix, e.name);
190
+ const abs = path_1.default.join(cur, e.name);
191
+ if (e.isDirectory()) {
192
+ await walk(abs, rel);
193
+ }
194
+ else if (e.isFile()) {
195
+ result.push(rel.replace(/\\/g, '/'));
196
+ }
197
+ }
198
+ }
199
+ await walk(baseDir, '');
200
+ return result.sort();
201
+ }
202
+ async function maybeLoadSigner() {
203
+ const { loadSigner } = await import('../lib/keys.js');
204
+ return loadSigner();
205
+ }
206
+ // Legacy fallback: archive all files into a single blob (manifest + base64 contents)
207
+ async function pushQuiltLegacyAction(dir, opts) {
208
+ const cwd = process.cwd();
209
+ const absDir = path_1.default.resolve(dir);
210
+ const entries = await collectFiles(absDir);
211
+ if (!entries.length)
212
+ throw new Error(`Directory is empty: ${absDir}`);
213
+ const files = {};
214
+ for (const rel of entries) {
215
+ const abs = path_1.default.join(absDir, rel);
216
+ const meta = await (0, fs_1.computeFileMeta)(abs);
217
+ const data = await promises_1.default.readFile(abs);
218
+ files[(0, fs_1.pathToPosix)(rel)] = { data, meta };
219
+ }
220
+ const manifest = schema_1.ManifestSchema.parse({
221
+ version: 1,
222
+ quilt_id: 'legacy-archive',
223
+ root_hash: (0, manifest_1.computeRootHash)(Object.fromEntries(Object.entries(files).map(([rel, info]) => [rel, info.meta]))),
224
+ files: Object.fromEntries(Object.entries(files).map(([rel, info]) => [rel, info.meta])),
225
+ });
226
+ const archive = {
227
+ manifest,
228
+ files: Object.fromEntries(Object.entries(files).map(([rel, info]) => [rel, info.data.toString('base64')])),
229
+ };
230
+ const payload = Buffer.from(JSON.stringify(archive));
231
+ const svc = await walrus_1.WalrusService.fromRepo(cwd);
232
+ const signerInfo = await maybeLoadSigner();
233
+ const epochs = opts.epochs && opts.epochs > 0 ? opts.epochs : 1;
234
+ const res = await svc.writeBlob({
235
+ blob: payload,
236
+ signer: signerInfo.signer,
237
+ epochs,
238
+ deletable: opts.deletable !== false,
239
+ attributes: { root_hash: manifest.root_hash, kind: 'quilt-archive' },
240
+ });
241
+ // eslint-disable-next-line no-console
242
+ console.log(ui_1.colors.green(`Uploaded legacy quilt archive from ${absDir}`));
243
+ // eslint-disable-next-line no-console
244
+ console.log(` archive blobId: ${ui_1.colors.hash(res.blobId)}`);
245
+ // eslint-disable-next-line no-console
246
+ console.log(` root_hash: ${ui_1.colors.hash(manifest.root_hash)}`);
247
+ }
248
+ async function pullQuiltLegacyAction(blobId, outDir) {
249
+ const svc = await walrus_1.WalrusService.fromRepo();
250
+ const bytes = await svc.readBlob(blobId);
251
+ const archive = JSON.parse(Buffer.from(bytes).toString('utf8'));
252
+ const manifest = schema_1.ManifestSchema.parse(archive.manifest);
253
+ const index = {};
254
+ for (const [rel, b64] of Object.entries(archive.files)) {
255
+ const data = Buffer.from(b64, 'base64');
256
+ const computed = { hash: (0, serialize_1.sha256Base64)(data), size: data.length };
257
+ const expected = manifest.files[rel];
258
+ if (!expected)
259
+ throw new Error(`Manifest missing entry for ${rel}`);
260
+ if (expected.hash !== computed.hash || expected.size !== computed.size)
261
+ throw new Error(`Hash/size mismatch for ${rel}`);
262
+ index[rel] = expected;
263
+ }
264
+ const computedRoot = (0, manifest_1.computeRootHash)(index);
265
+ if (computedRoot !== manifest.root_hash) {
266
+ throw new Error(`root_hash mismatch: manifest=${manifest.root_hash} computed=${computedRoot}`);
267
+ }
268
+ for (const [rel, b64] of Object.entries(archive.files)) {
269
+ const data = Buffer.from(b64, 'base64');
270
+ const outPath = path_1.default.join(outDir, rel);
271
+ await promises_1.default.mkdir(path_1.default.dirname(outPath), { recursive: true });
272
+ await promises_1.default.writeFile(outPath, data);
273
+ const meta = manifest.files[rel];
274
+ const mode = parseInt(meta.mode, 10);
275
+ if (!Number.isNaN(mode))
276
+ await promises_1.default.chmod(outPath, mode & 0o777);
277
+ }
278
+ // eslint-disable-next-line no-console
279
+ console.log(ui_1.colors.green(`Downloaded legacy quilt archive to ${outDir}`));
280
+ // eslint-disable-next-line no-console
281
+ console.log(` manifest root_hash: ${ui_1.colors.hash(manifest.root_hash)}`);
282
+ }