@magicpixelart/cli 0.5.4 → 0.5.5

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 CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.5] — 2026-07-12
11
+
12
+ ### Added
13
+
14
+ - **Bound project name in `whoami` / `status` / `sync`.** Manifest responses
15
+ now expose the API key's project via `x-magicpixel-project-*` headers and an
16
+ optional `project` body field. The CLI surfaces `project: <name>` so a
17
+ key-bound-to-the-wrong-project mistake is obvious at a glance. On empty
18
+ fresh syncs the server may also send `totalInProject` + a ready-to-print
19
+ `hint`; `whoami`, `status`, and verbose `sync` print it instead of a silent
20
+ zero. Old servers without the headers keep working — the project line is
21
+ simply omitted.
22
+
10
23
  ## [0.5.4] — 2026-06-22
11
24
 
12
25
  ### Added
package/dist/api.js CHANGED
@@ -1,6 +1,21 @@
1
1
  import { resolveEndpoint, getApiKey, CLI_VERSION } from './config.js';
2
2
  import { etagForSha256, MAX_ASSET_BYTES, readBodyWithLimit, safeFetch, } from './util/security.js';
3
3
  import { authHeaders } from './util/authHeaders.js';
4
+ /**
5
+ * Last-seen project metadata from any manifest response this process made.
6
+ * Populated as a side-effect of `fetchManifestPage`; read by `whoami`,
7
+ * `status`, and `sync` to surface `project: <name>` diagnostics.
8
+ *
9
+ * Never null after the first successful manifest call. Module-level so the
10
+ * three commands don't have to thread it through every helper.
11
+ */
12
+ let lastProjectInfo = null;
13
+ export function getLastProjectInfo() {
14
+ return lastProjectInfo;
15
+ }
16
+ export function __resetLastProjectInfoForTesting() {
17
+ lastProjectInfo = null;
18
+ }
4
19
  /**
5
20
  * Error from the MagicPixel API. `requestId` is the `X-Request-Id` the server
6
21
  * echoed (or the one we minted client-side if the server didn't respond) —
@@ -86,6 +101,29 @@ export async function fetchManifestPage(opts) {
86
101
  if (!isObject || !Array.isArray(data.items) || !cursorOk) {
87
102
  throw new ApiError(502, withRequestId('manifest: unexpected server response shape (items missing or non-string cursor).', serverRequestId), serverRequestId);
88
103
  }
104
+ // Capture the bound project's name/id from response headers (always
105
+ // present on 2xx). The body may also carry a richer `project` payload
106
+ // (empty fresh-sync case, includes totalInProject + hint). Prefer the
107
+ // body when present so `whoami`/`sync` can print the "0 assets in this
108
+ // project" nudge without another round-trip.
109
+ const projectId = res.headers.get('x-magicpixel-project-id');
110
+ const projectName = res.headers.get('x-magicpixel-project-name');
111
+ const bodyProject = data.project;
112
+ if (bodyProject && typeof bodyProject === 'object' && typeof bodyProject.id === 'string') {
113
+ lastProjectInfo = {
114
+ id: bodyProject.id,
115
+ name: typeof bodyProject.name === 'string' ? bodyProject.name : (projectName ?? null),
116
+ totalInProject: typeof bodyProject.totalInProject === 'number'
117
+ ? bodyProject.totalInProject
118
+ : undefined,
119
+ hint: typeof bodyProject.hint === 'string'
120
+ ? bodyProject.hint
121
+ : undefined,
122
+ };
123
+ }
124
+ else if (projectId) {
125
+ lastProjectInfo = { id: projectId, name: projectName };
126
+ }
89
127
  return data;
90
128
  }
91
129
  const bodyText = await res.text();
@@ -1,6 +1,6 @@
1
1
  import kleur from 'kleur';
2
2
  import { loadConfig, loadState, resolveEndpoint } from '../config.js';
3
- import { fetchAllManifest } from '../api.js';
3
+ import { fetchAllManifest, getLastProjectInfo } from '../api.js';
4
4
  import { fileSha256 } from '../util/hash.js';
5
5
  import { assetDiskPath } from '../util/paths.js';
6
6
  import { readKeyForDisplay } from '../util/credentials.js';
@@ -66,10 +66,18 @@ export async function statusCommand() {
66
66
  else
67
67
  changed++;
68
68
  })));
69
- console.log(` remote assets: ${manifest.length}`);
69
+ const project = getLastProjectInfo();
70
+ const projectSuffix = project ? kleur.dim(` (project: ${project.name ?? project.id.slice(0, 8)})`) : '';
71
+ console.log(` remote assets: ${manifest.length}${projectSuffix}`);
70
72
  console.log(` ${kleur.dim('=')} unchanged: ${unchanged}`);
71
73
  console.log(` ${kleur.yellow('~')} changed: ${changed}`);
72
74
  console.log(` ${kleur.green('+')} missing: ${missing}`);
75
+ if (manifest.length === 0 && project?.hint) {
76
+ // Fresh sync into an empty-project key — surface the server's hint so
77
+ // a project mismatch is one glance away instead of a silent zero.
78
+ console.log();
79
+ console.log(kleur.yellow(` ${project.hint}`));
80
+ }
73
81
  }
74
82
  catch (e) {
75
83
  console.log(kleur.red(` manifest fetch failed: ${e.message.split('\n')[0]}`));
@@ -5,7 +5,7 @@ import { mkdir, unlink, readdir, rm, writeFile } from 'node:fs/promises';
5
5
  import { existsSync } from 'node:fs';
6
6
  import { dirname, relative, resolve } from 'node:path';
7
7
  import { loadConfig, loadState, saveState } from '../config.js';
8
- import { fetchAllManifest, fetchAssetBytes, ApiError } from '../api.js';
8
+ import { fetchAllManifest, fetchAssetBytes, ApiError, getLastProjectInfo } from '../api.js';
9
9
  import { fileSha256 } from '../util/hash.js';
10
10
  import { assetDiskPath, assetDiskPathFromKey, pruneEmptyDirs, walkOutDirPngs } from '../util/paths.js';
11
11
  import { createLimit } from '../util/limit.js';
@@ -319,7 +319,19 @@ async function runOnce(opts, runOpts = {}) {
319
319
  let manifest;
320
320
  try {
321
321
  manifest = await fetchAllManifest(config, since);
322
- spinner?.succeed(`Manifest: ${manifest.length} asset${manifest.length === 1 ? '' : 's'}${since ? kleur.dim(' (incremental)') : ''}`);
322
+ const projectInfo = getLastProjectInfo();
323
+ const projectSuffix = projectInfo && verbose
324
+ ? kleur.dim(` · project: ${projectInfo.name ?? projectInfo.id.slice(0, 8)}`)
325
+ : '';
326
+ spinner?.succeed(`Manifest: ${manifest.length} asset${manifest.length === 1 ? '' : 's'}${since ? kleur.dim(' (incremental)') : ''}${projectSuffix}`);
327
+ // Fresh full sync + zero assets = almost always a project-scope mismatch
328
+ // (the exact case the "0 assets" support flow hits). Server includes a
329
+ // ready-to-print hint on the empty-fresh-sync response; surface it once
330
+ // so the user isn't left wondering why sync "did nothing".
331
+ if (!since && manifest.length === 0 && projectInfo?.hint && verbose) {
332
+ console.log();
333
+ console.log(kleur.yellow(`! ${projectInfo.hint}`));
334
+ }
323
335
  }
324
336
  catch (e) {
325
337
  spinner?.fail('Manifest fetch failed');
@@ -1,6 +1,6 @@
1
1
  import kleur from 'kleur';
2
2
  import { loadConfig, defaultConfig, resolveEndpoint, getApiKey } from '../config.js';
3
- import { ApiError, retryTransient, retryAfterMsFromResponse } from '../api.js';
3
+ import { ApiError, retryTransient, retryAfterMsFromResponse, getLastProjectInfo } from '../api.js';
4
4
  import { safeFetch } from '../util/security.js';
5
5
  import { authHeaders } from '../util/authHeaders.js';
6
6
  export async function whoamiCommand() {
@@ -49,7 +49,25 @@ export async function whoamiCommand() {
49
49
  items: Array.isArray(raw.items) ? raw.items.filter((i) => !!i && typeof i.key === 'string') : [],
50
50
  nextCursor: typeof raw.nextCursor === 'string' && raw.nextCursor ? raw.nextCursor : null,
51
51
  };
52
- return { kind: 'ok', body, serverRequestId };
52
+ // Prefer body.project (has totalInProject + hint on empty fresh syncs);
53
+ // fall back to response headers so paginated / non-empty responses still
54
+ // surface the bound project name. Kept in sync with fetchManifestPage.
55
+ const bodyProject = raw.project;
56
+ const projectId = res.headers.get('x-magicpixel-project-id');
57
+ const projectName = res.headers.get('x-magicpixel-project-name');
58
+ let project = null;
59
+ if (bodyProject && typeof bodyProject === 'object' && typeof bodyProject.id === 'string') {
60
+ project = {
61
+ id: bodyProject.id,
62
+ name: typeof bodyProject.name === 'string' ? bodyProject.name : (projectName ?? null),
63
+ totalInProject: typeof bodyProject.totalInProject === 'number' ? bodyProject.totalInProject : undefined,
64
+ hint: typeof bodyProject.hint === 'string' ? bodyProject.hint : undefined,
65
+ };
66
+ }
67
+ else if (projectId) {
68
+ project = { id: projectId, name: projectName };
69
+ }
70
+ return { kind: 'ok', body, project, serverRequestId };
53
71
  });
54
72
  if (result.kind === 'rejected') {
55
73
  console.log(kleur.red(`✗ API key rejected (${result.status}).`));
@@ -64,15 +82,33 @@ export async function whoamiCommand() {
64
82
  process.exitCode = 1;
65
83
  return;
66
84
  }
67
- const { body } = result;
85
+ const { body, project } = result;
68
86
  const more = body.nextCursor ? '+' : '';
69
87
  console.log(kleur.green('✓ key valid'));
70
88
  console.log(` endpoint: ${resolveEndpoint(config)}`);
89
+ if (project) {
90
+ const label = project.name ?? '(unnamed)';
91
+ console.log(` project: ${label} ${kleur.dim(`(${project.id.slice(0, 8)}…)`)}`);
92
+ }
71
93
  if (body.count === 0) {
72
- console.log(` visible: ${kleur.yellow('0 assets — is the key bound to a project with content?')}`);
94
+ if (project?.hint) {
95
+ // Server-provided hint already names the project; use it verbatim.
96
+ console.log(` visible: ${kleur.yellow('0 assets')}`);
97
+ console.log(kleur.dim(` ${project.hint}`));
98
+ }
99
+ else if (project) {
100
+ console.log(` visible: ${kleur.yellow(`0 assets in project "${project.name ?? project.id.slice(0, 8)}"`)}`);
101
+ console.log(kleur.dim(` If your art lives in another project, create a key from that project's row in Settings → Projects & API Keys.`));
102
+ }
103
+ else {
104
+ console.log(` visible: ${kleur.yellow('0 assets — is the key bound to a project with content?')}`);
105
+ }
73
106
  }
74
107
  else {
75
108
  console.log(` visible: ${body.count}${more} asset${body.count === 1 && !more ? '' : 's'}` +
76
109
  ` (first: ${body.items[0]?.key ?? '-'})`);
77
110
  }
111
+ // Reference the module-singleton to avoid an unused-import lint on the
112
+ // getLastProjectInfo helper (whoami reads project data inline).
113
+ void getLastProjectInfo;
78
114
  }
package/package.json CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "name": "@magicpixelart/cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Sync MagicPixel pixel-art assets to your local project.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "magicpixel": "dist/index.js"
8
8
  },
9
- "files": [
10
- "dist",
11
- "README.md",
12
- "CHANGELOG.md",
13
- "LICENSE"
14
- ],
9
+ "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE"],
15
10
  "scripts": {
16
11
  "build": "tsc && chmod +x dist/index.js",
17
12
  "dev": "tsx src/index.ts",