@petercjl/topazlabscli 0.2.1 → 0.2.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/README.md CHANGED
@@ -15,7 +15,7 @@ npm install --global @petercjl/topazlabscli
15
15
  topazlabscli skill install --agent all
16
16
  ```
17
17
 
18
- The CLI checks npm for a newer stable release before operational commands, at most once every six hours. When an update is available it upgrades itself, refreshes installed Agent Skills, and then resumes the original command. It discovers npm through the running Node installation, preserves SealSeek's managed global prefix/cache, and discovers Windows OpenSSH through the standard system location, so it also works in Agent runtimes with a restricted `PATH`. A temporary npm outage does not block video processing. `topazlabscli update` forces an immediate manual update.
18
+ The CLI checks npm for a newer stable release before operational commands, at most once every six hours. It first uses the registry already configured for npm and automatically tries `https://registry.npmmirror.com/` if that registry is unavailable. The successful registry is also used for installation, without changing the user's `.npmrc`. When an update is available the CLI upgrades itself, refreshes installed Agent Skills, and then resumes the original command. It discovers npm through the running Node installation, preserves SealSeek's managed global prefix/cache, and discovers Windows OpenSSH through the standard system location, so it also works in Agent runtimes with a restricted `PATH`. A temporary registry outage does not block video processing. `topazlabscli update` forces an immediate manual update.
19
19
 
20
20
  On Windows, SealSeek Skills are installed into `%USERPROFILE%\.sealseek\workspace\skills` when that workspace is present. The CLI automatically uses a managed copy because SealSeek rejects junctions that resolve outside the workspace Skill root; subsequent CLI updates refresh the copy from the npm package. The copy includes a local runtime manifest so the Agent can invoke the canonical package even when its PATH is restricted. `SEALSEEK_SKILLS_HOME` remains available as an explicit override.
21
21
 
@@ -84,4 +84,6 @@ Configuration is stored outside the package:
84
84
  - macOS/Linux: `${XDG_CONFIG_HOME:-~/.config}/topazlabscli/config.json`
85
85
  - Override for testing or automation: `TOPAZLABSCLI_CONFIG`
86
86
 
87
+ Update registry selection is CLI-local. `settings set update-registry <url>` sets a preferred registry, and `settings set update-registry auto` restores automatic selection. `TOPAZLABSCLI_UPDATE_REGISTRY` can provide one or more comma-separated preferred registries for managed environments.
88
+
87
89
  Do not publish configuration files, keys, internal addresses, media, Topaz model files, or authentication data.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@petercjl/topazlabscli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Cross-Agent CLI and portable Skill for queued remote Topaz Video AI processing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,7 +39,7 @@ Use the CLI as the single execution surface. Do not reproduce SSH, SFTP, queue,
39
39
 
40
40
  Configuration, hostnames, addresses, usernames, SSH identities, VPN details, media, Topaz binaries, models, and credentials are external to this Skill and npm package. Installation does not grant access to a workstation. Treat the configured server and Topaz license as user-managed resources.
41
41
 
42
- Before operational commands, the CLI performs a cached npm update check. A newer stable package is installed automatically, installed Agent Skills are refreshed, and the original command resumes under the new version. The CLI resolves npm through the running Agent's Node installation when PATH is restricted. Registry, npm, and Skill-refresh failures produce a warning and continue with the installed version.
42
+ Before operational commands, the CLI performs a cached npm update check. It tries the user's current npm registry and then its built-in reachable-registry fallback without changing the user's global npm configuration. A newer stable package is installed from the same registry that answered the version check, installed Agent Skills are refreshed, and the original command resumes under the new version. The CLI resolves npm through the running Agent's Node installation when PATH is restricted. Registry, npm, and Skill-refresh failures produce a warning and continue with the installed version. Treat `doctor`'s `updates.registry` check as advisory; a failed update source does not make video processing unavailable.
43
43
 
44
44
  Do not overwrite a local output unless the user has authorized that exact existing target. The remote worker retains job inputs, outputs, status, and logs for operator review; cleanup is an administrative action outside version 0.2.
45
45
 
package/src/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { CliError, requireValue } from "./errors.mjs";
8
8
  import { run } from "./process.mjs";
9
9
  import { psLiteral, runPowerShell, selectEndpoint, sftpGet, sftpPut, startPowerShellDetached } from "./ssh.mjs";
10
10
  import { skillInstall, skillSource, skillStatus } from "./skill.mjs";
11
- import { maybeAutoUpdate } from "./update.mjs";
11
+ import { installLatestPackage, maybeAutoUpdate, queryLatestVersion, updateRegistryWarning } from "./update.mjs";
12
12
 
13
13
  const require = createRequire(import.meta.url);
14
14
  const pkg = require("../package.json");
@@ -19,7 +19,7 @@ const CAPABILITIES = {
19
19
  package: pkg.name,
20
20
  version: pkg.version,
21
21
  commands: ["version", "capabilities", "doctor", "settings", "target", "connection", "worker", "model", "job", "process", "skill", "update"],
22
- automatic_updates: { enabled_by_default: true, registry_check_hours: 6, refreshes_installed_skills: true },
22
+ automatic_updates: { enabled_by_default: true, registry_check_hours: 6, registry_fallback: true, refreshes_installed_skills: true },
23
23
  presets: [{ id: PRESET, model: "prob-4", output: "aspect-preserving 1080p", fps: "source", concurrency: 1 }],
24
24
  agents: { codex: "tested", sealseek_windows: "tested" },
25
25
  worker_os: ["windows"],
@@ -60,7 +60,7 @@ function help() {
60
60
  return `topazlabscli ${pkg.version}\n\n` +
61
61
  `Commands:\n` +
62
62
  ` version | capabilities | doctor\n` +
63
- ` settings show | set auto-update <on|off> | set update-check-hours <hours>\n` +
63
+ ` settings show | set auto-update <on|off> | set update-check-hours <hours> | set update-registry <auto|url>\n` +
64
64
  ` target add <name> --endpoint <label=host>... --user <user> [--identity <path>] [--workspace <windows-path>] [--default]\n` +
65
65
  ` target list\n` +
66
66
  ` connection check [--target <name>]\n` +
@@ -152,7 +152,7 @@ export function defaultOutputPath(inputPath) {
152
152
  return path.join(path.dirname(resolved), `${path.basename(resolved, path.extname(resolved))}-topaz-1080p${extension}`);
153
153
  }
154
154
 
155
- async function doctor(requestedTarget) {
155
+ async function doctor(requestedTarget, updateInfo = null) {
156
156
  const checks = [];
157
157
  for (const command of ["ssh", "sftp", "node", "npm"]) {
158
158
  const args = command === "node" || command === "npm" ? ["--version"] : command === "sftp" ? ["-h"] : ["-V"];
@@ -169,6 +169,16 @@ async function doctor(requestedTarget) {
169
169
  });
170
170
  }
171
171
  checks.push({ id: "config", ok: fs.existsSync(configPath()), detail: configPath() });
172
+ if (updateInfo) {
173
+ checks.push({
174
+ id: "updates.registry",
175
+ ok: Boolean(updateInfo.registry),
176
+ required: false,
177
+ detail: updateInfo.registry
178
+ ? { registry: updateInfo.registry, latest: updateInfo.latest || null, checked: updateInfo.checked }
179
+ : { warning: updateInfo.warning || "Update registry was not checked.", attempts: updateInfo.attempts || [] }
180
+ });
181
+ }
172
182
  if (requestedTarget || fs.existsSync(configPath())) {
173
183
  try {
174
184
  const { target, endpoint } = await getConnectedTarget(requestedTarget);
@@ -183,7 +193,7 @@ async function doctor(requestedTarget) {
183
193
  checks.push({ id: "connection", ok: false, detail: error.details || error.message });
184
194
  }
185
195
  }
186
- return { ok: checks.every((item) => item.ok), checks };
196
+ return { ok: checks.filter((item) => item.required !== false).every((item) => item.ok), checks };
187
197
  }
188
198
 
189
199
  export async function main(rawArgs) {
@@ -194,11 +204,12 @@ export async function main(rawArgs) {
194
204
  if (command === "version" || command === "--version" || command === "-V") return output(pkg.version, json);
195
205
  if (command === "capabilities") return output(CAPABILITIES, json);
196
206
 
207
+ let updateInfo = null;
197
208
  if (!["update", "settings"].includes(command)) {
198
- const update = await maybeAutoUpdate(rawArgs, pkg);
199
- if (update.warning) process.stderr.write(`[AUTO_UPDATE_WARNING] ${update.warning}\n`);
200
- if (update.reexecuted) {
201
- process.exitCode = update.exitCode;
209
+ updateInfo = await maybeAutoUpdate(rawArgs, pkg);
210
+ if (updateInfo.warning) process.stderr.write(`[AUTO_UPDATE_WARNING] ${updateInfo.warning}\n`);
211
+ if (updateInfo.reexecuted) {
212
+ process.exitCode = updateInfo.exitCode;
202
213
  return;
203
214
  }
204
215
  }
@@ -217,6 +228,9 @@ export async function main(rawArgs) {
217
228
  const hours = Number(value);
218
229
  if (!Number.isFinite(hours) || hours < 0) throw new CliError("SETTING_INVALID", "update-check-hours must be zero or a positive number.");
219
230
  config.settings.update_check_hours = hours;
231
+ } else if (name === "update-registry") {
232
+ if (value !== "auto" && !/^https?:\/\//i.test(value)) throw new CliError("SETTING_INVALID", "update-registry must be auto or an http(s) URL.");
233
+ config.settings.update_registry = value;
220
234
  } else throw new CliError("SETTING_UNKNOWN", `Unknown setting: ${name}`);
221
235
  const saved = saveConfig(config);
222
236
  return output({ path: saved, settings: config.settings }, json);
@@ -224,7 +238,7 @@ export async function main(rawArgs) {
224
238
  throw new CliError("COMMAND_UNKNOWN", `Unknown settings action: ${action}`);
225
239
  }
226
240
 
227
- if (command === "doctor") return output(await doctor(option(args, "--target")), json);
241
+ if (command === "doctor") return output(await doctor(option(args, "--target"), updateInfo), json);
228
242
 
229
243
  if (command === "target") {
230
244
  const action = args.shift();
@@ -319,14 +333,17 @@ export async function main(rawArgs) {
319
333
  }
320
334
 
321
335
  if (command === "update") {
336
+ const config = loadConfig();
337
+ const query = await queryLatestVersion(pkg, config);
338
+ if (!query.ok) throw new CliError("UPDATE_FAILED", updateRegistryWarning(query.attempts), { attempts: query.attempts });
322
339
  const previousSkills = skillStatus("all");
323
- const result = await run("npm", ["install", "-g", `${pkg.name}@latest`]);
340
+ const result = await installLatestPackage(pkg, query.registry);
324
341
  if (result.code !== 0) throw new CliError("UPDATE_FAILED", result.stderr.trim() || "npm update failed.");
325
342
  const skills = [];
326
343
  for (const existing of previousSkills.filter((item) => item.installed)) {
327
344
  skills.push(...skillInstall(existing.agent, existing.mode || "link", true));
328
345
  }
329
- return output({ package: pkg.name, updated: true, skills, detail: result.stdout.trim() }, json);
346
+ return output({ package: pkg.name, updated: true, registry: query.registry, latest: query.latest, skills, detail: result.stdout.trim() }, json);
330
347
  }
331
348
  throw new CliError("COMMAND_UNKNOWN", `Unknown command: ${command}`);
332
349
  }
package/src/config.mjs CHANGED
@@ -8,7 +8,7 @@ export function emptyConfig() {
8
8
  schema_version: 1,
9
9
  default_target: null,
10
10
  targets: {},
11
- settings: { auto_update: true, update_check_hours: 6 }
11
+ settings: { auto_update: true, update_check_hours: 6, update_registry: "auto" }
12
12
  };
13
13
  }
14
14
 
@@ -29,7 +29,10 @@ export function loadConfig({ required = false } = {}) {
29
29
  auto_update: parsed.settings?.auto_update !== false,
30
30
  update_check_hours: Number.isFinite(parsed.settings?.update_check_hours)
31
31
  ? parsed.settings.update_check_hours
32
- : 6
32
+ : 6,
33
+ update_registry: typeof parsed.settings?.update_registry === "string"
34
+ ? parsed.settings.update_registry
35
+ : "auto"
33
36
  }
34
37
  };
35
38
  } catch (error) {
package/src/update.mjs CHANGED
@@ -6,6 +6,8 @@ import { run, runInherited } from "./process.mjs";
6
6
  import { skillInstall, skillStatus } from "./skill.mjs";
7
7
 
8
8
  const DEFAULT_INTERVAL_HOURS = 6;
9
+ const DEFAULT_MIRROR_REGISTRY = "https://registry.npmmirror.com/";
10
+ const UPDATE_TIMEOUT_MS = 8000;
9
11
  const UPDATE_GUARD = "TOPAZLABSCLI_AUTO_UPDATE_GUARD";
10
12
 
11
13
  function numericParts(version) {
@@ -41,6 +43,87 @@ function automaticUpdateEnabled(config, env) {
41
43
  return config.settings?.auto_update !== false;
42
44
  }
43
45
 
46
+ function normalizeRegistry(value) {
47
+ const text = String(value || "").trim();
48
+ if (!/^https?:\/\//i.test(text)) return null;
49
+ return text.endsWith("/") ? text : `${text}/`;
50
+ }
51
+
52
+ function uniqueRegistries(values) {
53
+ return [...new Set(values.map(normalizeRegistry).filter(Boolean))];
54
+ }
55
+
56
+ export async function resolveUpdateRegistries(config, dependencies = {}) {
57
+ const env = dependencies.env || process.env;
58
+ const execute = dependencies.run || run;
59
+ const configured = config.settings?.update_registry;
60
+ const overrides = String(env.TOPAZLABSCLI_UPDATE_REGISTRY || "")
61
+ .split(",")
62
+ .map((item) => item.trim())
63
+ .filter(Boolean);
64
+ const preferred = configured && configured !== "auto" ? [configured] : [];
65
+ let npmRegistry = null;
66
+ try {
67
+ const result = await execute("npm", ["config", "get", "registry"], {
68
+ env,
69
+ timeoutMs: dependencies.timeoutMs || UPDATE_TIMEOUT_MS
70
+ });
71
+ if (result.code === 0) npmRegistry = result.stdout.trim();
72
+ } catch {}
73
+ return uniqueRegistries([...overrides, ...preferred, npmRegistry, DEFAULT_MIRROR_REGISTRY]);
74
+ }
75
+
76
+ export async function queryLatestVersion(pkg, config, dependencies = {}) {
77
+ const env = dependencies.env || process.env;
78
+ const execute = dependencies.run || run;
79
+ const registries = dependencies.registries || await resolveUpdateRegistries(config, dependencies);
80
+ const attempts = [];
81
+ for (const registry of registries) {
82
+ let result;
83
+ try {
84
+ result = await execute("npm", ["view", pkg.name, "version", "--json", "--registry", registry], {
85
+ env: {
86
+ ...env,
87
+ npm_config_fetch_timeout: env.npm_config_fetch_timeout || "5000",
88
+ npm_config_fetch_retries: "0"
89
+ },
90
+ timeoutMs: dependencies.timeoutMs || UPDATE_TIMEOUT_MS
91
+ });
92
+ } catch (error) {
93
+ attempts.push({ registry, ok: false, detail: error.message });
94
+ continue;
95
+ }
96
+ if (result.code !== 0) {
97
+ attempts.push({
98
+ registry,
99
+ ok: false,
100
+ detail: result.timedOut ? "timed out" : (result.stderr.trim() || "registry query failed")
101
+ });
102
+ continue;
103
+ }
104
+ let latest;
105
+ try { latest = JSON.parse(result.stdout.trim()); }
106
+ catch { latest = result.stdout.trim().replace(/^"|"$/g, ""); }
107
+ attempts.push({ registry, ok: true, latest });
108
+ return { ok: true, latest, registry, attempts };
109
+ }
110
+ return { ok: false, attempts };
111
+ }
112
+
113
+ function registryFailureMessage(attempts) {
114
+ if (!attempts.length) return "No valid npm update registry is configured.";
115
+ return `Unable to check npm for updates: ${attempts.map((item) => `${item.registry} (${item.detail})`).join("; ")}`;
116
+ }
117
+
118
+ export async function installLatestPackage(pkg, registry, dependencies = {}) {
119
+ const env = dependencies.env || process.env;
120
+ const execute = dependencies.run || run;
121
+ return execute("npm", ["install", "--global", `${pkg.name}@latest`, "--registry", registry], {
122
+ env,
123
+ timeoutMs: dependencies.installTimeoutMs
124
+ });
125
+ }
126
+
44
127
  export async function maybeAutoUpdate(rawArgs, pkg, dependencies = {}) {
45
128
  const env = dependencies.env || process.env;
46
129
  if (env[UPDATE_GUARD] === "1") return { checked: false, reason: "guard" };
@@ -54,39 +137,27 @@ export async function maybeAutoUpdate(rawArgs, pkg, dependencies = {}) {
54
137
  const intervalMs = Math.max(0, intervalHours) * 60 * 60 * 1000;
55
138
  const state = readState(stateFile);
56
139
  if (intervalMs > 0 && Number.isFinite(state.last_checked_at) && now - state.last_checked_at < intervalMs) {
57
- return { checked: false, reason: "fresh", latest: state.latest || null };
140
+ return { checked: false, reason: "fresh", latest: state.latest || null, registry: state.registry || null };
58
141
  }
59
142
 
60
- const execute = dependencies.run || run;
61
- let query;
62
- try {
63
- query = await execute("npm", ["view", pkg.name, "version", "--json"], {
64
- env: { ...env, npm_config_fetch_timeout: env.npm_config_fetch_timeout || "5000", npm_config_fetch_retries: "0" }
65
- });
66
- } catch (error) {
67
- return { checked: true, warning: `Unable to check npm for updates: ${error.message}` };
68
- }
69
- if (query.code !== 0) {
70
- return { checked: true, warning: query.stderr.trim() || "Unable to check npm for updates." };
71
- }
143
+ const query = await queryLatestVersion(pkg, config, dependencies);
144
+ if (!query.ok) return { checked: true, warning: registryFailureMessage(query.attempts), attempts: query.attempts };
72
145
 
73
- let latest;
74
- try { latest = JSON.parse(query.stdout.trim()); }
75
- catch { latest = query.stdout.trim().replace(/^"|"$/g, ""); }
76
- writeState(stateFile, { last_checked_at: now, latest, current: pkg.version });
77
- if (!isNewerVersion(latest, pkg.version)) return { checked: true, updated: false, latest };
146
+ const { latest, registry } = query;
147
+ writeState(stateFile, { last_checked_at: now, latest, current: pkg.version, registry });
148
+ if (!isNewerVersion(latest, pkg.version)) return { checked: true, updated: false, latest, registry, attempts: query.attempts };
78
149
 
79
150
  const getSkillStatus = dependencies.skillStatus || skillStatus;
80
151
  const installSkill = dependencies.skillInstall || skillInstall;
81
152
  const installedSkills = getSkillStatus("all").filter((item) => item.installed);
82
153
  let install;
83
154
  try {
84
- install = await execute("npm", ["install", "--global", `${pkg.name}@latest`], { env });
155
+ install = await installLatestPackage(pkg, registry, dependencies);
85
156
  } catch (error) {
86
- return { checked: true, warning: `Automatic npm update failed: ${error.message}`, latest };
157
+ return { checked: true, warning: `Automatic npm update failed: ${error.message}`, latest, registry };
87
158
  }
88
159
  if (install.code !== 0) {
89
- return { checked: true, warning: install.stderr.trim() || "Automatic npm update failed.", latest };
160
+ return { checked: true, warning: install.stderr.trim() || "Automatic npm update failed.", latest, registry };
90
161
  }
91
162
  const warnings = [];
92
163
  for (const item of installedSkills) {
@@ -99,9 +170,13 @@ export async function maybeAutoUpdate(rawArgs, pkg, dependencies = {}) {
99
170
  const child = await reexecute(process.execPath, [binScript, ...rawArgs], {
100
171
  env: { ...env, [UPDATE_GUARD]: "1" }
101
172
  });
102
- return { checked: true, updated: true, latest, reexecuted: true, exitCode: child.code ?? 1, warnings };
173
+ return { checked: true, updated: true, latest, registry, reexecuted: true, exitCode: child.code ?? 1, warnings };
103
174
  } catch (error) {
104
175
  warnings.push(`Updated package could not restart the command: ${error.message}`);
105
- return { checked: true, updated: true, latest, warning: warnings.join(" ") };
176
+ return { checked: true, updated: true, latest, registry, warning: warnings.join(" ") };
106
177
  }
107
178
  }
179
+
180
+ export function updateRegistryWarning(attempts) {
181
+ return registryFailureMessage(attempts);
182
+ }