@jskit-ai/jskit-cli 0.2.68 → 0.2.70

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.68",
3
+ "version": "0.2.70",
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.67",
24
- "@jskit-ai/kernel": "0.1.59",
25
- "@jskit-ai/shell-web": "0.1.58"
23
+ "@jskit-ai/jskit-catalog": "0.1.69",
24
+ "@jskit-ai/kernel": "0.1.61",
25
+ "@jskit-ai/shell-web": "0.1.60"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -30,18 +30,24 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
30
30
  verify: Object.freeze({
31
31
  name: "verify",
32
32
  summary: "Run the JSKIT baseline app verification flow.",
33
- usage: "jskit app verify",
34
- options: Object.freeze([]),
33
+ usage: "jskit app verify [--against <base-ref>]",
34
+ options: Object.freeze([
35
+ Object.freeze({
36
+ label: "--against <base-ref>",
37
+ description: "Resolve changed-file checks against a branch, tag, or commit in addition to any local dirty UI files."
38
+ })
39
+ ]),
35
40
  defaults: Object.freeze([
36
41
  "Runs npm scripts lint, test, test:client, and build only when those scripts are present.",
37
42
  "Runs jskit doctor after the normal app checks.",
43
+ "Use --against <base-ref> in CI or PR validation so doctor evaluates changed-file checks against the branch delta too.",
38
44
  "The scaffolded npm run verify wrapper can append npm run --if-present verify:app afterwards."
39
45
  ])
40
46
  }),
41
47
  "verify-ui": Object.freeze({
42
48
  name: "verify-ui",
43
49
  summary: "Run a targeted Playwright command and write a UI verification receipt for jskit doctor.",
44
- usage: "jskit app verify-ui --command <shell-command> --feature <label> --auth-mode <mode>",
50
+ usage: "jskit app verify-ui --command <shell-command> --feature <label> --auth-mode <mode> [--against <base-ref>]",
45
51
  options: Object.freeze([
46
52
  Object.freeze({
47
53
  label: "--command <shell-command>",
@@ -54,12 +60,16 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
54
60
  Object.freeze({
55
61
  label: "--auth-mode <mode>",
56
62
  description: "Auth path used by the Playwright flow: none | dev-auth-login-as | session-bootstrap | custom-local."
63
+ }),
64
+ Object.freeze({
65
+ label: "--against <base-ref>",
66
+ description: "Record changed UI files against a branch, tag, or commit instead of only the current dirty worktree."
57
67
  })
58
68
  ]),
59
69
  defaults: Object.freeze([
60
70
  "Requires a git working tree so the receipt can record the currently changed UI files.",
61
71
  "Writes .jskit/verification/ui.json after the command succeeds.",
62
- "Doctor expects the receipt to match the current dirty UI file set."
72
+ "Doctor expects the receipt to match the current dirty UI file set, or the same --against <base-ref> delta when used."
63
73
  ])
64
74
  }),
65
75
  "update-packages": Object.freeze({
@@ -176,6 +186,9 @@ function buildAppCommandOptionMeta(subcommandName = "") {
176
186
  if (definition.name === "update-packages" || definition.name === "release") {
177
187
  optionMeta.registry = { inputType: "text" };
178
188
  }
189
+ if (definition.name === "verify" || definition.name === "verify-ui") {
190
+ optionMeta.against = { inputType: "text" };
191
+ }
179
192
  if (definition.name === "verify-ui") {
180
193
  optionMeta.command = { inputType: "text" };
181
194
  optionMeta.feature = { inputType: "text" };
@@ -9,6 +9,9 @@ const BASELINE_VERIFY_SCRIPTS = Object.freeze([
9
9
 
10
10
  async function runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
11
11
  const { createCliError } = ctx;
12
+ const inlineOptions =
13
+ options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {};
14
+ const against = String(inlineOptions.against || "").trim();
12
15
 
13
16
  if (options?.dryRun) {
14
17
  throw createCliError("jskit app verify does not support --dry-run.", { exitCode: 1 });
@@ -23,7 +26,12 @@ async function runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdou
23
26
  });
24
27
  }
25
28
 
26
- await runLocalJskit(appRoot, ["doctor"], {
29
+ const doctorArgs = ["doctor"];
30
+ if (against) {
31
+ doctorArgs.push("--against", against);
32
+ }
33
+
34
+ await runLocalJskit(appRoot, doctorArgs, {
27
35
  stdout,
28
36
  stderr,
29
37
  createCliError
@@ -24,6 +24,7 @@ async function runAppVerifyUiCommand(ctx = {}, { appRoot = "", options = {}, std
24
24
  const command = String(inlineOptions.command || "").trim();
25
25
  const feature = String(inlineOptions.feature || "").trim();
26
26
  const authMode = String(inlineOptions["auth-mode"] || "").trim();
27
+ const against = String(inlineOptions.against || "").trim();
27
28
 
28
29
  if (!command) {
29
30
  throw createCliError("jskit app verify-ui requires --command <shell-command>.", {
@@ -52,16 +53,18 @@ async function runAppVerifyUiCommand(ctx = {}, { appRoot = "", options = {}, std
52
53
  );
53
54
  }
54
55
 
55
- const changedUiState = resolveChangedUiFilesFromGit(appRoot);
56
+ const changedUiState = resolveChangedUiFilesFromGit(appRoot, { against });
56
57
  if (!changedUiState.available) {
57
- throw createCliError("jskit app verify-ui requires a git working tree so it can record changed UI files.", {
58
- exitCode: 1
59
- });
58
+ const message = against
59
+ ? `jskit app verify-ui could not resolve changed UI files against ${JSON.stringify(against)}: ${changedUiState.error || "unknown git error"}.`
60
+ : "jskit app verify-ui requires a git working tree so it can record changed UI files.";
61
+ throw createCliError(message, { exitCode: 1 });
60
62
  }
61
63
  if (changedUiState.paths.length < 1) {
62
- throw createCliError("jskit app verify-ui found no changed UI files in src/ or packages/.", {
63
- exitCode: 1
64
- });
64
+ const message = against
65
+ ? `jskit app verify-ui found no changed UI files in src/ or packages/ against ${JSON.stringify(against)}.`
66
+ : "jskit app verify-ui found no changed UI files in src/ or packages/.";
67
+ throw createCliError(message, { exitCode: 1 });
65
68
  }
66
69
 
67
70
  runExternalShellCommand(command, {
@@ -84,6 +87,7 @@ async function runAppVerifyUiCommand(ctx = {}, { appRoot = "", options = {}, std
84
87
  feature,
85
88
  command,
86
89
  authMode,
90
+ ...(against ? { against } : {}),
87
91
  changedUiFiles: changedUiState.paths
88
92
  },
89
93
  null,
@@ -1154,20 +1154,32 @@ function createHealthCommands(ctx = {}) {
1154
1154
  }
1155
1155
  }
1156
1156
 
1157
- async function collectUiVerificationDoctorIssues({ appRoot, issues }) {
1157
+ async function collectUiVerificationDoctorIssues({ appRoot, issues, against = "" }) {
1158
1158
  if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
1159
1159
  return;
1160
1160
  }
1161
1161
 
1162
- const changedUiState = resolveChangedUiFilesFromGit(appRoot);
1163
- if (!changedUiState.available || changedUiState.paths.length < 1) {
1162
+ const normalizedAgainst = String(against || "").trim();
1163
+ const changedUiState = resolveChangedUiFilesFromGit(appRoot, {
1164
+ against: normalizedAgainst
1165
+ });
1166
+ if (!changedUiState.available) {
1167
+ if (normalizedAgainst) {
1168
+ issues.push(
1169
+ `[ui:verification] could not resolve changed UI files against ${JSON.stringify(normalizedAgainst)}: ${changedUiState.error || "unknown git error"}`
1170
+ );
1171
+ }
1172
+ return;
1173
+ }
1174
+ if (changedUiState.paths.length < 1) {
1164
1175
  return;
1165
1176
  }
1166
1177
 
1167
1178
  const receiptPath = path.join(appRoot, UI_VERIFICATION_RECEIPT_RELATIVE_PATH);
1168
1179
  if (!(await fileExists(receiptPath))) {
1180
+ const againstSegment = normalizedAgainst ? ` --against ${JSON.stringify(normalizedAgainst)}` : "";
1169
1181
  issues.push(
1170
- `[ui:verification] changed UI files require a matching ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} receipt. Run jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>. Current files: ${changedUiState.paths.join(", ")}`
1182
+ `[ui:verification] changed UI files require a matching ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} receipt. Run jskit app verify-ui${againstSegment} --command "<playwright command>" --feature "<label>" --auth-mode <mode>. Current files: ${changedUiState.paths.join(", ")}`
1171
1183
  );
1172
1184
  return;
1173
1185
  }
@@ -1190,6 +1202,13 @@ function createHealthCommands(ctx = {}) {
1190
1202
  return;
1191
1203
  }
1192
1204
 
1205
+ if (normalizedAgainst && receipt.against !== normalizedAgainst) {
1206
+ issues.push(
1207
+ `[ui:verification] ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} was recorded against ${JSON.stringify(receipt.against || "<dirty-worktree>")} but doctor is checking against ${JSON.stringify(normalizedAgainst)}. Re-run jskit app verify-ui with the same --against value.`
1208
+ );
1209
+ return;
1210
+ }
1211
+
1193
1212
  if (JSON.stringify(receipt.changedUiFiles) !== JSON.stringify(changedUiState.paths)) {
1194
1213
  issues.push(
1195
1214
  `[ui:verification] ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} does not match the current changed UI file set. Re-run jskit app verify-ui after the latest UI edits. Current files: ${changedUiState.paths.join(", ")}`
@@ -1251,6 +1270,7 @@ function createHealthCommands(ctx = {}) {
1251
1270
 
1252
1271
  async function commandDoctor({ cwd, options, stdout }) {
1253
1272
  const appRoot = await resolveAppRootFromCwd(cwd);
1273
+ const against = String(options?.inlineOptions?.against || "").trim();
1254
1274
  const { lock } = await loadLockFile(appRoot);
1255
1275
  const packageRegistry = await loadPackageRegistry();
1256
1276
  const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
@@ -1292,7 +1312,8 @@ function createHealthCommands(ctx = {}) {
1292
1312
  });
1293
1313
  await collectUiVerificationDoctorIssues({
1294
1314
  appRoot,
1295
- issues
1315
+ issues,
1316
+ against
1296
1317
  });
1297
1318
  await collectFeatureLaneDoctorIssues({
1298
1319
  appRoot,
@@ -475,14 +475,15 @@ const COMMAND_DESCRIPTORS = Object.freeze({
475
475
  defaults: Object.freeze([
476
476
  "Validates lock entries, managed files, and registry visibility.",
477
477
  "Reports issues as plain text by default.",
478
- "Use --json for machine-readable diagnostics."
478
+ "Use --json for machine-readable diagnostics.",
479
+ "Use --against <base-ref> when changed-file checks should compare against a branch, tag, or commit."
479
480
  ]),
480
- fullUse: "jskit doctor [--json]",
481
+ fullUse: "jskit doctor [--against <base-ref>] [--json]",
481
482
  showHelpOnBareInvocation: false,
482
483
  handlerName: "commandDoctor",
483
484
  allowedFlagKeys: Object.freeze(["json"]),
484
- inlineOptionMode: "none",
485
- allowedValueOptionNames: Object.freeze([])
485
+ inlineOptionMode: "enumerated",
486
+ allowedValueOptionNames: Object.freeze(["against"])
486
487
  }),
487
488
  "lint-descriptors": Object.freeze({
488
489
  command: "lint-descriptors",
@@ -61,17 +61,26 @@ function readGitPathList(appRoot = "", args = []) {
61
61
  if (result?.error || result?.status !== 0) {
62
62
  return {
63
63
  ok: false,
64
- paths: []
64
+ paths: [],
65
+ error: normalizeText(result?.stderr) || normalizeText(result?.error?.message)
65
66
  };
66
67
  }
67
68
 
68
69
  return {
69
70
  ok: true,
70
- paths: sortUniqueStrings(String(result.stdout || "").split(/\r?\n/u))
71
+ paths: sortUniqueStrings(String(result.stdout || "").split(/\r?\n/u)),
72
+ error: ""
71
73
  };
72
74
  }
73
75
 
74
- function resolveChangedUiFilesFromGit(appRoot = "") {
76
+ function resolveChangedPathsFromGit(
77
+ appRoot = "",
78
+ {
79
+ against = "",
80
+ pathspecs = ["src", "packages"]
81
+ } = {}
82
+ ) {
83
+ const normalizedAgainst = normalizeText(against);
75
84
  const gitRepoCheck = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
76
85
  cwd: appRoot || process.cwd(),
77
86
  encoding: "utf8"
@@ -84,26 +93,72 @@ function resolveChangedUiFilesFromGit(appRoot = "") {
84
93
  ) {
85
94
  return {
86
95
  available: false,
87
- paths: []
96
+ paths: [],
97
+ against: normalizedAgainst,
98
+ mode: normalizedAgainst ? "against-base-ref" : "dirty-worktree",
99
+ error: "Current directory is not inside a git work tree."
88
100
  };
89
101
  }
90
102
 
91
- const changedPathSets = [
92
- readGitPathList(appRoot, ["diff", "--name-only", "--relative", "--cached", "--", "src", "packages"]),
93
- readGitPathList(appRoot, ["diff", "--name-only", "--relative", "--", "src", "packages"]),
94
- readGitPathList(appRoot, ["ls-files", "--others", "--exclude-standard", "--", "src", "packages"])
95
- ];
103
+ const normalizedPathspecs = sortUniqueStrings(pathspecs);
104
+ const pathspecArgs = normalizedPathspecs.length > 0 ? ["--", ...normalizedPathspecs] : [];
105
+ const changedPathSets = [];
106
+
107
+ if (normalizedAgainst) {
108
+ const baseDiff = readGitPathList(appRoot, [
109
+ "diff",
110
+ "--name-only",
111
+ "--relative",
112
+ `${normalizedAgainst}...HEAD`,
113
+ ...pathspecArgs
114
+ ]);
115
+ if (!baseDiff.ok) {
116
+ return {
117
+ available: false,
118
+ paths: [],
119
+ against: normalizedAgainst,
120
+ mode: "against-base-ref",
121
+ error:
122
+ baseDiff.error ||
123
+ `Could not resolve git diff against ${JSON.stringify(normalizedAgainst)}.`
124
+ };
125
+ }
126
+ changedPathSets.push(baseDiff);
127
+ }
128
+
129
+ changedPathSets.push(
130
+ readGitPathList(appRoot, ["diff", "--name-only", "--relative", "--cached", ...pathspecArgs]),
131
+ readGitPathList(appRoot, ["diff", "--name-only", "--relative", ...pathspecArgs]),
132
+ readGitPathList(appRoot, ["ls-files", "--others", "--exclude-standard", ...pathspecArgs])
133
+ );
96
134
 
97
135
  const changedPaths = sortUniqueStrings(
98
136
  changedPathSets
99
137
  .filter((entry) => entry.ok)
100
138
  .flatMap((entry) => entry.paths)
101
- .filter((relativePath) => isUiVerificationPath(relativePath))
102
139
  );
103
140
 
104
141
  return {
105
142
  available: true,
106
- paths: changedPaths
143
+ paths: changedPaths,
144
+ against: normalizedAgainst,
145
+ mode: normalizedAgainst ? "against-base-ref" : "dirty-worktree",
146
+ error: ""
147
+ };
148
+ }
149
+
150
+ function resolveChangedUiFilesFromGit(appRoot = "", { against = "" } = {}) {
151
+ const changedPathState = resolveChangedPathsFromGit(appRoot, {
152
+ against,
153
+ pathspecs: ["src", "packages"]
154
+ });
155
+ if (!changedPathState.available) {
156
+ return changedPathState;
157
+ }
158
+
159
+ return {
160
+ ...changedPathState,
161
+ paths: changedPathState.paths.filter((relativePath) => isUiVerificationPath(relativePath))
107
162
  };
108
163
  }
109
164
 
@@ -117,6 +172,7 @@ function normalizeUiVerificationReceipt(rawReceipt = {}) {
117
172
  feature: normalizeText(receipt.feature),
118
173
  command: normalizeText(receipt.command),
119
174
  authMode: normalizeText(receipt.authMode),
175
+ against: normalizeText(receipt.against),
120
176
  changedUiFiles: sortUniqueStrings(receipt.changedUiFiles)
121
177
  });
122
178
  }
@@ -143,6 +199,7 @@ export {
143
199
  isUiVerificationPath,
144
200
  isValidUiVerificationReceipt,
145
201
  normalizeUiVerificationReceipt,
202
+ resolveChangedPathsFromGit,
146
203
  resolveChangedUiFilesFromGit,
147
204
  sortUniqueStrings
148
205
  };