@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 +3 -1
- package/package.json +1 -1
- package/skill/topazlabscli/SKILL.md +1 -1
- package/src/cli.mjs +29 -12
- package/src/config.mjs +5 -2
- package/src/update.mjs +98 -23
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
|
|
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
|
@@ -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
|
|
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
|
-
|
|
199
|
-
if (
|
|
200
|
-
if (
|
|
201
|
-
process.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
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
+
}
|