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,160 @@
|
|
|
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.pullAction = pullAction;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const ui_1 = require("../lib/ui");
|
|
10
|
+
const repo_1 = require("../lib/repo");
|
|
11
|
+
const fetch_1 = require("./fetch");
|
|
12
|
+
const checkout_1 = require("./checkout");
|
|
13
|
+
const state_1 = require("../lib/state");
|
|
14
|
+
const fs_1 = require("../lib/fs");
|
|
15
|
+
const schema_1 = require("../lib/schema");
|
|
16
|
+
const manifest_1 = require("../lib/manifest");
|
|
17
|
+
const walrus_1 = require("../lib/walrus");
|
|
18
|
+
async function pullAction() {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.log(ui_1.colors.header('Starting pull...'));
|
|
21
|
+
const witPath = await (0, repo_1.requireWitDir)();
|
|
22
|
+
const headRefPathInitial = await (0, state_1.readHeadRefPath)(witPath);
|
|
23
|
+
const localHeadInitial = await (0, state_1.readRef)(headRefPathInitial);
|
|
24
|
+
const cleanResult = await ensureCleanWorktree(witPath, localHeadInitial);
|
|
25
|
+
if (!cleanResult.ok) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.log(ui_1.colors.red(cleanResult.reason));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Update remote metadata
|
|
31
|
+
await (0, fetch_1.fetchAction)();
|
|
32
|
+
const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
|
|
33
|
+
const localHead = await (0, state_1.readRef)(headRefPath);
|
|
34
|
+
const remoteHead = await (0, repo_1.readRemoteRef)(witPath);
|
|
35
|
+
if (!remoteHead) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.log(ui_1.colors.yellow('Remote has no head; nothing to pull.'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (localHead === remoteHead) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(ui_1.colors.green('Already up to date.'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const canFastForward = !localHead || (await isAncestor(witPath, localHead, remoteHead));
|
|
46
|
+
if (!canFastForward) {
|
|
47
|
+
throw new Error('Local history diverged; pull requires fast-forward. Please reset or clone.');
|
|
48
|
+
}
|
|
49
|
+
// Fast-forward by checking out remote head
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log(ui_1.colors.cyan(`Fast-forwarding to ${remoteHead} ...`));
|
|
52
|
+
const ok = await (0, checkout_1.checkoutAction)(remoteHead);
|
|
53
|
+
if (!ok) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.log(ui_1.colors.red('Pull aborted (checkout failed).'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.log(ui_1.colors.green('Pull complete.'));
|
|
60
|
+
}
|
|
61
|
+
async function ensureCleanWorktree(witPath, localHead) {
|
|
62
|
+
const indexPath = path_1.default.join(witPath, 'index');
|
|
63
|
+
const index = await (0, fs_1.readIndex)(indexPath);
|
|
64
|
+
const ig = await (0, fs_1.buildIgnore)(process.cwd());
|
|
65
|
+
const tracked = new Set(Object.keys(index));
|
|
66
|
+
const workspaceFiles = await (0, fs_1.walkFiles)(process.cwd(), ig, process.cwd(), tracked);
|
|
67
|
+
const workspaceMeta = {};
|
|
68
|
+
for (const file of workspaceFiles) {
|
|
69
|
+
const rel = (0, fs_1.pathToPosix)(path_1.default.relative(process.cwd(), file));
|
|
70
|
+
workspaceMeta[rel] = await (0, fs_1.computeFileMeta)(file);
|
|
71
|
+
}
|
|
72
|
+
for (const [rel, meta] of Object.entries(workspaceMeta)) {
|
|
73
|
+
const indexed = index[rel];
|
|
74
|
+
if (!indexed)
|
|
75
|
+
return { ok: false, reason: 'Worktree has untracked files; clean or commit before pull.' };
|
|
76
|
+
if (indexed.hash !== meta.hash || indexed.size !== meta.size || indexed.mode !== meta.mode) {
|
|
77
|
+
return { ok: false, reason: 'Worktree has modifications; clean or commit before pull.' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const rel of Object.keys(index)) {
|
|
81
|
+
if (!workspaceMeta[rel]) {
|
|
82
|
+
return { ok: false, reason: 'Worktree has deletions; clean or commit before pull.' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Ensure index matches local HEAD (no staged changes)
|
|
86
|
+
if (localHead) {
|
|
87
|
+
const headCommit = await (0, state_1.readCommitById)(witPath, localHead);
|
|
88
|
+
const headFiles = await loadHeadFiles(witPath, headCommit);
|
|
89
|
+
if (headFiles && !sameFiles(index, headFiles)) {
|
|
90
|
+
return { ok: false, reason: 'Index differs from HEAD; clean or reset before pull.' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
function sameFiles(a, b) {
|
|
96
|
+
const keysA = Object.keys(a).sort();
|
|
97
|
+
const keysB = Object.keys(b).sort();
|
|
98
|
+
if (keysA.length !== keysB.length)
|
|
99
|
+
return false;
|
|
100
|
+
for (let i = 0; i < keysA.length; i += 1) {
|
|
101
|
+
if (keysA[i] !== keysB[i])
|
|
102
|
+
return false;
|
|
103
|
+
const ma = a[keysA[i]];
|
|
104
|
+
const mb = b[keysA[i]];
|
|
105
|
+
if (!mb || ma.hash !== mb.hash || ma.size !== mb.size || ma.mode !== mb.mode)
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
async function isAncestor(witPath, ancestorId, descendantId) {
|
|
111
|
+
let current = descendantId;
|
|
112
|
+
const visited = new Set();
|
|
113
|
+
while (current && !visited.has(current)) {
|
|
114
|
+
if (current === ancestorId)
|
|
115
|
+
return true;
|
|
116
|
+
visited.add(current);
|
|
117
|
+
const commit = await (0, state_1.readCommitById)(witPath, current);
|
|
118
|
+
current = commit.parent;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
async function loadHeadFiles(witPath, commit) {
|
|
123
|
+
if (commit.tree?.files && Object.keys(commit.tree.files).length) {
|
|
124
|
+
return commit.tree.files;
|
|
125
|
+
}
|
|
126
|
+
const manifestId = commit.tree?.manifest_id;
|
|
127
|
+
if (!manifestId)
|
|
128
|
+
return null;
|
|
129
|
+
const file = path_1.default.join(witPath, 'objects', 'manifests', `${manifestIdToFile(manifestId)}.json`);
|
|
130
|
+
let manifest = null;
|
|
131
|
+
try {
|
|
132
|
+
const raw = await promises_1.default.readFile(file, 'utf8');
|
|
133
|
+
manifest = schema_1.ManifestSchema.parse(JSON.parse(raw));
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (err?.code !== 'ENOENT')
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
if (!manifest) {
|
|
140
|
+
const walrusSvc = await walrus_1.WalrusService.fromRepo();
|
|
141
|
+
const buf = Buffer.from(await walrusSvc.readBlob(manifestId));
|
|
142
|
+
manifest = schema_1.ManifestSchema.parse(JSON.parse(buf.toString('utf8')));
|
|
143
|
+
await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
|
|
144
|
+
await promises_1.default.writeFile(file, canonicalManifest(manifest), 'utf8');
|
|
145
|
+
}
|
|
146
|
+
const computed = (0, manifest_1.computeRootHash)(Object.fromEntries(Object.entries(manifest.files).map(([rel, meta]) => [
|
|
147
|
+
rel,
|
|
148
|
+
{ hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime },
|
|
149
|
+
])));
|
|
150
|
+
if (commit.tree?.root_hash && commit.tree.root_hash !== computed) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return manifest.files;
|
|
154
|
+
}
|
|
155
|
+
function manifestIdToFile(id) {
|
|
156
|
+
return id.replace(/\//g, '_').replace(/\+/g, '-');
|
|
157
|
+
}
|
|
158
|
+
function canonicalManifest(m) {
|
|
159
|
+
return JSON.stringify(m, null, 2) + '\n';
|
|
160
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
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.pushAction = pushAction;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const client_1 = require("@mysten/sui/client");
|
|
10
|
+
const walrus_1 = require("@mysten/walrus");
|
|
11
|
+
const ui_1 = require("../lib/ui");
|
|
12
|
+
const manifest_1 = require("../lib/manifest");
|
|
13
|
+
const serialize_1 = require("../lib/serialize");
|
|
14
|
+
const fs_1 = require("../lib/fs");
|
|
15
|
+
const state_1 = require("../lib/state");
|
|
16
|
+
const repo_1 = require("../lib/repo");
|
|
17
|
+
const walrus_2 = require("../lib/walrus");
|
|
18
|
+
const keys_1 = require("../lib/keys");
|
|
19
|
+
const suiRepo_1 = require("../lib/suiRepo");
|
|
20
|
+
const schema_1 = require("../lib/schema");
|
|
21
|
+
const seal_1 = require("../lib/seal");
|
|
22
|
+
const constants_1 = require("../lib/constants");
|
|
23
|
+
async function pushAction() {
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.log(ui_1.colors.header('Starting push...'));
|
|
26
|
+
const witPath = await (0, repo_1.requireWitDir)();
|
|
27
|
+
const repoCfg = await (0, repo_1.readRepoConfig)(witPath);
|
|
28
|
+
const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
|
|
29
|
+
const headId = await (0, state_1.readRef)(headRefPath);
|
|
30
|
+
if (!headId) {
|
|
31
|
+
throw new Error('No commits to push. Run `wit commit` first.');
|
|
32
|
+
}
|
|
33
|
+
const signerInfo = await (0, keys_1.loadSigner)();
|
|
34
|
+
await ensureAuthorOrSetDefault(witPath, repoCfg, signerInfo.address);
|
|
35
|
+
const resourcesOk = await assertResourcesOk(signerInfo.address);
|
|
36
|
+
if (!resourcesOk)
|
|
37
|
+
return;
|
|
38
|
+
const headCommit = await (0, state_1.readCommitById)(witPath, headId);
|
|
39
|
+
const computedRoot = (0, manifest_1.computeRootHash)(headCommit.tree.files);
|
|
40
|
+
if (computedRoot !== headCommit.tree.root_hash) {
|
|
41
|
+
throw new Error('HEAD root_hash does not match file list. Re-commit or fix index.');
|
|
42
|
+
}
|
|
43
|
+
const commitMap = await (0, state_1.readCommitIdMap)(witPath);
|
|
44
|
+
const { chain, baseRemoteId } = await collectChain(witPath, headId, commitMap);
|
|
45
|
+
const resolved = await (0, walrus_2.resolveWalrusConfig)(process.cwd());
|
|
46
|
+
const suiClient = new client_1.SuiClient({ url: resolved.suiRpcUrl });
|
|
47
|
+
let createdRepo = false;
|
|
48
|
+
let repoId = repoCfg.repo_id;
|
|
49
|
+
if (!repoId) {
|
|
50
|
+
const isPrivate = repoCfg.seal_policy_id === 'pending' || !!repoCfg.seal_policy_id;
|
|
51
|
+
repoId = await (0, suiRepo_1.createRepository)(suiClient, signerInfo.signer, {
|
|
52
|
+
name: repoCfg.repo_name,
|
|
53
|
+
description: repoCfg.repo_name,
|
|
54
|
+
isPrivate: isPrivate,
|
|
55
|
+
});
|
|
56
|
+
repoCfg.repo_id = repoId;
|
|
57
|
+
createdRepo = true;
|
|
58
|
+
await (0, repo_1.writeRepoConfig)(witPath, repoCfg);
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.log(ui_1.colors.green(`Created on-chain repository ${repoId}`));
|
|
61
|
+
}
|
|
62
|
+
const onchainState = await (0, suiRepo_1.fetchRepositoryStateWithRetry)(suiClient, repoId);
|
|
63
|
+
// Update local seal policy ID if it was pending or changed
|
|
64
|
+
if (onchainState.sealPolicyId && repoCfg.seal_policy_id !== onchainState.sealPolicyId) {
|
|
65
|
+
repoCfg.seal_policy_id = onchainState.sealPolicyId;
|
|
66
|
+
await (0, repo_1.writeRepoConfig)(witPath, repoCfg);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(ui_1.colors.cyan(`Seal policy updated from chain: ${onchainState.sealPolicyId}`));
|
|
69
|
+
}
|
|
70
|
+
const currentPolicyId = repoCfg.seal_policy_id === 'pending' ? null : repoCfg.seal_policy_id;
|
|
71
|
+
if (onchainState.headCommit && baseRemoteId !== onchainState.headCommit) {
|
|
72
|
+
throw new Error('Remote head diverges from local history; run `wit pull`/`fetch` or reset first.');
|
|
73
|
+
}
|
|
74
|
+
const existingHeadRemote = commitMap[headId];
|
|
75
|
+
if (existingHeadRemote && onchainState.headCommit === existingHeadRemote) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.log(ui_1.colors.green('Remote already up to date; nothing to push.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Align parent expectations with on-chain head when possible
|
|
81
|
+
const parentRemoteHint = headCommit.parent ? commitMap[headCommit.parent] : null;
|
|
82
|
+
let parentAnchor = baseRemoteId;
|
|
83
|
+
if (onchainState.headCommit) {
|
|
84
|
+
if (!parentAnchor && parentRemoteHint === onchainState.headCommit) {
|
|
85
|
+
parentAnchor = onchainState.headCommit;
|
|
86
|
+
}
|
|
87
|
+
else if (parentAnchor && parentAnchor !== onchainState.headCommit) {
|
|
88
|
+
throw new Error('Remote head diverges from local history; run `wit pull`/`fetch` or reset first.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const walrusSvc = await walrus_2.WalrusService.fromRepo();
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.log(ui_1.colors.cyan(`Commits to upload: ${chain.length}`));
|
|
94
|
+
let parentRemoteId = parentAnchor ?? onchainState.headCommit ?? null;
|
|
95
|
+
let lastManifestId = null;
|
|
96
|
+
let lastQuiltId = null;
|
|
97
|
+
let lastCommitRemoteId = null;
|
|
98
|
+
const nextMap = { ...commitMap };
|
|
99
|
+
for (let i = 0; i < chain.length; i += 1) {
|
|
100
|
+
const item = chain[i];
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.log(ui_1.colors.cyan(`Uploading commit ${i + 1}/${chain.length}: ${item.id}`));
|
|
103
|
+
const uploaded = await uploadCommitSnapshot(witPath, item.id, item.commit, parentRemoteId, walrusSvc, signerInfo.signer, currentPolicyId, suiClient);
|
|
104
|
+
if (!uploaded) {
|
|
105
|
+
// Upload failed with friendly message already printed
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
nextMap[item.id] = uploaded.commitId;
|
|
109
|
+
parentRemoteId = uploaded.commitId;
|
|
110
|
+
lastManifestId = uploaded.manifestId;
|
|
111
|
+
lastQuiltId = uploaded.quiltId;
|
|
112
|
+
lastCommitRemoteId = uploaded.commitId;
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.log(ui_1.colors.green(`Uploaded commit ${item.id} -> ${uploaded.commitId}`));
|
|
115
|
+
}
|
|
116
|
+
if (!lastCommitRemoteId || !lastManifestId || !lastQuiltId) {
|
|
117
|
+
throw new Error('Missing push artifacts; cannot update remote head.');
|
|
118
|
+
}
|
|
119
|
+
await (0, state_1.writeCommitIdMap)(witPath, nextMap);
|
|
120
|
+
await (0, suiRepo_1.updateRepositoryHead)(suiClient, signerInfo.signer, {
|
|
121
|
+
repoId,
|
|
122
|
+
commitId: lastCommitRemoteId,
|
|
123
|
+
manifestId: lastManifestId,
|
|
124
|
+
quiltId: lastQuiltId,
|
|
125
|
+
expectedVersion: onchainState.version,
|
|
126
|
+
parentCommit: onchainState.headCommit,
|
|
127
|
+
});
|
|
128
|
+
// eslint-disable-next-line no-console
|
|
129
|
+
console.log(ui_1.colors.cyan('On-chain head updated'));
|
|
130
|
+
const updatedRemote = {
|
|
131
|
+
repo_id: repoId,
|
|
132
|
+
head_commit: lastCommitRemoteId,
|
|
133
|
+
head_manifest: lastManifestId,
|
|
134
|
+
head_quilt: lastQuiltId,
|
|
135
|
+
version: onchainState.version + 1,
|
|
136
|
+
};
|
|
137
|
+
await (0, repo_1.writeRemoteState)(witPath, updatedRemote);
|
|
138
|
+
await (0, repo_1.writeRemoteRef)(witPath, lastCommitRemoteId);
|
|
139
|
+
// eslint-disable-next-line no-console
|
|
140
|
+
console.log(ui_1.colors.green('Push complete'));
|
|
141
|
+
// eslint-disable-next-line no-console
|
|
142
|
+
console.log(`Remote head: ${ui_1.colors.hash(lastCommitRemoteId)}`);
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.log(`Manifest: ${ui_1.colors.hash(lastManifestId)}`);
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.log(`Quilt: ${ui_1.colors.hash(lastQuiltId)}`);
|
|
147
|
+
if (createdRepo) {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.log(ui_1.colors.cyan('Initial push: repository created on chain and remote state recorded.'));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function collectChain(witPath, headId, map) {
|
|
153
|
+
const chain = [];
|
|
154
|
+
let cursor = headId;
|
|
155
|
+
let baseRemoteId = null;
|
|
156
|
+
while (cursor) {
|
|
157
|
+
const commit = await (0, state_1.readCommitById)(witPath, cursor);
|
|
158
|
+
chain.unshift({ id: cursor, commit });
|
|
159
|
+
if (!commit.parent) {
|
|
160
|
+
baseRemoteId = null;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
const mapped = map[commit.parent];
|
|
164
|
+
if (mapped) {
|
|
165
|
+
baseRemoteId = mapped;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
cursor = commit.parent;
|
|
169
|
+
}
|
|
170
|
+
return { chain, baseRemoteId };
|
|
171
|
+
}
|
|
172
|
+
async function uploadCommitSnapshot(witPath, localCommitId, commit, parentRemoteId, walrusSvc, signer, policyId, suiClient) {
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.log(ui_1.colors.cyan(' Building and verifying file snapshot...'));
|
|
175
|
+
const entries = Object.entries(commit.tree.files).sort((a, b) => a[0].localeCompare(b[0]));
|
|
176
|
+
const files = [];
|
|
177
|
+
for (const [rel, meta] of entries) {
|
|
178
|
+
const buf = await (0, fs_1.readBlob)(witPath, meta.hash);
|
|
179
|
+
if (!buf) {
|
|
180
|
+
throw new Error(`Missing blob for ${rel} (${meta.hash}); ensure index is complete.`);
|
|
181
|
+
}
|
|
182
|
+
const computed = (0, serialize_1.sha256Base64)(buf);
|
|
183
|
+
if (computed !== meta.hash || buf.length !== meta.size) {
|
|
184
|
+
throw new Error(`File verification failed for ${rel}; re-add and retry.`);
|
|
185
|
+
}
|
|
186
|
+
files.push({ rel, meta, data: buf });
|
|
187
|
+
}
|
|
188
|
+
// Prepare blobs (encrypt if policyId is present)
|
|
189
|
+
const walrusBlobs = await Promise.all(files.map(async ({ rel, data, meta }) => {
|
|
190
|
+
let contents = data;
|
|
191
|
+
let enc;
|
|
192
|
+
if (policyId) {
|
|
193
|
+
const encrypted = await (0, seal_1.encryptWithSeal)(data, policyId, constants_1.WIT_PACKAGE_ID, suiClient);
|
|
194
|
+
contents = encrypted.cipher;
|
|
195
|
+
enc = encrypted.meta;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
contents,
|
|
199
|
+
identifier: rel,
|
|
200
|
+
tags: {
|
|
201
|
+
hash: meta.hash,
|
|
202
|
+
size: String(meta.size),
|
|
203
|
+
mode: meta.mode,
|
|
204
|
+
mtime: String(meta.mtime),
|
|
205
|
+
...(enc
|
|
206
|
+
? {
|
|
207
|
+
enc_alg: enc.alg,
|
|
208
|
+
enc_iv: enc.iv,
|
|
209
|
+
enc_tag: enc.tag,
|
|
210
|
+
enc_policy: enc.policy_id,
|
|
211
|
+
enc_package: enc.package_id,
|
|
212
|
+
enc_sealed_key: enc.sealed_session_key,
|
|
213
|
+
enc_size: String(contents.length),
|
|
214
|
+
}
|
|
215
|
+
: {}),
|
|
216
|
+
},
|
|
217
|
+
enc,
|
|
218
|
+
};
|
|
219
|
+
}));
|
|
220
|
+
const encMetas = walrusBlobs.map((b) => b.enc);
|
|
221
|
+
const walrusInputs = walrusBlobs.map(({ contents, identifier, tags }) => ({ contents, identifier, tags }));
|
|
222
|
+
const quiltRes = await tryWithRetry(() => walrusSvc.writeQuilt({
|
|
223
|
+
blobs: walrusInputs,
|
|
224
|
+
signer,
|
|
225
|
+
epochs: 1,
|
|
226
|
+
deletable: true,
|
|
227
|
+
}));
|
|
228
|
+
if (!quiltRes)
|
|
229
|
+
return null;
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
console.log(ui_1.colors.cyan(` Quilt uploaded: ${quiltRes.quiltId}`));
|
|
232
|
+
const walrusFiles = walrusBlobs.map((b) => walrus_1.WalrusFile.from({
|
|
233
|
+
contents: b.contents,
|
|
234
|
+
identifier: b.identifier,
|
|
235
|
+
tags: b.tags,
|
|
236
|
+
}));
|
|
237
|
+
const filesRes = await tryWithRetry(() => walrusSvc.getClient().writeFiles({ files: walrusFiles, signer, epochs: 1, deletable: true }));
|
|
238
|
+
if (!filesRes)
|
|
239
|
+
return null;
|
|
240
|
+
// eslint-disable-next-line no-console
|
|
241
|
+
console.log(ui_1.colors.cyan(' File index written to Walrus'));
|
|
242
|
+
const rootHash = (0, manifest_1.computeRootHash)(commit.tree.files);
|
|
243
|
+
if (rootHash !== commit.tree.root_hash) {
|
|
244
|
+
throw new Error(`Commit ${localCommitId} root_hash mismatch; recommit or fix index.`);
|
|
245
|
+
}
|
|
246
|
+
const manifest = schema_1.ManifestSchema.parse({
|
|
247
|
+
version: 1,
|
|
248
|
+
quilt_id: quiltRes.quiltId,
|
|
249
|
+
root_hash: rootHash,
|
|
250
|
+
files: Object.fromEntries(files.map(({ rel, meta }, idx) => {
|
|
251
|
+
const enc = encMetas[idx];
|
|
252
|
+
return [
|
|
253
|
+
rel,
|
|
254
|
+
{
|
|
255
|
+
...meta,
|
|
256
|
+
id: filesRes[idx]?.id || filesRes[idx]?.blobId || filesRes[idx]?.blob_id || '',
|
|
257
|
+
...(enc ? { enc: enc } : {}),
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
})),
|
|
261
|
+
});
|
|
262
|
+
const manifestSerialized = (0, serialize_1.canonicalStringify)(manifest);
|
|
263
|
+
const manifestUpload = await walrusSvc.writeBlob({
|
|
264
|
+
blob: Buffer.from(manifestSerialized),
|
|
265
|
+
signer,
|
|
266
|
+
epochs: 1,
|
|
267
|
+
deletable: true,
|
|
268
|
+
});
|
|
269
|
+
const manifestId = manifestUpload.blobId;
|
|
270
|
+
// eslint-disable-next-line no-console
|
|
271
|
+
console.log(ui_1.colors.cyan(` Manifest uploaded: ${manifestId}`));
|
|
272
|
+
await cacheJson(path_1.default.join(witPath, 'objects', 'manifests', `${(0, state_1.idToFileName)(manifestId)}.json`), manifestSerialized);
|
|
273
|
+
const remoteCommit = {
|
|
274
|
+
tree: {
|
|
275
|
+
root_hash: commit.tree.root_hash,
|
|
276
|
+
manifest_id: manifestId,
|
|
277
|
+
quilt_id: quiltRes.quiltId,
|
|
278
|
+
},
|
|
279
|
+
parent: parentRemoteId,
|
|
280
|
+
author: commit.author,
|
|
281
|
+
message: commit.message,
|
|
282
|
+
timestamp: commit.timestamp,
|
|
283
|
+
extras: {
|
|
284
|
+
...(commit.extras || { patch_id: null, tags: {} }),
|
|
285
|
+
tags: {
|
|
286
|
+
...(commit.extras?.tags || {}),
|
|
287
|
+
...(policyId ? { seal_policy_id: policyId } : {}),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
const remoteSerialized = (0, serialize_1.canonicalStringify)(remoteCommit);
|
|
292
|
+
const commitUpload = await walrusSvc.writeBlob({
|
|
293
|
+
blob: Buffer.from(remoteSerialized),
|
|
294
|
+
signer,
|
|
295
|
+
epochs: 1,
|
|
296
|
+
deletable: true,
|
|
297
|
+
});
|
|
298
|
+
const remoteCommitId = commitUpload.blobId;
|
|
299
|
+
// eslint-disable-next-line no-console
|
|
300
|
+
console.log(ui_1.colors.cyan(` Remote commit uploaded: ${remoteCommitId}`));
|
|
301
|
+
await cacheJson(path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(remoteCommitId)}.json`), remoteSerialized);
|
|
302
|
+
return { manifestId, quiltId: quiltRes.quiltId, commitId: remoteCommitId };
|
|
303
|
+
}
|
|
304
|
+
async function tryWithRetry(fn, attempts = 3, delayMs = 1200) {
|
|
305
|
+
let lastErr;
|
|
306
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
307
|
+
try {
|
|
308
|
+
return await fn();
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
lastErr = err;
|
|
312
|
+
// eslint-disable-next-line no-console
|
|
313
|
+
console.log(ui_1.colors.yellow(`Walrus upload attempt ${i + 1}/${attempts} failed: ${err?.message || err}`));
|
|
314
|
+
if (i < attempts - 1) {
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// eslint-disable-next-line no-console
|
|
320
|
+
console.log(ui_1.colors.red(`Walrus upload failed after ${attempts} attempts: ${lastErr?.message || lastErr}. Please check network/relay and retry (push is safe to re-run).`));
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
async function cacheJson(filePath, content) {
|
|
324
|
+
await (0, fs_1.ensureDirForFile)(filePath);
|
|
325
|
+
await promises_1.default.writeFile(filePath, content, 'utf8');
|
|
326
|
+
}
|
|
327
|
+
async function ensureAuthorOrSetDefault(witPath, repoCfg, signerAddress) {
|
|
328
|
+
const current = (repoCfg.author || '').trim().toLowerCase();
|
|
329
|
+
const signer = signerAddress.toLowerCase();
|
|
330
|
+
if (!current || current === 'unknown') {
|
|
331
|
+
const next = { ...repoCfg, author: signerAddress };
|
|
332
|
+
await (0, repo_1.writeRepoConfig)(witPath, next);
|
|
333
|
+
// eslint-disable-next-line no-console
|
|
334
|
+
console.log(ui_1.colors.cyan(`Author set to active address ${signerAddress}`));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (current !== signer) {
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.warn(`Warning: author (${repoCfg.author}) differs from signer (${signerAddress}). Push will use signer.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function assertResourcesOk(address) {
|
|
343
|
+
const res = await (0, keys_1.checkResources)(address);
|
|
344
|
+
// eslint-disable-next-line no-console
|
|
345
|
+
console.log(ui_1.colors.cyan(`Using account ${address}`));
|
|
346
|
+
const fmt = (amt) => (amt !== undefined ? amt.toString() : 'unknown');
|
|
347
|
+
// eslint-disable-next-line no-console
|
|
348
|
+
console.log(ui_1.colors.cyan(` SUI: ${fmt(res.suiBalance)} (min ${res.minSui})`));
|
|
349
|
+
// eslint-disable-next-line no-console
|
|
350
|
+
console.log(ui_1.colors.cyan(` WAL: ${res.walError ? 'query failed' : fmt(res.walBalance)} (min ${res.minWal})`));
|
|
351
|
+
if (res.error) {
|
|
352
|
+
// eslint-disable-next-line no-console
|
|
353
|
+
console.log(ui_1.colors.red(`Failed to query balances: ${res.error}`));
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (res.walError) {
|
|
357
|
+
// eslint-disable-next-line no-console
|
|
358
|
+
console.log(ui_1.colors.red(`Failed to query WAL balance: ${res.walError}. Please fund or switch account.`));
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (res.hasMinSui === false) {
|
|
362
|
+
// eslint-disable-next-line no-console
|
|
363
|
+
console.log(ui_1.colors.red(`Insufficient SUI balance (need at least ${res.minSui} MIST). Please fund or switch account.`));
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
if (res.hasMinWal === false) {
|
|
367
|
+
// eslint-disable-next-line no-console
|
|
368
|
+
console.warn(`Warning: WAL balance below threshold (${res.minWal} min).`);
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|