Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export async function add(name : string, {verbose = false, dev = false, lock} :
}

pkgDetails = {
name: name,
name: asName || name,
repo: '',
version: ver,
};
Expand Down
27 changes: 20 additions & 7 deletions cli/commands/available-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import process from 'node:process';
import chalk from 'chalk';
import {mainActor} from '../api/actors.js';
import {Config} from '../types.js';
import {getDepName} from '../helpers/get-dep-name.js';
import {getDepName, getDepPinnedVersion} from '../helpers/get-dep-name.js';
import {SemverPart} from '../declarations/main/main.did.js';

// [pkg, oldVersion, newVersion]
export async function getAvailableUpdates(config : Config, pkg ?: string) : Promise<Array<[string, string, string]>> {
Expand All @@ -11,25 +12,37 @@ export async function getAvailableUpdates(config : Config, pkg ?: string) : Prom
let allDeps = [...deps, ...devDeps].filter((dep) => dep.version);
let depsToUpdate = pkg ? allDeps.filter((dep) => dep.name === pkg) : allDeps;

// skip pinned dependencies
depsToUpdate = depsToUpdate.filter((dep) => getDepName(dep.name) === dep.name);
// skip hard pinned dependencies (e.g. "base@X.Y.Z")
depsToUpdate = depsToUpdate.filter((dep) => getDepName(dep.name) === dep.name || getDepPinnedVersion(dep.name).split('.').length !== 3);
Comment on lines +15 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Hard-pin detection is brittle (pre-release/build tags misclassified).

split('.') misses cases like 1.2.3-rc.1 and may incorrectly update hard-pinned deps. Parse with a regex and normalize the pin.

Apply:

-// skip hard pinned dependencies (e.g. "base@X.Y.Z")
-depsToUpdate = depsToUpdate.filter((dep) => getDepName(dep.name) === dep.name || getDepPinnedVersion(dep.name).split('.').length !== 3);
+// skip hard-pinned dependencies (e.g. "base@X.Y.Z" or "base@X.Y.Z-rc.1")
+depsToUpdate = depsToUpdate.filter((dep) => {
+  const pin = normalizePin(getDepPinnedVersion(dep.name));
+  const hardPinned = /^\d+\.\d+\.\d+(?:[-+].*)?$/.test(pin);
+  return !hardPinned;
+});

Outside selected lines: also make pkg filtering pin/alias-aware, otherwise mops update core won’t select core@1:

- let depsToUpdate = pkg ? allDeps.filter((dep) => dep.name === pkg) : allDeps;
+ const pkgBase = pkg ? getDepName(pkg) : '';
+ let depsToUpdate = pkg ? allDeps.filter((dep) => getDepName(dep.name) === pkgBase) : allDeps;

Support code to add near the top of the file:

function normalizePin(pin: string): string {
  return pin.trim().replace(/^v/i, '');
}


let getCurrentVersion = (pkg : string) => {
let getCurrentVersion = (pkg : string, updateVersion : string) => {
for (let dep of allDeps) {
if (dep.name === pkg && dep.version) {
if (getDepName(dep.name) === pkg && dep.version) {
let pinnedVersion = getDepPinnedVersion(dep.name);
if (pinnedVersion && !updateVersion.startsWith(pinnedVersion)) {
continue;
}
return dep.version;
}
}
return '';
};
Comment on lines +18 to 29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Pin check using startsWith() is wrong (e.g., pin "1" matches "10.x").

This can enable invalid updates. Use boundary-aware matching and exclude rows when the pin doesn’t match.

Apply:

-let getCurrentVersion = (pkg : string, updateVersion : string) => {
+let getCurrentVersion = (pkg : string, updateVersion : string) => {
   for (let dep of allDeps) {
-    if (getDepName(dep.name) === pkg && dep.version) {
-      let pinnedVersion = getDepPinnedVersion(dep.name);
-      if (pinnedVersion && !updateVersion.startsWith(pinnedVersion)) {
-        continue;
-      }
-      return dep.version;
-    }
+    if (!dep.version) continue;
+    if (getDepName(dep.name) !== pkg) continue;
+    const pin = normalizePin(getDepPinnedVersion(dep.name));
+    if (pin && !matchesPin(pin, updateVersion)) continue;
+    return dep.version;
   }
   return '';
 };

Support code to add once (near normalizePin):

function matchesPin(pin: string, version: string): boolean {
  const v = normalizePin(version);
  if (/^\d+$/.test(pin)) return new RegExp(`^${pin}\\.\\d+\\.\\d+(?:[-+].*)?$`).test(v);
  if (/^\d+\\.\d+$/.test(pin)) return new RegExp(`^${pin}\\.\\d+(?:[-+].*)?$`).test(v);
  if (/^\d+\\.\d+\\.\d+(?:[-+].*)?$/.test(pin)) return new RegExp(`^${pin}(?:$|[-+])`).test(v);
  return true;
}
🤖 Prompt for AI Agents
In cli/commands/available-updates.ts around lines 18 to 29, the pin check
currently uses startsWith() which incorrectly matches pins like "1" to versions
"10.x"; replace that logic with a boundary-aware matcher: add the provided
matchesPin(pin, version) helper (place it once near normalizePin) and use
matchesPin(pinnedVersion, dep.version) instead of
updateVersion.startsWith(pinnedVersion); if matchesPin returns false, skip that
dep (exclude the row). Ensure the helper handles integer, "major.minor", and
full "major.minor.patch" pin formats as described so pins only match proper
version boundaries.


let actor = await mainActor();
let res = await actor.getHighestSemverBatch(depsToUpdate.map((dep) => [dep.name, dep.version || '', {major: null}]));
let res = await actor.getHighestSemverBatch(depsToUpdate.map((dep) => {
let semverPart : SemverPart = {major: null};
let name = getDepName(dep.name);
let pinnedVersion = getDepPinnedVersion(dep.name);
if (pinnedVersion) {
semverPart = pinnedVersion.split('.').length === 1 ? {minor: null} : {patch: null};
}
return [name, dep.version || '', semverPart];
}));

if ('err' in res) {
console.log(chalk.red('Error:'), res.err);
process.exit(1);
}

return res.ok.filter((dep) => dep[1] !== getCurrentVersion(dep[0])).map((dep) => [dep[0], getCurrentVersion(dep[0]), dep[1]]);
return res.ok.filter((dep) => dep[1] !== getCurrentVersion(dep[0], dep[1])).map((dep) => [dep[0], getCurrentVersion(dep[0], dep[1]), dep[1]]);
}
9 changes: 8 additions & 1 deletion cli/commands/outdated.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chalk from 'chalk';
import {checkConfigFile, readConfig} from '../mops.js';
import {getAvailableUpdates} from './available-updates.js';
import {getDepName, getDepPinnedVersion} from '../helpers/get-dep-name.js';

export async function outdated() {
if (!checkConfigFile()) {
Expand All @@ -15,8 +16,14 @@ export async function outdated() {
}
else {
console.log('Available updates:');
let allDeps = [...Object.keys(config.dependencies || {}), ...Object.keys(config['dev-dependencies'] || {})];
for (let dep of available) {
console.log(`${dep[0]} ${chalk.yellow(dep[1])} -> ${chalk.green(dep[2])}`);
let name = allDeps.find((d) => {
let pinnedVersion = getDepPinnedVersion(d);
return getDepName(d) === dep[0] && (!pinnedVersion || dep[1].startsWith(pinnedVersion));
}) || dep[0];

console.log(`${name} ${chalk.yellow(dep[1])} -> ${chalk.green(dep[2])}`);
}
}
}
21 changes: 19 additions & 2 deletions cli/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {checkConfigFile, getGithubCommit, parseGithubURL, readConfig} from '../m
import {add} from './add.js';
import {getAvailableUpdates} from './available-updates.js';
import {checkIntegrity} from '../integrity.js';
import {getDepName, getDepPinnedVersion} from '../helpers/get-dep-name.js';

type UpdateOptions = {
verbose ?: boolean;
Expand Down Expand Up @@ -51,8 +52,24 @@ export async function update(pkg ?: string, {lock} : UpdateOptions = {}) {
}
else {
for (let dep of available) {
let dev = !!config['dev-dependencies']?.[dep[0]];
await add(`${dep[0]}@${dep[2]}`, {dev});
let devDeps = Object.keys(config['dev-dependencies'] || {});
let allDeps = [...Object.keys(config.dependencies || {}), ...devDeps];

let dev = false;
for (let d of devDeps) {
let pinnedVersion = getDepPinnedVersion(d);
if (getDepName(d) === dep[0] && (!pinnedVersion || dep[1].startsWith(pinnedVersion))) {
dev = true;
break;
}
}

let asName = allDeps.find((d) => {
let pinnedVersion = getDepPinnedVersion(d);
return getDepName(d) === dep[0] && (!pinnedVersion || dep[1].startsWith(pinnedVersion));
}) || dep[0];

await add(`${dep[0]}@${dep[2]}`, {dev}, asName);
}
}

Expand Down
4 changes: 4 additions & 0 deletions cli/helpers/get-dep-name.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export function getDepName(name : string) : string {
return name.split('@')[0] || '';
}

export function getDepPinnedVersion(name : string) : string {
return name.split('@')[1] || '';
}