@jskit-ai/jskit-cli 0.2.64 → 0.2.66

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.64",
3
+ "version": "0.2.66",
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.63",
24
- "@jskit-ai/kernel": "0.1.55",
25
- "@jskit-ai/shell-web": "0.1.54"
23
+ "@jskit-ai/jskit-catalog": "0.1.65",
24
+ "@jskit-ai/kernel": "0.1.57",
25
+ "@jskit-ai/shell-web": "0.1.56"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { access, readdir, readFile } from "node:fs/promises";
4
+ import { buildCrudFieldContractMap } from "@jskit-ai/kernel/shared/support/crudFieldContract";
4
5
  import {
5
6
  buildAppCommandOptionMeta,
6
7
  listAppCommandDefinitions
@@ -659,26 +660,7 @@ async function discoverResourceDisplayFields(appRoot, resourceFile = "") {
659
660
  if (!resource || typeof resource !== "object") {
660
661
  return [];
661
662
  }
662
- const fieldKeys = new Set();
663
-
664
- const outputSchemaProperties = resource?.operations?.view?.outputValidator?.schema?.properties;
665
- if (outputSchemaProperties && typeof outputSchemaProperties === "object") {
666
- for (const key of Object.keys(outputSchemaProperties)) {
667
- if (key === resource?.contract?.lookup?.containerKey) {
668
- continue;
669
- }
670
- fieldKeys.add(key);
671
- }
672
- }
673
-
674
- for (const fieldMeta of Array.isArray(resource?.fieldMeta) ? resource.fieldMeta : []) {
675
- const key = normalizeText(fieldMeta?.key);
676
- if (key) {
677
- fieldKeys.add(key);
678
- }
679
- }
680
-
681
- return uniqueSorted([...fieldKeys]);
663
+ return uniqueSorted(Object.keys(buildCrudFieldContractMap(resource)));
682
664
  } catch {
683
665
  return [];
684
666
  }
@@ -196,7 +196,10 @@ async function applyFileMutations(
196
196
  managedMigrations,
197
197
  touchedFiles,
198
198
  warnings = [],
199
- existingManagedFiles = []
199
+ existingManagedFiles = [],
200
+ {
201
+ reapplyManagedAppFiles = false
202
+ } = {}
200
203
  ) {
201
204
  const existingManagedFilesByPath = new Map();
202
205
  for (const managedFileValue of ensureArray(existingManagedFiles)) {
@@ -242,8 +245,16 @@ async function applyFileMutations(
242
245
  const relativeTargetPath = normalizeRelativePath(appRoot, targetPath);
243
246
  const previous = await readFileBufferIfExists(targetPath);
244
247
  const existingManaged = existingManagedFilesByPath.get(relativeTargetPath);
248
+ const existingManagedHash = String(existingManaged?.hash || "").trim();
249
+ const currentContentMatchesManagedVersion =
250
+ previous.exists &&
251
+ existingManagedHash &&
252
+ hashBuffer(previous.buffer) === existingManagedHash;
253
+ const canSafelyReapplyManagedAppFile =
254
+ reapplyManagedAppFiles === true &&
255
+ (!previous.exists || currentContentMatchesManagedVersion);
245
256
 
246
- if (mutation.ownership === "app" && existingManaged) {
257
+ if (mutation.ownership === "app" && existingManaged && !canSafelyReapplyManagedAppFile) {
247
258
  managedFiles.push({
248
259
  ...existingManaged,
249
260
  path: relativeTargetPath,
@@ -30,80 +30,11 @@ import {
30
30
  } from "./templateContext.js";
31
31
  import { normalizeMutationRelativeFilePath } from "./mutationPathUtils.js";
32
32
 
33
- const SETTINGS_FIELDS_CONTRACT_TARGETS = Object.freeze({
34
- "packages/main/src/shared/resources/consoleSettingsFields.js": Object.freeze({
35
- contractId: "console.settings-fields.v1",
36
- marker: "@jskit-contract console.settings-fields.v1",
37
- requiredSnippets: Object.freeze([
38
- "defineField",
39
- "resetConsoleSettingsFields"
40
- ])
41
- }),
42
- "packages/main/src/shared/resources/workspaceSettingsFields.js": Object.freeze({
43
- contractId: "users.settings-fields.workspace.v1",
44
- marker: "@jskit-contract users.settings-fields.workspace.v1",
45
- requiredSnippets: Object.freeze([
46
- "defineField",
47
- "resetWorkspaceSettingsFields"
48
- ])
49
- })
50
- });
51
33
  const PRE_FILE_CONFIG_MUTATION_TARGETS = new Set([
52
34
  "config/public.js",
53
35
  "config/server.js"
54
36
  ]);
55
37
 
56
- function resolveSettingsFieldsContractTarget(relativeFile = "") {
57
- const normalizedRelativeFile = normalizeMutationRelativeFilePath(relativeFile);
58
- if (!normalizedRelativeFile) {
59
- return null;
60
- }
61
- const target = SETTINGS_FIELDS_CONTRACT_TARGETS[normalizedRelativeFile];
62
- if (!target) {
63
- return null;
64
- }
65
- return {
66
- normalizedRelativeFile,
67
- target
68
- };
69
- }
70
-
71
- async function validateSettingsFieldsContractMutationTarget({
72
- appRoot,
73
- relativeFile,
74
- packageId
75
- } = {}) {
76
- const contractTarget = resolveSettingsFieldsContractTarget(relativeFile);
77
- if (!contractTarget) {
78
- return;
79
- }
80
-
81
- const { normalizedRelativeFile, target } = contractTarget;
82
- const absoluteFile = path.join(appRoot, normalizedRelativeFile);
83
- const existing = await readFileBufferIfExists(absoluteFile);
84
- if (!existing.exists) {
85
- throw createCliError(
86
- `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing. ` +
87
- `Install @jskit-ai/console-core to scaffold ${target.contractId}.`
88
- );
89
- }
90
-
91
- const source = existing.buffer.toString("utf8");
92
- if (!source.includes(target.marker)) {
93
- throw createCliError(
94
- `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing contract marker "${target.marker}".`
95
- );
96
- }
97
- for (const snippet of target.requiredSnippets) {
98
- if (source.includes(snippet)) {
99
- continue;
100
- }
101
- throw createCliError(
102
- `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} must include "${snippet}" for ${target.contractId}.`
103
- );
104
- }
105
- }
106
-
107
38
  async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
108
39
  for (const mutation of textMutations) {
109
40
  const when = normalizeMutationWhen(mutation?.when);
@@ -167,11 +98,6 @@ async function applyTextMutations(packageEntry, appRoot, textMutations, options,
167
98
  if (position !== "top" && position !== "bottom") {
168
99
  throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "position" must be "top" or "bottom".`);
169
100
  }
170
- await validateSettingsFieldsContractMutationTarget({
171
- appRoot,
172
- relativeFile,
173
- packageId: packageEntry.packageId
174
- });
175
101
 
176
102
  const absoluteFile = path.join(appRoot, relativeFile);
177
103
  const previous = await readFileBufferIfExists(absoluteFile);
@@ -18,6 +18,9 @@ import {
18
18
  applyPackageJsonField,
19
19
  removePackageJsonField
20
20
  } from "./appState.js";
21
+ import {
22
+ loadMutationWhenConfigContext
23
+ } from "./ioAndMigrations.js";
21
24
  import {
22
25
  isGeneratorPackageEntry,
23
26
  loadAppLocalPackageRegistry
@@ -82,6 +85,44 @@ function cloneManagedArray(value = []) {
82
85
  }));
83
86
  }
84
87
 
88
+ function normalizeModeToken(value = "") {
89
+ return String(value || "").trim().toLowerCase();
90
+ }
91
+
92
+ function isWorkspaceCapableTenancyMode(value = "") {
93
+ const normalized = normalizeModeToken(value);
94
+ return normalized === "personal" || normalized === "workspaces";
95
+ }
96
+
97
+ async function collectInstallWarnings({
98
+ packageEntry,
99
+ appRoot,
100
+ appPackageJson
101
+ }) {
102
+ const warnings = [];
103
+
104
+ if (packageEntry?.packageId !== "@jskit-ai/users-core") {
105
+ return warnings;
106
+ }
107
+
108
+ const configContext = await loadMutationWhenConfigContext(appRoot);
109
+ const tenancyMode = normalizeModeToken(ensureObject(configContext).merged?.tenancyMode);
110
+ const runtimeDependencies = ensureObject(appPackageJson.dependencies);
111
+ const devDependencies = ensureObject(appPackageJson.devDependencies);
112
+ const hasWorkspacesCore = Boolean(
113
+ runtimeDependencies["@jskit-ai/workspaces-core"] || devDependencies["@jskit-ai/workspaces-core"]
114
+ );
115
+
116
+ if (isWorkspaceCapableTenancyMode(tenancyMode) && !hasWorkspacesCore) {
117
+ warnings.push(
118
+ `users-core selected the workspace users scaffold because config.tenancyMode is "${tenancyMode}". ` +
119
+ 'Install @jskit-ai/workspaces-core so the app gets the required "app" and "admin" surfaces and workspace helpers.'
120
+ );
121
+ }
122
+
123
+ return warnings;
124
+ }
125
+
85
126
  function resolveManagedSourceRecord(packageEntry, existingInstall = {}) {
86
127
  const existingSource = ensureObject(existingInstall.source);
87
128
  if (Object.keys(existingSource).length > 0) {
@@ -462,7 +503,10 @@ async function applyPackageInstall({
462
503
  managedRecord.managed.migrations,
463
504
  touchedFiles,
464
505
  mutationWarnings,
465
- ensureArray(existingManaged.files)
506
+ ensureArray(existingManaged.files),
507
+ {
508
+ reapplyManagedAppFiles: Object.keys(existingInstall).length > 0
509
+ }
466
510
  );
467
511
 
468
512
  await applyTextMutations(
@@ -483,6 +527,12 @@ async function applyPackageInstall({
483
527
  touchedFiles
484
528
  );
485
529
 
530
+ mutationWarnings.push(...await collectInstallWarnings({
531
+ packageEntry,
532
+ appRoot,
533
+ appPackageJson
534
+ }));
535
+
486
536
  if (generatorPackage) {
487
537
  delete lock.installedPackages[packageEntry.packageId];
488
538
  } else {
@@ -5,7 +5,7 @@ const APP_SCRIPT_WRAPPERS = Object.freeze({
5
5
  release: "jskit app release"
6
6
  });
7
7
 
8
- const LEGACY_APP_SCRIPT_VALUES = Object.freeze({
8
+ const COPIED_APP_SCRIPT_VALUES = Object.freeze({
9
9
  verify: Object.freeze([
10
10
  "npm run lint && npm run test && npm run test:client && npm run build && npx jskit doctor"
11
11
  ]),
@@ -20,7 +20,7 @@ const LEGACY_APP_SCRIPT_VALUES = Object.freeze({
20
20
  ])
21
21
  });
22
22
 
23
- const LEGACY_APP_SCRIPT_FILES = Object.freeze([
23
+ const COPIED_APP_SCRIPT_FILES = Object.freeze([
24
24
  "scripts/update-jskit-packages.sh",
25
25
  "scripts/link-local-jskit-packages.sh",
26
26
  "scripts/release.sh"
@@ -120,7 +120,7 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
120
120
  }),
121
121
  "adopt-managed-scripts": Object.freeze({
122
122
  name: "adopt-managed-scripts",
123
- summary: "Rewrite legacy scaffolded maintenance scripts to the modern jskit app wrappers.",
123
+ summary: "Rewrite copied scaffolded maintenance scripts to the managed jskit app wrappers.",
124
124
  usage: "jskit app adopt-managed-scripts [--dry-run] [--force]",
125
125
  options: Object.freeze([
126
126
  Object.freeze({
@@ -129,13 +129,13 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
129
129
  }),
130
130
  Object.freeze({
131
131
  label: "--force",
132
- description: "Replace customized script values too, and remove the legacy copied maintenance scripts if they exist."
132
+ description: "Replace customized script values too, and remove copied maintenance scripts if they exist."
133
133
  })
134
134
  ]),
135
135
  defaults: Object.freeze([
136
136
  "Known scaffolded script values are rewritten automatically.",
137
137
  "Customized script values are reported and left alone unless --force is used.",
138
- "This command is the migration path for existing apps that still carry copied JSKIT maintenance scripts."
138
+ "This command is for apps that still carry copied JSKIT maintenance scripts."
139
139
  ])
140
140
  })
141
141
  });
@@ -190,8 +190,8 @@ function buildAppCommandOptionMeta(subcommandName = "") {
190
190
 
191
191
  export {
192
192
  APP_SCRIPT_WRAPPERS,
193
- LEGACY_APP_SCRIPT_VALUES,
194
- LEGACY_APP_SCRIPT_FILES,
193
+ COPIED_APP_SCRIPT_VALUES,
194
+ COPIED_APP_SCRIPT_FILES,
195
195
  APP_COMMAND_DEFINITIONS,
196
196
  listAppCommandDefinitions,
197
197
  resolveAppCommandDefinition,
@@ -2,8 +2,8 @@ import path from "node:path";
2
2
  import { rm } from "node:fs/promises";
3
3
  import {
4
4
  APP_SCRIPT_WRAPPERS,
5
- LEGACY_APP_SCRIPT_FILES,
6
- LEGACY_APP_SCRIPT_VALUES
5
+ COPIED_APP_SCRIPT_FILES,
6
+ COPIED_APP_SCRIPT_VALUES
7
7
  } from "../appCommandCatalog.js";
8
8
  import { fileExists, isTruthyFlag } from "./shared.js";
9
9
 
@@ -22,10 +22,10 @@ function shouldRewriteScript(currentValue = "", scriptName = "", force = false)
22
22
  reason: "already-current"
23
23
  };
24
24
  }
25
- if ((LEGACY_APP_SCRIPT_VALUES[scriptName] || []).includes(normalizedCurrentValue)) {
25
+ if ((COPIED_APP_SCRIPT_VALUES[scriptName] || []).includes(normalizedCurrentValue)) {
26
26
  return {
27
27
  rewrite: true,
28
- reason: "legacy"
28
+ reason: "copied"
29
29
  };
30
30
  }
31
31
  if (force) {
@@ -84,12 +84,12 @@ async function runAppAdoptManagedScriptsCommand(ctx = {}, { appRoot = "", option
84
84
  });
85
85
  }
86
86
 
87
- const removableLegacyFiles = [];
87
+ const removableCopiedFiles = [];
88
88
  if (force) {
89
- for (const relativePath of LEGACY_APP_SCRIPT_FILES) {
89
+ for (const relativePath of COPIED_APP_SCRIPT_FILES) {
90
90
  const absolutePath = path.join(appRoot, relativePath);
91
91
  if (await fileExists(absolutePath)) {
92
- removableLegacyFiles.push({
92
+ removableCopiedFiles.push({
93
93
  relativePath,
94
94
  absolutePath
95
95
  });
@@ -102,12 +102,12 @@ async function runAppAdoptManagedScriptsCommand(ctx = {}, { appRoot = "", option
102
102
  }
103
103
 
104
104
  if (!dryRun && force) {
105
- for (const { absolutePath } of removableLegacyFiles) {
105
+ for (const { absolutePath } of removableCopiedFiles) {
106
106
  await rm(absolutePath, { recursive: true, force: true });
107
107
  }
108
108
  }
109
109
 
110
- if (changedScripts.length < 1 && skippedScripts.length < 1 && removableLegacyFiles.length < 1) {
110
+ if (changedScripts.length < 1 && skippedScripts.length < 1 && removableCopiedFiles.length < 1) {
111
111
  stdout.write("[adopt-managed-scripts] package.json already uses the managed JSKIT wrappers.\n");
112
112
  return 0;
113
113
  }
@@ -118,7 +118,7 @@ async function runAppAdoptManagedScriptsCommand(ctx = {}, { appRoot = "", option
118
118
  for (const record of skippedScripts) {
119
119
  stdout.write(`[adopt-managed-scripts] kept customized script ${record.scriptName}: ${record.currentValue}\n`);
120
120
  }
121
- for (const record of removableLegacyFiles) {
121
+ for (const record of removableCopiedFiles) {
122
122
  stdout.write(`[adopt-managed-scripts] ${dryRun ? "would remove" : "removed"} ${record.relativePath}\n`);
123
123
  }
124
124
 
@@ -128,16 +128,33 @@ async function runPackageMigrationsCommand(ctx = {}, { positional, options, cwd,
128
128
  warnings: migrationWarnings
129
129
  }, null, 2)}\n`);
130
130
  } else {
131
- io.stdout.write(`Generated migrations (${scope}).\n`);
132
- io.stdout.write(`Resolved packages (${requestedPackageIds.length}):\n`);
133
- for (const packageId of requestedPackageIds) {
134
- io.stdout.write(`- ${packageId}\n`);
131
+ io.stdout.write(`Generated managed migrations (${scope}).\n`);
132
+ io.stdout.write(`Packages needing migration sync (${requestedPackageIds.length}):\n`);
133
+ if (requestedPackageIds.length > 0) {
134
+ for (const packageId of requestedPackageIds) {
135
+ io.stdout.write(`- ${packageId}\n`);
136
+ }
137
+ } else {
138
+ io.stdout.write("- none\n");
139
+ io.stdout.write(
140
+ " Installed packages are already migration-synced, or they do not ship JSKIT-managed migrations.\n"
141
+ );
135
142
  }
136
- io.stdout.write(`Touched files (${touchedFileList.length}):\n`);
137
- for (const touchedFile of touchedFileList) {
138
- io.stdout.write(`- ${touchedFile}\n`);
143
+
144
+ io.stdout.write(`Migration files written (${touchedFileList.length}):\n`);
145
+ if (touchedFileList.length > 0) {
146
+ for (const touchedFile of touchedFileList) {
147
+ io.stdout.write(`- ${touchedFile}\n`);
148
+ }
149
+ } else {
150
+ io.stdout.write("- none\n");
151
+ io.stdout.write(" No managed migration files were written in this run.\n");
139
152
  }
153
+
140
154
  io.stdout.write(`Lock file: ${normalizeRelativePath(appRoot, lockPath)}\n`);
155
+ io.stdout.write(
156
+ "Reminder: this command only writes or refreshes JSKIT-managed migration files. Run `npm run db:migrate` separately to apply pending migrations to the database.\n"
157
+ );
141
158
  if (options.verbose && migrationWarnings.length > 0) {
142
159
  io.stdout.write(`Warnings (${migrationWarnings.length}):\n`);
143
160
  for (const warning of migrationWarnings) {