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,263 @@
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.cloneAction = cloneAction;
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 ui_1 = require("../lib/ui");
11
+ const walrus_1 = require("../lib/walrus");
12
+ const suiRepo_1 = require("../lib/suiRepo");
13
+ const schema_1 = require("../lib/schema");
14
+ const manifest_1 = require("../lib/manifest");
15
+ const serialize_1 = require("../lib/serialize");
16
+ const fs_1 = require("../lib/fs");
17
+ const state_1 = require("../lib/state");
18
+ const repo_1 = require("../lib/repo");
19
+ const seal_1 = require("../lib/seal");
20
+ const keys_1 = require("../lib/keys");
21
+ const DEFAULT_RELAYS = ['https://upload-relay.testnet.walrus.space'];
22
+ const DEFAULT_NETWORK = 'testnet';
23
+ async function cloneAction(repoId) {
24
+ if (!repoId) {
25
+ throw new Error('Usage: wit clone <repo_id>');
26
+ }
27
+ // eslint-disable-next-line no-console
28
+ console.log(ui_1.colors.header('Starting clone...'));
29
+ // Prepare .wit layout and config
30
+ const witPath = await ensureLayout(process.cwd(), repoId);
31
+ const resolved = await (0, walrus_1.resolveWalrusConfig)(process.cwd());
32
+ const suiClient = new client_1.SuiClient({ url: resolved.suiRpcUrl });
33
+ const walrusSvc = await walrus_1.WalrusService.fromRepo();
34
+ const signerInfo = await (0, keys_1.loadSigner)();
35
+ // Fetch on-chain head
36
+ const onchain = await (0, suiRepo_1.fetchRepositoryStateWithRetry)(suiClient, repoId);
37
+ if (!onchain.headCommit || !onchain.headManifest || !onchain.headQuilt) {
38
+ // eslint-disable-next-line no-console
39
+ console.log(ui_1.colors.yellow('Remote repository has no head. Nothing to clone.'));
40
+ return;
41
+ }
42
+ // Download manifest
43
+ // eslint-disable-next-line no-console
44
+ console.log(ui_1.colors.cyan('Downloading manifest...'));
45
+ const manifestBuf = Buffer.from(await walrusSvc.readBlob(onchain.headManifest));
46
+ const manifest = schema_1.ManifestSchema.parse(JSON.parse(manifestBuf.toString('utf8')));
47
+ const computedRoot = (0, manifest_1.computeRootHash)(Object.fromEntries(Object.entries(manifest.files).map(([rel, meta]) => [
48
+ rel,
49
+ { hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime },
50
+ ])));
51
+ if (computedRoot !== manifest.root_hash) {
52
+ throw new Error('Manifest root_hash mismatch; aborting clone.');
53
+ }
54
+ await cacheJson(path_1.default.join(witPath, 'objects', 'manifests', `${(0, state_1.idToFileName)(onchain.headManifest)}.json`), (0, serialize_1.canonicalStringify)(manifest));
55
+ // Persist repo config seal policy if present
56
+ if (onchain.sealPolicyId) {
57
+ try {
58
+ const cfgRaw = await promises_1.default.readFile(path_1.default.join(witPath, 'config.json'), 'utf8');
59
+ const cfg = JSON.parse(cfgRaw);
60
+ cfg.seal_policy_id = onchain.sealPolicyId;
61
+ await (0, repo_1.writeRepoConfig)(witPath, cfg);
62
+ }
63
+ catch {
64
+ // best-effort
65
+ }
66
+ }
67
+ const sealPolicyId = onchain.sealPolicyId || null;
68
+ const hasEncrypted = Object.values(manifest.files).some((meta) => meta.enc);
69
+ if (hasEncrypted && !sealPolicyId) {
70
+ throw new Error('Encrypted repository detected but no seal policy id on chain.');
71
+ }
72
+ // Download remote commit
73
+ // eslint-disable-next-line no-console
74
+ console.log(ui_1.colors.cyan('Downloading commit...'));
75
+ const commitBuf = Buffer.from(await walrusSvc.readBlob(onchain.headCommit));
76
+ const commit = parseRemoteCommit(commitBuf);
77
+ if (commit.tree.root_hash !== manifest.root_hash) {
78
+ throw new Error('Commit root_hash does not match manifest; aborting clone.');
79
+ }
80
+ await cacheJson(path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(onchain.headCommit)}.json`), commitBuf.toString('utf8'));
81
+ // Fetch files by id
82
+ const entries = Object.entries(manifest.files);
83
+ // eslint-disable-next-line no-console
84
+ console.log(ui_1.colors.cyan(`Downloading ${entries.length} files from Walrus...`));
85
+ const index = {};
86
+ for (let i = 0; i < entries.length; i += 1) {
87
+ const [rel, meta] = entries[i];
88
+ const data = await fetchFileBytes(walrusSvc, manifest, rel, meta);
89
+ let plain;
90
+ try {
91
+ if (meta.enc) {
92
+ // Map metadata to EncryptionMeta
93
+ const encAny = meta.enc;
94
+ const encMeta = {
95
+ alg: encAny.alg || encAny.enc_alg,
96
+ policy_id: encAny.policy_id || encAny.enc_policy,
97
+ package_id: encAny.package_id || encAny.enc_package || '0x0', // Fallback or read from meta
98
+ sealed_session_key: encAny.sealed_session_key || encAny.enc_sealed_key,
99
+ iv: encAny.iv || encAny.enc_iv,
100
+ tag: encAny.tag || encAny.enc_tag,
101
+ };
102
+ plain = await (0, seal_1.decryptWithSeal)(data, encMeta, signerInfo.signer, suiClient);
103
+ }
104
+ else {
105
+ plain = data;
106
+ }
107
+ }
108
+ catch (err) {
109
+ // Handle decryption errors gracefully
110
+ if (err.message?.includes('NoAccess')) {
111
+ // eslint-disable-next-line no-console
112
+ console.log(ui_1.colors.red('❌ Access denied: You are not whitelisted for this private repository.'));
113
+ // eslint-disable-next-line no-console
114
+ console.log(ui_1.colors.yellow(` Policy ID: ${meta.enc?.policy_id || sealPolicyId}`));
115
+ // eslint-disable-next-line no-console
116
+ console.log(ui_1.colors.yellow(' Please contact the repository owner to be added to the whitelist.'));
117
+ process.exit(1);
118
+ }
119
+ if (err.message?.includes('Timeout')) {
120
+ // eslint-disable-next-line no-console
121
+ console.log(ui_1.colors.red('❌ Connection timeout: Unable to reach Seal decryption servers.'));
122
+ // eslint-disable-next-line no-console
123
+ console.log(ui_1.colors.yellow(' Please check your network connection and try again.'));
124
+ process.exit(1);
125
+ }
126
+ // For other errors, show the file that failed
127
+ // eslint-disable-next-line no-console
128
+ console.log(ui_1.colors.red(`❌ Failed to decrypt ${rel}: ${err.message}`));
129
+ process.exit(1);
130
+ }
131
+ const hash = (0, serialize_1.sha256Base64)(plain);
132
+ if (hash !== meta.hash || plain.length !== meta.size) {
133
+ throw new Error(`Hash/size mismatch for ${rel}`);
134
+ }
135
+ const abs = path_1.default.join(process.cwd(), rel);
136
+ await (0, fs_1.ensureDirForFile)(abs);
137
+ await promises_1.default.writeFile(abs, plain);
138
+ const mode = parseInt(meta.mode, 10) & 0o777;
139
+ await promises_1.default.chmod(abs, mode);
140
+ index[rel] = { hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime };
141
+ }
142
+ // Write index and refs/state
143
+ await (0, fs_1.writeIndex)(path_1.default.join(witPath, 'index'), index);
144
+ await ensureHeadFiles(witPath, onchain.headCommit);
145
+ await (0, repo_1.writeRemoteRef)(witPath, onchain.headCommit);
146
+ await (0, repo_1.writeRemoteState)(witPath, {
147
+ repo_id: repoId,
148
+ head_commit: onchain.headCommit,
149
+ head_manifest: onchain.headManifest,
150
+ head_quilt: onchain.headQuilt,
151
+ version: onchain.version,
152
+ });
153
+ const commitMap = await readCommitIdMapSafe(witPath);
154
+ await downloadCommitChain(walrusSvc, onchain.headCommit, witPath, commitMap);
155
+ await (0, state_1.writeCommitIdMap)(witPath, commitMap);
156
+ // eslint-disable-next-line no-console
157
+ console.log(ui_1.colors.green('Clone complete.'));
158
+ // eslint-disable-next-line no-console
159
+ console.log(`Head: ${ui_1.colors.hash(onchain.headCommit)}`);
160
+ // eslint-disable-next-line no-console
161
+ console.log(`Manifest: ${ui_1.colors.hash(onchain.headManifest)}`);
162
+ // eslint-disable-next-line no-console
163
+ console.log(`Quilt: ${ui_1.colors.hash(onchain.headQuilt)}`);
164
+ }
165
+ async function fetchFileBytes(walrusSvc, manifest, rel, meta) {
166
+ // Prefer quilt fetch when quilt_id is present; fallback to direct file id
167
+ if (manifest.quilt_id) {
168
+ try {
169
+ const bytes = await walrusSvc.readQuiltFile(manifest.quilt_id, rel);
170
+ return Buffer.from(bytes);
171
+ }
172
+ catch (err) {
173
+ // fallback to direct id path below
174
+ }
175
+ }
176
+ if (meta.id) {
177
+ const files = await walrusSvc.getClient().getFiles({ ids: [meta.id] });
178
+ const file = files[0];
179
+ const data = Buffer.from(await file.bytes());
180
+ const tags = await file.getTags();
181
+ if (tags?.hash && tags.hash !== meta.hash) {
182
+ throw new Error(`Tag hash mismatch for ${rel}`);
183
+ }
184
+ return data;
185
+ }
186
+ throw new Error(`Manifest entry missing file id for ${rel}`);
187
+ }
188
+ async function ensureLayout(cwd, repoId) {
189
+ const witPath = path_1.default.join(cwd, '.wit');
190
+ await promises_1.default.mkdir(witPath, { recursive: true });
191
+ const subdirs = [
192
+ 'refs/heads',
193
+ 'refs/remotes',
194
+ 'objects/blobs',
195
+ 'objects/commits',
196
+ 'objects/manifests',
197
+ 'objects/quilts',
198
+ 'objects/maps',
199
+ 'state',
200
+ ];
201
+ await Promise.all(subdirs.map((d) => promises_1.default.mkdir(path_1.default.join(witPath, d), { recursive: true })));
202
+ const cfgPath = path_1.default.join(witPath, 'config.json');
203
+ try {
204
+ await promises_1.default.access(cfgPath);
205
+ }
206
+ catch (err) {
207
+ if (err?.code === 'ENOENT') {
208
+ const cfg = {
209
+ repo_name: repoId,
210
+ repo_id: repoId,
211
+ network: DEFAULT_NETWORK,
212
+ relays: DEFAULT_RELAYS,
213
+ author: 'unknown',
214
+ key_alias: 'default',
215
+ seal_policy_id: null,
216
+ created_at: new Date().toISOString(),
217
+ };
218
+ await (0, repo_1.writeRepoConfig)(witPath, cfg);
219
+ }
220
+ else {
221
+ throw err;
222
+ }
223
+ }
224
+ await promises_1.default.writeFile(path_1.default.join(witPath, 'HEAD'), 'refs/heads/main\n', 'utf8');
225
+ return witPath;
226
+ }
227
+ async function ensureHeadFiles(witPath, headCommit) {
228
+ const headRefPath = path_1.default.join(witPath, 'refs', 'heads', 'main');
229
+ await promises_1.default.writeFile(headRefPath, `${headCommit}\n`, 'utf8');
230
+ }
231
+ function parseRemoteCommit(buf) {
232
+ const parsed = JSON.parse(buf.toString('utf8'));
233
+ if (!parsed?.tree?.root_hash || !parsed?.tree?.manifest_id) {
234
+ throw new Error('Invalid remote commit object');
235
+ }
236
+ return parsed;
237
+ }
238
+ async function cacheJson(filePath, content) {
239
+ await (0, fs_1.ensureDirForFile)(filePath);
240
+ await promises_1.default.writeFile(filePath, content, 'utf8');
241
+ }
242
+ async function readCommitIdMapSafe(witPath) {
243
+ try {
244
+ return await (0, state_1.readCommitIdMap)(witPath);
245
+ }
246
+ catch {
247
+ return {};
248
+ }
249
+ }
250
+ async function downloadCommitChain(walrusSvc, startId, witPath, map) {
251
+ const seen = new Set();
252
+ let current = startId;
253
+ while (current && !seen.has(current)) {
254
+ seen.add(current);
255
+ const buf = Buffer.from(await walrusSvc.readBlob(current));
256
+ const commit = parseRemoteCommit(buf);
257
+ await cacheJson(path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(current)}.json`), buf.toString('utf8'));
258
+ if (!map[current]) {
259
+ map[current] = current;
260
+ }
261
+ current = commit.parent;
262
+ }
263
+ }
@@ -0,0 +1,150 @@
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.commitAction = commitAction;
7
+ exports.logAction = logAction;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const serialize_1 = require("../lib/serialize");
11
+ const fs_1 = require("../lib/fs");
12
+ const state_1 = require("../lib/state");
13
+ const repo_1 = require("../lib/repo");
14
+ const manifest_1 = require("../lib/manifest");
15
+ const ui_1 = require("../lib/ui");
16
+ const WIT_DIR = '.wit';
17
+ async function commitAction(opts) {
18
+ const message = opts.message;
19
+ if (!message) {
20
+ throw new Error('Commit message is required (use -m/--message).');
21
+ }
22
+ const witPath = await requireWitDir();
23
+ const indexPath = path_1.default.join(witPath, 'index');
24
+ const index = await (0, fs_1.readIndex)(indexPath);
25
+ if (Object.keys(index).length === 0) {
26
+ // eslint-disable-next-line no-console
27
+ console.warn('Index is empty. Nothing to commit.');
28
+ return;
29
+ }
30
+ const config = await readConfig(witPath);
31
+ if (!config.author || config.author === 'unknown') {
32
+ // eslint-disable-next-line no-console
33
+ console.warn(ui_1.colors.yellow('Warning: author is unknown. Set author in .wit/config.json or ~/.witconfig.'));
34
+ }
35
+ const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
36
+ const parent = await (0, state_1.readRef)(headRefPath);
37
+ const rootHash = (0, manifest_1.computeRootHash)(index);
38
+ const commit = {
39
+ tree: {
40
+ root_hash: rootHash,
41
+ manifest_id: null,
42
+ quilt_id: null,
43
+ files: index,
44
+ },
45
+ parent,
46
+ author: config.author || 'unknown',
47
+ message,
48
+ timestamp: Math.floor(Date.now() / 1000),
49
+ extras: { patch_id: null, tags: {} },
50
+ };
51
+ const serialized = (0, serialize_1.canonicalStringify)(commit);
52
+ const commitId = (0, serialize_1.sha256Base64)(serialized);
53
+ await writeCommitObject(witPath, commitId, serialized);
54
+ await promises_1.default.writeFile(headRefPath, `${commitId}\n`, 'utf8');
55
+ await updateCommitMap(witPath, commitId);
56
+ // eslint-disable-next-line no-console
57
+ console.log(ui_1.colors.green(`Committed ${commitId}`));
58
+ }
59
+ async function logAction() {
60
+ const witPath = await requireWitDir();
61
+ const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
62
+ const head = await (0, state_1.readRef)(headRefPath);
63
+ const remoteHead = await (0, repo_1.readRemoteRef)(witPath);
64
+ const commitMap = await (0, state_1.readCommitIdMap)(witPath);
65
+ // If the local HEAD has already been pushed and maps to remoteHead, treat it as the same commit.
66
+ const headRemoteMapped = head ? commitMap[head] : null;
67
+ const remoteAligned = head && remoteHead && headRemoteMapped === remoteHead;
68
+ const seen = new Set();
69
+ if (head) {
70
+ // eslint-disable-next-line no-console
71
+ console.log(ui_1.colors.header('Local (HEAD):'));
72
+ let currentId = head;
73
+ while (currentId) {
74
+ const commit = await readCommit(witPath, currentId);
75
+ printCommit(currentId, commit);
76
+ seen.add(currentId);
77
+ currentId = commit.parent;
78
+ }
79
+ if (remoteAligned) {
80
+ // eslint-disable-next-line no-console
81
+ console.log(ui_1.colors.gray(`(remote id: ${ui_1.colors.hash(remoteHead)})`));
82
+ }
83
+ }
84
+ else {
85
+ // eslint-disable-next-line no-console
86
+ console.log('No local commits yet.');
87
+ }
88
+ if (remoteHead && (!head || remoteHead !== head) && !remoteAligned) {
89
+ // eslint-disable-next-line no-console
90
+ console.log(ui_1.colors.header('Remote (remotes/main):'));
91
+ let currentId = remoteHead;
92
+ while (currentId && !seen.has(currentId)) {
93
+ const commit = await readCommit(witPath, currentId);
94
+ printCommit(currentId, commit);
95
+ seen.add(currentId);
96
+ currentId = commit.parent;
97
+ }
98
+ }
99
+ }
100
+ async function requireWitDir() {
101
+ const dir = path_1.default.join(process.cwd(), WIT_DIR);
102
+ try {
103
+ await promises_1.default.access(dir);
104
+ return dir;
105
+ }
106
+ catch {
107
+ throw new Error('Not a wit repository (missing .wit). Run `wit init` first.');
108
+ }
109
+ }
110
+ async function readConfig(witPath) {
111
+ const file = path_1.default.join(witPath, 'config.json');
112
+ try {
113
+ const raw = await promises_1.default.readFile(file, 'utf8');
114
+ return JSON.parse(raw);
115
+ }
116
+ catch (err) {
117
+ if (err?.code === 'ENOENT') {
118
+ return {};
119
+ }
120
+ throw err;
121
+ }
122
+ }
123
+ async function writeCommitObject(witPath, commitId, serialized) {
124
+ const file = path_1.default.join(witPath, 'objects', 'commits', `${(0, state_1.idToFileName)(commitId)}.json`);
125
+ await promises_1.default.writeFile(file, serialized, 'utf8');
126
+ }
127
+ async function readCommit(witPath, commitId) {
128
+ return (0, state_1.readCommitById)(witPath, commitId);
129
+ }
130
+ function printCommit(id, commit) {
131
+ // eslint-disable-next-line no-console
132
+ console.log(ui_1.colors.header(`commit ${ui_1.colors.hash(id)}`));
133
+ // eslint-disable-next-line no-console
134
+ console.log(`Author: ${ui_1.colors.author(commit.author)}`);
135
+ // eslint-disable-next-line no-console
136
+ console.log(`Date: ${ui_1.colors.date(new Date(commit.timestamp * 1000).toISOString())}`);
137
+ // eslint-disable-next-line no-console
138
+ console.log();
139
+ // eslint-disable-next-line no-console
140
+ console.log(` ${commit.message}`);
141
+ // eslint-disable-next-line no-console
142
+ console.log();
143
+ }
144
+ async function updateCommitMap(witPath, commitId) {
145
+ const map = await (0, state_1.readCommitIdMap)(witPath);
146
+ if (!map[commitId]) {
147
+ map[commitId] = null;
148
+ await (0, state_1.writeCommitIdMap)(witPath, map);
149
+ }
150
+ }
@@ -0,0 +1,159 @@
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.diffAction = diffAction;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const diff_1 = require("diff");
10
+ const isbinaryfile_1 = require("isbinaryfile");
11
+ const fs_1 = require("../lib/fs");
12
+ const state_1 = require("../lib/state");
13
+ const ui_1 = require("../lib/ui");
14
+ const WIT_DIR = '.wit';
15
+ async function diffAction(opts) {
16
+ const cwd = process.cwd();
17
+ const witPath = await requireWitDir();
18
+ const indexPath = path_1.default.join(witPath, 'index');
19
+ const index = await (0, fs_1.readIndex)(indexPath);
20
+ if (opts.cached) {
21
+ const headRefPath = await (0, state_1.readHeadRefPath)(witPath);
22
+ const headId = await (0, state_1.readRef)(headRefPath);
23
+ if (!headId) {
24
+ // eslint-disable-next-line no-console
25
+ console.warn('No commits to diff against.');
26
+ return;
27
+ }
28
+ const commit = await (0, state_1.readCommitById)(witPath, headId);
29
+ const changes = await diffIndex(commit.tree.files, index, {
30
+ loadBase: (rel, meta) => (0, fs_1.readBlob)(witPath, meta.hash),
31
+ loadTarget: (rel, meta) => (0, fs_1.readBlob)(witPath, meta.hash),
32
+ baseLabel: 'HEAD',
33
+ targetLabel: 'index',
34
+ cwd,
35
+ });
36
+ printChanges('index vs HEAD', changes);
37
+ }
38
+ else {
39
+ const tracked = new Set(Object.keys(index));
40
+ const ig = await (0, fs_1.buildIgnore)(cwd);
41
+ const workspaceFiles = await (0, fs_1.walkFiles)(cwd, ig, cwd, tracked);
42
+ const workspaceMeta = {};
43
+ for (const file of workspaceFiles) {
44
+ const rel = (0, fs_1.pathToPosix)(path_1.default.relative(cwd, file));
45
+ workspaceMeta[rel] = await (0, fs_1.computeFileMeta)(file);
46
+ }
47
+ const changes = await diffIndex(index, workspaceMeta, {
48
+ loadBase: (rel, meta) => (0, fs_1.readBlob)(witPath, meta.hash),
49
+ loadTarget: async (rel) => {
50
+ try {
51
+ return await promises_1.default.readFile(path_1.default.join(cwd, rel));
52
+ }
53
+ catch (err) {
54
+ if (err?.code === 'ENOENT')
55
+ return null;
56
+ throw err;
57
+ }
58
+ },
59
+ baseLabel: 'index',
60
+ targetLabel: 'worktree',
61
+ cwd,
62
+ });
63
+ printChanges('worktree vs index', changes);
64
+ }
65
+ }
66
+ async function diffIndex(base, target, ctx) {
67
+ const changes = [];
68
+ const paths = new Set([...Object.keys(base), ...Object.keys(target)]);
69
+ for (const rel of Array.from(paths).sort()) {
70
+ const a = base[rel];
71
+ const b = target[rel];
72
+ if (!a && b) {
73
+ const targetBuf = await ctx.loadTarget(rel, b);
74
+ const binary = await isBinaryBuffers(undefined, targetBuf);
75
+ const patch = binary ? undefined : createPatch(rel, '', targetBuf?.toString('utf8') ?? '', ctx.baseLabel, ctx.targetLabel);
76
+ changes.push({ path: rel, kind: 'A', binary, patch });
77
+ }
78
+ else if (a && !b) {
79
+ const baseBuf = await ctx.loadBase(rel, a);
80
+ const binary = await isBinaryBuffers(baseBuf, undefined);
81
+ const patch = binary ? undefined : createPatch(rel, baseBuf?.toString('utf8') ?? '', '', ctx.baseLabel, ctx.targetLabel);
82
+ changes.push({ path: rel, kind: 'D', binary, patch });
83
+ }
84
+ else if (a && b && !sameMeta(a, b)) {
85
+ const baseBuf = await ctx.loadBase(rel, a);
86
+ const targetBuf = await ctx.loadTarget(rel, b);
87
+ const binary = await isBinaryBuffers(baseBuf, targetBuf);
88
+ const patch = binary
89
+ ? undefined
90
+ : createPatch(rel, baseBuf?.toString('utf8') ?? '', targetBuf?.toString('utf8') ?? '', ctx.baseLabel, ctx.targetLabel);
91
+ changes.push({ path: rel, kind: 'M', binary, patch });
92
+ }
93
+ }
94
+ return changes;
95
+ }
96
+ function sameMeta(a, b) {
97
+ return a.hash === b.hash && a.size === b.size && a.mode === b.mode;
98
+ }
99
+ async function isBinaryBuffers(a, b) {
100
+ const checks = [a, b];
101
+ for (const buf of checks) {
102
+ if (!buf)
103
+ continue;
104
+ try {
105
+ if (await (0, isbinaryfile_1.isBinaryFile)(buf))
106
+ return true;
107
+ }
108
+ catch {
109
+ // fallback silent; continue to next buffer
110
+ }
111
+ }
112
+ return false;
113
+ }
114
+ function printChanges(title, changes) {
115
+ if (!changes.length) {
116
+ // eslint-disable-next-line no-console
117
+ console.log('No differences.');
118
+ return;
119
+ }
120
+ // eslint-disable-next-line no-console
121
+ console.log(ui_1.colors.header(`# Diff (${title})`));
122
+ changes.forEach((c) => {
123
+ const kindLabel = c.kind === 'A' ? 'Added' : c.kind === 'D' ? 'Deleted' : 'Modified';
124
+ const binaryLabel = c.binary ? 'binary' : 'text';
125
+ const color = c.kind === 'A' ? ui_1.colors.green : c.kind === 'D' ? ui_1.colors.red : ui_1.colors.yellow;
126
+ // eslint-disable-next-line no-console
127
+ console.log(color(`${c.kind}\t[${binaryLabel}]\t${c.path}\t${kindLabel}`));
128
+ if (!c.binary && c.patch) {
129
+ console.log(formatPatch(c.patch));
130
+ }
131
+ });
132
+ }
133
+ function formatPatch(patch) {
134
+ return patch
135
+ .split('\n')
136
+ .map((line) => {
137
+ if (line.startsWith('+'))
138
+ return ui_1.colors.green(line);
139
+ if (line.startsWith('-'))
140
+ return ui_1.colors.red(line);
141
+ if (line.startsWith('@@'))
142
+ return ui_1.colors.cyan(line);
143
+ return line;
144
+ })
145
+ .join('\n');
146
+ }
147
+ function createPatch(rel, a, b, aLabel, bLabel) {
148
+ return (0, diff_1.createTwoFilesPatch)(`${aLabel}:${rel}`, `${bLabel}:${rel}`, a, b, undefined, undefined, { context: 3 });
149
+ }
150
+ async function requireWitDir() {
151
+ const dir = path_1.default.join(process.cwd(), WIT_DIR);
152
+ try {
153
+ await promises_1.default.access(dir);
154
+ return dir;
155
+ }
156
+ catch {
157
+ throw new Error('Not a wit repository (missing .wit). Run `wit init` first.');
158
+ }
159
+ }