@jskit-ai/jskit-cli 0.2.134 → 0.2.135

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.134",
3
+ "version": "0.2.135",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,7 +20,7 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.129",
23
+ "@jskit-ai/jskit-catalog": "0.1.130",
24
24
  "@jskit-ai/kernel": "0.1.120",
25
25
  "@jskit-ai/shell-web": "0.1.119",
26
26
  "@vue/compiler-sfc": "^3.5.29",
@@ -75,7 +75,7 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
75
75
  }),
76
76
  "update-packages": Object.freeze({
77
77
  name: "update-packages",
78
- summary: "Update installed @jskit-ai dependencies and refresh managed migrations.",
78
+ summary: "Update @jskit-ai dependencies across the app and its npm workspaces.",
79
79
  usage: "jskit app update-packages [--registry <url>] [--dry-run]",
80
80
  options: Object.freeze([
81
81
  Object.freeze({
@@ -88,9 +88,9 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
88
88
  })
89
89
  ]),
90
90
  defaults: Object.freeze([
91
- "Runtime and dev @jskit-ai dependencies are updated separately so package.json sections stay correct.",
92
- "Each package is moved to the latest available major.x range and npm saves the resolved exact version.",
93
- "Managed migrations and the composed CI workflow are refreshed afterwards unless --dry-run is used."
91
+ "Root runtime, development, optional, and peer dependencies are installed at their exact latest registry versions.",
92
+ "JSKIT ranges in npm workspace manifests and package descriptors are aligned with the latest major release, then workspace resolutions are refreshed in the lockfile.",
93
+ "The command reports elapsed progress and refreshes managed migrations and the composed CI workflow unless --dry-run is used."
94
94
  ])
95
95
  }),
96
96
  "sync-ci": Object.freeze({
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { access, mkdir, readFile, readdir, rm, symlink } from "node:fs/promises";
4
4
 
@@ -80,6 +80,94 @@ function runExternalCommand(
80
80
  });
81
81
  }
82
82
 
83
+ function runExternalCommandAsync(
84
+ command,
85
+ args = [],
86
+ {
87
+ cwd = "",
88
+ env = {},
89
+ stdout,
90
+ stderr,
91
+ quiet = false,
92
+ createCliError
93
+ } = {}
94
+ ) {
95
+ return new Promise((resolve, reject) => {
96
+ let capturedStdout = "";
97
+ let capturedStderr = "";
98
+ let settled = false;
99
+ let child;
100
+
101
+ const finish = (result) => {
102
+ if (settled) {
103
+ return;
104
+ }
105
+ settled = true;
106
+ try {
107
+ resolve(ensureCommandSucceeded(result, command, {
108
+ createCliError,
109
+ cwd,
110
+ stdout,
111
+ stderr,
112
+ quiet: true
113
+ }));
114
+ } catch (error) {
115
+ reject(error);
116
+ }
117
+ };
118
+
119
+ try {
120
+ child = spawn(command, Array.isArray(args) ? args : [], {
121
+ cwd: cwd || process.cwd(),
122
+ env: {
123
+ ...process.env,
124
+ ...env
125
+ },
126
+ shell: process.platform === "win32"
127
+ });
128
+ } catch (error) {
129
+ finish({
130
+ error,
131
+ status: null,
132
+ stdout: capturedStdout,
133
+ stderr: capturedStderr
134
+ });
135
+ return;
136
+ }
137
+
138
+ child.stdout?.setEncoding("utf8");
139
+ child.stderr?.setEncoding("utf8");
140
+ child.stdout?.on("data", (chunk) => {
141
+ capturedStdout += chunk;
142
+ if (!quiet) {
143
+ stdout?.write(chunk);
144
+ }
145
+ });
146
+ child.stderr?.on("data", (chunk) => {
147
+ capturedStderr += chunk;
148
+ if (!quiet) {
149
+ stderr?.write(chunk);
150
+ }
151
+ });
152
+ child.once("error", (error) => {
153
+ finish({
154
+ error,
155
+ status: null,
156
+ stdout: capturedStdout,
157
+ stderr: capturedStderr
158
+ });
159
+ });
160
+ child.once("close", (status, signal) => {
161
+ finish({
162
+ error: signal ? new Error(`${command} terminated by signal ${signal}.`) : null,
163
+ status,
164
+ stdout: capturedStdout,
165
+ stderr: capturedStderr
166
+ });
167
+ });
168
+ });
169
+ }
170
+
83
171
  function runExternalShellCommand(
84
172
  commandText,
85
173
  {
@@ -155,6 +243,32 @@ async function runLocalJskit(
155
243
  });
156
244
  }
157
245
 
246
+ async function runLocalJskitAsync(
247
+ appRoot,
248
+ args = [],
249
+ {
250
+ stdout,
251
+ stderr,
252
+ createCliError,
253
+ quiet = false
254
+ } = {}
255
+ ) {
256
+ const localJskitBin = resolveLocalJskitBin(appRoot);
257
+ if (!(await fileExists(localJskitBin))) {
258
+ throw createCliError(`Local jskit binary not found at ${path.relative(appRoot, localJskitBin)}. Run npm install first.`, {
259
+ exitCode: 1
260
+ });
261
+ }
262
+
263
+ return runExternalCommandAsync(localJskitBin, args, {
264
+ cwd: appRoot,
265
+ stdout,
266
+ stderr,
267
+ quiet,
268
+ createCliError
269
+ });
270
+ }
271
+
158
272
  async function resolveLocalRepoRoot({ appRoot = "", explicitRepoRoot = "" } = {}) {
159
273
  const explicit = normalizeText(explicitRepoRoot);
160
274
  if (explicit) {
@@ -295,10 +409,12 @@ export {
295
409
  normalizeText,
296
410
  isTruthyFlag,
297
411
  runExternalCommand,
412
+ runExternalCommandAsync,
298
413
  runExternalShellCommand,
299
414
  formatUtcReleaseTimestamp,
300
415
  resolveLocalJskitBin,
301
416
  runLocalJskit,
417
+ runLocalJskitAsync,
302
418
  resolveLocalRepoRoot,
303
419
  discoverLocalPackageMap,
304
420
  linkPackageBinEntries,
@@ -1,20 +1,65 @@
1
- import { runLocalJskit, runExternalCommand } from "./shared.js";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ runExternalCommandAsync,
6
+ runLocalJskitAsync
7
+ } from "./shared.js";
8
+
9
+ const DEPENDENCY_SECTIONS = Object.freeze([
10
+ Object.freeze({
11
+ installArgs: Object.freeze(["--save-exact"]),
12
+ label: "runtime",
13
+ name: "dependencies"
14
+ }),
15
+ Object.freeze({
16
+ installArgs: Object.freeze(["--save-dev", "--save-exact"]),
17
+ label: "development",
18
+ name: "devDependencies"
19
+ }),
20
+ Object.freeze({
21
+ installArgs: Object.freeze(["--save-optional", "--save-exact"]),
22
+ label: "optional",
23
+ name: "optionalDependencies"
24
+ }),
25
+ Object.freeze({
26
+ installArgs: Object.freeze(["--save-peer", "--save-exact"]),
27
+ label: "peer",
28
+ name: "peerDependencies"
29
+ })
30
+ ]);
31
+ const JSKIT_PACKAGE_PATTERN = /^@jskit-ai\/[a-z0-9._-]+$/iu;
32
+ const PROGRESS_INTERVAL_MS = 5_000;
2
33
 
3
34
  function collectJskitPackageNames(packageMap = {}) {
4
35
  return Object.keys(packageMap && typeof packageMap === "object" ? packageMap : {})
5
- .filter((name) => String(name || "").startsWith("@jskit-ai/"))
36
+ .filter((name) => JSKIT_PACKAGE_PATTERN.test(String(name || "")))
6
37
  .sort((left, right) => left.localeCompare(right));
7
38
  }
8
39
 
9
- function resolveMajorRangeFromVersion(packageName = "", rawVersion = "", createCliError) {
40
+ function collectManifestJskitPackageNames(packageJson = {}) {
41
+ const packageNames = new Set();
42
+ for (const section of DEPENDENCY_SECTIONS) {
43
+ for (const packageName of collectJskitPackageNames(packageJson?.[section.name])) {
44
+ packageNames.add(packageName);
45
+ }
46
+ }
47
+ return [...packageNames].sort((left, right) => left.localeCompare(right));
48
+ }
49
+
50
+ function resolveExactVersion(packageName = "", rawVersion = "", createCliError) {
10
51
  const normalizedVersion = String(rawVersion || "").trim();
11
- const match = normalizedVersion.match(/^(\d+)\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/u);
12
- if (!match) {
52
+ if (!/^\d+\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/u.test(normalizedVersion)) {
13
53
  throw createCliError(`Invalid latest version for ${packageName}: ${normalizedVersion || "<empty>"}.`, {
14
54
  exitCode: 1
15
55
  });
16
56
  }
17
- return `${match[1]}.x`;
57
+ return normalizedVersion;
58
+ }
59
+
60
+ function resolveMajorRange(packageName = "", version = "", createCliError) {
61
+ const exactVersion = resolveExactVersion(packageName, version, createCliError);
62
+ return `${exactVersion.slice(0, exactVersion.indexOf("."))}.x`;
18
63
  }
19
64
 
20
65
  function resolveRegistryArgs(registryUrl = "") {
@@ -25,8 +70,359 @@ function resolveRegistryArgs(registryUrl = "") {
25
70
  return ["--registry", normalizedRegistryUrl];
26
71
  }
27
72
 
28
- function resolveInstallSpecs(packageNames = [], resolveMajorRange) {
29
- return packageNames.map((packageName) => `${packageName}@${resolveMajorRange(packageName)}`);
73
+ function resolveInstallSpecs(packageNames = [], latestVersions = new Map()) {
74
+ return packageNames.map((packageName) => `${packageName}@${latestVersions.get(packageName)}`);
75
+ }
76
+
77
+ function formatElapsedTime(elapsedMilliseconds = 0) {
78
+ const elapsedSeconds = Math.max(0, Math.floor(Number(elapsedMilliseconds) / 1000));
79
+ if (elapsedSeconds < 1) {
80
+ return "under 1s";
81
+ }
82
+ if (elapsedSeconds < 60) {
83
+ return `${elapsedSeconds}s`;
84
+ }
85
+
86
+ const minutes = Math.floor(elapsedSeconds / 60);
87
+ const seconds = elapsedSeconds % 60;
88
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
89
+ }
90
+
91
+ async function runWithProgress(task, {
92
+ activity,
93
+ progressIntervalMs = PROGRESS_INTERVAL_MS,
94
+ stdout,
95
+ step
96
+ } = {}) {
97
+ const normalizedActivity = String(activity || "running update").trim();
98
+ const normalizedStep = String(step || "Update").trim();
99
+ const startedAt = Date.now();
100
+ stdout?.write(`[jskit:update] ${normalizedStep}: ${normalizedActivity}.\n`);
101
+
102
+ const progressTimer = setInterval(() => {
103
+ stdout?.write(
104
+ `[jskit:update] ${normalizedStep} is still running (${formatElapsedTime(Date.now() - startedAt)} elapsed): ${normalizedActivity}.\n`
105
+ );
106
+ }, progressIntervalMs);
107
+ progressTimer.unref();
108
+
109
+ try {
110
+ const result = await task();
111
+ stdout?.write(
112
+ `[jskit:update] ${normalizedStep} complete in ${formatElapsedTime(Date.now() - startedAt)}.\n`
113
+ );
114
+ return result;
115
+ } finally {
116
+ clearInterval(progressTimer);
117
+ }
118
+ }
119
+
120
+ function hasNpmWorkspaces(packageJson = {}) {
121
+ const workspaces = Array.isArray(packageJson?.workspaces)
122
+ ? packageJson.workspaces
123
+ : packageJson?.workspaces?.packages;
124
+ return Array.isArray(workspaces) && workspaces.some((workspace) => String(workspace || "").trim());
125
+ }
126
+
127
+ async function readJson(filePath) {
128
+ return JSON.parse(await readFile(filePath, "utf8"));
129
+ }
130
+
131
+ async function readOptionalFile(filePath) {
132
+ try {
133
+ return await readFile(filePath, "utf8");
134
+ } catch (error) {
135
+ if (error?.code === "ENOENT") {
136
+ return "";
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ async function resolveWorkspaceDirectories({
143
+ appRoot,
144
+ createCliError,
145
+ packageJson,
146
+ stderr,
147
+ stdout
148
+ }) {
149
+ if (!hasNpmWorkspaces(packageJson)) {
150
+ return [];
151
+ }
152
+
153
+ const result = await runExternalCommandAsync("npm", ["query", ".workspace", "--json"], {
154
+ cwd: appRoot,
155
+ stdout,
156
+ stderr,
157
+ quiet: true,
158
+ createCliError
159
+ });
160
+
161
+ let entries;
162
+ try {
163
+ entries = JSON.parse(String(result.stdout || "[]"));
164
+ } catch (error) {
165
+ throw createCliError(`npm returned invalid workspace metadata: ${error instanceof Error ? error.message : String(error)}.`, {
166
+ exitCode: 1
167
+ });
168
+ }
169
+ if (!Array.isArray(entries)) {
170
+ throw createCliError("npm returned invalid workspace metadata: expected a JSON array.", {
171
+ exitCode: 1
172
+ });
173
+ }
174
+
175
+ const normalizedAppRoot = path.resolve(appRoot);
176
+ const workspaceDirectories = new Set();
177
+ for (const entry of entries) {
178
+ const location = String(entry?.location || "").trim();
179
+ if (!location) {
180
+ continue;
181
+ }
182
+ const workspaceDirectory = path.resolve(normalizedAppRoot, location);
183
+ if (
184
+ workspaceDirectory === normalizedAppRoot ||
185
+ !workspaceDirectory.startsWith(`${normalizedAppRoot}${path.sep}`)
186
+ ) {
187
+ throw createCliError(`npm returned a workspace outside the app root: ${location}.`, {
188
+ exitCode: 1
189
+ });
190
+ }
191
+ workspaceDirectories.add(workspaceDirectory);
192
+ }
193
+
194
+ return [...workspaceDirectories].sort((left, right) => left.localeCompare(right));
195
+ }
196
+
197
+ function descriptorJskitPackageNames(source = "") {
198
+ const packageNames = new Set();
199
+ const patterns = [
200
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1\s*:\s*(["'])[^"']+\3/giu,
201
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1\s*:\s*\{[^{}]*?(?:["'](?:version|value)["']|version|value)\s*:\s*(["'])[^"']+\3/giu
202
+ ];
203
+ for (const pattern of patterns) {
204
+ for (const match of String(source || "").matchAll(pattern)) {
205
+ packageNames.add(match[2]);
206
+ }
207
+ }
208
+ return [...packageNames].sort((left, right) => left.localeCompare(right));
209
+ }
210
+
211
+ async function loadWorkspacePackages(workspaceDirectories = []) {
212
+ const workspacePackages = [];
213
+ for (const directory of workspaceDirectories) {
214
+ const packageJsonPath = path.join(directory, "package.json");
215
+ const descriptorPath = path.join(directory, "package.descriptor.mjs");
216
+ workspacePackages.push({
217
+ descriptorPath,
218
+ descriptorSource: await readOptionalFile(descriptorPath),
219
+ directory,
220
+ packageJson: await readJson(packageJsonPath),
221
+ packageJsonPath
222
+ });
223
+ }
224
+ return workspacePackages;
225
+ }
226
+
227
+ async function resolveLatestVersions(packageNames = [], latestVersions = new Map(), {
228
+ appRoot,
229
+ createCliError,
230
+ registryArgs,
231
+ stderr,
232
+ stdout
233
+ }) {
234
+ const unresolvedPackageNames = packageNames
235
+ .filter((packageName) => !latestVersions.has(packageName))
236
+ .sort((left, right) => left.localeCompare(right));
237
+
238
+ for (const [index, packageName] of unresolvedPackageNames.entries()) {
239
+ stdout?.write(
240
+ `[jskit:update] resolving latest package ${index + 1}/${unresolvedPackageNames.length}: ${packageName}.\n`
241
+ );
242
+ const result = await runExternalCommandAsync(
243
+ "npm",
244
+ ["view", ...registryArgs, packageName, "version"],
245
+ {
246
+ cwd: appRoot,
247
+ stdout,
248
+ stderr,
249
+ quiet: true,
250
+ createCliError
251
+ }
252
+ );
253
+ const version = resolveExactVersion(packageName, result.stdout, createCliError);
254
+ latestVersions.set(packageName, version);
255
+ stdout?.write(`[jskit:update] resolved ${packageName}@${version}.\n`);
256
+ }
257
+
258
+ return latestVersions;
259
+ }
260
+
261
+ function updateWorkspaceManifest(packageJson = {}, latestVersions = new Map(), createCliError) {
262
+ const updates = [];
263
+ for (const section of DEPENDENCY_SECTIONS) {
264
+ const packageMap = packageJson?.[section.name];
265
+ for (const packageName of collectJskitPackageNames(packageMap)) {
266
+ const targetRange = resolveMajorRange(packageName, latestVersions.get(packageName), createCliError);
267
+ if (packageMap[packageName] === targetRange) {
268
+ continue;
269
+ }
270
+ packageMap[packageName] = targetRange;
271
+ updates.push(`${packageName}@${targetRange}`);
272
+ }
273
+ }
274
+ return updates;
275
+ }
276
+
277
+ function updateWorkspaceDescriptor(source = "", latestVersions = new Map(), createCliError) {
278
+ const updates = [];
279
+ const replaceVersion = (match, keyQuote, packageName, separator, valueQuote, currentVersion) => {
280
+ const targetRange = resolveMajorRange(packageName, latestVersions.get(packageName), createCliError);
281
+ if (currentVersion === targetRange) {
282
+ return match;
283
+ }
284
+ updates.push(`${packageName}@${targetRange}`);
285
+ return `${keyQuote}${packageName}${keyQuote}${separator}${valueQuote}${targetRange}${valueQuote}`;
286
+ };
287
+ let nextSource = String(source || "").replace(
288
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1(\s*:\s*)(["'])([^"']+)\4/giu,
289
+ replaceVersion
290
+ );
291
+ nextSource = nextSource.replace(
292
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1(\s*:\s*\{[^{}]*?(?:["'](?:version|value)["']|version|value)\s*:\s*)(["'])([^"']+)\4/giu,
293
+ replaceVersion
294
+ );
295
+ return {
296
+ nextSource,
297
+ updates
298
+ };
299
+ }
300
+
301
+ async function synchronizeWorkspacePackageSpecs({
302
+ appRoot,
303
+ createCliError,
304
+ dryRun,
305
+ latestVersions,
306
+ packageJson,
307
+ registryArgs,
308
+ stderr,
309
+ stdout
310
+ }) {
311
+ const workspaceDirectories = await resolveWorkspaceDirectories({
312
+ appRoot,
313
+ createCliError,
314
+ packageJson,
315
+ stderr,
316
+ stdout
317
+ });
318
+ const workspacePackages = await loadWorkspacePackages(workspaceDirectories);
319
+ const workspaceJskitPackages = new Set();
320
+ for (const workspacePackage of workspacePackages) {
321
+ for (const packageName of collectManifestJskitPackageNames(workspacePackage.packageJson)) {
322
+ workspaceJskitPackages.add(packageName);
323
+ }
324
+ for (const packageName of descriptorJskitPackageNames(workspacePackage.descriptorSource)) {
325
+ workspaceJskitPackages.add(packageName);
326
+ }
327
+ }
328
+
329
+ const packageNames = [...workspaceJskitPackages].sort((left, right) => left.localeCompare(right));
330
+ await resolveLatestVersions(packageNames, latestVersions, {
331
+ appRoot,
332
+ createCliError,
333
+ registryArgs,
334
+ stderr,
335
+ stdout
336
+ });
337
+
338
+ const changedFiles = [];
339
+ for (const workspacePackage of workspacePackages) {
340
+ const manifestUpdates = updateWorkspaceManifest(
341
+ workspacePackage.packageJson,
342
+ latestVersions,
343
+ createCliError
344
+ );
345
+ if (manifestUpdates.length > 0) {
346
+ const relativePath = path.relative(appRoot, workspacePackage.packageJsonPath).replaceAll(path.sep, "/");
347
+ changedFiles.push(relativePath);
348
+ stdout?.write(`[jskit:update] workspace manifest ${relativePath} -> ${manifestUpdates.join(", ")}\n`);
349
+ if (!dryRun) {
350
+ await writeFile(
351
+ workspacePackage.packageJsonPath,
352
+ `${JSON.stringify(workspacePackage.packageJson, null, 2)}\n`,
353
+ "utf8"
354
+ );
355
+ }
356
+ }
357
+
358
+ if (!workspacePackage.descriptorSource) {
359
+ continue;
360
+ }
361
+ const descriptorResult = updateWorkspaceDescriptor(
362
+ workspacePackage.descriptorSource,
363
+ latestVersions,
364
+ createCliError
365
+ );
366
+ if (descriptorResult.updates.length < 1) {
367
+ continue;
368
+ }
369
+ const relativePath = path.relative(appRoot, workspacePackage.descriptorPath).replaceAll(path.sep, "/");
370
+ changedFiles.push(relativePath);
371
+ stdout?.write(`[jskit:update] workspace descriptor ${relativePath} -> ${descriptorResult.updates.join(", ")}\n`);
372
+ if (!dryRun) {
373
+ await writeFile(workspacePackage.descriptorPath, descriptorResult.nextSource, "utf8");
374
+ }
375
+ }
376
+
377
+ return {
378
+ changedFiles,
379
+ packageNames
380
+ };
381
+ }
382
+
383
+ async function updateRootPackages({
384
+ appRoot,
385
+ createCliError,
386
+ dryRun,
387
+ latestVersions,
388
+ packageJson,
389
+ registryArgs,
390
+ stderr,
391
+ stdout
392
+ }) {
393
+ const rootPackageNames = collectManifestJskitPackageNames(packageJson);
394
+ if (rootPackageNames.length < 1) {
395
+ stdout?.write("[jskit:update] no root @jskit-ai packages found.\n");
396
+ return false;
397
+ }
398
+
399
+ await resolveLatestVersions(rootPackageNames, latestVersions, {
400
+ appRoot,
401
+ createCliError,
402
+ registryArgs,
403
+ stderr,
404
+ stdout
405
+ });
406
+ const dryRunArgs = dryRun ? ["--dry-run"] : [];
407
+ for (const section of DEPENDENCY_SECTIONS) {
408
+ const packageNames = collectJskitPackageNames(packageJson?.[section.name]);
409
+ if (packageNames.length < 1) {
410
+ continue;
411
+ }
412
+ const installSpecs = resolveInstallSpecs(packageNames, latestVersions);
413
+ stdout?.write(`[jskit:update] updating ${section.label} packages: ${installSpecs.join(" ")}\n`);
414
+ await runExternalCommandAsync(
415
+ "npm",
416
+ ["install", ...section.installArgs, ...registryArgs, ...dryRunArgs, ...installSpecs],
417
+ {
418
+ cwd: appRoot,
419
+ stdout,
420
+ stderr,
421
+ createCliError
422
+ }
423
+ );
424
+ }
425
+ return true;
30
426
  }
31
427
 
32
428
  async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
@@ -38,73 +434,111 @@ async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {
38
434
 
39
435
  await assertAppManagedCiWorkflowUnmodified({ appRoot });
40
436
  const { packageJson } = await loadAppPackageJson(appRoot);
41
- const runtimePackages = collectJskitPackageNames(packageJson?.dependencies);
42
- const devPackages = collectJskitPackageNames(packageJson?.devDependencies);
43
437
  const registryUrl = String(options?.inlineOptions?.registry || "").trim();
44
438
  const registryArgs = resolveRegistryArgs(registryUrl);
45
439
  const dryRun = options?.dryRun === true;
46
-
47
- if (runtimePackages.length < 1 && devPackages.length < 1) {
48
- stdout.write("[jskit:update] no @jskit-ai packages found in dependencies.\n");
49
- return 0;
50
- }
51
-
52
- const resolveMajorRange = (packageName) => {
53
- const result = runExternalCommand("npm", ["view", ...registryArgs, packageName, "version"], {
54
- cwd: appRoot,
55
- stdout,
56
- stderr,
57
- quiet: true,
58
- createCliError
59
- });
60
- return resolveMajorRangeFromVersion(packageName, result.stdout, createCliError);
61
- };
62
-
63
- const runtimeSpecs = resolveInstallSpecs(runtimePackages, resolveMajorRange);
64
- const devSpecs = resolveInstallSpecs(devPackages, resolveMajorRange);
65
- const dryRunArgs = dryRun ? ["--dry-run"] : [];
440
+ const latestVersions = new Map();
66
441
 
67
442
  if (dryRun) {
68
- stdout.write("[jskit:update] dry-run mode enabled.\n");
443
+ stdout?.write("[jskit:update] dry-run mode enabled.\n");
69
444
  }
70
445
 
71
- if (runtimeSpecs.length > 0) {
72
- stdout.write(`[jskit:update] updating runtime packages: ${runtimeSpecs.join(" ")}\n`);
73
- runExternalCommand("npm", ["install", "--save-exact", ...registryArgs, ...dryRunArgs, ...runtimeSpecs], {
74
- cwd: appRoot,
446
+ await runWithProgress(
447
+ async () => {
448
+ const updatedRootPackages = await updateRootPackages({
449
+ appRoot,
450
+ createCliError,
451
+ dryRun,
452
+ latestVersions,
453
+ packageJson,
454
+ registryArgs,
455
+ stderr,
456
+ stdout
457
+ });
458
+ if (!updatedRootPackages || dryRun) {
459
+ return;
460
+ }
461
+ stdout?.write("[jskit:update] generating managed migrations for changed packages.\n");
462
+ await runLocalJskitAsync(appRoot, ["migrations", "changed"], {
463
+ stdout,
464
+ stderr,
465
+ createCliError
466
+ });
467
+ stdout?.write("[jskit:update] synchronizing the managed CI workflow.\n");
468
+ await runLocalJskitAsync(appRoot, ["app", "sync-ci"], {
469
+ stdout,
470
+ stderr,
471
+ createCliError
472
+ });
473
+ },
474
+ {
475
+ activity: dryRun
476
+ ? "checking root JSKIT package updates"
477
+ : "updating root JSKIT packages and managed artifacts",
75
478
  stdout,
76
- stderr,
77
- createCliError
78
- });
79
- }
479
+ step: "Step 1/3"
480
+ }
481
+ );
80
482
 
81
- if (devSpecs.length > 0) {
82
- stdout.write(`[jskit:update] updating dev packages: ${devSpecs.join(" ")}\n`);
83
- runExternalCommand("npm", ["install", "--save-dev", "--save-exact", ...registryArgs, ...dryRunArgs, ...devSpecs], {
84
- cwd: appRoot,
483
+ let workspaceResult = {
484
+ changedFiles: [],
485
+ packageNames: []
486
+ };
487
+ await runWithProgress(
488
+ async () => {
489
+ workspaceResult = await synchronizeWorkspacePackageSpecs({
490
+ appRoot,
491
+ createCliError,
492
+ dryRun,
493
+ latestVersions,
494
+ packageJson,
495
+ registryArgs,
496
+ stderr,
497
+ stdout
498
+ });
499
+ },
500
+ {
501
+ activity: "aligning JSKIT ranges in workspace manifests and descriptors",
85
502
  stdout,
86
- stderr,
87
- createCliError
88
- });
89
- }
503
+ step: "Step 2/3"
504
+ }
505
+ );
506
+ stdout?.write(
507
+ `[jskit:update] Step 2/3 summary: ${workspaceResult.changedFiles.length} workspace files ${dryRun ? "would change" : "changed"}.\n`
508
+ );
90
509
 
91
- if (!dryRun) {
92
- stdout.write("[jskit:update] generating managed migrations for changed packages.\n");
93
- await runLocalJskit(appRoot, ["migrations", "changed"], {
94
- stdout,
95
- stderr,
96
- createCliError
97
- });
98
- stdout.write("[jskit:update] synchronizing the managed CI workflow.\n");
99
- await runLocalJskit(appRoot, ["app", "sync-ci"], {
100
- stdout,
101
- stderr,
102
- createCliError
103
- });
510
+ if (dryRun) {
511
+ stdout?.write(
512
+ `[jskit:update] Step 3/3 skipped in dry-run mode: ${workspaceResult.packageNames.length} workspace JSKIT packages would be refreshed.\n`
513
+ );
514
+ } else if (workspaceResult.packageNames.length > 0) {
515
+ await runWithProgress(
516
+ () => runExternalCommandAsync(
517
+ "npm",
518
+ ["update", ...registryArgs, "--workspaces", ...workspaceResult.packageNames],
519
+ {
520
+ cwd: appRoot,
521
+ stdout,
522
+ stderr,
523
+ createCliError
524
+ }
525
+ ),
526
+ {
527
+ activity: `refreshing ${workspaceResult.packageNames.length} workspace JSKIT packages and updating the lockfile`,
528
+ stdout,
529
+ step: "Step 3/3"
530
+ }
531
+ );
532
+ } else {
533
+ stdout?.write("[jskit:update] Step 3/3 skipped: no workspace JSKIT packages were found.\n");
104
534
  }
105
535
 
106
- stdout.write("[jskit:update] done.\n");
536
+ stdout?.write("[jskit:update] done.\n");
107
537
  return 0;
108
538
  }
109
539
 
110
- export { runAppUpdatePackagesCommand };
540
+ export {
541
+ formatElapsedTime,
542
+ runAppUpdatePackagesCommand,
543
+ runWithProgress
544
+ };