@magicpixelart/cli 0.1.0 → 0.1.1
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/README.md +7 -5
- package/dist/api.js +12 -6
- package/dist/commands/init.js +4 -2
- package/dist/commands/sync.js +27 -4
- package/dist/config.js +1 -1
- package/dist/index.js +10 -3
- package/dist/util/emitIndex.js +23 -12
- package/dist/version.js +8 -0
- package/package.json +4 -8
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @magicpixelart/cli
|
|
2
2
|
|
|
3
3
|
Sync MagicPixel pixel-art assets to your local project as flattened PNGs. Zero runtime — just files on disk that your bundler picks up.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install --save-dev @
|
|
9
|
-
# or: pnpm add -D @
|
|
10
|
-
# or: bun add -d @
|
|
8
|
+
npm install --save-dev @magicpixelart/cli
|
|
9
|
+
# or: pnpm add -D @magicpixelart/cli
|
|
10
|
+
# or: bun add -d @magicpixelart/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Requires Node.js ≥ 18.
|
|
@@ -23,6 +23,8 @@ npx magicpixel sync --watch # keeps assets fresh while you work
|
|
|
23
23
|
|
|
24
24
|
Get an API key at [magicpixel.art/settings](https://magicpixel.art/settings) → API Keys. Each key is bound to one project.
|
|
25
25
|
|
|
26
|
+
The key is read **only** from the `MAGICPIXEL_API_KEY` environment variable — never from `magicpixel.json` or any other file. This is intentional: it keeps secrets out of repos and CI logs.
|
|
27
|
+
|
|
26
28
|
## Typed asset index (default on)
|
|
27
29
|
|
|
28
30
|
If `emitIndex` is true in `magicpixel.json`, every sync writes `<outDir>/index.ts`:
|
|
@@ -93,7 +95,7 @@ jobs:
|
|
|
93
95
|
- uses: actions/checkout@v4
|
|
94
96
|
- uses: actions/setup-node@v4
|
|
95
97
|
with: { node-version: 20 }
|
|
96
|
-
- run: npx -y @
|
|
98
|
+
- run: npx -y @magicpixelart/cli sync --prune --quiet
|
|
97
99
|
env:
|
|
98
100
|
MAGICPIXEL_API_KEY: ${{ secrets.MAGICPIXEL_API_KEY }}
|
|
99
101
|
- uses: peter-evans/create-pull-request@v6
|
package/dist/api.js
CHANGED
|
@@ -57,12 +57,18 @@ export async function fetchManifestPage(opts) {
|
|
|
57
57
|
export async function fetchAllManifest(config, since) {
|
|
58
58
|
const out = [];
|
|
59
59
|
let cursor;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
// Cycle guard: caps total pages so a buggy server cursor can't hang the CLI.
|
|
61
|
+
// At 500 entries/page this is 100k assets — well past any realistic project.
|
|
62
|
+
const MAX_PAGES = 200;
|
|
63
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
64
|
+
const res = await fetchManifestPage({ config, since, cursor, limit: 500 });
|
|
65
|
+
out.push(...res.items);
|
|
66
|
+
cursor = res.nextCursor ?? undefined;
|
|
67
|
+
if (!cursor)
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`manifest: pagination exceeded ${MAX_PAGES} pages — possible server cursor loop.\n` +
|
|
71
|
+
` Fix: re-run with --full, or report at https://github.com/magicpixel/cli/issues.`);
|
|
66
72
|
}
|
|
67
73
|
/**
|
|
68
74
|
* Download a single asset by key. Returns null on 304 (not modified).
|
package/dist/commands/init.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function initCommand(opts) {
|
|
|
20
20
|
if (interactive) {
|
|
21
21
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
22
22
|
try {
|
|
23
|
-
console.log(kleur.bold('Set up @
|
|
23
|
+
console.log(kleur.bold('Set up @magicpixelart/cli'));
|
|
24
24
|
if (framework)
|
|
25
25
|
console.log(kleur.dim(` Detected: ${framework}`));
|
|
26
26
|
console.log();
|
|
@@ -115,9 +115,11 @@ function isImportableOutDir(outDir) {
|
|
|
115
115
|
async function ensureGitignore() {
|
|
116
116
|
const path = resolve(process.cwd(), '.gitignore');
|
|
117
117
|
const marker = '.magicpixel/';
|
|
118
|
+
// Accept any of these as "already ignored" so we don't append a duplicate.
|
|
119
|
+
const equivalents = new Set(['.magicpixel', '.magicpixel/', '/.magicpixel', '/.magicpixel/']);
|
|
118
120
|
if (existsSync(path)) {
|
|
119
121
|
const current = await readFile(path, 'utf8');
|
|
120
|
-
if (current.split('\n').some((l) => l.trim()
|
|
122
|
+
if (current.split('\n').some((l) => equivalents.has(l.trim())))
|
|
121
123
|
return false;
|
|
122
124
|
await appendFile(path, (current.endsWith('\n') ? '' : '\n') + `\n# MagicPixel CLI state\n${marker}\n`);
|
|
123
125
|
return true;
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { mkdir, writeFile, unlink, readdir, rm, rename } from 'node:fs/promises';
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
5
6
|
import { dirname, relative, resolve } from 'node:path';
|
|
@@ -19,16 +20,26 @@ export async function syncCommand(opts) {
|
|
|
19
20
|
async function watchLoop(opts) {
|
|
20
21
|
const intervalSec = typeof opts.watch === 'string' ? Math.max(2, parseInt(opts.watch, 10) || 10) : 10;
|
|
21
22
|
console.log(kleur.dim(`[watch] polling every ${intervalSec}s. Ctrl+C to stop.`));
|
|
23
|
+
let stopping = false;
|
|
24
|
+
const onSigint = () => {
|
|
25
|
+
if (stopping)
|
|
26
|
+
return;
|
|
27
|
+
stopping = true;
|
|
28
|
+
// \x1b[2K\r clears any in-flight status line before the goodbye message.
|
|
29
|
+
process.stdout.write('\x1b[2K\r');
|
|
30
|
+
console.log(kleur.dim('[watch] stopped.'));
|
|
31
|
+
process.exit(0);
|
|
32
|
+
};
|
|
33
|
+
process.on('SIGINT', onSigint);
|
|
22
34
|
// Run once eagerly so first sync isn't delayed by the interval.
|
|
23
35
|
let inFlight = false;
|
|
24
36
|
const tick = async () => {
|
|
25
|
-
if (inFlight)
|
|
37
|
+
if (inFlight || stopping)
|
|
26
38
|
return;
|
|
27
39
|
inFlight = true;
|
|
28
40
|
try {
|
|
29
41
|
const r = await runOnce({ ...opts, quiet: true, watch: false });
|
|
30
42
|
if (r.downloaded > 0 || r.pruned > 0) {
|
|
31
|
-
// \x1b[2K\r clears any residual "no changes" line from the previous tick.
|
|
32
43
|
process.stdout.write('\x1b[2K\r');
|
|
33
44
|
console.log(`${kleur.dim(timestamp())} ${kleur.green('✓')} ${r.downloaded} downloaded` +
|
|
34
45
|
(r.pruned ? `, ${r.pruned} pruned` : '') +
|
|
@@ -46,8 +57,7 @@ async function watchLoop(opts) {
|
|
|
46
57
|
}
|
|
47
58
|
};
|
|
48
59
|
await tick();
|
|
49
|
-
|
|
50
|
-
while (true) {
|
|
60
|
+
while (!stopping) {
|
|
51
61
|
await new Promise((r) => setTimeout(r, intervalSec * 1000));
|
|
52
62
|
await tick();
|
|
53
63
|
}
|
|
@@ -58,6 +68,11 @@ async function runOnce(opts) {
|
|
|
58
68
|
const startedAt = new Date().toISOString();
|
|
59
69
|
const concurrency = Math.max(1, Math.min(opts.concurrency ?? 6, 16));
|
|
60
70
|
const quiet = !!opts.quiet;
|
|
71
|
+
// Warn loudly when a custom endpoint is configured — easy to forget a test
|
|
72
|
+
// override before committing magicpixel.json to a real repo / CI.
|
|
73
|
+
if (!quiet && config.endpoint) {
|
|
74
|
+
console.log(kleur.yellow(`! using custom endpoint: ${config.endpoint}`));
|
|
75
|
+
}
|
|
61
76
|
// Incremental: only fetch what changed since last successful sync.
|
|
62
77
|
// --full forces a from-scratch fetch.
|
|
63
78
|
const since = opts.full ? undefined : state.lastSync;
|
|
@@ -142,6 +157,14 @@ async function runOnce(opts) {
|
|
|
142
157
|
result.unchanged++;
|
|
143
158
|
}
|
|
144
159
|
else {
|
|
160
|
+
// Verify the payload matches what the manifest advertised. Guards
|
|
161
|
+
// against silently writing a corrupted body to disk.
|
|
162
|
+
if (entry.sha256) {
|
|
163
|
+
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
164
|
+
if (actual !== entry.sha256) {
|
|
165
|
+
throw new Error(`sha256 mismatch (expected ${entry.sha256.slice(0, 12)}…, got ${actual.slice(0, 12)}…)`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
145
168
|
await mkdir(dirname(diskPath), { recursive: true });
|
|
146
169
|
const tmp = `${diskPath}.${process.pid}.tmp`;
|
|
147
170
|
await writeFile(tmp, bytes);
|
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
3
3
|
import kleur from 'kleur';
|
|
4
4
|
import { initCommand } from './commands/init.js';
|
|
5
5
|
import { syncCommand } from './commands/sync.js';
|
|
@@ -8,6 +8,7 @@ import { removeCommand } from './commands/remove.js';
|
|
|
8
8
|
import { listCommand } from './commands/list.js';
|
|
9
9
|
import { statusCommand } from './commands/status.js';
|
|
10
10
|
import { whoamiCommand } from './commands/whoami.js';
|
|
11
|
+
import { CLI_VERSION } from './version.js';
|
|
11
12
|
// Node version guard
|
|
12
13
|
const major = Number(process.versions.node.split('.')[0]);
|
|
13
14
|
if (major < 18) {
|
|
@@ -18,7 +19,7 @@ const program = new Command();
|
|
|
18
19
|
program
|
|
19
20
|
.name('magicpixel')
|
|
20
21
|
.description('Sync MagicPixel pixel-art assets to your local project')
|
|
21
|
-
.version(
|
|
22
|
+
.version(CLI_VERSION);
|
|
22
23
|
const wrap = (fn) => async (...args) => {
|
|
23
24
|
try {
|
|
24
25
|
await fn(...args);
|
|
@@ -44,7 +45,13 @@ program
|
|
|
44
45
|
.option('--full', 'Ignore lastSync state; re-fetch the full manifest')
|
|
45
46
|
.option('-w, --watch [seconds]', 'Poll for changes (default 10s)', (v) => v ?? true)
|
|
46
47
|
.option('-q, --quiet', 'Minimal output (for CI)')
|
|
47
|
-
.option('-c, --concurrency <n>', 'Parallel downloads (1–16, default 6)', (v) =>
|
|
48
|
+
.option('-c, --concurrency <n>', 'Parallel downloads (1–16, default 6)', (v) => {
|
|
49
|
+
const n = parseInt(v, 10);
|
|
50
|
+
if (!Number.isFinite(n) || String(n) !== v.trim() || n < 1 || n > 16) {
|
|
51
|
+
throw new InvalidArgumentError(`expected an integer 1–16, got "${v}".`);
|
|
52
|
+
}
|
|
53
|
+
return n;
|
|
54
|
+
})
|
|
48
55
|
.action(wrap(async (opts) => syncCommand(opts)));
|
|
49
56
|
program
|
|
50
57
|
.command('add <glob>')
|
package/dist/util/emitIndex.js
CHANGED
|
@@ -10,22 +10,36 @@ import { resolve } from 'node:path';
|
|
|
10
10
|
* time. No runtime, no bundler config.
|
|
11
11
|
*/
|
|
12
12
|
export async function emitTypedIndex(outDir, manifest, cwd = process.cwd()) {
|
|
13
|
-
|
|
13
|
+
// Dedupe by key. A well-behaved manifest never has duplicates, but a server
|
|
14
|
+
// bug here would otherwise emit duplicate object keys and break TS compile.
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const sorted = [...manifest]
|
|
17
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
18
|
+
.filter((e) => {
|
|
19
|
+
if (seen.has(e.key)) {
|
|
20
|
+
console.warn(`magicpixel: duplicate manifest key "${e.key}" — keeping first.`);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
seen.add(e.key);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
14
26
|
const lines = [
|
|
15
|
-
'// AUTOGENERATED by @
|
|
27
|
+
'// AUTOGENERATED by @magicpixelart/cli. Do not edit.',
|
|
16
28
|
'// Re-runs of `magicpixel sync` overwrite this file.',
|
|
17
29
|
'',
|
|
18
30
|
];
|
|
19
|
-
|
|
31
|
+
// Use positional identifiers (asset_0, asset_1, …) so keys that differ only
|
|
32
|
+
// by non-alphanumeric chars (e.g. `player/walk` vs `player_walk`) can't
|
|
33
|
+
// collide on the JS identifier and break the consumer's TS build.
|
|
34
|
+
sorted.forEach((e, i) => {
|
|
20
35
|
const rel = e.folder ? `./${e.folder}/${e.slug}.png` : `./${e.slug}.png`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
36
|
+
lines.push(`import asset_${i} from ${JSON.stringify(rel)};`);
|
|
37
|
+
});
|
|
24
38
|
lines.push('');
|
|
25
39
|
lines.push('export const MagicPixelAssets = {');
|
|
26
|
-
|
|
27
|
-
lines.push(` ${JSON.stringify(e.key)}: asset_${
|
|
28
|
-
}
|
|
40
|
+
sorted.forEach((e, i) => {
|
|
41
|
+
lines.push(` ${JSON.stringify(e.key)}: asset_${i},`);
|
|
42
|
+
});
|
|
29
43
|
lines.push('} as const;');
|
|
30
44
|
lines.push('');
|
|
31
45
|
lines.push('export type MagicPixelAssetKey = keyof typeof MagicPixelAssets;');
|
|
@@ -34,6 +48,3 @@ export async function emitTypedIndex(outDir, manifest, cwd = process.cwd()) {
|
|
|
34
48
|
await writeFile(path, lines.join('\n'), 'utf8');
|
|
35
49
|
return path;
|
|
36
50
|
}
|
|
37
|
-
function slugToIdent(key) {
|
|
38
|
-
return key.replace(/[^a-zA-Z0-9]/g, '_');
|
|
39
|
-
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// Single source of truth: read version from package.json at runtime so
|
|
3
|
+
// `npm version <bump>` propagates everywhere without code edits.
|
|
4
|
+
// dist/version.js resolves `../package.json` → tools/cli/package.json.
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
export const CLI_VERSION = pkg.version;
|
|
8
|
+
export const CLI_USER_AGENT = `@magicpixelart/cli/${CLI_VERSION} (node ${process.versions.node})`;
|
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magicpixelart/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Sync MagicPixel pixel-art assets to your local project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"magicpixel": "dist/index.js"
|
|
7
|
+
"magicpixel": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"README.md",
|
|
12
|
-
"LICENSE"
|
|
13
|
-
],
|
|
9
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
14
10
|
"scripts": {
|
|
15
11
|
"build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
16
12
|
"dev": "tsx src/index.ts",
|
|
@@ -30,7 +26,7 @@
|
|
|
30
26
|
"url": "git+https://github.com/handpoke/magicpixel-cli.git"
|
|
31
27
|
},
|
|
32
28
|
"bugs": {
|
|
33
|
-
"url": "https://github.com/magicpixel
|
|
29
|
+
"url": "https://github.com/handpoke/magicpixel-cli/issues"
|
|
34
30
|
},
|
|
35
31
|
"license": "MIT",
|
|
36
32
|
"author": "MagicPixel",
|