@yswgaicx/yswg-img-cli 0.1.7 → 0.1.8

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 CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  `yswg-img` wraps the YSWG Monkey Genius image generation page as a CLI.
4
4
 
5
+ ## Install
6
+
7
+ Install from npm on any machine with Node.js 22 or newer:
8
+
9
+ ```bash
10
+ npm install -g @yswgaicx/yswg-img-cli@latest
11
+ yswg-img --help
12
+ ```
13
+
14
+ Upgrade an existing global install:
15
+
16
+ ```bash
17
+ npm install -g @yswgaicx/yswg-img-cli@latest
18
+ yswg-img version --json
19
+ ```
20
+
21
+ `yswg-img` checks npm for newer versions at most once every 12 hours and writes
22
+ upgrade hints to stderr, so `--json` stdout stays parseable. Disable this check
23
+ with `--no-update-check` or `YSWG_IMG_CLI_NO_UPDATE_CHECK=1`.
24
+
5
25
  ## Install Locally
6
26
 
7
27
  ```bash
@@ -55,6 +75,12 @@ yswg-img models --json
55
75
  yswg-img models refresh --json
56
76
  ```
57
77
 
78
+ - Check whether the globally installed CLI is current:
79
+
80
+ ```bash
81
+ yswg-img version --json
82
+ ```
83
+
58
84
  - For up to 4 parallel images in one backend request, call:
59
85
 
60
86
  ```bash
@@ -224,6 +250,7 @@ overridden.
224
250
  - `YSWG_REFRESH_TOKEN`
225
251
  - `YSWG_BASE_URL`
226
252
  - `YSWG_WS_PATH`
253
+ - `YSWG_IMG_CLI_NO_UPDATE_CHECK`
227
254
 
228
255
  Default app name: `npm`.
229
256
  Default app ID: `2066371323654864898`.
@@ -247,6 +274,30 @@ yswg-img models refresh --json
247
274
  If generation reports a model/group validation mismatch, refresh the cache
248
275
  before submitting the same task again.
249
276
 
277
+ ## Version Updates
278
+
279
+ Use `yswg-img version --json` for a machine-readable version check:
280
+
281
+ ```json
282
+ {
283
+ "currentVersion": "0.1.7",
284
+ "latestVersion": "0.1.8",
285
+ "updateAvailable": true,
286
+ "upgradeCommand": "npm install -g @yswgaicx/yswg-img-cli@latest"
287
+ }
288
+ ```
289
+
290
+ Use `yswg-img upgrade --json` to print the canonical upgrade command without
291
+ touching npm:
292
+
293
+ ```bash
294
+ yswg-img upgrade --json
295
+ ```
296
+
297
+ The CLI caches npm latest metadata in `~/.yswg-img-cli/update-check.json`.
298
+ Regular commands emit update hints to stderr only, preserving stdout for JSON
299
+ parsers and agents.
300
+
250
301
  ## Code Structure
251
302
 
252
303
  - `bin/yswg-img.js`: executable entrypoint only.
@@ -255,5 +306,6 @@ before submitting the same task again.
255
306
  - `src/api.js`: YSWG HTTP API client.
256
307
  - `src/config.js`: config loading, env/flag precedence, email normalization.
257
308
  - `src/model-cache.js`: built-in model metadata plus local cache refresh/read helpers.
309
+ - `src/version-check.js`: npm latest lookup, update cache, version comparison, upgrade command.
258
310
  - `src/args.js`: minimal CLI argument parsing.
259
311
  - `src/image-compress.js`: frontend-compatible reference image compression.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yswgaicx/yswg-img-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "CLI wrapper for YSWG Monkey Genius image generation.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { parseArgs, readFlag, splitCsv } from "./args.js";
2
2
  import { loadConfig, normalizeEmail, resolveConfig, saveConfig, DEFAULT_APP_ID, DEFAULT_APP_NAME, DEFAULT_CONFIG_PATH } from "./config.js";
3
3
  import { loadModelCache, refreshModelCache } from "./model-cache.js";
4
+ import { buildUpgradeCommand, checkForUpdate, PACKAGE_NAME } from "./version-check.js";
4
5
  import { YswgApi } from "./api.js";
5
6
  import {
6
7
  buildGeneratePayload,
@@ -26,6 +27,8 @@ export function buildHelpText() {
26
27
  Commands:
27
28
  auth send-code --email <name|email>
28
29
  auth login --email <name|email> --code <6 digits>
30
+ version [--json]
31
+ upgrade [--json]
29
32
  models [refresh] [--json]
30
33
  templates [--json]
31
34
  amazon asin <asin> [--max 7] [--json]
@@ -46,9 +49,10 @@ Agent usage:
46
49
  For long tasks, use --no-wait first, then tasks recover --task-id <id>.
47
50
  Keep generated image file paths and short summaries in agent context; do not paste image bytes or base64.
48
51
  Amazon ASIN gallery lookup: amazon asin <asin> --max 7 --json returns asin, title, images.
52
+ Upgrade globally with: ${buildUpgradeCommand()}
49
53
 
50
54
  Environment:
51
- YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_BASE_URL, YSWG_WS_PATH
55
+ YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_BASE_URL, YSWG_WS_PATH, YSWG_IMG_CLI_NO_UPDATE_CHECK
52
56
 
53
57
  Config:
54
58
  ${DEFAULT_CONFIG_PATH}
@@ -71,6 +75,17 @@ function assertAuthenticated(resolved) {
71
75
  if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
72
76
  }
73
77
 
78
+ function shouldSkipUpdateCheck(flags) {
79
+ return Boolean(flags.noUpdateCheck || process.env.YSWG_IMG_CLI_NO_UPDATE_CHECK);
80
+ }
81
+
82
+ async function emitUpdateHint({ flags, checkForUpdateFn, writeErr }) {
83
+ if (shouldSkipUpdateCheck(flags)) return;
84
+ const result = await checkForUpdateFn();
85
+ if (!result?.updateAvailable) return;
86
+ writeErr(`yswg-img: 发现新版本 ${result.latestVersion},当前版本 ${result.currentVersion}。升级:${result.upgradeCommand}\n`);
87
+ }
88
+
74
89
  function getUserId(config) {
75
90
  return config.user?.userId || config.user?.id;
76
91
  }
@@ -258,8 +273,10 @@ export async function runCli(argv, {
258
273
  saveConfigFn = saveConfig,
259
274
  loadModelCacheFn = loadModelCache,
260
275
  refreshModelCacheFn = refreshModelCache,
276
+ checkForUpdateFn = checkForUpdate,
261
277
  createApi = (resolved) => new YswgApi(resolved),
262
278
  write = (text) => process.stdout.write(text),
279
+ writeErr = (text) => process.stderr.write(text),
263
280
  } = {}) {
264
281
  const parsed = parseArgs(argv);
265
282
  const { command, subcommand, flags, positionals } = parsed;
@@ -268,6 +285,21 @@ export async function runCli(argv, {
268
285
  write(buildHelpText());
269
286
  return;
270
287
  }
288
+
289
+ if (command === "version") {
290
+ writeOutput(write, await checkForUpdateFn({ force: true }), json);
291
+ return;
292
+ }
293
+
294
+ if (command === "upgrade") {
295
+ writeOutput(write, {
296
+ packageName: PACKAGE_NAME,
297
+ upgradeCommand: buildUpgradeCommand(),
298
+ checkCommand: "yswg-img version --json",
299
+ }, json);
300
+ return;
301
+ }
302
+
271
303
  rejectAppIdOverride(flags);
272
304
 
273
305
  const config = await loadConfigFn();
@@ -280,6 +312,7 @@ export async function runCli(argv, {
280
312
  if (check?.registered === false) throw new Error(check.message || "email is not registered");
281
313
  await api.sendCode(email);
282
314
  writeOutput(write, { ok: true, email, message: "验证码已发送,请在企业微信中查收" }, json);
315
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
283
316
  return;
284
317
  }
285
318
 
@@ -299,6 +332,7 @@ export async function runCli(argv, {
299
332
  email,
300
333
  });
301
334
  writeOutput(write, { ok: true, email, configPath: DEFAULT_CONFIG_PATH }, json);
335
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
302
336
  return;
303
337
  }
304
338
 
@@ -309,22 +343,32 @@ export async function runCli(argv, {
309
343
  fetchModels: () => api.models(resolved.appId),
310
344
  });
311
345
  writeOutput(write, { ok: true, ...result }, json);
346
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
312
347
  return;
313
348
  }
314
349
  writeOutput(write, await loadModelCacheFn(), json);
350
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
315
351
  return;
316
352
  }
317
353
 
318
354
  if (command === "templates") {
319
355
  assertAuthenticated(resolved);
320
356
  writeOutput(write, await api.templates(resolved.appId), json);
357
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
321
358
  return;
322
359
  }
323
360
 
324
- if (command === "amazon" && await handleAmazonCommand({ api, flags, resolved, subcommand, positionals, json, write })) return;
325
- if (command === "tasks" && await handleTaskCommand({ api, config, flags, resolved, subcommand, json, write })) return;
361
+ if (command === "amazon" && await handleAmazonCommand({ api, flags, resolved, subcommand, positionals, json, write })) {
362
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
363
+ return;
364
+ }
365
+ if (command === "tasks" && await handleTaskCommand({ api, config, flags, resolved, subcommand, json, write })) {
366
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
367
+ return;
368
+ }
326
369
  if (command === "generate") {
327
370
  await handleGenerate({ api, config, flags, resolved, json, write });
371
+ await emitUpdateHint({ flags, checkForUpdateFn, writeErr });
328
372
  return;
329
373
  }
330
374
 
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
7
+
8
+ export const PACKAGE_NAME = packageJson.name;
9
+ export const CURRENT_VERSION = packageJson.version;
10
+ export const DEFAULT_UPDATE_CACHE_PATH = join(homedir(), ".yswg-img-cli", "update-check.json");
11
+ export const UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
12
+
13
+ function parseVersion(version) {
14
+ return String(version || "")
15
+ .replace(/^v/i, "")
16
+ .split("-")[0]
17
+ .split(".")
18
+ .map((part) => Number.parseInt(part, 10) || 0);
19
+ }
20
+
21
+ export function compareVersions(left, right) {
22
+ const a = parseVersion(left);
23
+ const b = parseVersion(right);
24
+ const length = Math.max(a.length, b.length, 3);
25
+ for (let index = 0; index < length; index += 1) {
26
+ const diff = (a[index] || 0) - (b[index] || 0);
27
+ if (diff > 0) return 1;
28
+ if (diff < 0) return -1;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ export function buildUpgradeCommand() {
34
+ return `npm install -g ${PACKAGE_NAME}@latest`;
35
+ }
36
+
37
+ async function fetchNpmLatestVersion(packageName = PACKAGE_NAME) {
38
+ const encodedName = encodeURIComponent(packageName);
39
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}/latest`, {
40
+ headers: { accept: "application/json" },
41
+ });
42
+ if (!response.ok) throw new Error(`npm registry returned ${response.status}`);
43
+ const data = await response.json();
44
+ if (!data?.version) throw new Error("npm registry response missing version");
45
+ return data.version;
46
+ }
47
+
48
+ async function readFreshCache(cachePath, now, ttlMs) {
49
+ try {
50
+ const cache = JSON.parse(await readFile(cachePath, "utf8"));
51
+ const checkedAt = new Date(cache.checkedAt).getTime();
52
+ if (!Number.isFinite(checkedAt) || now.getTime() - checkedAt > ttlMs) return null;
53
+ return cache.latestVersion ? cache : null;
54
+ } catch (error) {
55
+ if (error.code === "ENOENT") return null;
56
+ return null;
57
+ }
58
+ }
59
+
60
+ async function writeCache(cachePath, payload) {
61
+ await mkdir(dirname(cachePath), { recursive: true });
62
+ await writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
63
+ }
64
+
65
+ export async function checkForUpdate({
66
+ cachePath = DEFAULT_UPDATE_CACHE_PATH,
67
+ currentVersion = CURRENT_VERSION,
68
+ fetchLatestVersion = fetchNpmLatestVersion,
69
+ now = new Date(),
70
+ ttlMs = UPDATE_CHECK_TTL_MS,
71
+ force = false,
72
+ } = {}) {
73
+ try {
74
+ const cached = force ? null : await readFreshCache(cachePath, now, ttlMs);
75
+ const latestVersion = cached?.latestVersion || await fetchLatestVersion(PACKAGE_NAME);
76
+ if (!cached) {
77
+ await writeCache(cachePath, {
78
+ checkedAt: now.toISOString(),
79
+ latestVersion,
80
+ });
81
+ }
82
+ return {
83
+ currentVersion,
84
+ latestVersion,
85
+ updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
86
+ upgradeCommand: buildUpgradeCommand(),
87
+ };
88
+ } catch (error) {
89
+ return {
90
+ currentVersion,
91
+ latestVersion: currentVersion,
92
+ updateAvailable: false,
93
+ upgradeCommand: buildUpgradeCommand(),
94
+ error: error.message,
95
+ };
96
+ }
97
+ }