@jacebenson/jsn 4.3.13-dev.104.1 → 4.3.13-dev.106.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 CHANGED
@@ -23,8 +23,12 @@ npm install -g @jacebenson/jsn@dev
23
23
 
24
24
  Dev builds check the dev channel for updates during normal use, at most once
25
25
  per day. Run `jsn version --check` to check immediately. To install the newest
26
- dev build, run `npm install -g @jacebenson/jsn@dev` again. Updates are manual.
27
- Return to stable with `npm install -g @jacebenson/jsn@latest`.
26
+ dev build, run `jsn upgrade` (alias: `jsn update`). It preserves your installed
27
+ channel and skips installation when no newer version is available. Use
28
+ `--dry-run` to print the npm command without network access or installation,
29
+ `--force` to reinstall, or `--channel dev|latest` to switch channels explicitly.
30
+ For example, `jsn upgrade --channel latest` returns to stable. npm installation
31
+ permissions still apply; the command does not elevate privileges automatically.
28
32
 
29
33
  Pushes to `main` promote the commit to `dev` and publish a tested prerelease
30
34
  under npm's `dev` tag. Versions include the workflow run number and attempt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.13-dev.104.1",
3
+ "version": "4.3.13-dev.106.1",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -39,6 +39,7 @@ import { transactionsCmd } from './commands/transactions.js';
39
39
  import { platformCmd } from './commands/platform.js';
40
40
  import { codesearchCmd } from './commands/codesearch.js';
41
41
  import { versionCmd } from './commands/version.js';
42
+ import { upgradeCmd } from './commands/upgrade.js';
42
43
  import { skillCmd, checkSkill } from './commands/skill.js';
43
44
  import { getVersion, checkLatest, getReleaseChannel, getInstallCommand } from './commands/version.js';
44
45
  import { docsCmd } from './commands/docs/docs.js';
@@ -438,6 +439,7 @@ export function buildCLI() {
438
439
  .command(skillCmd(wrap))
439
440
  .command(docsCmd(wrap))
440
441
  .command(versionCmd(wrap))
442
+ .command(upgradeCmd(wrap))
441
443
  .command(discoverCmd(wrap, (argv) => visibleCommandRegistry(cliInstance.commandRegistry(), app.config, argv)))
442
444
  .demandCommand(1, 'You must specify a command')
443
445
  // Completion plumbing under a hidden command name, so the explicit
@@ -5,7 +5,7 @@ const BASIC_COMMANDS = new Set([
5
5
  'knowledge',
6
6
  'groups', 'groupmembers', 'grouproles', 'users', 'setup', 'auth', 'skill',
7
7
  'cmdb',
8
- 'version', 'docs', 'history', 'discover', 'completion',
8
+ 'version', 'upgrade', 'docs', 'history', 'discover', 'completion',
9
9
  'catalogitems',
10
10
  ]);
11
11
 
@@ -0,0 +1,69 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { declareCapabilities } from '../capabilities.js';
3
+ import { AppError, CodeAPI, CodeNetwork } from '../errors.js';
4
+ import { checkLatest, getReleaseChannel, getVersion } from './version.js';
5
+
6
+ for (const name of ['upgrade', 'update']) {
7
+ declareCapabilities(name, { noInstance: true, skipDailyChecks: true });
8
+ }
9
+
10
+ function installPackage(args, { structured }) {
11
+ const result = spawnSync('npm', args, { stdio: structured ? 'pipe' : 'inherit', encoding: 'utf8' });
12
+ if (result.error || result.status !== 0) {
13
+ throw new AppError(CodeAPI, 'npm could not upgrade jsn.', 'Check npm installation permissions and connectivity, then retry.');
14
+ }
15
+ }
16
+
17
+ function isNewer(candidate, installed) {
18
+ const parse = (value) => {
19
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
20
+ if (!match) throw new AppError(CodeAPI, 'The registry returned an invalid package version.');
21
+ return { core: match.slice(1, 4).map(Number), pre: match[4]?.split('.') };
22
+ };
23
+ const a = parse(candidate), b = parse(installed);
24
+ for (let i = 0; i < 3; i++) {
25
+ if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i];
26
+ }
27
+ if (!a.pre || !b.pre) return !a.pre && Boolean(b.pre);
28
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
29
+ if (a.pre[i] === b.pre[i]) continue;
30
+ if (a.pre[i] === undefined) return false;
31
+ if (b.pre[i] === undefined) return true;
32
+ const an = /^\d+$/.test(a.pre[i]), bn = /^\d+$/.test(b.pre[i]);
33
+ if (an && bn) return Number(a.pre[i]) > Number(b.pre[i]);
34
+ if (an !== bn) return !an;
35
+ return a.pre[i] > b.pre[i];
36
+ }
37
+ return false;
38
+ }
39
+
40
+ export function upgradeCmd(wrap, { version = getVersion, check = checkLatest, install = installPackage } = {}) {
41
+ return {
42
+ command: 'upgrade',
43
+ aliases: ['update'],
44
+ describe: 'Upgrade jsn through npm (preserves the installed release channel)',
45
+ builder: (y) => y
46
+ .option('channel', { type: 'string', choices: ['dev', 'latest'], describe: 'Choose the npm release channel' })
47
+ .option('dry-run', { type: 'boolean', describe: 'Show the install command without installing' })
48
+ .option('force', { type: 'boolean', describe: 'Reinstall even when the selected version is not newer' }),
49
+ handler: wrap(async (argv, app) => {
50
+ const current = version();
51
+ const channel = argv.channel || getReleaseChannel(current);
52
+ const args = ['install', '-g', `@jacebenson/jsn@${channel}`];
53
+ const command = `npm ${args.join(' ')}`;
54
+ if (argv.dryRun || argv['dry-run']) {
55
+ app.ok({ version: current, channel, dryRun: true, installCmd: command }, { summary: command });
56
+ return;
57
+ }
58
+ const latest = await check({ version: current, channel });
59
+ if (!latest) throw new AppError(CodeNetwork, 'Could not check the npm release channel.', 'Check connectivity and retry; use --dry-run to inspect the command.');
60
+ const switching = argv.channel && channel !== getReleaseChannel(current);
61
+ if (!argv.force && !switching && !isNewer(latest, current)) {
62
+ app.ok({ version: current, latest, channel, upToDate: true }, { summary: `jsn ${current} — no newer version available` });
63
+ return;
64
+ }
65
+ await install(args, { structured: Boolean(!process.stdout.isTTY || argv.json || argv.get || argv.format === 'json') });
66
+ app.ok({ previousVersion: current, version: latest, channel, upgraded: true }, { summary: `Installed jsn from the ${channel} channel (registry version ${latest})` });
67
+ }),
68
+ };
69
+ }
@@ -32,13 +32,13 @@ export function cancelBackgroundChecks() {
32
32
  backgroundChecks.clear();
33
33
  }
34
34
 
35
- export async function checkLatest({ timeout = 1500, background = false, version = getVersion() } = {}) {
35
+ export async function checkLatest({ timeout = 1500, background = false, version = getVersion(), channel = getReleaseChannel(version) } = {}) {
36
36
  const controller = new AbortController();
37
37
  const timer = setTimeout(() => controller.abort(), timeout);
38
38
  timer.unref();
39
39
  if (background) backgroundChecks.add(controller);
40
40
  try {
41
- const res = await fetch(`https://registry.npmjs.org/${NPM_PKG}/${getReleaseChannel(version)}`, { signal: controller.signal });
41
+ const res = await fetch(`https://registry.npmjs.org/${NPM_PKG}/${channel}`, { signal: controller.signal });
42
42
  if (!res.ok) return null;
43
43
  const data = await res.json();
44
44
  return typeof data.version === 'string' ? data.version : null;