@jskit-ai/jskit-cli 0.2.54 → 0.2.56

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.54",
3
+ "version": "0.2.56",
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.53",
24
- "@jskit-ai/kernel": "0.1.45",
25
- "@jskit-ai/shell-web": "0.1.44"
23
+ "@jskit-ai/jskit-catalog": "0.1.55",
24
+ "@jskit-ai/kernel": "0.1.47",
25
+ "@jskit-ai/shell-web": "0.1.46"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -232,6 +232,7 @@ function removeEnvValue(content, key, expectedValue, previous) {
232
232
  }
233
233
 
234
234
  export {
235
+ directoryLooksLikeJskitAppRoot,
235
236
  resolveAppRootFromCwd,
236
237
  loadAppPackageJson,
237
238
  createDefaultLock,
@@ -13,6 +13,7 @@ import { runAppLinkLocalPackagesCommand } from "./appCommands/linkLocalPackages.
13
13
  import { runAppReleaseCommand } from "./appCommands/release.js";
14
14
  import { runAppUpdatePackagesCommand } from "./appCommands/updatePackages.js";
15
15
  import { runAppVerifyCommand } from "./appCommands/verify.js";
16
+ import { runAppVerifyUiCommand } from "./appCommands/verifyUi.js";
16
17
 
17
18
  function renderAppHelp(stream, definition = null) {
18
19
  const color = createColorFormatter(stream);
@@ -135,6 +136,9 @@ function createAppCommands(ctx = {}) {
135
136
  if (definition.name === "verify") {
136
137
  return runAppVerifyCommand(ctx, { appRoot, options, stdout, stderr });
137
138
  }
139
+ if (definition.name === "verify-ui") {
140
+ return runAppVerifyUiCommand(ctx, { appRoot, options, stdout, stderr });
141
+ }
138
142
  if (definition.name === "update-packages") {
139
143
  return runAppUpdatePackagesCommand(ctx, { appRoot, options, stdout, stderr });
140
144
  }
@@ -38,6 +38,30 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
38
38
  "The scaffolded npm run verify wrapper can append npm run --if-present verify:app afterwards."
39
39
  ])
40
40
  }),
41
+ "verify-ui": Object.freeze({
42
+ name: "verify-ui",
43
+ 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>",
45
+ options: Object.freeze([
46
+ Object.freeze({
47
+ label: "--command <shell-command>",
48
+ description: "Targeted Playwright command to run, for example: npx playwright test tests/e2e/contacts.spec.ts -g filters."
49
+ }),
50
+ Object.freeze({
51
+ label: "--feature <label>",
52
+ description: "Short human label for the UI feature or flow that was verified."
53
+ }),
54
+ Object.freeze({
55
+ label: "--auth-mode <mode>",
56
+ description: "Auth path used by the Playwright flow: none | dev-auth-login-as | session-bootstrap | custom-local."
57
+ })
58
+ ]),
59
+ defaults: Object.freeze([
60
+ "Requires a git working tree so the receipt can record the currently changed UI files.",
61
+ "Writes .jskit/verification/ui.json after the command succeeds.",
62
+ "Doctor expects the receipt to match the current dirty UI file set."
63
+ ])
64
+ }),
41
65
  "update-packages": Object.freeze({
42
66
  name: "update-packages",
43
67
  summary: "Update installed @jskit-ai dependencies and refresh managed migrations.",
@@ -152,6 +176,11 @@ function buildAppCommandOptionMeta(subcommandName = "") {
152
176
  if (definition.name === "update-packages" || definition.name === "release") {
153
177
  optionMeta.registry = { inputType: "text" };
154
178
  }
179
+ if (definition.name === "verify-ui") {
180
+ optionMeta.command = { inputType: "text" };
181
+ optionMeta.feature = { inputType: "text" };
182
+ optionMeta["auth-mode"] = { inputType: "text" };
183
+ }
155
184
  if (definition.name === "link-local-packages") {
156
185
  optionMeta["repo-root"] = { inputType: "text" };
157
186
  }
@@ -80,6 +80,36 @@ function runExternalCommand(
80
80
  });
81
81
  }
82
82
 
83
+ function runExternalShellCommand(
84
+ commandText,
85
+ {
86
+ cwd = "",
87
+ env = {},
88
+ stdout,
89
+ stderr,
90
+ quiet = false,
91
+ createCliError
92
+ } = {}
93
+ ) {
94
+ const result = spawnSync(String(commandText || ""), {
95
+ cwd: cwd || process.cwd(),
96
+ encoding: "utf8",
97
+ shell: true,
98
+ env: {
99
+ ...process.env,
100
+ ...env
101
+ }
102
+ });
103
+
104
+ return ensureCommandSucceeded(result, String(commandText || "").trim() || "shell command", {
105
+ createCliError,
106
+ cwd,
107
+ stdout,
108
+ stderr,
109
+ quiet
110
+ });
111
+ }
112
+
83
113
  function formatUtcReleaseTimestamp(date = new Date()) {
84
114
  const year = date.getUTCFullYear();
85
115
  const month = String(date.getUTCMonth() + 1).padStart(2, "0");
@@ -265,6 +295,7 @@ export {
265
295
  normalizeText,
266
296
  isTruthyFlag,
267
297
  runExternalCommand,
298
+ runExternalShellCommand,
268
299
  formatUtcReleaseTimestamp,
269
300
  resolveLocalJskitBin,
270
301
  runLocalJskit,
@@ -0,0 +1,102 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { runExternalShellCommand } from "./shared.js";
4
+ import {
5
+ directoryLooksLikeJskitAppRoot
6
+ } from "../../cliRuntime/appState.js";
7
+ import {
8
+ UI_VERIFICATION_RECEIPT_RELATIVE_PATH,
9
+ UI_VERIFICATION_RECEIPT_VERSION,
10
+ UI_VERIFICATION_RUNNER,
11
+ isUiVerificationAuthMode,
12
+ resolveChangedUiFilesFromGit
13
+ } from "../../shared/uiVerification.js";
14
+
15
+ async function runAppVerifyUiCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
16
+ const { createCliError } = ctx;
17
+
18
+ if (options?.dryRun) {
19
+ throw createCliError("jskit app verify-ui does not support --dry-run.", { exitCode: 1 });
20
+ }
21
+
22
+ const inlineOptions =
23
+ options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {};
24
+ const command = String(inlineOptions.command || "").trim();
25
+ const feature = String(inlineOptions.feature || "").trim();
26
+ const authMode = String(inlineOptions["auth-mode"] || "").trim();
27
+
28
+ if (!command) {
29
+ throw createCliError("jskit app verify-ui requires --command <shell-command>.", {
30
+ exitCode: 1
31
+ });
32
+ }
33
+ if (!feature) {
34
+ throw createCliError("jskit app verify-ui requires --feature <label>.", {
35
+ exitCode: 1
36
+ });
37
+ }
38
+ if (!isUiVerificationAuthMode(authMode)) {
39
+ throw createCliError(
40
+ "jskit app verify-ui requires --auth-mode <none|dev-auth-login-as|session-bootstrap|custom-local>.",
41
+ {
42
+ exitCode: 1
43
+ }
44
+ );
45
+ }
46
+ if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
47
+ throw createCliError(
48
+ "jskit app verify-ui only works in a JSKIT app root (expected app.json or .jskit/lock.json).",
49
+ {
50
+ exitCode: 1
51
+ }
52
+ );
53
+ }
54
+
55
+ const changedUiState = resolveChangedUiFilesFromGit(appRoot);
56
+ 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
+ });
60
+ }
61
+ 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
+ });
65
+ }
66
+
67
+ runExternalShellCommand(command, {
68
+ cwd: appRoot,
69
+ stdout,
70
+ stderr,
71
+ createCliError
72
+ });
73
+
74
+ const recordedAt = new Date().toISOString();
75
+ const receiptPath = path.join(appRoot, UI_VERIFICATION_RECEIPT_RELATIVE_PATH);
76
+ await mkdir(path.dirname(receiptPath), { recursive: true });
77
+ await writeFile(
78
+ receiptPath,
79
+ `${JSON.stringify(
80
+ {
81
+ version: UI_VERIFICATION_RECEIPT_VERSION,
82
+ runner: UI_VERIFICATION_RUNNER,
83
+ recordedAt,
84
+ feature,
85
+ command,
86
+ authMode,
87
+ changedUiFiles: changedUiState.paths
88
+ },
89
+ null,
90
+ 2
91
+ )}\n`,
92
+ "utf8"
93
+ );
94
+
95
+ stdout.write(
96
+ `[verify-ui] wrote ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} for ${changedUiState.paths.length} changed UI file(s).\n`
97
+ );
98
+
99
+ return 0;
100
+ }
101
+
102
+ export { runAppVerifyUiCommand };
@@ -7,9 +7,16 @@ import {
7
7
  ensureObject,
8
8
  sortStrings
9
9
  } from "../shared/collectionUtils.js";
10
+ import {
11
+ UI_VERIFICATION_RECEIPT_RELATIVE_PATH,
12
+ isValidUiVerificationReceipt,
13
+ normalizeUiVerificationReceipt,
14
+ resolveChangedUiFilesFromGit
15
+ } from "../shared/uiVerification.js";
10
16
 
11
17
  function createHealthCommands(ctx = {}) {
12
18
  const {
19
+ directoryLooksLikeJskitAppRoot,
13
20
  resolveAppRootFromCwd,
14
21
  loadLockFile,
15
22
  loadPackageRegistry,
@@ -621,6 +628,49 @@ function createHealthCommands(ctx = {}) {
621
628
  }
622
629
  }
623
630
 
631
+ async function collectUiVerificationDoctorIssues({ appRoot, issues }) {
632
+ if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
633
+ return;
634
+ }
635
+
636
+ const changedUiState = resolveChangedUiFilesFromGit(appRoot);
637
+ if (!changedUiState.available || changedUiState.paths.length < 1) {
638
+ return;
639
+ }
640
+
641
+ const receiptPath = path.join(appRoot, UI_VERIFICATION_RECEIPT_RELATIVE_PATH);
642
+ if (!(await fileExists(receiptPath))) {
643
+ issues.push(
644
+ `[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(", ")}`
645
+ );
646
+ return;
647
+ }
648
+
649
+ let parsedReceipt = null;
650
+ try {
651
+ parsedReceipt = JSON.parse(await readFile(receiptPath, "utf8"));
652
+ } catch (error) {
653
+ issues.push(
654
+ `[ui:verification] ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
655
+ );
656
+ return;
657
+ }
658
+
659
+ const receipt = normalizeUiVerificationReceipt(parsedReceipt);
660
+ if (!isValidUiVerificationReceipt(receipt)) {
661
+ issues.push(
662
+ `[ui:verification] ${UI_VERIFICATION_RECEIPT_RELATIVE_PATH} is incomplete. It must include version, runner, recordedAt, feature, command, authMode, and changedUiFiles from jskit app verify-ui.`
663
+ );
664
+ return;
665
+ }
666
+
667
+ if (JSON.stringify(receipt.changedUiFiles) !== JSON.stringify(changedUiState.paths)) {
668
+ issues.push(
669
+ `[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(", ")}`
670
+ );
671
+ }
672
+ }
673
+
624
674
  function collectDiLabelParityIssuesForPackage({ packageEntry, packageInsights }) {
625
675
  const packageId = String(packageEntry?.packageId || "").trim();
626
676
  const descriptor = ensureObject(packageEntry?.descriptor);
@@ -713,6 +763,10 @@ function createHealthCommands(ctx = {}) {
713
763
  appRoot,
714
764
  issues
715
765
  });
766
+ await collectUiVerificationDoctorIssues({
767
+ appRoot,
768
+ issues
769
+ });
716
770
 
717
771
  const payload = {
718
772
  appRoot,
@@ -6,6 +6,7 @@ function createCommandHandlerDeps(deps = {}) {
6
6
  writeWrappedItems: deps.writeWrappedItems,
7
7
  normalizeRelativePath: deps.normalizeRelativePath,
8
8
  normalizeRelativePosixPath: deps.normalizeRelativePosixPath,
9
+ directoryLooksLikeJskitAppRoot: deps.directoryLooksLikeJskitAppRoot,
9
10
  resolveAppRootFromCwd: deps.resolveAppRootFromCwd,
10
11
  loadLockFile: deps.loadLockFile,
11
12
  loadPackageRegistry: deps.loadPackageRegistry,
@@ -33,6 +33,7 @@ import {
33
33
  readFileBufferIfExists
34
34
  } from "../cliRuntime/ioAndMigrations.js";
35
35
  import {
36
+ directoryLooksLikeJskitAppRoot,
36
37
  resolveAppRootFromCwd,
37
38
  loadAppPackageJson,
38
39
  loadLockFile,
@@ -97,6 +98,7 @@ const commandHandlers = createCommandHandlers(
97
98
  writeWrappedItems,
98
99
  normalizeRelativePath,
99
100
  normalizeRelativePosixPath,
101
+ directoryLooksLikeJskitAppRoot,
100
102
  resolveAppRootFromCwd,
101
103
  loadLockFile,
102
104
  loadPackageRegistry,
@@ -0,0 +1,148 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ const UI_VERIFICATION_RECEIPT_VERSION = 1;
5
+ const UI_VERIFICATION_RECEIPT_RELATIVE_PATH = ".jskit/verification/ui.json";
6
+ const UI_VERIFICATION_RUNNER = "playwright";
7
+ const UI_VERIFICATION_AUTH_MODES = new Set([
8
+ "none",
9
+ "dev-auth-login-as",
10
+ "session-bootstrap",
11
+ "custom-local"
12
+ ]);
13
+
14
+ const UI_EXTENSION_SET = new Set([
15
+ ".vue"
16
+ ]);
17
+
18
+ const UI_SCRIPT_PATH_PATTERNS = Object.freeze([
19
+ /^src\/components\//u,
20
+ /^src\/composables\//u,
21
+ /^src\/layouts\//u,
22
+ /^src\/pages\//u,
23
+ /^src\/stores\//u,
24
+ /^src\/placement\.[A-Za-z0-9]+$/u,
25
+ /^packages\/[^/]+(?:-web)?\/src\/client\//u
26
+ ]);
27
+
28
+ function normalizeText(value = "") {
29
+ return String(value || "").trim();
30
+ }
31
+
32
+ function sortUniqueStrings(values = []) {
33
+ return [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))].sort((left, right) =>
34
+ left.localeCompare(right)
35
+ );
36
+ }
37
+
38
+ function isUiVerificationAuthMode(value = "") {
39
+ return UI_VERIFICATION_AUTH_MODES.has(normalizeText(value));
40
+ }
41
+
42
+ function isUiVerificationPath(relativePath = "") {
43
+ const normalizedPath = normalizeText(relativePath).replace(/\\/g, "/");
44
+ if (!normalizedPath) {
45
+ return false;
46
+ }
47
+
48
+ if (UI_EXTENSION_SET.has(path.extname(normalizedPath).toLowerCase())) {
49
+ return normalizedPath.startsWith("src/") || normalizedPath.startsWith("packages/");
50
+ }
51
+
52
+ return UI_SCRIPT_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
53
+ }
54
+
55
+ function readGitPathList(appRoot = "", args = []) {
56
+ const result = spawnSync("git", Array.isArray(args) ? args : [], {
57
+ cwd: appRoot || process.cwd(),
58
+ encoding: "utf8"
59
+ });
60
+
61
+ if (result?.error || result?.status !== 0) {
62
+ return {
63
+ ok: false,
64
+ paths: []
65
+ };
66
+ }
67
+
68
+ return {
69
+ ok: true,
70
+ paths: sortUniqueStrings(String(result.stdout || "").split(/\r?\n/u))
71
+ };
72
+ }
73
+
74
+ function resolveChangedUiFilesFromGit(appRoot = "") {
75
+ const gitRepoCheck = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
76
+ cwd: appRoot || process.cwd(),
77
+ encoding: "utf8"
78
+ });
79
+
80
+ if (
81
+ gitRepoCheck?.error ||
82
+ gitRepoCheck?.status !== 0 ||
83
+ normalizeText(gitRepoCheck.stdout).toLowerCase() !== "true"
84
+ ) {
85
+ return {
86
+ available: false,
87
+ paths: []
88
+ };
89
+ }
90
+
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
+ ];
96
+
97
+ const changedPaths = sortUniqueStrings(
98
+ changedPathSets
99
+ .filter((entry) => entry.ok)
100
+ .flatMap((entry) => entry.paths)
101
+ .filter((relativePath) => isUiVerificationPath(relativePath))
102
+ );
103
+
104
+ return {
105
+ available: true,
106
+ paths: changedPaths
107
+ };
108
+ }
109
+
110
+ function normalizeUiVerificationReceipt(rawReceipt = {}) {
111
+ const receipt = rawReceipt && typeof rawReceipt === "object" && !Array.isArray(rawReceipt) ? rawReceipt : {};
112
+
113
+ return Object.freeze({
114
+ version: Number.isInteger(receipt.version) ? receipt.version : null,
115
+ runner: normalizeText(receipt.runner),
116
+ recordedAt: normalizeText(receipt.recordedAt),
117
+ feature: normalizeText(receipt.feature),
118
+ command: normalizeText(receipt.command),
119
+ authMode: normalizeText(receipt.authMode),
120
+ changedUiFiles: sortUniqueStrings(receipt.changedUiFiles)
121
+ });
122
+ }
123
+
124
+ function isValidUiVerificationReceipt(receipt) {
125
+ const normalizedReceipt = normalizeUiVerificationReceipt(receipt);
126
+
127
+ return (
128
+ normalizedReceipt.version === UI_VERIFICATION_RECEIPT_VERSION &&
129
+ normalizedReceipt.runner === UI_VERIFICATION_RUNNER &&
130
+ Boolean(normalizedReceipt.recordedAt) &&
131
+ Boolean(normalizedReceipt.feature) &&
132
+ Boolean(normalizedReceipt.command) &&
133
+ isUiVerificationAuthMode(normalizedReceipt.authMode)
134
+ );
135
+ }
136
+
137
+ export {
138
+ UI_VERIFICATION_AUTH_MODES,
139
+ UI_VERIFICATION_RECEIPT_RELATIVE_PATH,
140
+ UI_VERIFICATION_RECEIPT_VERSION,
141
+ UI_VERIFICATION_RUNNER,
142
+ isUiVerificationAuthMode,
143
+ isUiVerificationPath,
144
+ isValidUiVerificationReceipt,
145
+ normalizeUiVerificationReceipt,
146
+ resolveChangedUiFilesFromGit,
147
+ sortUniqueStrings
148
+ };