pgpm 0.2.18 → 0.4.0

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.
@@ -1,8 +1,5 @@
1
- import { sluggify } from '@launchql/core';
1
+ import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, scaffoldTemplate, sluggify } from '@launchql/core';
2
2
  import { Logger } from '@launchql/logger';
3
- // @ts-ignore - TypeScript module resolution issue with @launchql/templatizer
4
- import { loadTemplates, workspaceTemplate, writeRenderedTemplates } from '@launchql/templatizer';
5
- import { mkdirSync } from 'fs';
6
3
  import path from 'path';
7
4
  const log = new Logger('workspace-init');
8
5
  export default async function runWorkspaceSetup(argv, prompter) {
@@ -11,36 +8,35 @@ export default async function runWorkspaceSetup(argv, prompter) {
11
8
  name: 'name',
12
9
  message: 'Enter workspace name',
13
10
  required: true,
14
- type: 'text',
11
+ type: 'text'
15
12
  }
16
13
  ];
17
14
  const answers = await prompter.prompt(argv, workspaceQuestions);
18
- const { cwd } = argv;
15
+ const { cwd = process.cwd() } = argv;
19
16
  const targetPath = path.join(cwd, sluggify(answers.name));
20
- mkdirSync(targetPath, { recursive: true });
21
- log.success(`Created workspace directory: ${targetPath}`);
22
- // Determine template source
23
- let templates = workspaceTemplate;
24
- if (argv.repo) {
25
- const source = {
26
- type: 'github',
27
- path: argv.repo,
28
- branch: argv.fromBranch
29
- };
30
- log.info(`Loading templates from GitHub repository: ${argv.repo}`);
31
- const compiledTemplates = loadTemplates(source, 'workspace');
32
- templates = compiledTemplates.map((t) => t.render);
33
- }
34
- else if (argv.templatePath) {
35
- const source = {
36
- type: 'local',
37
- path: argv.templatePath
38
- };
39
- log.info(`Loading templates from local path: ${argv.templatePath}`);
40
- const compiledTemplates = loadTemplates(source, 'workspace');
41
- templates = compiledTemplates.map((t) => t.render);
42
- }
43
- writeRenderedTemplates(templates, targetPath, { ...argv, ...answers });
17
+ // Prevent double-echoed keystrokes by closing our prompter before template prompts.
18
+ prompter.close();
19
+ const templateRepo = argv.repo ?? DEFAULT_TEMPLATE_REPO;
20
+ const templatePath = argv.templatePath ?? 'workspace';
21
+ const scaffoldResult = await scaffoldTemplate({
22
+ type: 'workspace',
23
+ outputDir: targetPath,
24
+ templateRepo,
25
+ branch: argv.fromBranch,
26
+ templatePath,
27
+ answers: {
28
+ ...argv,
29
+ ...answers,
30
+ workspaceName: answers.name
31
+ },
32
+ toolName: DEFAULT_TEMPLATE_TOOL_NAME,
33
+ noTty: Boolean(argv.noTty || argv['no-tty'] || process.env.CI === 'true'),
34
+ cwd
35
+ });
36
+ const cacheMessage = scaffoldResult.cacheUsed
37
+ ? `Using cached templates from ${scaffoldResult.templateDir}`
38
+ : `Fetched templates into ${scaffoldResult.templateDir}`;
39
+ log.success(cacheMessage);
44
40
  log.success('Workspace templates rendered.');
45
41
  return { ...argv, ...answers, cwd: targetPath };
46
42
  }
@@ -0,0 +1,67 @@
1
+ import { Logger } from '@launchql/logger';
2
+ import { spawn } from 'child_process';
3
+ import { readAndParsePackageJson } from '../package';
4
+ import { fetchLatestVersion } from '../utils/npm-version';
5
+ import { cliExitWithError } from '../utils/cli-error';
6
+ const log = new Logger('update');
7
+ const updateUsageText = `
8
+ Update Command:
9
+
10
+ pgpm update [OPTIONS]
11
+
12
+ Install the latest version of pgpm from npm.
13
+
14
+ Options:
15
+ --help, -h Show this help message
16
+ --package <name> Override the package name (default: package.json name)
17
+ --registry <url> Use a custom npm registry
18
+ --dry-run Print the npm command without executing it
19
+ `;
20
+ const runNpmInstall = (pkgName, registry) => {
21
+ return new Promise((resolve, reject) => {
22
+ const args = ['install', '-g', pkgName];
23
+ if (registry) {
24
+ args.push('--registry', registry);
25
+ }
26
+ const child = spawn('npm', args, { stdio: 'inherit' });
27
+ child.on('error', reject);
28
+ child.on('exit', (code) => {
29
+ if (code === 0) {
30
+ resolve();
31
+ }
32
+ else {
33
+ reject(new Error(`npm install exited with code ${code}`));
34
+ }
35
+ });
36
+ });
37
+ };
38
+ export default async (argv, _prompter, _options) => {
39
+ if (argv.help || argv.h) {
40
+ console.log(updateUsageText);
41
+ process.exit(0);
42
+ }
43
+ const pkgJson = readAndParsePackageJson();
44
+ const pkgName = argv.package || pkgJson.name || 'pgpm';
45
+ const registry = argv.registry;
46
+ const dryRun = Boolean(argv['dry-run']);
47
+ const npmCommand = `npm install -g ${pkgName}${registry ? ` --registry ${registry}` : ''}`;
48
+ if (dryRun) {
49
+ log.info(`[dry-run] ${npmCommand}`);
50
+ return argv;
51
+ }
52
+ log.info(`Running: ${npmCommand}`);
53
+ try {
54
+ await runNpmInstall(pkgName, registry);
55
+ const latest = await fetchLatestVersion(pkgName);
56
+ if (latest) {
57
+ log.success(`Successfully updated ${pkgName} to version ${latest}.`);
58
+ }
59
+ else {
60
+ log.success(`npm install completed for ${pkgName}.`);
61
+ }
62
+ }
63
+ catch (error) {
64
+ await cliExitWithError(error instanceof Error ? error.message : String(error), { package: pkgName, registry });
65
+ }
66
+ return argv;
67
+ };
package/esm/commands.js CHANGED
@@ -2,6 +2,7 @@ import { teardownPgPools } from 'pg-cache';
2
2
  import add from './commands/add';
3
3
  import adminUsers from './commands/admin-users';
4
4
  import analyze from './commands/analyze';
5
+ import cache from './commands/cache';
5
6
  import clear from './commands/clear';
6
7
  import deploy from './commands/deploy';
7
8
  import docker from './commands/docker';
@@ -14,6 +15,7 @@ import kill from './commands/kill';
14
15
  import migrate from './commands/migrate';
15
16
  import _package from './commands/package';
16
17
  import plan from './commands/plan';
18
+ import updateCmd from './commands/update';
17
19
  import remove from './commands/remove';
18
20
  import renameCmd from './commands/rename';
19
21
  import revert from './commands/revert';
@@ -22,6 +24,7 @@ import verify from './commands/verify';
22
24
  import { readAndParsePackageJson } from './package';
23
25
  import { extractFirst, usageText } from './utils';
24
26
  import { cliExitWithError } from './utils/cli-error';
27
+ import { checkForUpdates } from './utils/update-check';
25
28
  const withPgTeardown = (fn, skipTeardown = false) => async (...args) => {
26
29
  try {
27
30
  await fn(...args);
@@ -54,7 +57,9 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
54
57
  install: pgt(install),
55
58
  migrate: pgt(migrate),
56
59
  analyze: pgt(analyze),
57
- rename: pgt(renameCmd)
60
+ rename: pgt(renameCmd),
61
+ cache,
62
+ update: updateCmd
58
63
  };
59
64
  };
60
65
  export const commands = async (argv, prompter, options) => {
@@ -84,6 +89,15 @@ export const commands = async (argv, prompter, options) => {
84
89
  ]);
85
90
  command = answer.command;
86
91
  }
92
+ try {
93
+ await checkForUpdates({
94
+ command,
95
+ pkgVersion: readAndParsePackageJson().version
96
+ });
97
+ }
98
+ catch {
99
+ // ignore update check failures
100
+ }
87
101
  newArgv = await prompter.prompt(newArgv, [
88
102
  {
89
103
  type: 'text',
package/esm/index.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync } from 'fs';
3
3
  import { CLI } from 'inquirerer';
4
4
  import { join } from 'path';
5
5
  import { commands, createPgpmCommandMap } from './commands';
6
+ export { createInitUsageText } from './commands/init';
6
7
  export { createPgpmCommandMap };
7
8
  export { default as add } from './commands/add';
8
9
  export { default as adminUsers } from './commands/admin-users';
@@ -21,6 +21,8 @@ export const usageText = `
21
21
  plan Generate module deployment plans
22
22
  package Package module for distribution
23
23
  export Export database migrations from existing databases
24
+ update Update pgpm to the latest version
25
+ cache Manage cached templates (clean)
24
26
 
25
27
  Database Administration:
26
28
  kill Terminate database connections and optionally drop databases
@@ -50,7 +52,7 @@ export const usageText = `
50
52
 
51
53
  Examples:
52
54
  pgpm deploy --help Show deploy command options
53
- pgpm init --workspace Initialize new workspace
55
+ pgpm init workspace Initialize new workspace
54
56
  pgpm install @pgpm/base32 Install a database module
55
57
  `;
56
58
  export function displayUsage() {
@@ -4,3 +4,6 @@ export * from './display';
4
4
  export * from './cli-error';
5
5
  export * from './deployed-changes';
6
6
  export * from './module-utils';
7
+ export * from './npm-version';
8
+ export * from './update-check';
9
+ export * from './update-config';
@@ -0,0 +1,46 @@
1
+ import { execFile } from 'child_process';
2
+ import semver from 'semver';
3
+ import { promisify } from 'util';
4
+ const execFileAsync = promisify(execFile);
5
+ export async function fetchLatestVersion(pkgName, timeoutMs = 5000) {
6
+ try {
7
+ const result = await execFileAsync('npm', ['view', pkgName, 'version', '--json'], {
8
+ timeout: timeoutMs,
9
+ windowsHide: true,
10
+ maxBuffer: 1024 * 1024
11
+ });
12
+ const stdout = typeof result === 'string'
13
+ ? result
14
+ : result?.stdout ?? '';
15
+ const parsed = parseVersionOutput(stdout);
16
+ return parsed ?? null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ const parseVersionOutput = (raw) => {
23
+ const trimmed = raw.trim();
24
+ if (!trimmed.length)
25
+ return null;
26
+ try {
27
+ const parsed = JSON.parse(trimmed);
28
+ if (typeof parsed === 'string')
29
+ return parsed;
30
+ if (Array.isArray(parsed) && parsed.length) {
31
+ return String(parsed[parsed.length - 1]);
32
+ }
33
+ }
34
+ catch {
35
+ // fall through to plain string parse
36
+ }
37
+ return trimmed || null;
38
+ };
39
+ export const compareVersions = (current, latest) => {
40
+ const currentSemver = semver.coerce(current);
41
+ const latestSemver = semver.coerce(latest);
42
+ if (currentSemver && latestSemver) {
43
+ return semver.compare(currentSemver, latestSemver);
44
+ }
45
+ return current.localeCompare(latest);
46
+ };
@@ -0,0 +1,57 @@
1
+ import { Logger } from '@launchql/logger';
2
+ import { UPDATE_CHECK_APPSTASH_KEY, UPDATE_CHECK_TTL_MS, UPDATE_PACKAGE_NAME } from '@launchql/types';
3
+ import { readAndParsePackageJson } from '../package';
4
+ import { compareVersions, fetchLatestVersion } from './npm-version';
5
+ import { readUpdateConfig, shouldCheck, writeUpdateConfig } from './update-config';
6
+ const log = new Logger('update-check');
7
+ const shouldSkip = (command) => {
8
+ if (process.env.PGPM_SKIP_UPDATE_CHECK)
9
+ return true;
10
+ if (process.env.CI === 'true')
11
+ return true;
12
+ if (command === 'update')
13
+ return true;
14
+ return false;
15
+ };
16
+ export async function checkForUpdates(options = {}) {
17
+ const { pkgName = UPDATE_PACKAGE_NAME, pkgVersion = readAndParsePackageJson().version, command, now = Date.now(), key = UPDATE_CHECK_APPSTASH_KEY, toolName, baseDir } = options;
18
+ if (shouldSkip(command)) {
19
+ return null;
20
+ }
21
+ try {
22
+ const existing = await readUpdateConfig({ toolName, baseDir, key });
23
+ let latestKnownVersion = existing?.latestKnownVersion ?? pkgVersion;
24
+ const needsCheck = shouldCheck(now, existing?.lastCheckedAt, UPDATE_CHECK_TTL_MS);
25
+ if (needsCheck) {
26
+ const fetched = await fetchLatestVersion(pkgName);
27
+ if (fetched) {
28
+ latestKnownVersion = fetched;
29
+ }
30
+ await writeUpdateConfig({
31
+ lastCheckedAt: now,
32
+ latestKnownVersion
33
+ }, { toolName, baseDir, key });
34
+ }
35
+ const comparison = compareVersions(pkgVersion, latestKnownVersion);
36
+ const isOutdated = comparison < 0;
37
+ if (isOutdated) {
38
+ const defaultUpdateCommand = pkgName === UPDATE_PACKAGE_NAME
39
+ ? 'Run pgpm update to upgrade.'
40
+ : `Run npm i -g ${pkgName}@latest to upgrade.`;
41
+ const updateInstruction = options.updateCommand ?? defaultUpdateCommand;
42
+ log.warn(`A new version of ${pkgName} is available (current ${pkgVersion}, latest ${latestKnownVersion}). ${updateInstruction}`);
43
+ await writeUpdateConfig({
44
+ lastCheckedAt: now,
45
+ latestKnownVersion
46
+ }, { toolName, baseDir, key });
47
+ }
48
+ return {
49
+ lastCheckedAt: now,
50
+ latestKnownVersion
51
+ };
52
+ }
53
+ catch (error) {
54
+ log.debug('Update check skipped due to error:', error);
55
+ return null;
56
+ }
57
+ }
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { appstash, resolve as resolveAppstash } from 'appstash';
4
+ import { UPDATE_CHECK_APPSTASH_KEY } from '@launchql/types';
5
+ const defaultToolName = 'pgpm';
6
+ const getConfigPath = (options = {}) => {
7
+ const toolName = options.toolName ?? defaultToolName;
8
+ const dirs = appstash(toolName, {
9
+ ensure: true,
10
+ baseDir: options.baseDir
11
+ });
12
+ const configDir = resolveAppstash(dirs, 'config');
13
+ if (!fs.existsSync(configDir)) {
14
+ fs.mkdirSync(configDir, { recursive: true });
15
+ }
16
+ const fileName = `${(options.key ?? UPDATE_CHECK_APPSTASH_KEY).replace(/[^a-z0-9-_]/gi, '_')}.json`;
17
+ return path.join(configDir, fileName);
18
+ };
19
+ export const shouldCheck = (now, lastCheckedAt, ttlMs) => {
20
+ if (!lastCheckedAt)
21
+ return true;
22
+ return now - lastCheckedAt > ttlMs;
23
+ };
24
+ export async function readUpdateConfig(options = {}) {
25
+ const configPath = getConfigPath(options);
26
+ if (!fs.existsSync(configPath)) {
27
+ return null;
28
+ }
29
+ try {
30
+ const contents = await fs.promises.readFile(configPath, 'utf8');
31
+ const parsed = JSON.parse(contents);
32
+ return parsed;
33
+ }
34
+ catch {
35
+ // Corrupted config – clear it
36
+ try {
37
+ await fs.promises.rm(configPath, { force: true });
38
+ }
39
+ catch {
40
+ // ignore
41
+ }
42
+ return null;
43
+ }
44
+ }
45
+ export async function writeUpdateConfig(config, options = {}) {
46
+ const configPath = getConfigPath(options);
47
+ await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
48
+ }
49
+ // Exposed for testing to locate the config path for a given namespace/baseDir
50
+ export const resolveUpdateConfigPath = (options = {}) => getConfigPath(options);
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { CLIOptions } from 'inquirerer';
3
3
  import { createPgpmCommandMap } from './commands';
4
+ export { createInitUsageText } from './commands/init';
4
5
  export { createPgpmCommandMap };
5
6
  export { default as add } from './commands/add';
6
7
  export { default as adminUsers } from './commands/admin-users';
package/index.js CHANGED
@@ -18,12 +18,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.options = exports.verify = exports.tag = exports.revert = exports.renameCmd = exports.remove = exports.plan = exports._package = exports.migrate = exports.kill = exports.install = exports.extension = exports._export = exports.env = exports.docker = exports.deploy = exports.clear = exports.analyze = exports.adminUsers = exports.add = exports.createPgpmCommandMap = void 0;
21
+ exports.options = exports.verify = exports.tag = exports.revert = exports.renameCmd = exports.remove = exports.plan = exports._package = exports.migrate = exports.kill = exports.install = exports.extension = exports._export = exports.env = exports.docker = exports.deploy = exports.clear = exports.analyze = exports.adminUsers = exports.add = exports.createPgpmCommandMap = exports.createInitUsageText = void 0;
22
22
  const fs_1 = require("fs");
23
23
  const inquirerer_1 = require("inquirerer");
24
24
  const path_1 = require("path");
25
25
  const commands_1 = require("./commands");
26
26
  Object.defineProperty(exports, "createPgpmCommandMap", { enumerable: true, get: function () { return commands_1.createPgpmCommandMap; } });
27
+ var init_1 = require("./commands/init");
28
+ Object.defineProperty(exports, "createInitUsageText", { enumerable: true, get: function () { return init_1.createInitUsageText; } });
27
29
  var add_1 = require("./commands/add");
28
30
  Object.defineProperty(exports, "add", { enumerable: true, get: function () { return __importDefault(add_1).default; } });
29
31
  var admin_users_1 = require("./commands/admin-users");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgpm",
3
- "version": "0.2.18",
3
+ "version": "0.4.0",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
5
  "description": "PostgreSQL Package Manager - Database migration and package management CLI",
6
6
  "main": "index.js",
@@ -37,6 +37,7 @@
37
37
  "@types/minimist": "^1.2.5",
38
38
  "@types/node": "^20.12.7",
39
39
  "@types/pg": "^8.15.2",
40
+ "@types/semver": "^7.5.8",
40
41
  "@types/shelljs": "^0.8.16",
41
42
  "glob": "^13.0.0",
42
43
  "makage": "^0.1.8",
@@ -44,16 +45,18 @@
44
45
  "ts-node": "^10.9.2"
45
46
  },
46
47
  "dependencies": {
47
- "@launchql/core": "^2.17.7",
48
- "@launchql/env": "^2.5.7",
49
- "@launchql/logger": "^1.1.13",
50
- "@launchql/templatizer": "^2.5.8",
51
- "@launchql/types": "^2.8.7",
48
+ "@launchql/core": "^2.18.0",
49
+ "@launchql/env": "^2.6.0",
50
+ "@launchql/logger": "^1.2.0",
51
+ "@launchql/types": "^2.10.0",
52
+ "appstash": "^0.2.4",
53
+ "create-gen-app": "^0.3.1",
52
54
  "inquirerer": "^2.1.8",
53
55
  "js-yaml": "^4.1.0",
54
56
  "minimist": "^1.2.8",
55
- "pg-cache": "^1.4.9",
56
- "pg-env": "^1.1.8",
57
+ "pg-cache": "^1.5.0",
58
+ "pg-env": "^1.2.0",
59
+ "semver": "^7.6.2",
57
60
  "shelljs": "^0.10.0",
58
61
  "yanse": "^0.1.5"
59
62
  },
@@ -69,5 +72,5 @@
69
72
  "pg",
70
73
  "pgsql"
71
74
  ],
72
- "gitHead": "5a0d7c7502624bd42a214f983fd7ab957e1fb3e3"
75
+ "gitHead": "b88e3d1ec9125d2cff1d731ddb1c1dabb299c6dd"
73
76
  }
@@ -1,3 +1,3 @@
1
1
  export declare function displayVersion(): void;
2
- export declare const usageText = "\n Usage: pgpm <command> [options]\n \n Core Database Operations:\n add Add database changes to plans and create SQL files\n deploy Deploy database changes and migrations\n verify Verify database state and migrations\n revert Revert database changes and migrations\n \n Project Management:\n init Initialize workspace or module\n extension Manage module dependencies\n plan Generate module deployment plans\n package Package module for distribution\n export Export database migrations from existing databases\n \n Database Administration:\n kill Terminate database connections and optionally drop databases\n install Install database modules\n tag Add tags to changes for versioning\n clear Clear database state\n remove Remove database changes\n analyze Analyze database structure\n rename Rename database changes\n admin-users Manage admin users\n \n Migration Tools:\n migrate Migration management subcommands\n init Initialize migration tracking\n status Show migration status\n list List all changes\n deps Show change dependencies\n \n Global Options:\n -h, --help Display this help information\n -v, --version Display version information\n --cwd <directory> Working directory (default: current directory)\n \n Individual Command Help:\n pgpm <command> --help Display detailed help for specific command\n pgpm <command> -h Display detailed help for specific command\n \n Examples:\n pgpm deploy --help Show deploy command options\n pgpm init --workspace Initialize new workspace\n pgpm install @pgpm/base32 Install a database module\n ";
2
+ export declare const usageText = "\n Usage: pgpm <command> [options]\n \n Core Database Operations:\n add Add database changes to plans and create SQL files\n deploy Deploy database changes and migrations\n verify Verify database state and migrations\n revert Revert database changes and migrations\n \n Project Management:\n init Initialize workspace or module\n extension Manage module dependencies\n plan Generate module deployment plans\n package Package module for distribution\n export Export database migrations from existing databases\n update Update pgpm to the latest version\n cache Manage cached templates (clean)\n \n Database Administration:\n kill Terminate database connections and optionally drop databases\n install Install database modules\n tag Add tags to changes for versioning\n clear Clear database state\n remove Remove database changes\n analyze Analyze database structure\n rename Rename database changes\n admin-users Manage admin users\n \n Migration Tools:\n migrate Migration management subcommands\n init Initialize migration tracking\n status Show migration status\n list List all changes\n deps Show change dependencies\n \n Global Options:\n -h, --help Display this help information\n -v, --version Display version information\n --cwd <directory> Working directory (default: current directory)\n \n Individual Command Help:\n pgpm <command> --help Display detailed help for specific command\n pgpm <command> -h Display detailed help for specific command\n \n Examples:\n pgpm deploy --help Show deploy command options\n pgpm init workspace Initialize new workspace\n pgpm install @pgpm/base32 Install a database module\n ";
3
3
  export declare function displayUsage(): void;
package/utils/display.js CHANGED
@@ -29,6 +29,8 @@ exports.usageText = `
29
29
  plan Generate module deployment plans
30
30
  package Package module for distribution
31
31
  export Export database migrations from existing databases
32
+ update Update pgpm to the latest version
33
+ cache Manage cached templates (clean)
32
34
 
33
35
  Database Administration:
34
36
  kill Terminate database connections and optionally drop databases
@@ -58,7 +60,7 @@ exports.usageText = `
58
60
 
59
61
  Examples:
60
62
  pgpm deploy --help Show deploy command options
61
- pgpm init --workspace Initialize new workspace
63
+ pgpm init workspace Initialize new workspace
62
64
  pgpm install @pgpm/base32 Install a database module
63
65
  `;
64
66
  function displayUsage() {
package/utils/index.d.ts CHANGED
@@ -4,3 +4,6 @@ export * from './display';
4
4
  export * from './cli-error';
5
5
  export * from './deployed-changes';
6
6
  export * from './module-utils';
7
+ export * from './npm-version';
8
+ export * from './update-check';
9
+ export * from './update-config';
package/utils/index.js CHANGED
@@ -20,3 +20,6 @@ __exportStar(require("./display"), exports);
20
20
  __exportStar(require("./cli-error"), exports);
21
21
  __exportStar(require("./deployed-changes"), exports);
22
22
  __exportStar(require("./module-utils"), exports);
23
+ __exportStar(require("./npm-version"), exports);
24
+ __exportStar(require("./update-check"), exports);
25
+ __exportStar(require("./update-config"), exports);
@@ -0,0 +1,2 @@
1
+ export declare function fetchLatestVersion(pkgName: string, timeoutMs?: number): Promise<string | null>;
2
+ export declare const compareVersions: (current: string, latest: string) => number;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compareVersions = void 0;
7
+ exports.fetchLatestVersion = fetchLatestVersion;
8
+ const child_process_1 = require("child_process");
9
+ const semver_1 = __importDefault(require("semver"));
10
+ const util_1 = require("util");
11
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
12
+ async function fetchLatestVersion(pkgName, timeoutMs = 5000) {
13
+ try {
14
+ const result = await execFileAsync('npm', ['view', pkgName, 'version', '--json'], {
15
+ timeout: timeoutMs,
16
+ windowsHide: true,
17
+ maxBuffer: 1024 * 1024
18
+ });
19
+ const stdout = typeof result === 'string'
20
+ ? result
21
+ : result?.stdout ?? '';
22
+ const parsed = parseVersionOutput(stdout);
23
+ return parsed ?? null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ const parseVersionOutput = (raw) => {
30
+ const trimmed = raw.trim();
31
+ if (!trimmed.length)
32
+ return null;
33
+ try {
34
+ const parsed = JSON.parse(trimmed);
35
+ if (typeof parsed === 'string')
36
+ return parsed;
37
+ if (Array.isArray(parsed) && parsed.length) {
38
+ return String(parsed[parsed.length - 1]);
39
+ }
40
+ }
41
+ catch {
42
+ // fall through to plain string parse
43
+ }
44
+ return trimmed || null;
45
+ };
46
+ const compareVersions = (current, latest) => {
47
+ const currentSemver = semver_1.default.coerce(current);
48
+ const latestSemver = semver_1.default.coerce(latest);
49
+ if (currentSemver && latestSemver) {
50
+ return semver_1.default.compare(currentSemver, latestSemver);
51
+ }
52
+ return current.localeCompare(latest);
53
+ };
54
+ exports.compareVersions = compareVersions;
@@ -0,0 +1,10 @@
1
+ import { UpdateCheckConfig } from '@launchql/types';
2
+ import { UpdateConfigOptions } from './update-config';
3
+ export interface CheckForUpdatesOptions extends UpdateConfigOptions {
4
+ pkgName?: string;
5
+ pkgVersion?: string;
6
+ command?: string;
7
+ now?: number;
8
+ updateCommand?: string;
9
+ }
10
+ export declare function checkForUpdates(options?: CheckForUpdatesOptions): Promise<UpdateCheckConfig | null>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkForUpdates = checkForUpdates;
4
+ const logger_1 = require("@launchql/logger");
5
+ const types_1 = require("@launchql/types");
6
+ const package_1 = require("../package");
7
+ const npm_version_1 = require("./npm-version");
8
+ const update_config_1 = require("./update-config");
9
+ const log = new logger_1.Logger('update-check');
10
+ const shouldSkip = (command) => {
11
+ if (process.env.PGPM_SKIP_UPDATE_CHECK)
12
+ return true;
13
+ if (process.env.CI === 'true')
14
+ return true;
15
+ if (command === 'update')
16
+ return true;
17
+ return false;
18
+ };
19
+ async function checkForUpdates(options = {}) {
20
+ const { pkgName = types_1.UPDATE_PACKAGE_NAME, pkgVersion = (0, package_1.readAndParsePackageJson)().version, command, now = Date.now(), key = types_1.UPDATE_CHECK_APPSTASH_KEY, toolName, baseDir } = options;
21
+ if (shouldSkip(command)) {
22
+ return null;
23
+ }
24
+ try {
25
+ const existing = await (0, update_config_1.readUpdateConfig)({ toolName, baseDir, key });
26
+ let latestKnownVersion = existing?.latestKnownVersion ?? pkgVersion;
27
+ const needsCheck = (0, update_config_1.shouldCheck)(now, existing?.lastCheckedAt, types_1.UPDATE_CHECK_TTL_MS);
28
+ if (needsCheck) {
29
+ const fetched = await (0, npm_version_1.fetchLatestVersion)(pkgName);
30
+ if (fetched) {
31
+ latestKnownVersion = fetched;
32
+ }
33
+ await (0, update_config_1.writeUpdateConfig)({
34
+ lastCheckedAt: now,
35
+ latestKnownVersion
36
+ }, { toolName, baseDir, key });
37
+ }
38
+ const comparison = (0, npm_version_1.compareVersions)(pkgVersion, latestKnownVersion);
39
+ const isOutdated = comparison < 0;
40
+ if (isOutdated) {
41
+ const defaultUpdateCommand = pkgName === types_1.UPDATE_PACKAGE_NAME
42
+ ? 'Run pgpm update to upgrade.'
43
+ : `Run npm i -g ${pkgName}@latest to upgrade.`;
44
+ const updateInstruction = options.updateCommand ?? defaultUpdateCommand;
45
+ log.warn(`A new version of ${pkgName} is available (current ${pkgVersion}, latest ${latestKnownVersion}). ${updateInstruction}`);
46
+ await (0, update_config_1.writeUpdateConfig)({
47
+ lastCheckedAt: now,
48
+ latestKnownVersion
49
+ }, { toolName, baseDir, key });
50
+ }
51
+ return {
52
+ lastCheckedAt: now,
53
+ latestKnownVersion
54
+ };
55
+ }
56
+ catch (error) {
57
+ log.debug('Update check skipped due to error:', error);
58
+ return null;
59
+ }
60
+ }
@@ -0,0 +1,10 @@
1
+ import { UpdateCheckConfig } from '@launchql/types';
2
+ export interface UpdateConfigOptions {
3
+ toolName?: string;
4
+ baseDir?: string;
5
+ key?: string;
6
+ }
7
+ export declare const shouldCheck: (now: number, lastCheckedAt: number | undefined, ttlMs: number) => boolean;
8
+ export declare function readUpdateConfig(options?: UpdateConfigOptions): Promise<UpdateCheckConfig | null>;
9
+ export declare function writeUpdateConfig(config: UpdateCheckConfig, options?: UpdateConfigOptions): Promise<void>;
10
+ export declare const resolveUpdateConfigPath: (options?: UpdateConfigOptions) => string;