@magicpixelart/cli 0.5.36 → 0.5.38
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/CHANGELOG.md +13 -5
- package/README.md +3 -0
- package/dist/commands/connect.js +4 -1
- package/dist/commands/sync.js +27 -9
- package/dist/index.js +1 -0
- package/dist/util/gameScan.js +43 -3
- package/dist/util/projectRoots.js +73 -0
- package/dist/util/watchCopy.js +0 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to `@magicpixelart/cli` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.5.38] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Full Unity projects also scan sprite dot-folders at the project root (e.g. `.SpineRaw`), not only `Assets/`. VCS/editor folders stay skipped.
|
|
12
|
+
- `sync` and `connect` report how many PNGs an `exclude` rule skipped, naming the rule.
|
|
13
|
+
|
|
14
|
+
## [0.5.37] - 2026-10-09
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `sync` stops before writing when the sprites it would download are connected to another game folder on this machine (e.g. run from the wrong repo), naming the right folder. `--here` overrides; README documents it.
|
|
18
|
+
|
|
8
19
|
## [0.5.36] - 2026-10-07
|
|
9
20
|
|
|
10
21
|
### Changed
|
|
@@ -26,13 +37,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
26
37
|
### Fixed
|
|
27
38
|
- Local edits to game files that were never changed in MagicPixel no longer fail as "also changed in MagicPixel". Push now sends the cloud timestamp it last saw, and sync no longer records the local file as MagicPixel's copy when the server has no cached hash.
|
|
28
39
|
|
|
29
|
-
### Changed
|
|
30
|
-
- Watch header now reads "N game sprites connected" — it counts game PNGs matched by `connect`, not files waiting in MagicPixel.
|
|
31
|
-
|
|
32
40
|
## [0.5.32] - 2026-10-05
|
|
33
41
|
|
|
34
|
-
###
|
|
35
|
-
-
|
|
42
|
+
### Changed
|
|
43
|
+
- Watch header now reads "N game sprites connected" — it counts game PNGs matched by `connect`, not files waiting in MagicPixel.
|
|
36
44
|
|
|
37
45
|
## [0.5.31] - 2026-10-01
|
|
38
46
|
|
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ No bundler config, no runtime, no extra package.
|
|
|
131
131
|
| `-c, --concurrency <n>` | Parallel downloads (1–16, default 6). |
|
|
132
132
|
| `-q, --quiet` | Minimal output (for CI). |
|
|
133
133
|
| `--only <folder...>` | Only push, pull and prune inside these game folders (repeatable). Everything else is left untouched. |
|
|
134
|
+
| `--here` | Download even if these sprites are connected to another game folder on this computer (sync otherwise stops and names that folder). Run sync from your game project folder. |
|
|
134
135
|
|
|
135
136
|
Each successful sync prints a per-file change list (`+` added, `~` modified, `↪` renamed, `-` pruned) so you (and any AI agent reading the logs) know exactly what to wire up.
|
|
136
137
|
|
|
@@ -162,6 +163,8 @@ To leave game files on disk but out of your library, add them to `exclude` in `m
|
|
|
162
163
|
{ "exclude": [".SpineRaw/rabbit_nft/**"] }
|
|
163
164
|
```
|
|
164
165
|
|
|
166
|
+
Sync prints how many PNGs your `exclude` rules skipped, so a forgotten rule is easy to spot. In full Unity projects sync scans `Assets/` plus sprite dot-folders at the project root (like `.SpineRaw`).
|
|
167
|
+
|
|
165
168
|
Excluded files are never uploaded, even when `connect` is `**`, and the CLI never deletes them from your game. Then delete the folder in the MagicPixel library (it goes to Trash) and it stays gone.
|
|
166
169
|
|
|
167
170
|
## Config (`magicpixel.json`)
|
package/dist/commands/connect.js
CHANGED
|
@@ -3,7 +3,7 @@ import ora from 'ora';
|
|
|
3
3
|
import { loadConfig, saveConfig } from '../config.js';
|
|
4
4
|
import { assertSafeGlob } from '../util/security.js';
|
|
5
5
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
6
|
-
import { indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
6
|
+
import { describeExcluded, indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
7
7
|
import { isAllSpritesGlob, nextConnectGlobs } from '../util/engineConnect.js';
|
|
8
8
|
import { cmd } from '../util/invoke.js';
|
|
9
9
|
import { runPush } from './push.js';
|
|
@@ -37,6 +37,9 @@ export async function connectCommand(glob) {
|
|
|
37
37
|
});
|
|
38
38
|
spinner.stop();
|
|
39
39
|
const matched = matchConnectGlobs(index, config.connect, config.exclude);
|
|
40
|
+
const skipped = describeExcluded(matched);
|
|
41
|
+
if (skipped)
|
|
42
|
+
console.log(kleur.dim(` ${skipped}`));
|
|
40
43
|
if (matched.entries.length === 0) {
|
|
41
44
|
console.log(kleur.yellow(` No PNGs matched. Try \`${cmd('search')} <name>\` to see indexed paths.`));
|
|
42
45
|
return;
|
package/dist/commands/sync.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createLimit } from '../util/limit.js';
|
|
|
12
12
|
import { emitTypedIndex, ensureAgentsDoc, scanDiskAssets } from '../util/emitIndex.js';
|
|
13
13
|
import { assertPathInsideRoot, assertSafeIoPath } from '../util/security.js';
|
|
14
14
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
15
|
-
import { indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
15
|
+
import { describeExcluded, indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
16
16
|
import { aliasCollisionKeys, collectSourceRelMap, isPathInside, syncDiskPathFromKey } from '../util/syncPath.js';
|
|
17
17
|
import { DEFAULT_UNITY_PPU, writeMissingUnityMetas } from '../util/unityMeta.js';
|
|
18
18
|
import { filterUnityManifest, isWorkingSetEntry, partitionWithheldEntries, shouldPruneDeselectedEntry, workingSetPullKeys } from '../util/unityFilter.js';
|
|
@@ -23,7 +23,7 @@ import { maxIsoTimestamp } from '../util/iso.js';
|
|
|
23
23
|
import { formatBytes } from '../util/format.js';
|
|
24
24
|
import { computePreviousKeyOrphans } from '../util/previousKeyOrphans.js';
|
|
25
25
|
import { cmd } from '../util/invoke.js';
|
|
26
|
-
import {
|
|
26
|
+
import { formatSlowTickLine, formatWatchSpriteLine, SLOW_TICK_HEARTBEAT_MS } from '../util/watchCopy.js';
|
|
27
27
|
import { canSkipWithoutHashing, decidePull, hasNewExplicitRelease } from '../util/pullDecision.js';
|
|
28
28
|
import { hasUnpushedLocalEdit } from '../util/localEdit.js';
|
|
29
29
|
import { shouldReconcile } from '../util/reconcile.js';
|
|
@@ -31,6 +31,7 @@ import { runPush } from './push.js';
|
|
|
31
31
|
import { claimResyncRequest, reportResyncRequest } from '../api.js';
|
|
32
32
|
import { runResync } from './resync.js';
|
|
33
33
|
import { keyInScope, pathInScope, resolveSyncScope, scopeFingerprint } from '../util/syncScope.js';
|
|
34
|
+
import { findForeignOwner, foreignOwnerMessage, recordProjectRoot } from '../util/projectRoots.js';
|
|
34
35
|
/** How often the watcher asks for a library "Resync from game" request. */
|
|
35
36
|
const RESYNC_POLL_MS = 15_000;
|
|
36
37
|
let lastResyncPollAt = 0;
|
|
@@ -227,7 +228,7 @@ async function watchLoop(opts) {
|
|
|
227
228
|
const onStatus = (msg) => {
|
|
228
229
|
if (opts.quiet)
|
|
229
230
|
return;
|
|
230
|
-
|
|
231
|
+
process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} ${msg}`)}`);
|
|
231
232
|
};
|
|
232
233
|
// Heartbeat: a tick that legitimately takes minutes (huge first pull, slow
|
|
233
234
|
// link) must not look like the silent hang a timeout-less fetch used to
|
|
@@ -307,7 +308,7 @@ async function watchLoop(opts) {
|
|
|
307
308
|
printChanges(r, /* indent */ ' ');
|
|
308
309
|
}
|
|
309
310
|
else {
|
|
310
|
-
|
|
311
|
+
process.stdout.write(`\r\x1b[2K${kleur.dim(`${timestamp()} Waiting for edits… (${r.unchanged} up to date)`)}`);
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
314
|
catch (e) {
|
|
@@ -688,6 +689,9 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
688
689
|
aliasCollisionKeys(sourceByKey, manifest.map((e) => e.key));
|
|
689
690
|
if (verbose && isEngineKind(projectKind)) {
|
|
690
691
|
console.log(kleur.dim(` game index → ${gameIndex.files.length} PNG${gameIndex.files.length === 1 ? '' : 's'} · working set ${connected.entries.length}`));
|
|
692
|
+
const skipped = describeExcluded(connected);
|
|
693
|
+
if (skipped)
|
|
694
|
+
console.log(kleur.dim(` ${skipped}`));
|
|
691
695
|
}
|
|
692
696
|
const pathForKey = (key) => syncDiskPathFromKey(config.outDir, key, sourceByKey);
|
|
693
697
|
// Every folder on disk that belongs to a withheld document. Anything already
|
|
@@ -879,6 +883,20 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
879
883
|
const remoteDiskPaths = new Set(manifest.map((e) => pathFor(e)));
|
|
880
884
|
const remoteKeySet = new Set(manifest.map((e) => e.key));
|
|
881
885
|
const previousKeySet = new Set(Object.values(previousAssets));
|
|
886
|
+
// Wrong-folder guard: sprites connected to another game folder on this
|
|
887
|
+
// machine would land here as stray copies under outDir. Stop before writing.
|
|
888
|
+
const projectIdForRoots = getLastProjectInfo()?.id;
|
|
889
|
+
if (projectIdForRoots && !opts.here) {
|
|
890
|
+
const owner = await findForeignOwner(projectIdForRoots, process.cwd(), toDownload.filter((e) => !sourceByKey.has(e.key)).map((e) => e.key));
|
|
891
|
+
if (owner) {
|
|
892
|
+
const msg = foreignOwnerMessage(owner, process.cwd(), `add ${kleur.cyan('--here')}`);
|
|
893
|
+
if (runOpts.watchMode) {
|
|
894
|
+
console.error(kleur.red(msg));
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
throw new Error(msg);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
882
900
|
const localPngs = needsLocalPngWalk(since, toDownload.length)
|
|
883
901
|
? await walkOutDirPngs(config.outDir)
|
|
884
902
|
: [];
|
|
@@ -1485,6 +1503,11 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
1485
1503
|
// Never let an AGENTS.md write failure break sync.
|
|
1486
1504
|
}
|
|
1487
1505
|
await saveState(nextState);
|
|
1506
|
+
// This folder owns real game paths → remember it so a sync elsewhere can
|
|
1507
|
+
// point back here instead of writing stray copies.
|
|
1508
|
+
if (projectIdForRoots && sourceByKey.size > 0) {
|
|
1509
|
+
await recordProjectRoot(projectIdForRoots, process.cwd());
|
|
1510
|
+
}
|
|
1488
1511
|
if (result.failed > 0 && verbose) {
|
|
1489
1512
|
console.log(kleur.yellow(`\n${result.failed} download${result.failed === 1 ? '' : 's'} failed — lastSync not advanced. Re-run to retry.`));
|
|
1490
1513
|
}
|
|
@@ -1570,11 +1593,6 @@ function progressText(done, total, bytes) {
|
|
|
1570
1593
|
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
1571
1594
|
return `${bar} ${done}/${total} ${kleur.dim(formatBytes(bytes))}`;
|
|
1572
1595
|
}
|
|
1573
|
-
/** Rewrite the watcher's single status row, clipped so it never wraps. */
|
|
1574
|
-
function writeStatusLine(msg) {
|
|
1575
|
-
const line = fitStatusLine(`${timestamp()} ${msg}`, process.stdout.columns);
|
|
1576
|
-
process.stdout.write(`\r\x1b[2K${kleur.dim(line)}`);
|
|
1577
|
-
}
|
|
1578
1596
|
function timestamp() {
|
|
1579
1597
|
const d = new Date();
|
|
1580
1598
|
return `[${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}]`;
|
package/dist/index.js
CHANGED
|
@@ -94,6 +94,7 @@ program
|
|
|
94
94
|
.option('-q, --quiet', 'Minimal output (for CI)')
|
|
95
95
|
.option('-c, --concurrency <n>', 'Parallel downloads (1–16, default 6)', parseConcurrency)
|
|
96
96
|
.option('--only <folder...>', 'Only sync these game folders (repeatable), e.g. Runtime/Sprites/Entities/decorations')
|
|
97
|
+
.option('--here', 'Download here even if these sprites are connected to another game folder')
|
|
97
98
|
.argument('[extra...]')
|
|
98
99
|
.addHelpText('after', '\nExamples:\n $ magicpixel sync --watch --only Runtime/Sprites/Enemies # one folder\n $ magicpixel sync # incremental sync\n $ magicpixel sync --full # ignore lastSync, re-check everything\n $ magicpixel sync -w # watch mode (2s; adaptive idle backoff; exit 2 after 5 auth failures)\n')
|
|
99
100
|
.action(wrap("sync", async (extra, opts) => {
|
package/dist/util/gameScan.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* globs select a working set; sync writes back to those original paths.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
|
-
import { opendir } from 'node:fs/promises';
|
|
7
|
+
import { opendir, readdir } from 'node:fs/promises';
|
|
8
8
|
import { relative, resolve } from 'node:path';
|
|
9
9
|
import { isEngineKind, resolveChildDir } from './framework.js';
|
|
10
10
|
import { assertPathInsideRoot, sanitizeSourceRel } from './security.js';
|
|
@@ -184,6 +184,13 @@ export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts
|
|
|
184
184
|
const startAt = isUnityGameProject(cwd, assetRoot) ? assetRoot : scanRoot;
|
|
185
185
|
const preferFirst = startAt === scanRoot ? assetRoot : null;
|
|
186
186
|
await walkPngs(startAt, scanRoot, destNorm, found, Infinity, io, report, stats, preferFirst);
|
|
187
|
+
// Full Unity games also keep sprite sources in root dot-folders next to
|
|
188
|
+
// Assets/ (e.g. `.SpineRaw`). Walk those too; Unity's own folders stay out.
|
|
189
|
+
if (startAt !== scanRoot) {
|
|
190
|
+
for (const dir of await rootSpriteDotDirs(scanRoot)) {
|
|
191
|
+
await walkPngs(dir, scanRoot, destNorm, found, Infinity, io, report, stats, null);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
187
194
|
report.flush?.();
|
|
188
195
|
opts.onProgress?.({ pngs: found.length, folders: stats.folders });
|
|
189
196
|
found.sort((a, b) => a.localeCompare(b));
|
|
@@ -195,15 +202,48 @@ export async function indexGamePngs(kind, cwd = process.cwd(), outDir = '', opts
|
|
|
195
202
|
}
|
|
196
203
|
return { files };
|
|
197
204
|
}
|
|
205
|
+
/** One-line summary of exclude-dropped files, or null when none. */
|
|
206
|
+
export function describeExcluded(r) {
|
|
207
|
+
if (!r.excluded)
|
|
208
|
+
return null;
|
|
209
|
+
return `${r.excluded.toLocaleString('en-US')} PNG${r.excluded === 1 ? '' : 's'} skipped by exclude rules (${r.topExclude}) — edit "exclude" in magicpixel.json to include them`;
|
|
210
|
+
}
|
|
211
|
+
/** Root-level dot-folders that may hold sprites (not VCS/editor/tool dirs). */
|
|
212
|
+
export async function rootSpriteDotDirs(root) {
|
|
213
|
+
try {
|
|
214
|
+
const ents = await readdir(root, { withFileTypes: true });
|
|
215
|
+
return ents
|
|
216
|
+
.filter((e) => e.isDirectory() && !e.isSymbolicLink() && e.name.startsWith('.') && !shouldSkipDir(e.name))
|
|
217
|
+
.map((e) => resolve(root, e.name))
|
|
218
|
+
.sort();
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
198
224
|
/** Filter the index by connect globs minus exclude globs (cwd-, asset-root-relative or manifest key). */
|
|
199
225
|
export function matchConnectGlobs(index, globs, exclude = []) {
|
|
200
226
|
if (globs.length === 0)
|
|
201
227
|
return { entries: [] };
|
|
202
228
|
const hit = (e, g) => matchGlob(e.sourceRel, g) || matchGlob(e.adoptRel, g) || matchGlob(e.key, g);
|
|
203
229
|
// `exclude` wins over `connect`: excluded game files are never uploaded.
|
|
204
|
-
const matched =
|
|
230
|
+
const matched = [];
|
|
231
|
+
const byRule = new Map();
|
|
232
|
+
let excluded = 0;
|
|
233
|
+
for (const e of index.files) {
|
|
234
|
+
if (!globs.some((g) => hit(e, g)))
|
|
235
|
+
continue;
|
|
236
|
+
const rule = exclude.find((g) => hit(e, g));
|
|
237
|
+
if (rule === undefined)
|
|
238
|
+
matched.push(e);
|
|
239
|
+
else {
|
|
240
|
+
excluded++;
|
|
241
|
+
byRule.set(rule, (byRule.get(rule) ?? 0) + 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
205
244
|
matched.sort((a, b) => a.sourceRel.localeCompare(b.sourceRel));
|
|
206
|
-
|
|
245
|
+
const topExclude = [...byRule.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
246
|
+
return { entries: matched, ...(excluded ? { excluded, topExclude } : {}) };
|
|
207
247
|
}
|
|
208
248
|
export function searchGameIndex(index, query) {
|
|
209
249
|
const q = query.trim().toLowerCase();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { atomicWrite } from './atomicWrite.js';
|
|
6
|
+
export function registryPath(home = homedir()) {
|
|
7
|
+
return resolve(home, '.magicpixel', 'project-roots.json');
|
|
8
|
+
}
|
|
9
|
+
async function readRegistry(home) {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(await readFile(registryPath(home), 'utf8'));
|
|
12
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Remember `root` as a home for this project's connected sprites. Best-effort. */
|
|
19
|
+
const recorded = new Set();
|
|
20
|
+
export async function recordProjectRoot(projectId, root, home) {
|
|
21
|
+
const memo = `${home ?? ''}\0${projectId}\0${resolve(root)}`;
|
|
22
|
+
if (recorded.has(memo))
|
|
23
|
+
return; // watch mode: once per run
|
|
24
|
+
recorded.add(memo);
|
|
25
|
+
try {
|
|
26
|
+
const reg = await readRegistry(home);
|
|
27
|
+
const abs = resolve(root);
|
|
28
|
+
const list = reg[projectId] ?? [];
|
|
29
|
+
if (list.includes(abs))
|
|
30
|
+
return;
|
|
31
|
+
reg[projectId] = [...list, abs].slice(-20);
|
|
32
|
+
const path = registryPath(home);
|
|
33
|
+
await mkdir(dirname(path), { recursive: true });
|
|
34
|
+
await atomicWrite(path, JSON.stringify(reg, null, 2));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* registry is advisory */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Keys another folder already syncs to a real game path. */
|
|
41
|
+
function connectedKeysAt(root) {
|
|
42
|
+
try {
|
|
43
|
+
const state = JSON.parse(readFileSync(resolve(root, '.magicpixel', 'state.json'), 'utf8'));
|
|
44
|
+
return new Set(Object.entries(state.synced ?? {}).filter(([, v]) => !!v?.sourceRel).map(([k]) => k));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return new Set();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Of the keys this run would download WITHOUT a local game path, how many
|
|
52
|
+
* belong to another registered folder. Returns the folder owning the most, or
|
|
53
|
+
* null when the download is safe.
|
|
54
|
+
*/
|
|
55
|
+
export async function findForeignOwner(projectId, cwd, downloadKeysWithoutLocalPath, home) {
|
|
56
|
+
if (downloadKeysWithoutLocalPath.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
const here = resolve(cwd);
|
|
59
|
+
const roots = ((await readRegistry(home))[projectId] ?? []).filter((r) => r !== here && existsSync(r));
|
|
60
|
+
let best = null;
|
|
61
|
+
for (const root of roots) {
|
|
62
|
+
const owned = connectedKeysAt(root);
|
|
63
|
+
const count = downloadKeysWithoutLocalPath.filter((k) => owned.has(k)).length;
|
|
64
|
+
if (count > 0 && (!best || count > best.count))
|
|
65
|
+
best = { root, count };
|
|
66
|
+
}
|
|
67
|
+
return best;
|
|
68
|
+
}
|
|
69
|
+
export function foreignOwnerMessage(owner, cwd, rerun) {
|
|
70
|
+
return (`This folder isn't the game these sprites are connected to.\n` +
|
|
71
|
+
` ${owner.count} sprite${owner.count === 1 ? '' : 's'} live in ${owner.root}, not ${resolve(cwd)}.\n` +
|
|
72
|
+
` Fix: cd "${owner.root}" and run sync there — or ${rerun} to download copies here anyway.`);
|
|
73
|
+
}
|
package/dist/util/watchCopy.js
CHANGED
|
@@ -39,19 +39,3 @@ export function formatSlowTickLine(elapsedSec, lastStatus) {
|
|
|
39
39
|
? `${what} — still working (${elapsed})`
|
|
40
40
|
: `Still working (${elapsed})`;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Fit a carriage-return status line to one terminal row. `\r\x1b[2K` only
|
|
44
|
-
* clears the row the cursor is on, so a line that wraps leaves its first rows
|
|
45
|
-
* behind and the next status prints after them (long "Looking through your
|
|
46
|
-
* game sprites… <folder>" lines piled up this way). No width (not a TTY) →
|
|
47
|
-
* unchanged.
|
|
48
|
-
*/
|
|
49
|
-
export function fitStatusLine(line, columns) {
|
|
50
|
-
if (!columns || columns < 2)
|
|
51
|
-
return line;
|
|
52
|
-
const max = columns - 1; // leave the last cell free so the cursor can't wrap
|
|
53
|
-
const chars = Array.from(line);
|
|
54
|
-
if (chars.length <= max)
|
|
55
|
-
return line;
|
|
56
|
-
return `${chars.slice(0, max - 1).join('')}…`;
|
|
57
|
-
}
|