@askrjs/cli 0.0.5 → 0.0.6

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
@@ -1,5 +1,8 @@
1
1
  # @askrjs/cli
2
2
 
3
+ [![CI](https://github.com/askrjs/askr-cli/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/askrjs/askr-cli/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/%40askrjs%2Fcli.svg)](https://www.npmjs.com/package/@askrjs/cli)
5
+
3
6
  Unified CLI for the Askr platform.
4
7
 
5
8
  `@askrjs/cli` bundles project scaffolding and static-site generation commands
@@ -117,15 +120,17 @@ and failed decisions without running an install.
117
120
  askr outdated
118
121
  askr update
119
122
  askr upgrade
123
+ askr upgrade --force
120
124
  askr update vite "@types/*"
121
125
  askr update --workspace "@scope/app" --tag next --json
122
126
  ```
123
127
 
124
- `askr update` writes safe range changes. `askr upgrade` additionally permits next-version
125
- major changes for stable packages and breaking minor changes for `0.x`
126
- packages. Peer requirements from co-dependencies are still enforced: a target
127
- is left for manual review if it would move outside another selected package's
128
- published peer range. `askr upgrade` never bypasses that compatibility guard.
128
+ `askr update` writes safe range changes. `askr upgrade` jointly selects the newest
129
+ peer-compatible published versions up to each package's configured dist-tag,
130
+ including breaking changes. Required peers must be present; optional peers may
131
+ be absent. `askr upgrade --force` writes the dist-tag targets directly and skips
132
+ peer checks. Positional selection is strict: unselected dependencies constrain
133
+ the solution but are never rewritten.
129
134
 
130
135
  The updater preserves exact, caret, tilde, and x-range styles. It can widen one
131
136
  bounded interval, and it updates only the highest clause of a simple OR union.
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { load } from "js-yaml";
6
6
  import { minimatch } from "minimatch";
7
+ import semver from "semver";
7
8
  //#region src/update/types.ts
8
9
  const DEPENDENCY_SECTIONS = [
9
10
  "dependencies",
@@ -307,6 +308,7 @@ async function discoverProject(options) {
307
308
  const selectedWorkspaces = options.workspacePatterns.length === 0 ? workspaces : workspaces.filter((workspace) => matchesAny(workspace.name, options.workspacePatterns));
308
309
  if (selectedWorkspaces.length === 0) throw new Error("No discovered workspace matches the requested --workspace filters.");
309
310
  const localNames = new Set(workspaces.map((workspace) => workspace.name));
311
+ const localVersions = new Map(workspaces.flatMap((workspace) => typeof workspace.manifest.version === "string" && semver.valid(workspace.manifest.version) ? [[workspace.name, workspace.manifest.version]] : []));
310
312
  return {
311
313
  root,
312
314
  workspaces,
@@ -316,7 +318,8 @@ async function discoverProject(options) {
316
318
  contextOccurrences: collectOccurrences(root, selectedWorkspaces, localNames, {
317
319
  ignore: [],
318
320
  tags: policy.tags
319
- }, [])
321
+ }, []),
322
+ localVersions
320
323
  };
321
324
  }
322
325
  //#endregion
@@ -0,0 +1,326 @@
1
+ import { n as isBreakingChange, r as rewriteRange, t as analyzeRange } from "./range-YUs9eimn.js";
2
+ import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
3
+ import semver from "semver";
4
+ //#region src/update/planner.ts
5
+ const STATUS_PRIORITY = {
6
+ current: 0,
7
+ local: 1,
8
+ safe: 2,
9
+ breaking: 3,
10
+ manual: 4,
11
+ error: 5
12
+ };
13
+ const publishedVersions = (packument) => Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null).sort(semver.compare);
14
+ function selectedTarget(packument, tag) {
15
+ const value = packument["dist-tags"]?.[tag];
16
+ return typeof value === "string" && semver.valid(value) ? value : null;
17
+ }
18
+ function resolveSpecificationVersion(specification, packument) {
19
+ const parsed = parseDependencySpecification(specification);
20
+ if (parsed.type === "tag") {
21
+ const value = packument["dist-tags"]?.[parsed.rawSpec];
22
+ return typeof value === "string" && semver.valid(value) ? value : null;
23
+ }
24
+ if (parsed.type === "version") return semver.valid(parsed.rawSpec);
25
+ if (parsed.type === "range") return semver.maxSatisfying(publishedVersions(packument), parsed.rawSpec);
26
+ return null;
27
+ }
28
+ function metadata(packument, version) {
29
+ const value = packument.versions?.[version];
30
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
31
+ }
32
+ function baseOccurrence(occurrence) {
33
+ return {
34
+ workspace: occurrence.workspace,
35
+ manifestPath: occurrence.manifestPath,
36
+ relativeManifestPath: occurrence.relativeManifestPath,
37
+ section: occurrence.section,
38
+ currentSpecification: occurrence.currentSpecification,
39
+ proposedSpecification: null,
40
+ allowedVersion: null,
41
+ selectedVersion: null
42
+ };
43
+ }
44
+ function planOne(occurrence, packument, failure, tag, mode, chosen, blocker) {
45
+ const base = baseOccurrence(occurrence);
46
+ if (occurrence.kind === "local") return {
47
+ targetVersion: null,
48
+ occurrence: {
49
+ ...base,
50
+ status: "local",
51
+ reason: occurrence.reason
52
+ }
53
+ };
54
+ if (occurrence.kind === "manual") return {
55
+ targetVersion: null,
56
+ occurrence: {
57
+ ...base,
58
+ status: "manual",
59
+ reason: occurrence.reason
60
+ }
61
+ };
62
+ if (failure) return {
63
+ targetVersion: null,
64
+ occurrence: {
65
+ ...base,
66
+ status: "error",
67
+ reason: failure
68
+ }
69
+ };
70
+ if (!packument) return {
71
+ targetVersion: null,
72
+ occurrence: {
73
+ ...base,
74
+ status: "error",
75
+ reason: "package metadata is unavailable"
76
+ }
77
+ };
78
+ const target = selectedTarget(packument, tag);
79
+ if (!target) return {
80
+ targetVersion: null,
81
+ occurrence: {
82
+ ...base,
83
+ status: "error",
84
+ reason: `dist-tag '${tag}' is not published`
85
+ }
86
+ };
87
+ if (!publishedVersions(packument).includes(target)) return {
88
+ targetVersion: null,
89
+ occurrence: {
90
+ ...base,
91
+ status: "error",
92
+ reason: `dist-tag '${tag}' does not identify a published version`
93
+ }
94
+ };
95
+ if (occurrence.kind === "current") return {
96
+ targetVersion: target,
97
+ occurrence: {
98
+ ...base,
99
+ status: "current",
100
+ reason: occurrence.reason
101
+ }
102
+ };
103
+ const allowed = resolveSpecificationVersion(occurrence.currentSpecification, packument);
104
+ if (!allowed) return {
105
+ targetVersion: target,
106
+ occurrence: {
107
+ ...base,
108
+ status: "manual",
109
+ reason: "no published version satisfies the current specification"
110
+ }
111
+ };
112
+ const selected = chosen ?? target;
113
+ const withVersions = {
114
+ ...base,
115
+ allowedVersion: allowed,
116
+ selectedVersion: selected
117
+ };
118
+ if (blocker) return {
119
+ targetVersion: target,
120
+ occurrence: {
121
+ ...withVersions,
122
+ status: "manual",
123
+ reason: blocker
124
+ }
125
+ };
126
+ if (semver.satisfies(selected, occurrence.currentSpecification) || !semver.gt(selected, allowed)) return {
127
+ targetVersion: target,
128
+ occurrence: {
129
+ ...withVersions,
130
+ status: "current",
131
+ reason: blocker ?? "selected version is already covered by the current specification"
132
+ }
133
+ };
134
+ const analysis = analyzeRange(occurrence.currentSpecification);
135
+ if (!analysis.shape) return {
136
+ targetVersion: target,
137
+ occurrence: {
138
+ ...withVersions,
139
+ status: "manual",
140
+ reason: analysis.reason
141
+ }
142
+ };
143
+ const breaking = isBreakingChange(allowed, selected);
144
+ if (mode === "update" && breaking) return {
145
+ targetVersion: target,
146
+ occurrence: {
147
+ ...withVersions,
148
+ status: "breaking",
149
+ reason: "breaking update is available via askr upgrade"
150
+ }
151
+ };
152
+ return {
153
+ targetVersion: target,
154
+ occurrence: {
155
+ ...withVersions,
156
+ proposedSpecification: rewriteRange(analysis.shape, selected, breaking),
157
+ status: breaking ? "breaking" : "safe",
158
+ reason: selected === target ? "selected tag target is eligible" : `compatible version ${selected} selected below ${tag}@${target}`
159
+ }
160
+ };
161
+ }
162
+ function optionalPeer(meta, peer) {
163
+ const value = meta.peerDependenciesMeta?.[peer];
164
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && value.optional === true);
165
+ }
166
+ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tags, cliTag, localVersions, mode) {
167
+ const selectedNames = new Set(selectedOccurrences.filter((entry) => entry.kind === "fetch").map((entry) => entry.package));
168
+ const byName = /* @__PURE__ */ new Map();
169
+ for (const item of context.filter((entry) => entry.workspace === workspace)) if (!byName.has(item.package)) byName.set(item.package, item);
170
+ const states = /* @__PURE__ */ new Map();
171
+ for (const [name, occurrence] of byName) {
172
+ const packument = packuments.get(name);
173
+ const current = packument ? resolveSpecificationVersion(occurrence.currentSpecification, packument) : localVersions.get(name) ?? null;
174
+ let candidates = current ? [current] : [];
175
+ if (selectedNames.has(name) && packument && current) {
176
+ const target = selectedTarget(packument, cliTag ?? tags[name] ?? "latest");
177
+ if (target) candidates = publishedVersions(packument).filter((version) => semver.gte(version, current) && semver.lte(version, target) && (mode === "upgrade" || !isBreakingChange(current, version))).sort(semver.rcompare);
178
+ }
179
+ states.set(name, {
180
+ occurrence,
181
+ selected: selectedNames.has(name),
182
+ current,
183
+ candidates
184
+ });
185
+ }
186
+ const variables = [...states.entries()].filter(([, state]) => state.selected && state.candidates.length > 0).sort(([a], [b]) => a.localeCompare(b));
187
+ const fixed = new Map([...states].flatMap(([name, state]) => state.selected ? [] : state.current ? [[name, state.current]] : []));
188
+ let best = null;
189
+ let bestChanged = -1;
190
+ let firstFailure = "no jointly peer-compatible published version set exists";
191
+ const validate = (choices) => {
192
+ const installed = new Map([...fixed, ...choices]);
193
+ for (const [name, version] of installed) {
194
+ const packument = packuments.get(name);
195
+ if (!packument) continue;
196
+ const meta = metadata(packument, version);
197
+ if (!meta) continue;
198
+ for (const [peer, requirement] of Object.entries(meta.peerDependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
199
+ if (typeof requirement !== "string") continue;
200
+ const providerChanged = states.get(name)?.current !== version;
201
+ const peerChanged = states.get(peer)?.selected && states.get(peer)?.current !== installed.get(peer);
202
+ if (!providerChanged && !peerChanged) continue;
203
+ const peerVersion = installed.get(peer) ?? localVersions.get(peer);
204
+ if (!peerVersion) {
205
+ if (optionalPeer(meta, peer)) continue;
206
+ return `${name}@${version} requires missing peer ${peer}@${requirement}`;
207
+ }
208
+ if (!semver.satisfies(peerVersion, requirement, { includePrerelease: true })) return `${name}@${version} requires ${peer}@${requirement}`;
209
+ }
210
+ }
211
+ return null;
212
+ };
213
+ const visit = (index, choices) => {
214
+ if (index === variables.length) {
215
+ const failure = validate(choices);
216
+ if (failure) {
217
+ firstFailure = failure;
218
+ return;
219
+ }
220
+ const changed = variables.filter(([name, state]) => choices.get(name) !== state.current).length;
221
+ if (changed > bestChanged) {
222
+ best = new Map(choices);
223
+ bestChanged = changed;
224
+ }
225
+ return;
226
+ }
227
+ const [name, state] = variables[index];
228
+ for (const version of state.candidates) {
229
+ choices.set(name, version);
230
+ visit(index + 1, choices);
231
+ }
232
+ };
233
+ visit(0, /* @__PURE__ */ new Map());
234
+ const blockers = /* @__PURE__ */ new Map();
235
+ if (!best) for (const [name] of variables) blockers.set(name, firstFailure);
236
+ const choices = best ?? new Map(variables.flatMap(([name, state]) => state.current ? [[name, state.current]] : []));
237
+ for (const [name, state] of variables) {
238
+ if (choices.get(name) !== state.current || state.candidates[0] === state.current) continue;
239
+ const attempted = new Map(choices).set(name, state.candidates[0]);
240
+ blockers.set(name, validate(attempted) ?? "no jointly peer-compatible update advances this dependency");
241
+ }
242
+ return {
243
+ choices,
244
+ blockers
245
+ };
246
+ }
247
+ function aggregateStatus(occurrences) {
248
+ return occurrences.reduce((status, occurrence) => STATUS_PRIORITY[occurrence.status] > STATUS_PRIORITY[status] ? occurrence.status : status, "current");
249
+ }
250
+ function summarize(decisions) {
251
+ const summary = {
252
+ packages: decisions.length,
253
+ occurrences: 0,
254
+ changedOccurrences: 0,
255
+ current: 0,
256
+ safe: 0,
257
+ breaking: 0,
258
+ local: 0,
259
+ manual: 0,
260
+ error: 0
261
+ };
262
+ for (const decision of decisions) {
263
+ decision.status = aggregateStatus(decision.occurrences);
264
+ decision.reason = decision.occurrences.find((entry) => entry.status === decision.status)?.reason ?? "";
265
+ summary[decision.status] += 1;
266
+ summary.occurrences += decision.occurrences.length;
267
+ summary.changedOccurrences += decision.occurrences.filter((entry) => entry.proposedSpecification).length;
268
+ }
269
+ return summary;
270
+ }
271
+ function planUpdates(options) {
272
+ const failures = options.failures ?? /* @__PURE__ */ new Map();
273
+ const tags = options.tags ?? {};
274
+ const mode = options.mode ?? (options.force ? "upgrade" : "update");
275
+ const context = options.contextOccurrences ?? options.occurrences;
276
+ const workspaceSolutions = /* @__PURE__ */ new Map();
277
+ if (mode !== "force") for (const workspace of new Set(options.occurrences.map((entry) => entry.workspace))) workspaceSolutions.set(workspace, solveWorkspace(workspace, options.occurrences.filter((entry) => entry.workspace === workspace), context, options.packuments, tags, options.cliTag, options.localVersions ?? /* @__PURE__ */ new Map(), mode));
278
+ const grouped = /* @__PURE__ */ new Map();
279
+ for (const occurrence of options.occurrences) grouped.set(occurrence.package, [...grouped.get(occurrence.package) ?? [], occurrence]);
280
+ const decisions = [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([packageName, occurrences]) => {
281
+ const selectedTag = options.cliTag ?? tags[packageName] ?? "latest";
282
+ const planned = occurrences.map((occurrence) => {
283
+ const solution = workspaceSolutions.get(occurrence.workspace);
284
+ const blocker = solution?.blockers.get(packageName);
285
+ const chosen = mode === "update" && !blocker ? void 0 : solution?.choices.get(packageName);
286
+ return planOne(occurrence, options.packuments.get(packageName), failures.get(packageName), selectedTag, mode, chosen, blocker);
287
+ });
288
+ const targetVersion = planned.find((entry) => entry.targetVersion)?.targetVersion ?? null;
289
+ const plannedOccurrences = planned.map((entry) => entry.occurrence);
290
+ const status = aggregateStatus(plannedOccurrences);
291
+ return {
292
+ package: packageName,
293
+ selectedTag,
294
+ targetVersion,
295
+ status,
296
+ reason: plannedOccurrences.find((entry) => entry.status === status)?.reason ?? "",
297
+ occurrences: plannedOccurrences
298
+ };
299
+ });
300
+ const selectedNames = new Set(grouped.keys());
301
+ for (const [packageName, failure] of [...failures].sort(([a], [b]) => a.localeCompare(b))) {
302
+ if (selectedNames.has(packageName)) continue;
303
+ const occurrences = context.filter((entry) => entry.package === packageName).map((entry) => ({
304
+ ...baseOccurrence(entry),
305
+ status: "error",
306
+ reason: `peer compatibility lookup failed: ${failure}`
307
+ }));
308
+ decisions.push({
309
+ package: packageName,
310
+ selectedTag: options.cliTag ?? tags[packageName] ?? "latest",
311
+ targetVersion: null,
312
+ status: "error",
313
+ reason: `peer compatibility lookup failed: ${failure}`,
314
+ occurrences
315
+ });
316
+ }
317
+ decisions.sort((a, b) => a.package.localeCompare(b.package));
318
+ const summary = summarize(decisions);
319
+ return {
320
+ decisions,
321
+ summary,
322
+ hasErrors: summary.error > 0
323
+ };
324
+ }
325
+ //#endregion
326
+ export { planUpdates };
@@ -130,6 +130,23 @@ function selectedVersions(packument, specifications) {
130
130
  }
131
131
  return [...selected].sort(semver.compare);
132
132
  }
133
+ function candidateVersions(packument, specifications) {
134
+ const versions = Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null);
135
+ const selected = selectedVersions(packument, specifications);
136
+ const lowerBounds = specifications.flatMap((specification) => {
137
+ const parsed = parseDependencySpecification(specification);
138
+ if (parsed.type === "version") return semver.valid(parsed.rawSpec) ? [parsed.rawSpec] : [];
139
+ if (parsed.type === "range") {
140
+ const current = semver.maxSatisfying(versions, parsed.rawSpec);
141
+ return current ? [current] : [];
142
+ }
143
+ return [];
144
+ });
145
+ if (lowerBounds.length === 0 || selected.length === 0) return selected;
146
+ const floor = lowerBounds.sort(semver.compare)[0];
147
+ const ceiling = selected.sort(semver.compare)[selected.length - 1];
148
+ return versions.filter((version) => semver.gte(version, floor) && semver.lte(version, ceiling));
149
+ }
133
150
  function mergeVersionMetadata(packument, value, selected) {
134
151
  const entries = Array.isArray(value) ? value : [value];
135
152
  const merged = /* @__PURE__ */ new Set();
@@ -142,6 +159,7 @@ function mergeVersionMetadata(packument, value, selected) {
142
159
  const metadata = packument.versions?.[entry.version];
143
160
  if (!isRecord(metadata)) continue;
144
161
  if (isRecord(entry.peerDependencies)) metadata.peerDependencies = entry.peerDependencies;
162
+ if (isRecord(entry.peerDependenciesMeta)) metadata.peerDependenciesMeta = entry.peerDependenciesMeta;
145
163
  merged.add(entry.version);
146
164
  }
147
165
  return selected.every((version) => merged.has(version));
@@ -154,13 +172,14 @@ async function executeNpmView(packageName, configuration, specifications) {
154
172
  "dist-tags"
155
173
  ], configuration, true));
156
174
  if (!packument) return null;
157
- const versions = selectedVersions(packument, specifications);
175
+ const versions = candidateVersions(packument, specifications);
158
176
  if (versions.length === 0) return packument;
159
177
  return mergeVersionMetadata(packument, await executeNpmJson([
160
178
  "view",
161
179
  `${packageName}@${versions.join(" || ")}`,
162
180
  "version",
163
- "peerDependencies"
181
+ "peerDependencies",
182
+ "peerDependenciesMeta"
164
183
  ], configuration, false), versions) ? packument : null;
165
184
  }
166
185
  function isPackument(value) {
package/dist/update.js CHANGED
@@ -14,6 +14,7 @@ function helpText(command) {
14
14
  " --workspace <glob> Select workspace names (repeatable)",
15
15
  " --tag <tag> Use one registry dist-tag for selected packages",
16
16
  " --json Emit one deterministic JSON object on stdout",
17
+ ...command === "upgrade" ? [" --force, -f Use tag targets without peer compatibility checks"] : [],
17
18
  " --help, -h Show this help message",
18
19
  "",
19
20
  "Notes:",
@@ -29,11 +30,12 @@ function optionValue(args, index, option, errors) {
29
30
  }
30
31
  return args[index + 1];
31
32
  }
32
- function parseArgs(args) {
33
+ function parseArgs(command, args) {
33
34
  const parsed = {
34
35
  cwd: process.cwd(),
35
36
  help: false,
36
37
  json: false,
38
+ force: false,
37
39
  packagePatterns: [],
38
40
  workspacePatterns: [],
39
41
  errors: []
@@ -45,6 +47,8 @@ function parseArgs(args) {
45
47
  else if (argument === "--") positionalOnly = true;
46
48
  else if (argument === "--help" || argument === "-h") parsed.help = true;
47
49
  else if (argument === "--json") parsed.json = true;
50
+ else if (argument === "--force" || argument === "-f") if (command === "upgrade") parsed.force = true;
51
+ else parsed.errors.push(`${argument} is only supported by askr upgrade`);
48
52
  else if (argument === "--cwd") {
49
53
  const value = optionValue(args, index, "--cwd", parsed.errors);
50
54
  if (value !== null) {
@@ -101,6 +105,7 @@ function serializableDecision(decision) {
101
105
  currentSpecification: occurrence.currentSpecification,
102
106
  proposedSpecification: occurrence.proposedSpecification,
103
107
  allowedVersion: occurrence.allowedVersion,
108
+ selectedVersion: occurrence.selectedVersion,
104
109
  status: occurrence.status,
105
110
  reason: occurrence.reason
106
111
  }))
@@ -132,6 +137,7 @@ function renderTable(rows) {
132
137
  const headings = [
133
138
  "Package",
134
139
  "Allowed",
140
+ "Chosen",
135
141
  "Latest",
136
142
  "Status",
137
143
  "Range"
@@ -147,6 +153,7 @@ function emitHuman(io, decisions, summary, applied, command) {
147
153
  const row = [
148
154
  decision.package,
149
155
  occurrence.allowedVersion ?? "-",
156
+ occurrence.selectedVersion ?? "-",
150
157
  decision.targetVersion ?? "-",
151
158
  occurrence.status,
152
159
  rangeText(occurrence, command)
@@ -186,11 +193,11 @@ function collectEdits(decisions) {
186
193
  }] : []));
187
194
  }
188
195
  async function defaultRegistry(root, packageNames, requirements) {
189
- const { fetchPackuments, loadNpmConfiguration } = await import("./registry-CyrRHRG5.js");
196
+ const { fetchPackuments, loadNpmConfiguration } = await import("./registry-D2APi6x0.js");
190
197
  return fetchPackuments(packageNames, await loadNpmConfiguration(root), { requirements });
191
198
  }
192
199
  async function runDependencyCli(command, args, io = console, runtime = {}) {
193
- const parsed = parseArgs(args);
200
+ const parsed = parseArgs(command, args);
194
201
  if (parsed.help && parsed.errors.length === 0) {
195
202
  io.log(helpText(command));
196
203
  return 0;
@@ -210,7 +217,7 @@ async function runDependencyCli(command, args, io = console, runtime = {}) {
210
217
  let root = null;
211
218
  let selectedWorkspaces = [];
212
219
  try {
213
- const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-Difb7Y4G.js"), import("./planner-VAj7qlxr.js")]);
220
+ const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-Djq8TxJu.js"), import("./planner-ZAX9SFjY.js")]);
214
221
  const project = await discoverProject({
215
222
  cwd: parsed.cwd,
216
223
  packagePatterns: parsed.packagePatterns,
@@ -237,7 +244,8 @@ async function runDependencyCli(command, args, io = console, runtime = {}) {
237
244
  failures: results.failures,
238
245
  tags: project.policy.tags,
239
246
  cliTag: parsed.tag,
240
- force: command === "upgrade"
247
+ mode: command === "upgrade" ? parsed.force ? "force" : "upgrade" : "update",
248
+ localVersions: project.localVersions
241
249
  });
242
250
  let applied = 0;
243
251
  const errors = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Unified CLI for the Askr platform",
5
5
  "homepage": "https://github.com/askrjs/askr-cli#readme",
6
6
  "bugs": {
@@ -1,285 +0,0 @@
1
- import { n as isBreakingChange, r as rewriteRange, t as analyzeRange } from "./range-YUs9eimn.js";
2
- import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
3
- import semver from "semver";
4
- //#region src/update/planner.ts
5
- const STATUS_PRIORITY = {
6
- current: 0,
7
- local: 1,
8
- safe: 2,
9
- breaking: 3,
10
- manual: 4,
11
- error: 5
12
- };
13
- function publishedVersions(packument) {
14
- return Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null).sort(semver.compare);
15
- }
16
- function selectedTarget(packument, tag) {
17
- const value = packument["dist-tags"]?.[tag];
18
- return typeof value === "string" && semver.valid(value) ? value : null;
19
- }
20
- function plannedOccurrence(occurrence, packument, failure, selectedTag, force) {
21
- const base = {
22
- workspace: occurrence.workspace,
23
- manifestPath: occurrence.manifestPath,
24
- relativeManifestPath: occurrence.relativeManifestPath,
25
- section: occurrence.section,
26
- currentSpecification: occurrence.currentSpecification,
27
- proposedSpecification: null,
28
- allowedVersion: null
29
- };
30
- if (occurrence.kind === "local") return {
31
- targetVersion: null,
32
- occurrence: {
33
- ...base,
34
- status: "local",
35
- reason: occurrence.reason
36
- }
37
- };
38
- if (occurrence.kind === "manual") return {
39
- targetVersion: null,
40
- occurrence: {
41
- ...base,
42
- status: "manual",
43
- reason: occurrence.reason
44
- }
45
- };
46
- if (failure) return {
47
- targetVersion: null,
48
- occurrence: {
49
- ...base,
50
- status: "error",
51
- reason: failure
52
- }
53
- };
54
- if (occurrence.kind === "current") return {
55
- targetVersion: packument ? selectedTarget(packument, selectedTag) : null,
56
- occurrence: {
57
- ...base,
58
- status: "current",
59
- reason: occurrence.reason
60
- }
61
- };
62
- if (!packument) return {
63
- targetVersion: null,
64
- occurrence: {
65
- ...base,
66
- status: "error",
67
- reason: "package metadata is unavailable"
68
- }
69
- };
70
- const targetVersion = selectedTarget(packument, selectedTag);
71
- if (!targetVersion) return {
72
- targetVersion: null,
73
- occurrence: {
74
- ...base,
75
- status: "error",
76
- reason: `dist-tag '${selectedTag}' is not published`
77
- }
78
- };
79
- const versions = publishedVersions(packument);
80
- if (!versions.includes(targetVersion)) return {
81
- targetVersion: null,
82
- occurrence: {
83
- ...base,
84
- status: "error",
85
- reason: `dist-tag '${selectedTag}' does not identify a published version`
86
- }
87
- };
88
- const allowedVersion = semver.maxSatisfying(versions, occurrence.currentSpecification);
89
- if (!allowedVersion) return {
90
- targetVersion,
91
- occurrence: {
92
- ...base,
93
- status: "manual",
94
- reason: "no published version satisfies the current specification"
95
- }
96
- };
97
- if (semver.satisfies(targetVersion, occurrence.currentSpecification) || !semver.gt(targetVersion, allowedVersion)) return {
98
- targetVersion,
99
- occurrence: {
100
- ...base,
101
- allowedVersion,
102
- status: "current",
103
- reason: "selected target is already covered by the current specification"
104
- }
105
- };
106
- const analysis = analyzeRange(occurrence.currentSpecification);
107
- if (!analysis.shape) return {
108
- targetVersion,
109
- occurrence: {
110
- ...base,
111
- allowedVersion,
112
- status: "manual",
113
- reason: analysis.reason
114
- }
115
- };
116
- const breaking = isBreakingChange(allowedVersion, targetVersion);
117
- const eligible = !breaking || force;
118
- return {
119
- targetVersion,
120
- occurrence: {
121
- ...base,
122
- allowedVersion,
123
- proposedSpecification: eligible ? rewriteRange(analysis.shape, targetVersion, breaking) : null,
124
- status: breaking ? "breaking" : "safe",
125
- reason: breaking ? eligible ? "latest version is eligible for askr upgrade" : "breaking update is available via askr upgrade" : "compatible update is available"
126
- }
127
- };
128
- }
129
- function occurrenceKey(occurrence, packageName) {
130
- return [
131
- occurrence.manifestPath,
132
- occurrence.section,
133
- packageName ?? occurrence.package,
134
- occurrence.currentSpecification
135
- ].join("\0");
136
- }
137
- function resolveSpecificationVersion(specification, packument) {
138
- const versions = publishedVersions(packument);
139
- const parsed = parseDependencySpecification(specification);
140
- if (parsed.type === "tag") {
141
- const tagged = packument["dist-tags"]?.[parsed.rawSpec];
142
- return typeof tagged === "string" && semver.valid(tagged) ? tagged : null;
143
- }
144
- if (parsed.type === "version") return semver.valid(parsed.rawSpec);
145
- if (parsed.type === "range") return semver.maxSatisfying(versions, parsed.rawSpec);
146
- return null;
147
- }
148
- function versionMetadata(packument, version) {
149
- const metadata = packument.versions?.[version];
150
- return metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : null;
151
- }
152
- function applyPeerCompatibilityGuard(decisions, contextOccurrences, packuments) {
153
- const plannedByKey = /* @__PURE__ */ new Map();
154
- for (const decision of decisions) for (const occurrence of decision.occurrences) plannedByKey.set(occurrenceKey(occurrence, decision.package), occurrence);
155
- const contextByWorkspace = /* @__PURE__ */ new Map();
156
- for (const occurrence of contextOccurrences) {
157
- const entries = contextByWorkspace.get(occurrence.workspace) ?? [];
158
- entries.push(occurrence);
159
- contextByWorkspace.set(occurrence.workspace, entries);
160
- }
161
- for (const [workspace, context] of contextByWorkspace) {
162
- const states = context.flatMap((dependency) => {
163
- if (!dependency.registryManaged) return [];
164
- const packument = packuments.get(dependency.package);
165
- if (!packument) return [];
166
- const planned = plannedByKey.get(occurrenceKey(dependency));
167
- const currentVersion = resolveSpecificationVersion(dependency.currentSpecification, packument);
168
- const futureVersion = resolveSpecificationVersion(planned?.proposedSpecification ?? dependency.currentSpecification, packument);
169
- return currentVersion && futureVersion ? [{
170
- dependency,
171
- packument,
172
- planned,
173
- currentVersion,
174
- futureVersion,
175
- changed: Boolean(planned?.proposedSpecification)
176
- }] : [];
177
- });
178
- const blockers = /* @__PURE__ */ new Map();
179
- for (const provider of states) {
180
- const currentPeers = versionMetadata(provider.packument, provider.currentVersion)?.peerDependencies ?? {};
181
- const futurePeers = versionMetadata(provider.packument, provider.futureVersion)?.peerDependencies ?? {};
182
- const peerNames = /* @__PURE__ */ new Set([...Object.keys(currentPeers), ...Object.keys(futurePeers)]);
183
- for (const peerName of peerNames) {
184
- const currentRequirement = currentPeers[peerName];
185
- const futureRequirement = futurePeers[peerName];
186
- if (futureRequirement !== void 0 && typeof futureRequirement !== "string") continue;
187
- for (const peer of states.filter((state) => state.dependency.package === peerName)) {
188
- const currentAccepted = typeof currentRequirement !== "string" || semver.satisfies(peer.currentVersion, currentRequirement);
189
- const futureAccepted = typeof futureRequirement !== "string" || semver.satisfies(peer.futureVersion, futureRequirement);
190
- if (!currentAccepted || futureAccepted) continue;
191
- const reason = `${provider.dependency.package}@${provider.futureVersion} requires ${peerName}@${futureRequirement}`;
192
- if (provider.changed && provider.planned) blockers.set(provider.planned, reason);
193
- if (peer.changed && peer.planned) blockers.set(peer.planned, reason);
194
- }
195
- }
196
- }
197
- for (const [planned, reason] of blockers) {
198
- planned.status = "manual";
199
- planned.proposedSpecification = null;
200
- planned.reason = reason;
201
- }
202
- }
203
- }
204
- function aggregateStatus(occurrences) {
205
- return occurrences.reduce((status, occurrence) => STATUS_PRIORITY[occurrence.status] > STATUS_PRIORITY[status] ? occurrence.status : status, "current");
206
- }
207
- function summarize(decisions) {
208
- const summary = {
209
- packages: decisions.length,
210
- occurrences: 0,
211
- changedOccurrences: 0,
212
- current: 0,
213
- safe: 0,
214
- breaking: 0,
215
- local: 0,
216
- manual: 0,
217
- error: 0
218
- };
219
- for (const decision of decisions) {
220
- decision.status = aggregateStatus(decision.occurrences);
221
- decision.reason = decision.occurrences.find((entry) => entry.status === decision.status)?.reason ?? "";
222
- summary[decision.status] += 1;
223
- summary.occurrences += decision.occurrences.length;
224
- summary.changedOccurrences += decision.occurrences.filter((entry) => entry.proposedSpecification).length;
225
- }
226
- return summary;
227
- }
228
- function planUpdates(options) {
229
- const failures = options.failures ?? /* @__PURE__ */ new Map();
230
- const tags = options.tags ?? {};
231
- const grouped = /* @__PURE__ */ new Map();
232
- for (const occurrence of options.occurrences) {
233
- const entries = grouped.get(occurrence.package) ?? [];
234
- entries.push(occurrence);
235
- grouped.set(occurrence.package, entries);
236
- }
237
- const decisions = [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([packageName, occurrences]) => {
238
- const selectedTag = options.cliTag ?? tags[packageName] ?? "latest";
239
- const planned = occurrences.map((occurrence) => plannedOccurrence(occurrence, options.packuments.get(packageName), failures.get(packageName), selectedTag, options.force ?? false));
240
- const targetVersion = planned.find((entry) => entry.targetVersion)?.targetVersion ?? null;
241
- const plannedOccurrences = planned.map((entry) => entry.occurrence);
242
- const status = aggregateStatus(plannedOccurrences);
243
- return {
244
- package: packageName,
245
- selectedTag,
246
- targetVersion,
247
- status,
248
- reason: plannedOccurrences.find((entry) => entry.status === status)?.reason ?? "",
249
- occurrences: plannedOccurrences
250
- };
251
- });
252
- const selectedNames = new Set(grouped.keys());
253
- for (const [packageName, failure] of [...failures].sort(([left], [right]) => left.localeCompare(right))) {
254
- if (selectedNames.has(packageName)) continue;
255
- const context = (options.contextOccurrences ?? []).filter((occurrence) => occurrence.package === packageName).map((occurrence) => ({
256
- workspace: occurrence.workspace,
257
- manifestPath: occurrence.manifestPath,
258
- relativeManifestPath: occurrence.relativeManifestPath,
259
- section: occurrence.section,
260
- currentSpecification: occurrence.currentSpecification,
261
- proposedSpecification: null,
262
- allowedVersion: null,
263
- status: "error",
264
- reason: `peer compatibility lookup failed: ${failure}`
265
- }));
266
- decisions.push({
267
- package: packageName,
268
- selectedTag: options.cliTag ?? tags[packageName] ?? "latest",
269
- targetVersion: null,
270
- status: "error",
271
- reason: `peer compatibility lookup failed: ${failure}`,
272
- occurrences: context
273
- });
274
- }
275
- decisions.sort((left, right) => left.package.localeCompare(right.package));
276
- applyPeerCompatibilityGuard(decisions, options.contextOccurrences ?? options.occurrences, options.packuments);
277
- const summary = summarize(decisions);
278
- return {
279
- decisions,
280
- summary,
281
- hasErrors: summary.error > 0
282
- };
283
- }
284
- //#endregion
285
- export { planUpdates };