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.
- package/LICENSE +201 -0
- package/README.md +41 -0
- package/dist/commands/account.js +109 -0
- package/dist/commands/checkout.js +213 -0
- package/dist/commands/clone.js +263 -0
- package/dist/commands/commit.js +150 -0
- package/dist/commands/diff.js +159 -0
- package/dist/commands/fetch.js +169 -0
- package/dist/commands/init.js +125 -0
- package/dist/commands/invite.js +72 -0
- package/dist/commands/list.js +214 -0
- package/dist/commands/plumbing.js +98 -0
- package/dist/commands/pull.js +160 -0
- package/dist/commands/push.js +371 -0
- package/dist/commands/registerCommands.js +183 -0
- package/dist/commands/removeUser.js +63 -0
- package/dist/commands/stub.js +11 -0
- package/dist/commands/transfer.js +46 -0
- package/dist/commands/walrusBlob.js +50 -0
- package/dist/commands/walrusQuilt.js +282 -0
- package/dist/commands/workspace.js +260 -0
- package/dist/index.js +46 -0
- package/dist/lib/config.js +49 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/fs.js +154 -0
- package/dist/lib/keys.js +224 -0
- package/dist/lib/manifest.js +37 -0
- package/dist/lib/quilt.js +38 -0
- package/dist/lib/repo.js +70 -0
- package/dist/lib/schema.js +53 -0
- package/dist/lib/seal.js +157 -0
- package/dist/lib/serialize.js +30 -0
- package/dist/lib/state.js +57 -0
- package/dist/lib/suiRepo.js +220 -0
- package/dist/lib/ui.js +51 -0
- package/dist/lib/validate.js +13 -0
- package/dist/lib/walrus.js +237 -0
- package/package.json +57 -0
|
@@ -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
|
+
}
|