@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 +4 -4
- package/src/server/commandHandlers/appCommandCatalog.js +17 -4
- package/src/server/commandHandlers/appCommands/verify.js +9 -1
- package/src/server/commandHandlers/appCommands/verifyUi.js +11 -7
- package/src/server/commandHandlers/health.js +26 -5
- package/src/server/core/commandCatalog.js +5 -4
- package/src/server/shared/uiVerification.js +68 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/jskit-cli",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/shell-web": "0.1.
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
1163
|
-
|
|
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: "
|
|
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
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
};
|