@wildwinter/simple-vc-lib 0.0.2

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,731 @@
1
+ import { join, dirname, resolve } from 'path';
2
+ import { existsSync, readFileSync, statSync, unlinkSync, rmSync, accessSync, constants, chmodSync, writeFileSync } from 'fs';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ const VALID_SYSTEMS = ['git', 'perforce', 'plastic', 'svn', 'filesystem'];
6
+
7
+ /**
8
+ * Load explicit VC configuration, if any.
9
+ * Checks the SIMPLE_VC environment variable first, then walks up from
10
+ * startDir looking for a .vcconfig JSON file.
11
+ *
12
+ * .vcconfig format: { "system": "git" | "perforce" | "plastic" | "svn" | "filesystem" }
13
+ *
14
+ * @param {string} startDir - Directory to begin walking upward from.
15
+ * @returns {{ system: string } | null}
16
+ */
17
+ function loadConfig(startDir) {
18
+ const envSystem = process.env.SIMPLE_VC?.toLowerCase();
19
+ if (envSystem && VALID_SYSTEMS.includes(envSystem)) {
20
+ return { system: envSystem };
21
+ }
22
+
23
+ let dir = startDir;
24
+ while (true) {
25
+ const configPath = join(dir, '.vcconfig');
26
+ if (existsSync(configPath)) {
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
29
+ const system = parsed?.system?.toLowerCase();
30
+ if (system && VALID_SYSTEMS.includes(system)) {
31
+ return { system };
32
+ }
33
+ } catch {
34
+ // Malformed .vcconfig — ignore and fall through to auto-detection.
35
+ }
36
+ }
37
+ const parent = dirname(dir);
38
+ if (parent === dir) break;
39
+ dir = parent;
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Run a CLI command synchronously.
47
+ * Arguments are passed as an array to avoid shell-injection and handle spaces in paths.
48
+ */
49
+ function runCommand(command, args, options = {}) {
50
+ const result = spawnSync(command, args, {
51
+ encoding: 'utf8',
52
+ timeout: options.timeout ?? 10000,
53
+ cwd: options.cwd,
54
+ windowsHide: true,
55
+ });
56
+
57
+ return {
58
+ exitCode: result.status ?? -1,
59
+ output: (result.stdout ?? '').trim(),
60
+ error: (result.stderr ?? '').trim(),
61
+ timedOut: result.signal === 'SIGTERM' || result.signal === 'SIGKILL',
62
+ };
63
+ }
64
+
65
+ // Maps a VCS root directory to its system name.
66
+ // Populated on first detection; avoids repeated directory walks for the same repo.
67
+ const _rootCache = new Map();
68
+
69
+ /** Clear the detection cache. Called when the provider override is cleared. */
70
+ function clearDetectorCache() {
71
+ _rootCache.clear();
72
+ }
73
+
74
+ /**
75
+ * Auto-detect the active version control system for a given file path.
76
+ * Walks up the directory tree looking for VC markers, then checks for Perforce.
77
+ * Falls back to 'filesystem' if nothing is found.
78
+ *
79
+ * @param {string} absPath - Absolute path of the file (may not exist yet).
80
+ * @returns {'git' | 'plastic' | 'svn' | 'perforce' | 'filesystem'}
81
+ */
82
+ function detectProvider(absPath) {
83
+ let dir;
84
+ try {
85
+ dir = statSync(absPath).isDirectory() ? absPath : dirname(absPath);
86
+ } catch {
87
+ // Path doesn't exist yet — use its parent directory.
88
+ dir = dirname(absPath);
89
+ }
90
+
91
+ // Check whether any ancestor is a known VCS root before doing any I/O.
92
+ let current = dir;
93
+ while (true) {
94
+ if (_rootCache.has(current)) return _rootCache.get(current);
95
+ const parent = dirname(current);
96
+ if (parent === current) break;
97
+ current = parent;
98
+ }
99
+
100
+ // Walk up looking for VC marker directories/files.
101
+ current = dir;
102
+ while (true) {
103
+ if (existsSync(join(current, '.git'))) {
104
+ _rootCache.set(current, 'git');
105
+ return 'git';
106
+ }
107
+ if (existsSync(join(current, '.plastic'))) {
108
+ _rootCache.set(current, 'plastic');
109
+ return 'plastic';
110
+ }
111
+ if (existsSync(join(current, '.svn'))) {
112
+ _rootCache.set(current, 'svn');
113
+ return 'svn';
114
+ }
115
+ const parent = dirname(current);
116
+ if (parent === current) break;
117
+ current = parent;
118
+ }
119
+
120
+ // Perforce has no marker directory; detect by running `p4 info`.
121
+ const p4 = runCommand('p4', ['info'], { timeout: 3000 });
122
+ if (p4.exitCode === 0 && p4.output.includes('Client name:')) return 'perforce';
123
+
124
+ return 'filesystem';
125
+ }
126
+
127
+ /**
128
+ * @typedef {'ok' | 'locked' | 'outOfDate' | 'error'} VCStatus
129
+ * @typedef {{ success: boolean, status: VCStatus, message: string }} VCResult
130
+ */
131
+
132
+ /** @returns {VCResult} */
133
+ function okResult(message = '') {
134
+ return { success: true, status: 'ok', message };
135
+ }
136
+
137
+ /** @returns {VCResult} */
138
+ function errorResult(status, message) {
139
+ return { success: false, status, message };
140
+ }
141
+
142
+ function isWritable$1(filePath) {
143
+ try {
144
+ accessSync(filePath, constants.W_OK);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ function makeWritable$1(filePath) {
152
+ try {
153
+ const mode = statSync(filePath).mode;
154
+ chmodSync(filePath, mode | 0o200);
155
+ return isWritable$1(filePath);
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ function git(args, cwd) {
162
+ return runCommand('git', args, { cwd });
163
+ }
164
+
165
+ /**
166
+ * Returns true if the file is tracked by git (exists in the index).
167
+ */
168
+ function isTracked$2(filePath) {
169
+ const result = git(['ls-files', '--error-unmatch', filePath], dirname(filePath));
170
+ return result.exitCode === 0;
171
+ }
172
+
173
+ /**
174
+ * Git provider.
175
+ *
176
+ * Git does not use file locking, so prepareToWrite only needs to ensure the
177
+ * file is writable at the OS level. finishedWrite stages new files with git add.
178
+ */
179
+ class GitProvider {
180
+ get name() { return 'git'; }
181
+
182
+ prepareToWrite(filePath) {
183
+ if (!existsSync(filePath)) return okResult();
184
+ if (isWritable$1(filePath)) return okResult();
185
+ if (makeWritable$1(filePath)) return okResult('File made writable');
186
+ return errorResult('error', `Cannot make file writable: ${filePath}`);
187
+ }
188
+
189
+ finishedWrite(filePath) {
190
+ if (!existsSync(filePath))
191
+ return errorResult('error', `'${filePath}' does not exist after write`);
192
+
193
+ if (isTracked$2(filePath)) return okResult();
194
+
195
+ const result = git(['add', filePath], dirname(filePath));
196
+ if (result.exitCode === 0) return okResult('File added to git');
197
+ return errorResult('error', `Cannot add '${filePath}' to git: ${result.error || result.output}`);
198
+ }
199
+
200
+ deleteFile(filePath) {
201
+ if (!existsSync(filePath)) return okResult();
202
+
203
+ if (isTracked$2(filePath)) {
204
+ const result = git(['rm', '--force', filePath], dirname(filePath));
205
+ if (result.exitCode === 0) return okResult();
206
+ return errorResult('error', `Cannot delete '${filePath}' from git: ${result.error || result.output}`);
207
+ }
208
+
209
+ try {
210
+ unlinkSync(filePath);
211
+ return okResult();
212
+ } catch (e) {
213
+ return errorResult('error', `Failed to delete file: ${e.message}`);
214
+ }
215
+ }
216
+
217
+ deleteFolder(folderPath) {
218
+ if (!existsSync(folderPath)) return okResult();
219
+
220
+ const listResult = git(['ls-files', folderPath], folderPath);
221
+ if (listResult.exitCode === 0 && listResult.output.length > 0) {
222
+ const rmResult = git(['rm', '-r', '--force', folderPath], folderPath);
223
+ if (rmResult.exitCode !== 0) {
224
+ return errorResult('error', `Cannot delete folder '${folderPath}' from git: ${rmResult.error || rmResult.output}`);
225
+ }
226
+ }
227
+
228
+ // Delete any remaining untracked files git rm left behind.
229
+ if (existsSync(folderPath)) {
230
+ try {
231
+ rmSync(folderPath, { recursive: true, force: true });
232
+ } catch (e) {
233
+ return errorResult('error', `Cannot delete folder '${folderPath}': ${e.message}`);
234
+ }
235
+ }
236
+
237
+ return okResult();
238
+ }
239
+ }
240
+
241
+ function isWritable(filePath) {
242
+ try {
243
+ accessSync(filePath, constants.W_OK);
244
+ return true;
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+
250
+ function makeWritable(filePath) {
251
+ try {
252
+ const mode = statSync(filePath).mode;
253
+ chmodSync(filePath, mode | 0o200); // Adds owner-write bit.
254
+ return isWritable(filePath);
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Plain-filesystem provider: no VC system, just file-system operations.
262
+ * Used as the fallback when no VC is detected.
263
+ */
264
+ class FilesystemProvider {
265
+ get name() { return 'filesystem'; }
266
+
267
+ prepareToWrite(filePath) {
268
+ if (!existsSync(filePath)) return okResult();
269
+ if (isWritable(filePath)) return okResult();
270
+ if (makeWritable(filePath)) return okResult('File made writable');
271
+ return errorResult('error', `Cannot make '${filePath}' writable`);
272
+ }
273
+
274
+ finishedWrite(filePath) {
275
+ if (!existsSync(filePath))
276
+ return errorResult('error', `'${filePath}' does not exist after write`);
277
+ return okResult();
278
+ }
279
+
280
+ deleteFile(filePath) {
281
+ if (!existsSync(filePath)) return okResult();
282
+ try {
283
+ unlinkSync(filePath);
284
+ return okResult();
285
+ } catch (e) {
286
+ return errorResult('error', `Cannot delete '${filePath}': ${e.message}`);
287
+ }
288
+ }
289
+
290
+ deleteFolder(folderPath) {
291
+ if (!existsSync(folderPath)) return okResult();
292
+ try {
293
+ rmSync(folderPath, { recursive: true, force: true });
294
+ return okResult();
295
+ } catch (e) {
296
+ return errorResult('error', `Cannot delete folder '${folderPath}': ${e.message}`);
297
+ }
298
+ }
299
+ }
300
+
301
+ const fs$2 = new FilesystemProvider();
302
+
303
+ function p4(args) {
304
+ return runCommand('p4', args);
305
+ }
306
+
307
+ /**
308
+ * Returns p4 fstat info for a file, or null if the file is not in the depot.
309
+ */
310
+ function fstat(filePath) {
311
+ const result = p4(['fstat', filePath]);
312
+ if (result.exitCode !== 0) return null;
313
+ return result.output;
314
+ }
315
+
316
+ function isInDepot(filePath) {
317
+ return fstat(filePath) !== null;
318
+ }
319
+
320
+ /**
321
+ * Perforce (Helix Core) provider.
322
+ *
323
+ * Uses the `p4` CLI. Assumes the workspace is already configured in the
324
+ * environment (P4PORT, P4USER, P4CLIENT, or via p4 config/tickets).
325
+ */
326
+ class PerforceProvider {
327
+ get name() { return 'perforce'; }
328
+
329
+ prepareToWrite(filePath) {
330
+ if (!existsSync(filePath)) return okResult();
331
+
332
+ if (!isInDepot(filePath)) {
333
+ return fs$2.prepareToWrite(filePath);
334
+ }
335
+
336
+ const result = p4(['edit', filePath]);
337
+ if (result.exitCode === 0) return okResult();
338
+
339
+ const combined = (result.output + ' ' + result.error).toLowerCase();
340
+ if (combined.includes('locked by')) {
341
+ return errorResult('locked', `'${filePath}' is locked by another user`);
342
+ }
343
+ if (combined.includes('out of date')) {
344
+ return errorResult('outOfDate', `'${filePath}' is out of date — sync before editing`);
345
+ }
346
+ return errorResult('error', `Cannot open '${filePath}' for editing: ${result.error || result.output}`);
347
+ }
348
+
349
+ finishedWrite(filePath) {
350
+ if (!existsSync(filePath))
351
+ return errorResult('error', `'${filePath}' does not exist after write`);
352
+
353
+ const info = fstat(filePath);
354
+ if (info !== null) return okResult();
355
+
356
+ const result = p4(['add', filePath]);
357
+ if (result.exitCode === 0) return okResult('File opened for add in Perforce');
358
+ return errorResult('error', `Cannot add '${filePath}' to Perforce: ${result.error || result.output}`);
359
+ }
360
+
361
+ deleteFile(filePath) {
362
+ if (!existsSync(filePath)) return okResult();
363
+
364
+ if (isInDepot(filePath)) {
365
+ const result = p4(['delete', filePath]);
366
+ if (result.exitCode === 0) return okResult();
367
+ return errorResult('error', `Cannot delete '${filePath}' from Perforce: ${result.error || result.output}`);
368
+ }
369
+
370
+ try {
371
+ unlinkSync(filePath);
372
+ return okResult();
373
+ } catch (e) {
374
+ return errorResult('error', `Cannot delete '${filePath}': ${e.message}`);
375
+ }
376
+ }
377
+
378
+ deleteFolder(folderPath) {
379
+ if (!existsSync(folderPath)) return okResult();
380
+
381
+ // The /... wildcard schedules the entire depot subtree for deletion.
382
+ const depotPath = folderPath.replace(/\\/g, '/') + '/...';
383
+ const result = p4(['delete', depotPath]);
384
+
385
+ if (existsSync(folderPath)) {
386
+ try {
387
+ rmSync(folderPath, { recursive: true, force: true });
388
+ } catch (e) {
389
+ return errorResult('error', `Cannot delete folder '${folderPath}': ${e.message}`);
390
+ }
391
+ }
392
+
393
+ // p4 delete of untracked path exits non-zero, but we still succeeded locally.
394
+ if (result.exitCode !== 0 && result.error && !result.error.includes('no such file')) {
395
+ return errorResult('error', `Cannot delete folder '${folderPath}' from Perforce: ${result.error || result.output}`);
396
+ }
397
+
398
+ return okResult();
399
+ }
400
+ }
401
+
402
+ const fs$1 = new FilesystemProvider();
403
+
404
+ function cm(args) {
405
+ return runCommand('cm', args);
406
+ }
407
+
408
+ /**
409
+ * Returns true if the file is tracked by Plastic SCM (Unity Version Control).
410
+ */
411
+ function isTracked$1(filePath) {
412
+ const result = cm(['status', '--short', filePath]);
413
+ if (result.exitCode !== 0) return false;
414
+ // Untracked files are reported with a '?' prefix.
415
+ const lines = result.output.split('\n').filter(l => l.trim());
416
+ return lines.length > 0 && !lines[0].startsWith('?');
417
+ }
418
+
419
+ /**
420
+ * Plastic SCM / Unity Version Control provider.
421
+ *
422
+ * Uses the `cm` CLI (Plastic SCM command-line client).
423
+ * Files under Plastic SCM are read-only until checked out.
424
+ */
425
+ class PlasticProvider {
426
+ get name() { return 'plastic'; }
427
+
428
+ prepareToWrite(filePath) {
429
+ if (!existsSync(filePath)) return okResult();
430
+
431
+ if (!isTracked$1(filePath)) {
432
+ return fs$1.prepareToWrite(filePath);
433
+ }
434
+
435
+ const result = cm(['co', filePath]);
436
+ if (result.exitCode === 0) return okResult();
437
+
438
+ const combined = (result.output + ' ' + result.error).toLowerCase();
439
+ if (combined.includes('locked') || combined.includes('exclusive')) {
440
+ return errorResult('locked', `'${filePath}' is locked`);
441
+ }
442
+ if (combined.includes('out of date') || combined.includes('not latest')) {
443
+ return errorResult('outOfDate', `'${filePath}' is out of date — update before editing`);
444
+ }
445
+ return errorResult('error', `Cannot check out '${filePath}': ${result.error || result.output}`);
446
+ }
447
+
448
+ finishedWrite(filePath) {
449
+ if (!existsSync(filePath))
450
+ return errorResult('error', `'${filePath}' does not exist after write`);
451
+
452
+ if (isTracked$1(filePath)) return okResult();
453
+
454
+ const result = cm(['add', filePath]);
455
+ if (result.exitCode === 0) return okResult('File added to Plastic SCM');
456
+ return errorResult('error', `Cannot add '${filePath}' to Plastic SCM: ${result.error || result.output}`);
457
+ }
458
+
459
+ deleteFile(filePath) {
460
+ if (!existsSync(filePath)) return okResult();
461
+
462
+ if (isTracked$1(filePath)) {
463
+ const result = cm(['remove', filePath]);
464
+ if (result.exitCode === 0) return okResult();
465
+ return errorResult('error', `Cannot delete '${filePath}' from Plastic SCM: ${result.error || result.output}`);
466
+ }
467
+
468
+ try {
469
+ unlinkSync(filePath);
470
+ return okResult();
471
+ } catch (e) {
472
+ return errorResult('error', `Cannot delete '${filePath}': ${e.message}`);
473
+ }
474
+ }
475
+
476
+ deleteFolder(folderPath) {
477
+ if (!existsSync(folderPath)) return okResult();
478
+
479
+ if (isTracked$1(folderPath)) {
480
+ // cm remove is recursive for directories.
481
+ const result = cm(['remove', folderPath]);
482
+ if (result.exitCode !== 0) {
483
+ return errorResult('error', `Cannot delete folder '${folderPath}' from Plastic SCM: ${result.error || result.output}`);
484
+ }
485
+ }
486
+
487
+ if (existsSync(folderPath)) {
488
+ try {
489
+ rmSync(folderPath, { recursive: true, force: true });
490
+ } catch (e) {
491
+ return errorResult('error', `Cannot delete folder '${folderPath}': ${e.message}`);
492
+ }
493
+ }
494
+
495
+ return okResult();
496
+ }
497
+ }
498
+
499
+ const fs = new FilesystemProvider();
500
+
501
+ function svn(args) {
502
+ return runCommand('svn', args);
503
+ }
504
+
505
+ /**
506
+ * Returns true if the file is tracked by SVN.
507
+ */
508
+ function isTracked(filePath) {
509
+ const result = svn(['info', filePath]);
510
+ return result.exitCode === 0;
511
+ }
512
+
513
+ /**
514
+ * Subversion (SVN) provider.
515
+ *
516
+ * SVN files are normally writable. The exception is files with the
517
+ * svn:needs-lock property, which are read-only until locked.
518
+ * prepareToWrite handles this by calling `svn lock`.
519
+ */
520
+ class SvnProvider {
521
+ get name() { return 'svn'; }
522
+
523
+ prepareToWrite(filePath) {
524
+ if (!existsSync(filePath)) return okResult();
525
+
526
+ const fsResult = fs.prepareToWrite(filePath);
527
+ if (fsResult.success) return okResult();
528
+
529
+ // File is read-only — only expected for files with svn:needs-lock set.
530
+ if (!isTracked(filePath)) {
531
+ return errorResult('error', `Cannot make '${filePath}' writable`);
532
+ }
533
+
534
+ const result = svn(['lock', filePath]);
535
+ if (result.exitCode === 0) return okResult('File locked in SVN');
536
+
537
+ const combined = (result.output + ' ' + result.error).toLowerCase();
538
+ if (combined.includes('locked by') || combined.includes('steal lock')) {
539
+ return errorResult('locked', `'${filePath}' is locked by another user`);
540
+ }
541
+ if (combined.includes('out of date')) {
542
+ return errorResult('outOfDate', `'${filePath}' is out of date — update before locking`);
543
+ }
544
+ return errorResult('error', `Cannot lock '${filePath}' in SVN: ${result.error || result.output}`);
545
+ }
546
+
547
+ finishedWrite(filePath) {
548
+ if (!existsSync(filePath))
549
+ return errorResult('error', `'${filePath}' does not exist after write`);
550
+
551
+ if (isTracked(filePath)) return okResult();
552
+
553
+ const result = svn(['add', filePath]);
554
+ if (result.exitCode === 0) return okResult('File added to SVN');
555
+ return errorResult('error', `Cannot add '${filePath}' to SVN: ${result.error || result.output}`);
556
+ }
557
+
558
+ deleteFile(filePath) {
559
+ if (!existsSync(filePath)) return okResult();
560
+
561
+ if (isTracked(filePath)) {
562
+ const result = svn(['delete', '--force', filePath]);
563
+ if (result.exitCode === 0) return okResult();
564
+ return errorResult('error', `Cannot delete '${filePath}' from SVN: ${result.error || result.output}`);
565
+ }
566
+
567
+ try {
568
+ unlinkSync(filePath);
569
+ return okResult();
570
+ } catch (e) {
571
+ return errorResult('error', `Failed to delete file: ${e.message}`);
572
+ }
573
+ }
574
+
575
+ deleteFolder(folderPath) {
576
+ if (!existsSync(folderPath)) return okResult();
577
+
578
+ if (isTracked(folderPath)) {
579
+ const result = svn(['delete', '--force', folderPath]);
580
+ if (result.exitCode !== 0) {
581
+ return errorResult('error', `Cannot delete folder '${folderPath}' from SVN: ${result.error || result.output}`);
582
+ }
583
+ } else {
584
+ try {
585
+ rmSync(folderPath, { recursive: true, force: true });
586
+ } catch (e) {
587
+ return errorResult('error', `Failed to delete folder: ${e.message}`);
588
+ }
589
+ }
590
+
591
+ return okResult();
592
+ }
593
+ }
594
+
595
+ const PROVIDER_MAP = {
596
+ git: () => new GitProvider(),
597
+ perforce: () => new PerforceProvider(),
598
+ plastic: () => new PlasticProvider(),
599
+ svn: () => new SvnProvider(),
600
+ filesystem: () => new FilesystemProvider(),
601
+ };
602
+
603
+ /** @type {object | null} An explicitly set provider that bypasses auto-detection. */
604
+ let _overrideProvider = null;
605
+
606
+ /**
607
+ * Override the provider used for all operations.
608
+ * Useful for testing or in environments where auto-detection is unreliable.
609
+ * Pass a provider instance, or null to clear the override.
610
+ *
611
+ * @param {object | null} provider
612
+ */
613
+ function setProvider(provider) {
614
+ _overrideProvider = provider;
615
+ }
616
+
617
+ /** Clear any previously set provider override, restoring auto-detection. */
618
+ function clearProvider() {
619
+ _overrideProvider = null;
620
+ clearDetectorCache();
621
+ }
622
+
623
+ function dirOf(p) {
624
+ const abs = resolve(p);
625
+ try {
626
+ return statSync(abs).isDirectory() ? abs : dirname(abs);
627
+ } catch {
628
+ return dirname(abs);
629
+ }
630
+ }
631
+
632
+ function resolveProvider(filePath) {
633
+ if (_overrideProvider) return _overrideProvider;
634
+
635
+ const dir = dirOf(filePath);
636
+ const config = loadConfig(dir);
637
+ if (config) {
638
+ const factory = PROVIDER_MAP[config.system];
639
+ if (factory) return factory();
640
+ }
641
+
642
+ const detected = detectProvider(resolve(filePath));
643
+ return (PROVIDER_MAP[detected] ?? PROVIDER_MAP.filesystem)();
644
+ }
645
+
646
+ /**
647
+ * Prepare a file path for writing.
648
+ * Checks out or unlocks the file in VC if it is read-only.
649
+ * No-op if the file does not yet exist.
650
+ *
651
+ * On failure, `status` may be 'locked', 'outOfDate', or 'error'.
652
+ *
653
+ * @param {string} filePath
654
+ */
655
+ function prepareToWrite(filePath) {
656
+ return resolveProvider(filePath).prepareToWrite(filePath);
657
+ }
658
+
659
+ /**
660
+ * Notify the library that a file has been written.
661
+ * Adds the file to VC if it is not yet tracked. No-op for existing tracked files.
662
+ *
663
+ * @param {string} filePath
664
+ */
665
+ function finishedWrite(filePath) {
666
+ return resolveProvider(filePath).finishedWrite(filePath);
667
+ }
668
+
669
+ /**
670
+ * Delete a file, marking it for deletion in VC if tracked.
671
+ *
672
+ * @param {string} filePath
673
+ */
674
+ function deleteFile(filePath) {
675
+ return resolveProvider(filePath).deleteFile(filePath);
676
+ }
677
+
678
+ /**
679
+ * Delete a folder and all its contents, marking tracked files for deletion in VC.
680
+ *
681
+ * @param {string} folderPath
682
+ */
683
+ function deleteFolder(folderPath) {
684
+ return resolveProvider(folderPath).deleteFolder(folderPath);
685
+ }
686
+
687
+ /**
688
+ * Write text to a file, handling VC checkout and registration automatically.
689
+ * Calls `prepareToWrite`, writes the file, then calls `finishedWrite`.
690
+ * Works whether or not the file already exists.
691
+ *
692
+ * On failure, returns the result from whichever step failed.
693
+ *
694
+ * @param {string} filePath
695
+ * @param {string} content
696
+ * @param {BufferEncoding} [encoding='utf8']
697
+ */
698
+ function writeTextFile(filePath, content, encoding = 'utf8') {
699
+ const prep = resolveProvider(filePath).prepareToWrite(filePath);
700
+ if (!prep.success) return prep;
701
+ try {
702
+ writeFileSync(filePath, content, { encoding });
703
+ } catch (e) {
704
+ return { success: false, status: 'error', message: e.message };
705
+ }
706
+ return resolveProvider(filePath).finishedWrite(filePath);
707
+ }
708
+
709
+ /**
710
+ * Write binary data to a file, handling VC checkout and registration automatically.
711
+ * Calls `prepareToWrite`, writes the file, then calls `finishedWrite`.
712
+ * Works whether or not the file already exists.
713
+ *
714
+ * On failure, returns the result from whichever step failed.
715
+ *
716
+ * @param {string} filePath
717
+ * @param {Buffer | Uint8Array} data
718
+ */
719
+ function writeBinaryFile(filePath, data) {
720
+ const prep = resolveProvider(filePath).prepareToWrite(filePath);
721
+ if (!prep.success) return prep;
722
+ try {
723
+ writeFileSync(filePath, data);
724
+ } catch (e) {
725
+ return { success: false, status: 'error', message: e.message };
726
+ }
727
+ return resolveProvider(filePath).finishedWrite(filePath);
728
+ }
729
+
730
+ export { FilesystemProvider, GitProvider, PerforceProvider, PlasticProvider, SvnProvider, clearProvider, deleteFile, deleteFolder, finishedWrite, prepareToWrite, setProvider, writeBinaryFile, writeTextFile };
731
+ //# sourceMappingURL=simpleVcLib.js.map