@jskit-ai/jskit-cli 0.2.134 → 0.2.136

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.136",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.129",
24
- "@jskit-ai/kernel": "0.1.120",
25
- "@jskit-ai/shell-web": "0.1.119",
23
+ "@jskit-ai/jskit-catalog": "0.1.131",
24
+ "@jskit-ai/kernel": "0.1.121",
25
+ "@jskit-ai/shell-web": "0.1.120",
26
26
  "@vue/compiler-sfc": "^3.5.29",
27
27
  "ts-morph": "^28.0.0",
28
28
  "yaml": "^2.8.3"
@@ -20,6 +20,7 @@ import {
20
20
  loadMutationWhenConfigContext,
21
21
  resolveAppRelativePathWithinRoot
22
22
  } from "../ioAndMigrations.js";
23
+ import { normalizeRelativePosixPath } from "../localPackageSupport.js";
23
24
  import {
24
25
  interpolateFileMutationRecord,
25
26
  renderTemplateFile,
@@ -39,7 +40,7 @@ async function prepareFileMutations(
39
40
  const existingManagedFilesByPath = new Map();
40
41
  for (const managedFileValue of ensureArray(existingManagedFiles)) {
41
42
  const managedFile = ensureObject(managedFileValue);
42
- const managedPath = String(managedFile.path || "").trim();
43
+ const managedPath = normalizeRelativePosixPath(managedFile.path);
43
44
  if (!managedPath) {
44
45
  continue;
45
46
  }
@@ -205,7 +206,7 @@ async function applyFileMutations(
205
206
  const existingManagedFilesByPath = new Map();
206
207
  for (const managedFileValue of ensureArray(existingManagedFiles)) {
207
208
  const managedFile = ensureObject(managedFileValue);
208
- const managedPath = String(managedFile.path || "").trim();
209
+ const managedPath = normalizeRelativePosixPath(managedFile.path);
209
210
  if (!managedPath) {
210
211
  continue;
211
212
  }
@@ -260,6 +261,7 @@ async function applyFileMutations(
260
261
  managedFiles.push({
261
262
  ...existingManaged,
262
263
  path: relativeTargetPath,
264
+ ownership: mutation.ownership,
263
265
  preserveOnRemove: mutation.preserveOnRemove,
264
266
  reason: mutation.reason || String(existingManaged.reason || ""),
265
267
  category: mutation.category || String(existingManaged.category || ""),
@@ -271,6 +273,7 @@ async function applyFileMutations(
271
273
  if (mutation.ownership === "app" && previous.exists && hashBuffer(previous.buffer) === renderedSourceHash) {
272
274
  managedFiles.push({
273
275
  path: relativeTargetPath,
276
+ ownership: mutation.ownership,
274
277
  hash: renderedSourceHash,
275
278
  hadPrevious: true,
276
279
  previousContentBase64: previous.buffer.toString("base64"),
@@ -289,6 +292,7 @@ async function applyFileMutations(
289
292
 
290
293
  managedFiles.push({
291
294
  path: relativeTargetPath,
295
+ ownership: mutation.ownership,
292
296
  hash: renderedSourceHash,
293
297
  hadPrevious: previous.exists,
294
298
  previousContentBase64: previous.exists ? previous.buffer.toString("base64") : "",
@@ -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,10 @@ 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
+ "Installed packages whose descriptor versions changed are reapplied with their saved options before managed migrations and the composed CI workflow are refreshed.",
94
+ "App-owned files retain local edits through the normal package-update ownership rules."
94
95
  ])
95
96
  }),
96
97
  "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,70 @@
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
+ import {
9
+ ensureObject,
10
+ sortStrings
11
+ } from "../../shared/collectionUtils.js";
12
+
13
+ const DEPENDENCY_SECTIONS = Object.freeze([
14
+ Object.freeze({
15
+ installArgs: Object.freeze(["--save-exact"]),
16
+ label: "runtime",
17
+ name: "dependencies"
18
+ }),
19
+ Object.freeze({
20
+ installArgs: Object.freeze(["--save-dev", "--save-exact"]),
21
+ label: "development",
22
+ name: "devDependencies"
23
+ }),
24
+ Object.freeze({
25
+ installArgs: Object.freeze(["--save-optional", "--save-exact"]),
26
+ label: "optional",
27
+ name: "optionalDependencies"
28
+ }),
29
+ Object.freeze({
30
+ installArgs: Object.freeze(["--save-peer", "--save-exact"]),
31
+ label: "peer",
32
+ name: "peerDependencies"
33
+ })
34
+ ]);
35
+ const JSKIT_PACKAGE_PATTERN = /^@jskit-ai\/[a-z0-9._-]+$/iu;
36
+ const PROGRESS_INTERVAL_MS = 5_000;
2
37
 
3
38
  function collectJskitPackageNames(packageMap = {}) {
4
- return Object.keys(packageMap && typeof packageMap === "object" ? packageMap : {})
5
- .filter((name) => String(name || "").startsWith("@jskit-ai/"))
6
- .sort((left, right) => left.localeCompare(right));
39
+ return sortStrings(
40
+ Object.keys(packageMap && typeof packageMap === "object" ? packageMap : {})
41
+ .filter((name) => JSKIT_PACKAGE_PATTERN.test(String(name || "")))
42
+ );
43
+ }
44
+
45
+ function collectManifestJskitPackageNames(packageJson = {}) {
46
+ const packageNames = new Set();
47
+ for (const section of DEPENDENCY_SECTIONS) {
48
+ for (const packageName of collectJskitPackageNames(packageJson?.[section.name])) {
49
+ packageNames.add(packageName);
50
+ }
51
+ }
52
+ return sortStrings([...packageNames]);
7
53
  }
8
54
 
9
- function resolveMajorRangeFromVersion(packageName = "", rawVersion = "", createCliError) {
55
+ function resolveExactVersion(packageName = "", rawVersion = "", createCliError) {
10
56
  const normalizedVersion = String(rawVersion || "").trim();
11
- const match = normalizedVersion.match(/^(\d+)\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/u);
12
- if (!match) {
57
+ if (!/^\d+\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/u.test(normalizedVersion)) {
13
58
  throw createCliError(`Invalid latest version for ${packageName}: ${normalizedVersion || "<empty>"}.`, {
14
59
  exitCode: 1
15
60
  });
16
61
  }
17
- return `${match[1]}.x`;
62
+ return normalizedVersion;
63
+ }
64
+
65
+ function resolveMajorRange(packageName = "", version = "", createCliError) {
66
+ const exactVersion = resolveExactVersion(packageName, version, createCliError);
67
+ return `${exactVersion.slice(0, exactVersion.indexOf("."))}.x`;
18
68
  }
19
69
 
20
70
  function resolveRegistryArgs(registryUrl = "") {
@@ -25,86 +75,554 @@ function resolveRegistryArgs(registryUrl = "") {
25
75
  return ["--registry", normalizedRegistryUrl];
26
76
  }
27
77
 
28
- function resolveInstallSpecs(packageNames = [], resolveMajorRange) {
29
- return packageNames.map((packageName) => `${packageName}@${resolveMajorRange(packageName)}`);
78
+ function resolveInstallSpecs(packageNames = [], latestVersions = new Map()) {
79
+ return packageNames.map((packageName) => `${packageName}@${latestVersions.get(packageName)}`);
30
80
  }
31
81
 
32
- async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
33
- const {
34
- createCliError,
35
- loadAppPackageJson,
36
- assertAppManagedCiWorkflowUnmodified
37
- } = ctx;
82
+ function collectChangedInstalledPackageIds(lock = {}, latestVersions = new Map()) {
83
+ const installedPackages = ensureObject(lock.installedPackages);
84
+ return sortStrings(
85
+ [...latestVersions.entries()]
86
+ .filter(([packageId, targetVersion]) => {
87
+ if (!Object.prototype.hasOwnProperty.call(installedPackages, packageId)) {
88
+ return false;
89
+ }
90
+ const installedPackage = ensureObject(installedPackages[packageId]);
91
+ return String(installedPackage.version || "").trim() !== targetVersion;
92
+ })
93
+ .map(([packageId]) => packageId)
94
+ );
95
+ }
38
96
 
39
- await assertAppManagedCiWorkflowUnmodified({ appRoot });
40
- const { packageJson } = await loadAppPackageJson(appRoot);
41
- const runtimePackages = collectJskitPackageNames(packageJson?.dependencies);
42
- const devPackages = collectJskitPackageNames(packageJson?.devDependencies);
43
- const registryUrl = String(options?.inlineOptions?.registry || "").trim();
44
- const registryArgs = resolveRegistryArgs(registryUrl);
45
- const dryRun = options?.dryRun === true;
97
+ async function reapplyChangedInstalledPackages({
98
+ appRoot,
99
+ createCliError,
100
+ dryRun,
101
+ latestVersions,
102
+ loadLockFile,
103
+ stderr,
104
+ stdout
105
+ }) {
106
+ const { lock } = await loadLockFile(appRoot);
107
+ const packageIds = collectChangedInstalledPackageIds(lock, latestVersions);
108
+ if (packageIds.length < 1) {
109
+ stdout?.write("[jskit:update] managed package state is already current.\n");
110
+ return;
111
+ }
46
112
 
47
- if (runtimePackages.length < 1 && devPackages.length < 1) {
48
- stdout.write("[jskit:update] no @jskit-ai packages found in dependencies.\n");
49
- return 0;
113
+ stdout?.write(`[jskit:update] managed packages requiring reapply: ${packageIds.join(", ")}.\n`);
114
+ if (dryRun) {
115
+ return;
50
116
  }
51
117
 
52
- const resolveMajorRange = (packageName) => {
53
- const result = runExternalCommand("npm", ["view", ...registryArgs, packageName, "version"], {
54
- cwd: appRoot,
118
+ for (const [index, packageId] of packageIds.entries()) {
119
+ const { lock: currentLock } = await loadLockFile(appRoot);
120
+ const currentVersion = String(
121
+ ensureObject(ensureObject(currentLock.installedPackages)[packageId]).version || ""
122
+ ).trim();
123
+ if (currentVersion === latestVersions.get(packageId)) {
124
+ continue;
125
+ }
126
+ stdout?.write(
127
+ `[jskit:update] reapplying managed package ${index + 1}/${packageIds.length}: ${packageId}.\n`
128
+ );
129
+ await runLocalJskitAsync(appRoot, ["update", "package", packageId], {
55
130
  stdout,
56
131
  stderr,
57
- quiet: true,
58
132
  createCliError
59
133
  });
60
- return resolveMajorRangeFromVersion(packageName, result.stdout, createCliError);
61
- };
134
+ }
62
135
 
63
- const runtimeSpecs = resolveInstallSpecs(runtimePackages, resolveMajorRange);
64
- const devSpecs = resolveInstallSpecs(devPackages, resolveMajorRange);
65
- const dryRunArgs = dryRun ? ["--dry-run"] : [];
136
+ const { lock: updatedLock } = await loadLockFile(appRoot);
137
+ const remainingPackageIds = collectChangedInstalledPackageIds(updatedLock, latestVersions);
138
+ if (remainingPackageIds.length > 0) {
139
+ throw createCliError(
140
+ `Managed package reapply finished with stale lock versions: ${remainingPackageIds.join(", ")}.`
141
+ );
142
+ }
143
+ stdout?.write("[jskit:update] managed package state is current.\n");
144
+ }
66
145
 
67
- if (dryRun) {
68
- stdout.write("[jskit:update] dry-run mode enabled.\n");
146
+ function formatElapsedTime(elapsedMilliseconds = 0) {
147
+ const elapsedSeconds = Math.max(0, Math.floor(Number(elapsedMilliseconds) / 1000));
148
+ if (elapsedSeconds < 1) {
149
+ return "under 1s";
150
+ }
151
+ if (elapsedSeconds < 60) {
152
+ return `${elapsedSeconds}s`;
69
153
  }
70
154
 
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,
75
- stdout,
76
- stderr,
77
- createCliError
155
+ const minutes = Math.floor(elapsedSeconds / 60);
156
+ const seconds = elapsedSeconds % 60;
157
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
158
+ }
159
+
160
+ async function runWithProgress(task, {
161
+ activity,
162
+ progressIntervalMs = PROGRESS_INTERVAL_MS,
163
+ stdout,
164
+ step
165
+ } = {}) {
166
+ const normalizedActivity = String(activity || "running update").trim();
167
+ const normalizedStep = String(step || "Update").trim();
168
+ const startedAt = Date.now();
169
+ stdout?.write(`[jskit:update] ${normalizedStep}: ${normalizedActivity}.\n`);
170
+
171
+ const progressTimer = setInterval(() => {
172
+ stdout?.write(
173
+ `[jskit:update] ${normalizedStep} is still running (${formatElapsedTime(Date.now() - startedAt)} elapsed): ${normalizedActivity}.\n`
174
+ );
175
+ }, progressIntervalMs);
176
+ progressTimer.unref();
177
+
178
+ try {
179
+ const result = await task();
180
+ stdout?.write(
181
+ `[jskit:update] ${normalizedStep} complete in ${formatElapsedTime(Date.now() - startedAt)}.\n`
182
+ );
183
+ return result;
184
+ } finally {
185
+ clearInterval(progressTimer);
186
+ }
187
+ }
188
+
189
+ function hasNpmWorkspaces(packageJson = {}) {
190
+ const workspaces = Array.isArray(packageJson?.workspaces)
191
+ ? packageJson.workspaces
192
+ : packageJson?.workspaces?.packages;
193
+ return Array.isArray(workspaces) && workspaces.some((workspace) => String(workspace || "").trim());
194
+ }
195
+
196
+ async function readJson(filePath) {
197
+ return JSON.parse(await readFile(filePath, "utf8"));
198
+ }
199
+
200
+ async function readOptionalFile(filePath) {
201
+ try {
202
+ return await readFile(filePath, "utf8");
203
+ } catch (error) {
204
+ if (error?.code === "ENOENT") {
205
+ return "";
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ async function resolveWorkspaceDirectories({
212
+ appRoot,
213
+ createCliError,
214
+ packageJson,
215
+ stderr,
216
+ stdout
217
+ }) {
218
+ if (!hasNpmWorkspaces(packageJson)) {
219
+ return [];
220
+ }
221
+
222
+ const result = await runExternalCommandAsync("npm", ["query", ".workspace", "--json"], {
223
+ cwd: appRoot,
224
+ stdout,
225
+ stderr,
226
+ quiet: true,
227
+ createCliError
228
+ });
229
+
230
+ let entries;
231
+ try {
232
+ entries = JSON.parse(String(result.stdout || "[]"));
233
+ } catch (error) {
234
+ throw createCliError(`npm returned invalid workspace metadata: ${error instanceof Error ? error.message : String(error)}.`, {
235
+ exitCode: 1
236
+ });
237
+ }
238
+ if (!Array.isArray(entries)) {
239
+ throw createCliError("npm returned invalid workspace metadata: expected a JSON array.", {
240
+ exitCode: 1
78
241
  });
79
242
  }
80
243
 
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,
85
- stdout,
86
- stderr,
87
- createCliError
244
+ const normalizedAppRoot = path.resolve(appRoot);
245
+ const workspaceDirectories = new Set();
246
+ for (const entry of entries) {
247
+ const location = String(entry?.location || "").trim();
248
+ if (!location) {
249
+ continue;
250
+ }
251
+ const workspaceDirectory = path.resolve(normalizedAppRoot, location);
252
+ if (
253
+ workspaceDirectory === normalizedAppRoot ||
254
+ !workspaceDirectory.startsWith(`${normalizedAppRoot}${path.sep}`)
255
+ ) {
256
+ throw createCliError(`npm returned a workspace outside the app root: ${location}.`, {
257
+ exitCode: 1
258
+ });
259
+ }
260
+ workspaceDirectories.add(workspaceDirectory);
261
+ }
262
+
263
+ return [...workspaceDirectories].sort((left, right) => left.localeCompare(right));
264
+ }
265
+
266
+ function descriptorJskitPackageNames(source = "") {
267
+ const packageNames = new Set();
268
+ const patterns = [
269
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1\s*:\s*(["'])[^"']+\3/giu,
270
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1\s*:\s*\{[^{}]*?(?:["'](?:version|value)["']|version|value)\s*:\s*(["'])[^"']+\3/giu
271
+ ];
272
+ for (const pattern of patterns) {
273
+ for (const match of String(source || "").matchAll(pattern)) {
274
+ packageNames.add(match[2]);
275
+ }
276
+ }
277
+ return [...packageNames].sort((left, right) => left.localeCompare(right));
278
+ }
279
+
280
+ async function loadWorkspacePackages(workspaceDirectories = []) {
281
+ const workspacePackages = [];
282
+ for (const directory of workspaceDirectories) {
283
+ const packageJsonPath = path.join(directory, "package.json");
284
+ const descriptorPath = path.join(directory, "package.descriptor.mjs");
285
+ workspacePackages.push({
286
+ descriptorPath,
287
+ descriptorSource: await readOptionalFile(descriptorPath),
288
+ directory,
289
+ packageJson: await readJson(packageJsonPath),
290
+ packageJsonPath
88
291
  });
89
292
  }
293
+ return workspacePackages;
294
+ }
90
295
 
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,
296
+ async function resolveLatestVersions(packageNames = [], latestVersions = new Map(), {
297
+ appRoot,
298
+ createCliError,
299
+ registryArgs,
300
+ stderr,
301
+ stdout
302
+ }) {
303
+ const unresolvedPackageNames = packageNames
304
+ .filter((packageName) => !latestVersions.has(packageName))
305
+ .sort((left, right) => left.localeCompare(right));
306
+
307
+ for (const [index, packageName] of unresolvedPackageNames.entries()) {
308
+ stdout?.write(
309
+ `[jskit:update] resolving latest package ${index + 1}/${unresolvedPackageNames.length}: ${packageName}.\n`
310
+ );
311
+ const result = await runExternalCommandAsync(
312
+ "npm",
313
+ ["view", ...registryArgs, packageName, "version"],
314
+ {
315
+ cwd: appRoot,
316
+ stdout,
317
+ stderr,
318
+ quiet: true,
319
+ createCliError
320
+ }
321
+ );
322
+ const version = resolveExactVersion(packageName, result.stdout, createCliError);
323
+ latestVersions.set(packageName, version);
324
+ stdout?.write(`[jskit:update] resolved ${packageName}@${version}.\n`);
325
+ }
326
+
327
+ return latestVersions;
328
+ }
329
+
330
+ function updateWorkspaceManifest(packageJson = {}, latestVersions = new Map(), createCliError) {
331
+ const updates = [];
332
+ for (const section of DEPENDENCY_SECTIONS) {
333
+ const packageMap = packageJson?.[section.name];
334
+ for (const packageName of collectJskitPackageNames(packageMap)) {
335
+ const targetRange = resolveMajorRange(packageName, latestVersions.get(packageName), createCliError);
336
+ if (packageMap[packageName] === targetRange) {
337
+ continue;
338
+ }
339
+ packageMap[packageName] = targetRange;
340
+ updates.push(`${packageName}@${targetRange}`);
341
+ }
342
+ }
343
+ return updates;
344
+ }
345
+
346
+ function updateWorkspaceDescriptor(source = "", latestVersions = new Map(), createCliError) {
347
+ const updates = [];
348
+ const replaceVersion = (match, keyQuote, packageName, separator, valueQuote, currentVersion) => {
349
+ const targetRange = resolveMajorRange(packageName, latestVersions.get(packageName), createCliError);
350
+ if (currentVersion === targetRange) {
351
+ return match;
352
+ }
353
+ updates.push(`${packageName}@${targetRange}`);
354
+ return `${keyQuote}${packageName}${keyQuote}${separator}${valueQuote}${targetRange}${valueQuote}`;
355
+ };
356
+ let nextSource = String(source || "").replace(
357
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1(\s*:\s*)(["'])([^"']+)\4/giu,
358
+ replaceVersion
359
+ );
360
+ nextSource = nextSource.replace(
361
+ /(["'])(@jskit-ai\/[a-z0-9._-]+)\1(\s*:\s*\{[^{}]*?(?:["'](?:version|value)["']|version|value)\s*:\s*)(["'])([^"']+)\4/giu,
362
+ replaceVersion
363
+ );
364
+ return {
365
+ nextSource,
366
+ updates
367
+ };
368
+ }
369
+
370
+ async function synchronizeWorkspacePackageSpecs({
371
+ appRoot,
372
+ createCliError,
373
+ dryRun,
374
+ latestVersions,
375
+ packageJson,
376
+ registryArgs,
377
+ stderr,
378
+ stdout
379
+ }) {
380
+ const workspaceDirectories = await resolveWorkspaceDirectories({
381
+ appRoot,
382
+ createCliError,
383
+ packageJson,
384
+ stderr,
385
+ stdout
386
+ });
387
+ const workspacePackages = await loadWorkspacePackages(workspaceDirectories);
388
+ const workspaceJskitPackages = new Set();
389
+ for (const workspacePackage of workspacePackages) {
390
+ for (const packageName of collectManifestJskitPackageNames(workspacePackage.packageJson)) {
391
+ workspaceJskitPackages.add(packageName);
392
+ }
393
+ for (const packageName of descriptorJskitPackageNames(workspacePackage.descriptorSource)) {
394
+ workspaceJskitPackages.add(packageName);
395
+ }
396
+ }
397
+
398
+ const packageNames = [...workspaceJskitPackages].sort((left, right) => left.localeCompare(right));
399
+ await resolveLatestVersions(packageNames, latestVersions, {
400
+ appRoot,
401
+ createCliError,
402
+ registryArgs,
403
+ stderr,
404
+ stdout
405
+ });
406
+
407
+ const changedFiles = [];
408
+ for (const workspacePackage of workspacePackages) {
409
+ const manifestUpdates = updateWorkspaceManifest(
410
+ workspacePackage.packageJson,
411
+ latestVersions,
96
412
  createCliError
97
- });
98
- stdout.write("[jskit:update] synchronizing the managed CI workflow.\n");
99
- await runLocalJskit(appRoot, ["app", "sync-ci"], {
100
- stdout,
101
- stderr,
413
+ );
414
+ if (manifestUpdates.length > 0) {
415
+ const relativePath = path.relative(appRoot, workspacePackage.packageJsonPath).replaceAll(path.sep, "/");
416
+ changedFiles.push(relativePath);
417
+ stdout?.write(`[jskit:update] workspace manifest ${relativePath} -> ${manifestUpdates.join(", ")}\n`);
418
+ if (!dryRun) {
419
+ await writeFile(
420
+ workspacePackage.packageJsonPath,
421
+ `${JSON.stringify(workspacePackage.packageJson, null, 2)}\n`,
422
+ "utf8"
423
+ );
424
+ }
425
+ }
426
+
427
+ if (!workspacePackage.descriptorSource) {
428
+ continue;
429
+ }
430
+ const descriptorResult = updateWorkspaceDescriptor(
431
+ workspacePackage.descriptorSource,
432
+ latestVersions,
102
433
  createCliError
103
- });
434
+ );
435
+ if (descriptorResult.updates.length < 1) {
436
+ continue;
437
+ }
438
+ const relativePath = path.relative(appRoot, workspacePackage.descriptorPath).replaceAll(path.sep, "/");
439
+ changedFiles.push(relativePath);
440
+ stdout?.write(`[jskit:update] workspace descriptor ${relativePath} -> ${descriptorResult.updates.join(", ")}\n`);
441
+ if (!dryRun) {
442
+ await writeFile(workspacePackage.descriptorPath, descriptorResult.nextSource, "utf8");
443
+ }
444
+ }
445
+
446
+ return {
447
+ changedFiles,
448
+ packageNames
449
+ };
450
+ }
451
+
452
+ async function updateRootPackages({
453
+ appRoot,
454
+ createCliError,
455
+ dryRun,
456
+ latestVersions,
457
+ packageJson,
458
+ registryArgs,
459
+ stderr,
460
+ stdout
461
+ }) {
462
+ const rootPackageNames = collectManifestJskitPackageNames(packageJson);
463
+ if (rootPackageNames.length < 1) {
464
+ stdout?.write("[jskit:update] no root @jskit-ai packages found.\n");
465
+ return false;
466
+ }
467
+
468
+ await resolveLatestVersions(rootPackageNames, latestVersions, {
469
+ appRoot,
470
+ createCliError,
471
+ registryArgs,
472
+ stderr,
473
+ stdout
474
+ });
475
+ const dryRunArgs = dryRun ? ["--dry-run"] : [];
476
+ for (const section of DEPENDENCY_SECTIONS) {
477
+ const packageNames = collectJskitPackageNames(packageJson?.[section.name]);
478
+ if (packageNames.length < 1) {
479
+ continue;
480
+ }
481
+ const installSpecs = resolveInstallSpecs(packageNames, latestVersions);
482
+ stdout?.write(`[jskit:update] updating ${section.label} packages: ${installSpecs.join(" ")}\n`);
483
+ await runExternalCommandAsync(
484
+ "npm",
485
+ ["install", ...section.installArgs, ...registryArgs, ...dryRunArgs, ...installSpecs],
486
+ {
487
+ cwd: appRoot,
488
+ stdout,
489
+ stderr,
490
+ createCliError
491
+ }
492
+ );
493
+ }
494
+ return true;
495
+ }
496
+
497
+ async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
498
+ const {
499
+ createCliError,
500
+ loadAppPackageJson,
501
+ loadLockFile,
502
+ assertAppManagedCiWorkflowUnmodified
503
+ } = ctx;
504
+
505
+ await assertAppManagedCiWorkflowUnmodified({ appRoot });
506
+ const { packageJson } = await loadAppPackageJson(appRoot);
507
+ const registryUrl = String(options?.inlineOptions?.registry || "").trim();
508
+ const registryArgs = resolveRegistryArgs(registryUrl);
509
+ const dryRun = options?.dryRun === true;
510
+ const latestVersions = new Map();
511
+
512
+ if (dryRun) {
513
+ stdout?.write("[jskit:update] dry-run mode enabled.\n");
514
+ }
515
+
516
+ await runWithProgress(
517
+ async () => {
518
+ const updatedRootPackages = await updateRootPackages({
519
+ appRoot,
520
+ createCliError,
521
+ dryRun,
522
+ latestVersions,
523
+ packageJson,
524
+ registryArgs,
525
+ stderr,
526
+ stdout
527
+ });
528
+ if (!updatedRootPackages) {
529
+ return;
530
+ }
531
+ await reapplyChangedInstalledPackages({
532
+ appRoot,
533
+ createCliError,
534
+ dryRun,
535
+ latestVersions,
536
+ loadLockFile,
537
+ stderr,
538
+ stdout
539
+ });
540
+ if (dryRun) {
541
+ return;
542
+ }
543
+ stdout?.write("[jskit:update] generating managed migrations for changed packages.\n");
544
+ await runLocalJskitAsync(appRoot, ["migrations", "changed"], {
545
+ stdout,
546
+ stderr,
547
+ createCliError
548
+ });
549
+ stdout?.write("[jskit:update] synchronizing the managed CI workflow.\n");
550
+ await runLocalJskitAsync(appRoot, ["app", "sync-ci"], {
551
+ stdout,
552
+ stderr,
553
+ createCliError
554
+ });
555
+ },
556
+ {
557
+ activity: dryRun
558
+ ? "checking root JSKIT package updates"
559
+ : "updating root JSKIT packages and managed artifacts",
560
+ stdout,
561
+ step: "Step 1/3"
562
+ }
563
+ );
564
+
565
+ let workspaceResult = {
566
+ changedFiles: [],
567
+ packageNames: []
568
+ };
569
+ await runWithProgress(
570
+ async () => {
571
+ workspaceResult = await synchronizeWorkspacePackageSpecs({
572
+ appRoot,
573
+ createCliError,
574
+ dryRun,
575
+ latestVersions,
576
+ packageJson,
577
+ registryArgs,
578
+ stderr,
579
+ stdout
580
+ });
581
+ },
582
+ {
583
+ activity: "aligning JSKIT ranges in workspace manifests and descriptors",
584
+ stdout,
585
+ step: "Step 2/3"
586
+ }
587
+ );
588
+ stdout?.write(
589
+ `[jskit:update] Step 2/3 summary: ${workspaceResult.changedFiles.length} workspace files ${dryRun ? "would change" : "changed"}.\n`
590
+ );
591
+
592
+ if (dryRun) {
593
+ stdout?.write(
594
+ `[jskit:update] Step 3/3 skipped in dry-run mode: ${workspaceResult.packageNames.length} workspace JSKIT packages would be refreshed.\n`
595
+ );
596
+ } else if (workspaceResult.packageNames.length > 0) {
597
+ await runWithProgress(
598
+ () => runExternalCommandAsync(
599
+ "npm",
600
+ ["update", ...registryArgs, "--workspaces", ...workspaceResult.packageNames],
601
+ {
602
+ cwd: appRoot,
603
+ stdout,
604
+ stderr,
605
+ createCliError
606
+ }
607
+ ),
608
+ {
609
+ activity: `refreshing ${workspaceResult.packageNames.length} workspace JSKIT packages and updating the lockfile`,
610
+ stdout,
611
+ step: "Step 3/3"
612
+ }
613
+ );
614
+ } else {
615
+ stdout?.write("[jskit:update] Step 3/3 skipped: no workspace JSKIT packages were found.\n");
104
616
  }
105
617
 
106
- stdout.write("[jskit:update] done.\n");
618
+ stdout?.write("[jskit:update] done.\n");
107
619
  return 0;
108
620
  }
109
621
 
110
- export { runAppUpdatePackagesCommand };
622
+ export {
623
+ collectChangedInstalledPackageIds,
624
+ formatElapsedTime,
625
+ reapplyChangedInstalledPackages,
626
+ runAppUpdatePackagesCommand,
627
+ runWithProgress
628
+ };
@@ -30,6 +30,7 @@ function createHealthCommands(ctx = {}) {
30
30
  inspectPackageOfferings,
31
31
  fileExists,
32
32
  normalizeRelativePath,
33
+ normalizeRelativePosixPath,
33
34
  path
34
35
  } = ctx;
35
36
 
@@ -1321,7 +1322,12 @@ function createHealthCommands(ctx = {}) {
1321
1322
  }
1322
1323
  }
1323
1324
 
1324
- async function collectMainPackageFeatureLaneWarnings({ appRoot, appLocalRegistry, warnings }) {
1325
+ async function collectMainPackageFeatureLaneWarnings({
1326
+ appRoot,
1327
+ appLocalRegistry,
1328
+ managedAppOwnedFilePaths,
1329
+ warnings
1330
+ }) {
1325
1331
  const mainPackageEntry = [...appLocalRegistry.values()].find((packageEntry) => {
1326
1332
  const packageId = String(packageEntry?.packageId || "").trim();
1327
1333
  const relativeDir = String(packageEntry?.relativeDir || "").trim();
@@ -1343,7 +1349,8 @@ function createHealthCommands(ctx = {}) {
1343
1349
  const extraDomainFiles = relativeServerFiles.filter(
1344
1350
  (relativePath) =>
1345
1351
  !MAIN_SERVER_BASELINE_RELATIVE_PATHS.has(relativePath) &&
1346
- MAIN_SERVER_DOMAIN_FILE_PATTERN.test(relativePath)
1352
+ MAIN_SERVER_DOMAIN_FILE_PATTERN.test(relativePath) &&
1353
+ !managedAppOwnedFilePaths.has(normalizeRelativePath(appRoot, path.join(rootDir, relativePath)))
1347
1354
  );
1348
1355
  if (extraDomainFiles.length > 0) {
1349
1356
  reasons.push(`extra server domain files: ${extraDomainFiles.join(", ")}`);
@@ -1438,7 +1445,13 @@ function createHealthCommands(ctx = {}) {
1438
1445
  }
1439
1446
  }
1440
1447
 
1441
- async function collectFeatureLaneDoctorIssues({ appRoot, appLocalRegistry, issues, warnings }) {
1448
+ async function collectFeatureLaneDoctorIssues({
1449
+ appRoot,
1450
+ appLocalRegistry,
1451
+ managedAppOwnedFilePaths,
1452
+ issues,
1453
+ warnings
1454
+ }) {
1442
1455
  const packageEntries = sortStrings([...appLocalRegistry.keys()])
1443
1456
  .map((packageId) => appLocalRegistry.get(packageId))
1444
1457
  .filter(Boolean);
@@ -1454,6 +1467,7 @@ function createHealthCommands(ctx = {}) {
1454
1467
  await collectMainPackageFeatureLaneWarnings({
1455
1468
  appRoot,
1456
1469
  appLocalRegistry,
1470
+ managedAppOwnedFilePaths,
1457
1471
  warnings
1458
1472
  });
1459
1473
  await collectHandmadeFeatureLaneWarnings({
@@ -2125,6 +2139,7 @@ function createHealthCommands(ctx = {}) {
2125
2139
  const issues = [];
2126
2140
  const warnings = [];
2127
2141
  const installed = ensureObject(lock.installedPackages);
2142
+ const managedAppOwnedFilePaths = new Set();
2128
2143
  await hydratePackageRegistryFromInstalledNodeModules({
2129
2144
  appRoot,
2130
2145
  packageRegistry: combinedPackageRegistry,
@@ -2148,7 +2163,10 @@ function createHealthCommands(ctx = {}) {
2148
2163
  const managed = ensureObject(lockEntry.managed);
2149
2164
  for (const fileChange of ensureArray(managed.files)) {
2150
2165
  const changeRecord = ensureObject(fileChange);
2151
- const relativePath = String(changeRecord.path || "").trim();
2166
+ const relativePath = normalizeRelativePosixPath(changeRecord.path);
2167
+ if (changeRecord.ownership === "app" && relativePath) {
2168
+ managedAppOwnedFilePaths.add(relativePath);
2169
+ }
2152
2170
  const absolutePath = path.join(appRoot, relativePath);
2153
2171
  if (!(await fileExists(absolutePath))) {
2154
2172
  issues.push(`${packageId}: managed file missing: ${relativePath}`);
@@ -2172,6 +2190,7 @@ function createHealthCommands(ctx = {}) {
2172
2190
  await collectFeatureLaneDoctorIssues({
2173
2191
  appRoot,
2174
2192
  appLocalRegistry,
2193
+ managedAppOwnedFilePaths,
2175
2194
  issues,
2176
2195
  warnings
2177
2196
  });