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,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
+ }