@magicpixelart/cli 0.6.1 → 0.6.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.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -0
- package/dist/commands/connect.js +7 -4
- package/dist/commands/consolidate.js +4 -3
- package/dist/commands/push.js +16 -5
- package/dist/commands/start.js +3 -0
- package/dist/commands/sync.js +4 -1
- package/dist/commands/why.js +18 -4
- package/dist/index.js +3 -1
- package/dist/util/commandTips.js +33 -0
- package/dist/util/engineConnect.js +39 -0
- package/dist/util/ui.js +19 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.6.2] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Merging setups cleans folder patterns (`//`, stray `~`, duplicates, ones a broader folder covers).
|
|
12
|
+
- `connect` cleans typed patterns and rejects empty ones.
|
|
13
|
+
- Sync warns when a hidden art folder (e.g. `.SpineRaw/rabbit`) has PNGs nothing watches, with the exact `connect` command.
|
|
14
|
+
- `why` lists watched patterns once and prints a ready-to-run `connect` command.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Upload progress shows the file's folder path (e.g. `…/rabbit/agni/ear_left_front`), not just its name.
|
|
18
|
+
- "Useful commands" after `start`, "Common tasks" in `--help`, and a short hint after `sync`, all from one shared list.
|
|
19
|
+
|
|
8
20
|
## [0.6.1] - 2026-10-10
|
|
9
21
|
|
|
10
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -102,6 +102,8 @@ No bundler config, no runtime, no extra package.
|
|
|
102
102
|
|
|
103
103
|
## Commands
|
|
104
104
|
|
|
105
|
+
Run `magicpixel --help` for a short "Common tasks" list. A folder isn't syncing? `magicpixel why <folder>` prints the `connect` command that fixes it.
|
|
106
|
+
|
|
105
107
|
| Command | What it does |
|
|
106
108
|
| --- | --- |
|
|
107
109
|
| `start [--force]` | One-command bootstrap. Init + login + first sync. The only command to tell new users to run. |
|
package/dist/commands/connect.js
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig, saveConfig } from '../config.js';
|
|
|
4
4
|
import { assertSafeGlob } from '../util/security.js';
|
|
5
5
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
6
6
|
import { describeExcluded, indexGamePngs, matchConnectGlobs, countingSpritesText } from '../util/gameScan.js';
|
|
7
|
-
import { isAllSpritesGlob, nextConnectGlobs } from '../util/engineConnect.js';
|
|
7
|
+
import { isAllSpritesGlob, nextConnectGlobs, normalizeConnectGlob, tidyConnectGlobs } from '../util/engineConnect.js';
|
|
8
8
|
import { cmd } from '../util/invoke.js';
|
|
9
9
|
import { runPush } from './push.js';
|
|
10
10
|
/** Plain-language label for a connect glob (`**` is not meaningful to players). */
|
|
@@ -17,14 +17,17 @@ export function describeWorkingSet(glob) {
|
|
|
17
17
|
* the original files — not a copy under outDir.
|
|
18
18
|
*/
|
|
19
19
|
export async function connectCommand(glob) {
|
|
20
|
-
const pattern = assertSafeGlob(glob);
|
|
20
|
+
const pattern = normalizeConnectGlob(assertSafeGlob(glob));
|
|
21
|
+
if (!pattern)
|
|
22
|
+
throw new Error(`"${glob}" isn't a folder pattern. Example: ${cmd('connect')} 'Assets/Sprites/**'`);
|
|
21
23
|
const config = await loadConfig();
|
|
22
|
-
const next = nextConnectGlobs(config.connect, pattern);
|
|
24
|
+
const next = tidyConnectGlobs(nextConnectGlobs(config.connect.map(normalizeConnectGlob), pattern));
|
|
23
25
|
const changed = next.length !== config.connect.length || next.some((g, i) => g !== config.connect[i]);
|
|
24
26
|
if (changed) {
|
|
27
|
+
const added = !config.connect.map(normalizeConnectGlob).includes(pattern);
|
|
25
28
|
config.connect = next;
|
|
26
29
|
await saveConfig(config);
|
|
27
|
-
console.log(ui.ok(`now syncing ${describeWorkingSet(pattern)}`));
|
|
30
|
+
console.log(ui.ok(added ? `now syncing ${describeWorkingSet(pattern)}` : 'tidied your folder patterns'));
|
|
28
31
|
}
|
|
29
32
|
const kind = await detectProjectKind();
|
|
30
33
|
if (!isEngineKind(kind)) {
|
|
@@ -10,6 +10,7 @@ import { readCredentialsSync, writeCredentials } from '../util/credentials.js';
|
|
|
10
10
|
import { indexGamePngs } from '../util/gameScan.js';
|
|
11
11
|
import { walkOutDirPngs } from '../util/paths.js';
|
|
12
12
|
import { anchorGlob, detectWorkspaceMembers } from '../util/workspace.js';
|
|
13
|
+
import { normalizeConnectGlob, tidyConnectGlobs } from '../util/engineConnect.js';
|
|
13
14
|
import { detectEngineKind, readWorkspaceMembers } from '../util/framework.js';
|
|
14
15
|
import { cmd } from '../util/invoke.js';
|
|
15
16
|
import { ensureGitignore } from './init.js';
|
|
@@ -30,9 +31,9 @@ export function mergeSetups(members, children) {
|
|
|
30
31
|
const configured = children.filter((c) => c.config);
|
|
31
32
|
const first = configured[0]?.config ?? {};
|
|
32
33
|
const uniq = (xs) => [...new Set(xs)];
|
|
33
|
-
const connect =
|
|
34
|
+
const connect = tidyConnectGlobs(configured.flatMap((c) => {
|
|
34
35
|
const globs = c.config.connect?.length ? c.config.connect : ['**'];
|
|
35
|
-
return globs.map((g) => anchorGlob(c.member, g));
|
|
36
|
+
return globs.map((g) => anchorGlob(c.member, normalizeConnectGlob(g)));
|
|
36
37
|
}));
|
|
37
38
|
const exclude = uniq(configured.flatMap((c) => (c.config.exclude ?? []).map((g) => anchorGlob(c.member, g))));
|
|
38
39
|
const include = uniq(configured.flatMap((c) => c.config.include ?? defaultConfig.include));
|
|
@@ -169,7 +170,7 @@ export async function consolidateCommand(opts = {}) {
|
|
|
169
170
|
if (current) {
|
|
170
171
|
merged.config = {
|
|
171
172
|
...current,
|
|
172
|
-
connect:
|
|
173
|
+
connect: tidyConnectGlobs([...(current.connect ?? []), ...merged.config.connect]),
|
|
173
174
|
exclude: [...new Set([...(current.exclude ?? []), ...merged.config.exclude])],
|
|
174
175
|
};
|
|
175
176
|
}
|
package/dist/commands/push.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* `POST /api/public/integration/ingest`.
|
|
8
8
|
*/
|
|
9
9
|
import kleur from 'kleur';
|
|
10
|
-
import { icon, progressBar, spinner as makeSpinner, ui } from '../util/ui.js';
|
|
10
|
+
import { icon, progressBar, shortenPath, spinner as makeSpinner, ui, visibleWidth } from '../util/ui.js';
|
|
11
11
|
import { mkdir, readFile, rename } from 'node:fs/promises';
|
|
12
12
|
import { dirname, relative, resolve } from 'node:path';
|
|
13
13
|
import { loadConfig, loadState, saveState, resetStateIfProjectChanged, PROJECT_CHANGED_MESSAGE } from '../config.js';
|
|
14
|
-
import { ensureEngineConnect } from '../util/engineConnect.js';
|
|
14
|
+
import { ensureEngineConnect, unwatchedHiddenFolders } from '../util/engineConnect.js';
|
|
15
15
|
import { PUSH_BATCH_SIZE, pushSpritesAdaptive, fetchManifestPage, getLastProjectInfo } from '../api.js';
|
|
16
16
|
import { hashFile } from '../util/hash.js';
|
|
17
17
|
import { createLimit } from '../util/limit.js';
|
|
@@ -80,6 +80,13 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
80
80
|
: { files: [] });
|
|
81
81
|
const matched = matchConnectGlobs(index, config.connect ?? [], config.exclude);
|
|
82
82
|
const sourceByKey = collectSourceRelMap(matched.entries, state.synced);
|
|
83
|
+
if (!quiet) {
|
|
84
|
+
const watched = new Set(matched.entries.map((e) => e.key));
|
|
85
|
+
for (const [folder, n] of unwatchedHiddenFolders(index.files, watched, config.exclude ?? []).slice(0, 5)) {
|
|
86
|
+
console.log(ui.warn(`${n.toLocaleString('en-US')} PNGs in ${folder} aren't watched.`));
|
|
87
|
+
console.log(ui.fix(`${cmd('connect')} '${folder}/**'`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
83
90
|
const disk = await walkOutDirPngs(config.outDir);
|
|
84
91
|
const absByKey = new Map(disk.map((a) => [a.key, a.abs]));
|
|
85
92
|
const entryByKey = new Map(matched.entries.map((e) => [e.key, e]));
|
|
@@ -256,10 +263,14 @@ export async function runPushWith(config, state, opts = {}) {
|
|
|
256
263
|
});
|
|
257
264
|
await saveState({ ...state, synced });
|
|
258
265
|
const doneN = Math.min(i + batch.length, sprites.length);
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
const last = batch[batch.length - 1];
|
|
267
|
+
const where = last ? fpByKey.get(last.key)?.sourceRel ?? last.key : '';
|
|
268
|
+
if (progress) {
|
|
269
|
+
const head = `Uploading ${progressBar(doneN, sprites.length)} `;
|
|
270
|
+
progress.text = head + kleur.dim(shortenPath(where, (process.stdout.columns ?? 80) - visibleWidth(head) - 4));
|
|
271
|
+
}
|
|
261
272
|
else
|
|
262
|
-
opts.onStatus?.(`${icon('up')} Uploading ${doneN}/${sprites.length} ${
|
|
273
|
+
opts.onStatus?.(`${icon('up')} Uploading ${doneN}/${sprites.length} ${shortenPath(where, 60)}…`);
|
|
263
274
|
}
|
|
264
275
|
}
|
|
265
276
|
catch (e) {
|
package/dist/commands/start.js
CHANGED
|
@@ -25,6 +25,7 @@ import { assertKeyValid } from '../util/auth.js';
|
|
|
25
25
|
import { ApiError } from '../api.js';
|
|
26
26
|
import { connectCommand } from './connect.js';
|
|
27
27
|
import { isAllSpritesGlob } from '../util/engineConnect.js';
|
|
28
|
+
import { COMMAND_TIPS, formatTipsBlock } from '../util/commandTips.js';
|
|
28
29
|
/** Validate a key; only auth rejections map to 'rejected' — other failures throw. */
|
|
29
30
|
export async function checkKey(key, config) {
|
|
30
31
|
try {
|
|
@@ -215,6 +216,8 @@ export async function startCommand(opts = {}) {
|
|
|
215
216
|
console.log();
|
|
216
217
|
console.log(kleur.dim(' Game sprites write back to their original path. New MagicPixel art lands in outDir.'));
|
|
217
218
|
console.log();
|
|
219
|
+
console.log(formatTipsBlock(COMMAND_TIPS.slice(1)));
|
|
220
|
+
console.log();
|
|
218
221
|
if (await hasDevScript()) {
|
|
219
222
|
const watchCmd = hasWatch ? 'npm run magicpixel:watch' : `${cmd('sync')} --watch`;
|
|
220
223
|
console.log(kleur.dim(' Tip: run your dev server and the watcher together with'));
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
-
import { canRedraw, card, fitLine, formatDuration, icon, progressBar, spinner as makeSpinner, ui } from '../util/ui.js';
|
|
2
|
+
import { canRedraw, card, isPlain, fitLine, formatDuration, icon, progressBar, spinner as makeSpinner, ui } from '../util/ui.js';
|
|
3
3
|
import { mkdir, unlink, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { dirname, relative, resolve } from 'node:path';
|
|
@@ -33,6 +33,7 @@ import { runResync } from './resync.js';
|
|
|
33
33
|
import { keyInScope, pathInScope, resolveSyncScope, scopeFingerprint } from '../util/syncScope.js';
|
|
34
34
|
import { findForeignOwner, foreignOwnerMessage, recordProjectRoot } from '../util/projectRoots.js';
|
|
35
35
|
import { placeBesideSiblings, splitStrayDownloads, straySkipMessage } from '../util/strayGuard.js';
|
|
36
|
+
import { syncFooterHint } from '../util/commandTips.js';
|
|
36
37
|
/** How often the watcher asks for a library "Resync from game" request. */
|
|
37
38
|
const RESYNC_POLL_MS = 15_000;
|
|
38
39
|
let lastResyncPollAt = 0;
|
|
@@ -1593,6 +1594,8 @@ async function runOnce(opts, runOpts = {}) {
|
|
|
1593
1594
|
printChanges(result, ' ', { includeRenames: renamed.length === 0 });
|
|
1594
1595
|
if (renamed.length > 0)
|
|
1595
1596
|
printRenames(renamed, { withHints: true });
|
|
1597
|
+
if (!runOpts.watchMode && !isPlain())
|
|
1598
|
+
console.log(kleur.dim(syncFooterHint()));
|
|
1596
1599
|
}
|
|
1597
1600
|
if (result.failed)
|
|
1598
1601
|
process.exitCode = 1;
|
package/dist/commands/why.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
-
import { spinner as makeSpinner } from '../util/ui.js';
|
|
2
|
+
import { spinner as makeSpinner, ui } from '../util/ui.js';
|
|
3
3
|
import { loadConfig, loadState } from '../config.js';
|
|
4
4
|
import { detectProjectKind, isEngineKind } from '../util/framework.js';
|
|
5
5
|
import { countingSpritesText, entryMatchesGlob, indexGamePngs, rootSpriteDotDirs } from '../util/gameScan.js';
|
|
@@ -27,10 +27,10 @@ export function explainSprite(entry, config, synced, currentSha) {
|
|
|
27
27
|
return { kind: 'unchanged' };
|
|
28
28
|
return { kind: 'changed', ...(typeof known.layers === 'number' ? { layers: known.layers } : {}) };
|
|
29
29
|
}
|
|
30
|
-
export function describeVerdict(v
|
|
30
|
+
export function describeVerdict(v) {
|
|
31
31
|
switch (v.kind) {
|
|
32
32
|
case 'not-watched':
|
|
33
|
-
return
|
|
33
|
+
return 'not watched — no folder pattern covers it';
|
|
34
34
|
case 'excluded':
|
|
35
35
|
return `skipped by exclude rule "${v.rule}" — remove it from "exclude" in magicpixel.json to sync this file`;
|
|
36
36
|
case 'new':
|
|
@@ -73,9 +73,13 @@ export async function whyCommand(pathArg) {
|
|
|
73
73
|
const synced = state.synced ?? {};
|
|
74
74
|
const counts = new Map();
|
|
75
75
|
const lines = [];
|
|
76
|
+
let unwatched = 0;
|
|
76
77
|
for (const e of files) {
|
|
77
78
|
const sha = (await hashFile(e.abs))?.sha256 ?? null;
|
|
78
|
-
const
|
|
79
|
+
const verdict = explainSprite(e, config, synced, sha);
|
|
80
|
+
if (verdict.kind === 'not-watched')
|
|
81
|
+
unwatched++;
|
|
82
|
+
const text = describeVerdict(verdict);
|
|
79
83
|
counts.set(text, (counts.get(text) ?? 0) + 1);
|
|
80
84
|
if (lines.length < 20)
|
|
81
85
|
lines.push(` ${e.sourceRel}\n ${kleur.dim('→')} ${text}`);
|
|
@@ -88,4 +92,14 @@ export async function whyCommand(pathArg) {
|
|
|
88
92
|
console.log(lines.join('\n'));
|
|
89
93
|
if (files.length > lines.length)
|
|
90
94
|
console.log(kleur.dim(` …and ${files.length - lines.length} more`));
|
|
95
|
+
if (unwatched > 0) {
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(kleur.dim(` Watching: ${(config.connect ?? []).join(', ') || 'nothing'}`));
|
|
98
|
+
console.log(ui.fix(whyConnectCommand(files[0].sourceRel, target), ' '));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Ready-to-run connect command for the folder the user asked about. */
|
|
102
|
+
export function whyConnectCommand(firstRel, target) {
|
|
103
|
+
const folder = firstRel.toLowerCase() === target ? firstRel.split('/').slice(0, -1).join('/') : firstRel.slice(0, target.length);
|
|
104
|
+
return `${cmd('connect')} '${folder ? `${folder}/**` : '**'}'`;
|
|
91
105
|
}
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { cleanStraysCommand } from './commands/cleanStrays.js';
|
|
|
25
25
|
import { parseWatchInterval, parseConcurrency } from './util/flagValidators.js';
|
|
26
26
|
import { CLI_VERSION } from './version.js';
|
|
27
27
|
import { cmd } from './util/invoke.js';
|
|
28
|
+
import { formatHelpTips } from './util/commandTips.js';
|
|
28
29
|
// Node version guard
|
|
29
30
|
const major = Number(process.versions.node.split('.')[0]);
|
|
30
31
|
if (major < 18) {
|
|
@@ -54,7 +55,8 @@ const program = new Command();
|
|
|
54
55
|
program
|
|
55
56
|
.name('magicpixel')
|
|
56
57
|
.description('Sync MagicPixel pixel-art assets to your local project')
|
|
57
|
-
.version(CLI_VERSION)
|
|
58
|
+
.version(CLI_VERSION)
|
|
59
|
+
.addHelpText('after', formatHelpTips());
|
|
58
60
|
const wrap = (commandName, fn) => async (...args) => {
|
|
59
61
|
try {
|
|
60
62
|
await fn(...args);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common commands shown after `start`, in `--help`, and hinted after `sync`.
|
|
3
|
+
* One list so the three places never drift apart.
|
|
4
|
+
*/
|
|
5
|
+
import kleur from 'kleur';
|
|
6
|
+
import { cmd } from './invoke.js';
|
|
7
|
+
export const COMMAND_TIPS = [
|
|
8
|
+
{ run: 'sync --watch', what: 'keep sprites fresh while you work' },
|
|
9
|
+
{ run: "connect '<folder>/**'", what: 'add a folder to sync (hidden ones like .SpineRaw/... too)' },
|
|
10
|
+
{ run: 'sync --only <folder>', what: 'sync just one folder' },
|
|
11
|
+
{ run: 'why <folder>', what: "see why files aren't uploading" },
|
|
12
|
+
{ run: 'resync <folder>', what: 'local files win for one folder' },
|
|
13
|
+
{ run: 'clean-strays', what: 'remove copies in the wrong place' },
|
|
14
|
+
{ run: 'status', what: 'check your setup and last sync' },
|
|
15
|
+
{ run: 'doctor', what: 'diagnose problems' },
|
|
16
|
+
];
|
|
17
|
+
const width = Math.max(...COMMAND_TIPS.map((t) => t.run.length));
|
|
18
|
+
/** "Useful commands" block for the end of `start`. */
|
|
19
|
+
export function formatTipsBlock(tips = COMMAND_TIPS) {
|
|
20
|
+
const prefix = cmd('').trimEnd();
|
|
21
|
+
return [
|
|
22
|
+
kleur.bold(' Useful commands'),
|
|
23
|
+
...tips.map((t) => ` ${kleur.cyan(`${prefix} ${t.run.padEnd(width)}`)} ${kleur.dim(t.what)}`),
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
/** "Common tasks" section for `magicpixel --help` (no colors: commander prints raw). */
|
|
27
|
+
export function formatHelpTips() {
|
|
28
|
+
return ['', 'Common tasks:', ...COMMAND_TIPS.map((t) => ` $ magicpixel ${t.run.padEnd(width)} # ${t.what}`), ''].join('\n');
|
|
29
|
+
}
|
|
30
|
+
/** One-line hint after a one-shot sync. */
|
|
31
|
+
export function syncFooterHint() {
|
|
32
|
+
return ` Not seeing a folder? Run \`${cmd('why')} <folder>\`. All commands: \`${cmd('--help')}\`.`;
|
|
33
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { saveConfig } from '../config.js';
|
|
2
2
|
import { detectProjectKind, isEngineKind } from './framework.js';
|
|
3
|
+
import { entryMatchesGlob } from './gameScan.js';
|
|
3
4
|
export const DEFAULT_ENGINE_CONNECT = ['**'];
|
|
4
5
|
const ALL_SPRITES_GLOBS = new Set(['**', '**/*', '**/**', '**/*.png', '**/**/*.png']);
|
|
5
6
|
export function isAllSpritesGlob(glob) {
|
|
@@ -19,6 +20,44 @@ export function nextConnectGlobs(current, pattern) {
|
|
|
19
20
|
return withoutAll;
|
|
20
21
|
return [...withoutAll, pattern];
|
|
21
22
|
}
|
|
23
|
+
/** Clean one folder pattern: `\` → `/`, collapse `//`, drop `./`, a trailing `/` or stray `~`. */
|
|
24
|
+
export function normalizeConnectGlob(glob) {
|
|
25
|
+
return glob
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/\\/g, '/')
|
|
28
|
+
.replace(/\/{2,}/g, '/')
|
|
29
|
+
.replace(/^(\.\/)+/, '')
|
|
30
|
+
.replace(/~+$/, '')
|
|
31
|
+
.replace(/\/+$/, '');
|
|
32
|
+
}
|
|
33
|
+
/** Normalize, dedupe, and drop patterns already covered by a broader `<folder>/**`. */
|
|
34
|
+
export function tidyConnectGlobs(globs) {
|
|
35
|
+
const clean = [...new Set(globs.map(normalizeConnectGlob).filter(Boolean))];
|
|
36
|
+
const roots = clean.filter((g) => g.endsWith('/**')).map((g) => g.slice(0, -3).toLowerCase());
|
|
37
|
+
return clean.filter((g) => {
|
|
38
|
+
const base = (g.endsWith('/**') ? g.slice(0, -3) : g).toLowerCase();
|
|
39
|
+
return !roots.some((r) => r !== base && (base === r || base.startsWith(`${r}/`)))
|
|
40
|
+
&& !(roots.includes(base) && !g.endsWith('/**'));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Hidden art folders (`<member>/.SpineRaw/<name>`) with PNGs that no pattern
|
|
45
|
+
* watches. Returns `[folder, count]`, largest first.
|
|
46
|
+
*/
|
|
47
|
+
export function unwatchedHiddenFolders(files, watchedKeys, exclude = [], min = 10) {
|
|
48
|
+
const counts = new Map();
|
|
49
|
+
for (const f of files) {
|
|
50
|
+
if (watchedKeys.has(f.key) || exclude.some((g) => entryMatchesGlob(f, g)))
|
|
51
|
+
continue;
|
|
52
|
+
const parts = f.sourceRel.split('/');
|
|
53
|
+
const i = parts.findIndex((p) => p.startsWith('.') && p.length > 1);
|
|
54
|
+
if (i < 0 || i + 2 >= parts.length)
|
|
55
|
+
continue;
|
|
56
|
+
const folder = parts.slice(0, i + 2).join('/');
|
|
57
|
+
counts.set(folder, (counts.get(folder) ?? 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
return [...counts].filter(([, n]) => n >= min).sort((a, b) => b[1] - a[1]);
|
|
60
|
+
}
|
|
22
61
|
export async function ensureEngineConnect(config, cwd = process.cwd(), persist = true) {
|
|
23
62
|
if (config.connect.length > 0)
|
|
24
63
|
return config;
|
package/dist/util/ui.js
CHANGED
|
@@ -71,6 +71,25 @@ export function visibleWidth(s) {
|
|
|
71
71
|
return w;
|
|
72
72
|
}
|
|
73
73
|
/** Cut a plain line to `max` visible columns. */
|
|
74
|
+
/**
|
|
75
|
+
* Path that fits in `max` columns, keeping the end (the file and its parent
|
|
76
|
+
* folders) and dropping leading folders: `…/rabbit/agni/ear_left_front.png`.
|
|
77
|
+
*/
|
|
78
|
+
export function shortenPath(path, max) {
|
|
79
|
+
const p = path.replace(/\.png$/i, '');
|
|
80
|
+
const limit = Math.max(12, max);
|
|
81
|
+
if (p.length <= limit)
|
|
82
|
+
return p;
|
|
83
|
+
const parts = p.split('/');
|
|
84
|
+
let out = parts[parts.length - 1];
|
|
85
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
86
|
+
const next = `${parts[i]}/${out}`;
|
|
87
|
+
if (next.length + 2 > limit)
|
|
88
|
+
break;
|
|
89
|
+
out = next;
|
|
90
|
+
}
|
|
91
|
+
return out.length + 2 <= limit ? `…/${out}` : `…${out.slice(-(limit - 1))}`;
|
|
92
|
+
}
|
|
74
93
|
export function fitLine(line, columns) {
|
|
75
94
|
const max = Math.max(20, (columns ?? 80) - 1);
|
|
76
95
|
if (visibleWidth(line) <= max)
|