@jskit-ai/jskit-cli 0.2.67 → 0.2.69
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/cliRuntime/mutationWhen.js +10 -2
- package/src/server/commandHandlers/appCommandCatalog.js +17 -4
- package/src/server/commandHandlers/appCommands/linkLocalPackages.js +23 -7
- package/src/server/commandHandlers/appCommands/verify.js +9 -1
- package/src/server/commandHandlers/appCommands/verifyUi.js +11 -7
- package/src/server/commandHandlers/health.js +397 -7
- package/src/server/commandHandlers/list.js +64 -2
- package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +60 -1
- package/src/server/commandHandlers/packageCommands/generate.js +2 -2
- package/src/server/commandHandlers/packageCommands/update.js +3 -1
- package/src/server/commandHandlers/show/renderPackageText.js +75 -0
- package/src/server/core/commandCatalog.js +13 -9
- package/src/server/core/usageHelp.js +6 -0
- 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.69",
|
|
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.68",
|
|
24
|
+
"@jskit-ai/kernel": "0.1.60",
|
|
25
|
+
"@jskit-ai/shell-web": "0.1.59"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": "20.x"
|
|
@@ -69,8 +69,9 @@ function normalizeMutationWhen(value) {
|
|
|
69
69
|
const notContains = String(source.notContains || "").trim();
|
|
70
70
|
const includes = ensureArray(source.in).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
71
71
|
const excludes = ensureArray(source.notIn).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
72
|
+
const hasText = typeof source.hasText === "boolean" ? source.hasText : null;
|
|
72
73
|
|
|
73
|
-
if (!option && !config && allConditions.length < 1 && anyConditions.length < 1) {
|
|
74
|
+
if (!option && !config && allConditions.length < 1 && anyConditions.length < 1 && hasText == null) {
|
|
74
75
|
return null;
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -84,7 +85,8 @@ function normalizeMutationWhen(value) {
|
|
|
84
85
|
contains,
|
|
85
86
|
notContains,
|
|
86
87
|
includes,
|
|
87
|
-
excludes
|
|
88
|
+
excludes,
|
|
89
|
+
hasText
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -270,6 +272,12 @@ function shouldApplyMutationWhen(
|
|
|
270
272
|
const notContains = normalizeWhenComparisonValue(when.notContains);
|
|
271
273
|
const includes = ensureArray(when.includes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
|
|
272
274
|
const excludes = ensureArray(when.excludes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
|
|
275
|
+
const hasText = typeof when.hasText === "boolean" ? when.hasText : null;
|
|
276
|
+
const sourceText = normalizeWhenSourceValue(sourceValue);
|
|
277
|
+
|
|
278
|
+
if (hasText != null && (sourceText.length > 0) !== hasText) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
273
281
|
|
|
274
282
|
if (equals && optionValue !== equals) {
|
|
275
283
|
return false;
|
|
@@ -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" };
|
|
@@ -60,6 +60,17 @@ async function verifySymlinkTarget(targetPath = "", sourceDir = "", {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
async function replaceWithSymlink(targetPath = "", sourceDir = "", {
|
|
64
|
+
packageName = ""
|
|
65
|
+
} = {}) {
|
|
66
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
67
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
68
|
+
await symlink(sourceDir, targetPath, resolveSymlinkType());
|
|
69
|
+
await verifySymlinkTarget(targetPath, sourceDir, {
|
|
70
|
+
packageName
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
async function maybeLinkCompanionPackages({
|
|
64
75
|
appRoot = "",
|
|
65
76
|
repoRoot = "",
|
|
@@ -105,15 +116,19 @@ async function maybeLinkCompanionPackages({
|
|
|
105
116
|
);
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
const
|
|
109
|
-
await
|
|
110
|
-
await rm(targetPath, { recursive: true, force: true });
|
|
111
|
-
await symlink(sourceDir, targetPath, resolveSymlinkType());
|
|
112
|
-
await verifySymlinkTarget(targetPath, sourceDir, {
|
|
119
|
+
const appTargetPath = path.join(appRoot, "node_modules", companion.packageName);
|
|
120
|
+
await replaceWithSymlink(appTargetPath, sourceDir, {
|
|
113
121
|
packageName: companion.packageName
|
|
114
122
|
});
|
|
115
123
|
stdout.write(`[link-local] linked ${companion.packageName} -> ${sourceDir}\n`);
|
|
116
124
|
linkedCount += 1;
|
|
125
|
+
|
|
126
|
+
const repoTargetPath = path.join(repoRoot, "node_modules", companion.packageName);
|
|
127
|
+
await replaceWithSymlink(repoTargetPath, sourceDir, {
|
|
128
|
+
packageName: `${companion.packageName} (repo root)`
|
|
129
|
+
});
|
|
130
|
+
stdout.write(`[link-local] linked repo companion ${companion.packageName} -> ${sourceDir}\n`);
|
|
131
|
+
linkedCount += 1;
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
return linkedCount;
|
|
@@ -150,8 +165,9 @@ async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options
|
|
|
150
165
|
|
|
151
166
|
for (const [packageDirName, sourceDir] of [...packageMap.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
152
167
|
const targetPath = path.join(scopeDirectory, packageDirName);
|
|
153
|
-
await
|
|
154
|
-
|
|
168
|
+
await replaceWithSymlink(targetPath, sourceDir, {
|
|
169
|
+
packageName: `@jskit-ai/${packageDirName}`
|
|
170
|
+
});
|
|
155
171
|
stdout.write(`[link-local] linked @jskit-ai/${packageDirName} -> ${sourceDir}\n`);
|
|
156
172
|
await linkPackageBinEntries({
|
|
157
173
|
appRoot,
|
|
@@ -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,
|
|
@@ -80,6 +80,34 @@ function createHealthCommands(ctx = {}) {
|
|
|
80
80
|
"useCrudView",
|
|
81
81
|
"useCrudAddEdit"
|
|
82
82
|
]);
|
|
83
|
+
const FEATURE_SERVER_SCAFFOLD_SHAPE = "feature-server-v1";
|
|
84
|
+
const FEATURE_SERVER_DEFAULT_LANE = "default";
|
|
85
|
+
const FEATURE_SERVER_JSON_REST_MODE = "json-rest";
|
|
86
|
+
const FEATURE_SERVER_PERSISTENT_MODES = new Set([
|
|
87
|
+
"json-rest",
|
|
88
|
+
"custom-knex"
|
|
89
|
+
]);
|
|
90
|
+
const FEATURE_SERVER_COMPLEX_MARKER_RELATIVE_PATHS = Object.freeze([
|
|
91
|
+
"src/server/inputSchemas.js",
|
|
92
|
+
"src/server/actions.js",
|
|
93
|
+
"src/server/service.js",
|
|
94
|
+
"src/server/repository.js",
|
|
95
|
+
"src/server/registerRoutes.js"
|
|
96
|
+
]);
|
|
97
|
+
const FEATURE_SERVER_PERSISTENCE_IMPORT_SOURCE_PATTERNS = Object.freeze([
|
|
98
|
+
/^@jskit-ai\/json-rest-api-core(?:\/|$)/u,
|
|
99
|
+
/^@jskit-ai\/database-runtime(?:\/|$)/u,
|
|
100
|
+
/^@jskit-ai\/database-runtime-mysql(?:\/|$)/u,
|
|
101
|
+
/^knex(?:\/|$)/u,
|
|
102
|
+
/^\.{1,2}\/.*repository(?:\.[A-Za-z0-9]+)?$/u
|
|
103
|
+
]);
|
|
104
|
+
const MAIN_SERVER_BASELINE_RELATIVE_PATHS = new Set([
|
|
105
|
+
"src/server/index.js",
|
|
106
|
+
"src/server/MainServiceProvider.js",
|
|
107
|
+
"src/server/loadAppConfig.js"
|
|
108
|
+
]);
|
|
109
|
+
const MAIN_SERVER_DOMAIN_FILE_PATTERN =
|
|
110
|
+
/^src\/server\/(?!(?:index|MainServiceProvider|loadAppConfig)\.[A-Za-z0-9]+$).+/u;
|
|
83
111
|
|
|
84
112
|
function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
|
|
85
113
|
const declaredTokens = new Set();
|
|
@@ -490,6 +518,331 @@ function createHealthCommands(ctx = {}) {
|
|
|
490
518
|
return bindings;
|
|
491
519
|
}
|
|
492
520
|
|
|
521
|
+
function collectStaticImportSummaries(sourceText = "") {
|
|
522
|
+
const imports = [];
|
|
523
|
+
const importPattern = /^\s*import\s+[\s\S]*?\s+from\s+["']([^"']+)["'];?/gmu;
|
|
524
|
+
|
|
525
|
+
for (const match of sourceText.matchAll(importPattern)) {
|
|
526
|
+
const sourcePath = String(match[1] || "").trim();
|
|
527
|
+
if (!sourcePath) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
imports.push({
|
|
531
|
+
sourcePath,
|
|
532
|
+
lineNumber: resolveLineNumberFromIndex(sourceText, match.index || 0)
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return imports;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function normalizeFeatureLaneMetadata(descriptor = {}) {
|
|
540
|
+
const metadata = ensureObject(ensureObject(ensureObject(descriptor).metadata).jskit);
|
|
541
|
+
return {
|
|
542
|
+
scaffoldShape: String(metadata.scaffoldShape || "").trim(),
|
|
543
|
+
scaffoldMode: String(metadata.scaffoldMode || "").trim(),
|
|
544
|
+
lane: String(metadata.lane || "").trim()
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function isFeatureLanePersistenceImportSource(sourcePath = "") {
|
|
549
|
+
const normalizedSourcePath = String(sourcePath || "").trim();
|
|
550
|
+
if (!normalizedSourcePath) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
return FEATURE_SERVER_PERSISTENCE_IMPORT_SOURCE_PATTERNS.some((pattern) => pattern.test(normalizedSourcePath));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function findFirstPatternMatch(sourceText = "", patternEntries = []) {
|
|
557
|
+
let firstMatch = null;
|
|
558
|
+
|
|
559
|
+
for (const rawEntry of ensureArray(patternEntries)) {
|
|
560
|
+
const entry = ensureObject(rawEntry);
|
|
561
|
+
const pattern = entry.pattern instanceof RegExp ? entry.pattern : null;
|
|
562
|
+
if (!pattern) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const match = pattern.exec(sourceText);
|
|
566
|
+
if (!match) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!firstMatch || (match.index || 0) < firstMatch.index) {
|
|
570
|
+
firstMatch = {
|
|
571
|
+
index: match.index || 0,
|
|
572
|
+
label: String(entry.label || pattern.source).trim() || pattern.source
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!firstMatch) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
...firstMatch,
|
|
583
|
+
lineNumber: resolveLineNumberFromIndex(sourceText, firstMatch.index)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function resolvePrimaryServerProviderPath(packageEntry) {
|
|
588
|
+
const rootDir = String(packageEntry?.rootDir || "").trim();
|
|
589
|
+
if (!rootDir) {
|
|
590
|
+
return "";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const providers = ensureArray(ensureObject(ensureObject(packageEntry?.descriptor).runtime).server?.providers);
|
|
594
|
+
for (const rawProvider of providers) {
|
|
595
|
+
const provider = ensureObject(rawProvider);
|
|
596
|
+
const entrypoint = String(provider.entrypoint || "").trim();
|
|
597
|
+
if (!entrypoint || entrypoint.includes("*")) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
return path.resolve(rootDir, entrypoint);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return "";
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function resolvePackageDisplayPath(packageEntry) {
|
|
607
|
+
const relativeDir = String(packageEntry?.relativeDir || "").trim();
|
|
608
|
+
if (relativeDir) {
|
|
609
|
+
return relativeDir;
|
|
610
|
+
}
|
|
611
|
+
return String(packageEntry?.packageId || "").trim() || "package";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function collectFeatureLaneRuleIssuesForPackage({ appRoot, packageEntry, issues }) {
|
|
615
|
+
const metadata = normalizeFeatureLaneMetadata(packageEntry?.descriptor);
|
|
616
|
+
if (metadata.scaffoldShape !== FEATURE_SERVER_SCAFFOLD_SHAPE) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const rootDir = String(packageEntry?.rootDir || "").trim();
|
|
621
|
+
if (!rootDir) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const packageDisplayPath = resolvePackageDisplayPath(packageEntry);
|
|
626
|
+
const isDefaultLane = metadata.lane === FEATURE_SERVER_DEFAULT_LANE;
|
|
627
|
+
const isPersistentMode = FEATURE_SERVER_PERSISTENT_MODES.has(metadata.scaffoldMode);
|
|
628
|
+
|
|
629
|
+
const servicePath = path.join(rootDir, "src", "server", "service.js");
|
|
630
|
+
if (isDefaultLane && (await fileExists(servicePath))) {
|
|
631
|
+
const serviceSource = await readFile(servicePath, "utf8");
|
|
632
|
+
const serviceRelativePath = normalizeRelativePath(appRoot, servicePath);
|
|
633
|
+
const knexMatch = findFirstPatternMatch(serviceSource, [
|
|
634
|
+
{ pattern: /\bjskit\.database\.knex\b/u, label: "jskit.database.knex" },
|
|
635
|
+
{ pattern: /\bcreateWithTransaction\s*\(/u, label: "createWithTransaction(...)" },
|
|
636
|
+
{ pattern: /\bknex\b/u, label: "knex" }
|
|
637
|
+
]);
|
|
638
|
+
if (knexMatch) {
|
|
639
|
+
issues.push(
|
|
640
|
+
`${serviceRelativePath}:${knexMatch.lineNumber}: [feature-lane:service-knex] default-lane service code must not use knex directly (${knexMatch.label}). Move persistence into src/server/repository.js or switch the package to an explicit weird-custom lane.`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const offendingServiceImports = collectStaticImportSummaries(serviceSource)
|
|
645
|
+
.filter((entry) => isFeatureLanePersistenceImportSource(entry.sourcePath));
|
|
646
|
+
if (offendingServiceImports.length > 0) {
|
|
647
|
+
const firstImport = offendingServiceImports[0];
|
|
648
|
+
const importSources = sortStrings(
|
|
649
|
+
[...new Set(offendingServiceImports.map((entry) => String(entry.sourcePath || "").trim()).filter(Boolean))]
|
|
650
|
+
);
|
|
651
|
+
issues.push(
|
|
652
|
+
`${serviceRelativePath}:${firstImport.lineNumber}: [feature-lane:service-persistence-import] default-lane service code must not import persistence helpers directly (${importSources.join(", ")}). Use the injected repository seam instead.`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const providerPath = resolvePrimaryServerProviderPath(packageEntry);
|
|
658
|
+
if (isDefaultLane && providerPath && (await fileExists(providerPath))) {
|
|
659
|
+
const providerSource = await readFile(providerPath, "utf8");
|
|
660
|
+
const providerRelativePath = normalizeRelativePath(appRoot, providerPath);
|
|
661
|
+
const providerPersistenceMatch = findFirstPatternMatch(providerSource, [
|
|
662
|
+
{ pattern: /\bcreateJsonRestContext\s*\(/u, label: "createJsonRestContext(...)" },
|
|
663
|
+
{ pattern: /\.resources\b/u, label: ".resources" },
|
|
664
|
+
{ pattern: /\bjskit\.database\.knex\b/u, label: "jskit.database.knex" },
|
|
665
|
+
{ pattern: /\bcreateWithTransaction\s*\(/u, label: "createWithTransaction(...)" },
|
|
666
|
+
{ pattern: /\bknex\s*\./u, label: "knex." }
|
|
667
|
+
]);
|
|
668
|
+
if (providerPersistenceMatch) {
|
|
669
|
+
issues.push(
|
|
670
|
+
`${providerRelativePath}:${providerPersistenceMatch.lineNumber}: [feature-lane:provider-persistence-direct] default-lane providers may wire repositories but must not perform persistence work directly (${providerPersistenceMatch.label}). Move json-rest/database calls into src/server/repository.js.`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const repositoryPath = path.join(rootDir, "src", "server", "repository.js");
|
|
676
|
+
if (isPersistentMode && !(await fileExists(repositoryPath))) {
|
|
677
|
+
issues.push(
|
|
678
|
+
`${packageDisplayPath}/src/server/repository.js: [feature-lane:repository-required] generated persistent feature packages require src/server/repository.js for scaffoldMode "${metadata.scaffoldMode}".`
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (
|
|
684
|
+
isDefaultLane &&
|
|
685
|
+
metadata.scaffoldMode === FEATURE_SERVER_JSON_REST_MODE &&
|
|
686
|
+
(await fileExists(repositoryPath))
|
|
687
|
+
) {
|
|
688
|
+
const repositorySource = await readFile(repositoryPath, "utf8");
|
|
689
|
+
const repositoryRelativePath = normalizeRelativePath(appRoot, repositoryPath);
|
|
690
|
+
const directDatabaseMatch = findFirstPatternMatch(repositorySource, [
|
|
691
|
+
{ pattern: /\bjskit\.database\.knex\b/u, label: "jskit.database.knex" },
|
|
692
|
+
{ pattern: /\bcreateWithTransaction\s*\(/u, label: "createWithTransaction(...)" },
|
|
693
|
+
{ pattern: /\bknex\b/u, label: "knex" }
|
|
694
|
+
]);
|
|
695
|
+
const usesJsonRest = /@jskit-ai\/json-rest-api-core|createJsonRestContext\s*\(|\.resources\b|json-rest-api/iu.test(repositorySource);
|
|
696
|
+
if (directDatabaseMatch || !usesJsonRest) {
|
|
697
|
+
const locationSuffix = directDatabaseMatch ? `:${directDatabaseMatch.lineNumber}` : "";
|
|
698
|
+
issues.push(
|
|
699
|
+
`${repositoryRelativePath}${locationSuffix}: [feature-lane:repository-default-path] default-lane persistent repositories must stay on internal json-rest-api. Use the injected json-rest-api seam or mark the package as an explicit weird-custom lane.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function collectMainPackageFeatureLaneWarnings({ appRoot, appLocalRegistry, warnings }) {
|
|
706
|
+
const mainPackageEntry = [...appLocalRegistry.values()].find((packageEntry) => {
|
|
707
|
+
const packageId = String(packageEntry?.packageId || "").trim();
|
|
708
|
+
const relativeDir = String(packageEntry?.relativeDir || "").trim();
|
|
709
|
+
return packageId === "@local/main" || relativeDir === "packages/main";
|
|
710
|
+
});
|
|
711
|
+
if (!mainPackageEntry) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const rootDir = String(mainPackageEntry.rootDir || "").trim();
|
|
716
|
+
if (!rootDir) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const reasons = [];
|
|
721
|
+
const serverFilePaths = [];
|
|
722
|
+
await collectAppSourceFiles(path.join(rootDir, "src", "server"), undefined, serverFilePaths);
|
|
723
|
+
const relativeServerFiles = sortStrings(serverFilePaths.map((absolutePath) => normalizeRelativePath(rootDir, absolutePath)));
|
|
724
|
+
const extraDomainFiles = relativeServerFiles.filter(
|
|
725
|
+
(relativePath) =>
|
|
726
|
+
!MAIN_SERVER_BASELINE_RELATIVE_PATHS.has(relativePath) &&
|
|
727
|
+
MAIN_SERVER_DOMAIN_FILE_PATTERN.test(relativePath)
|
|
728
|
+
);
|
|
729
|
+
if (extraDomainFiles.length > 0) {
|
|
730
|
+
reasons.push(`extra server domain files: ${extraDomainFiles.join(", ")}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const providerPath = path.join(rootDir, "src", "server", "MainServiceProvider.js");
|
|
734
|
+
if (await fileExists(providerPath)) {
|
|
735
|
+
const providerSource = await readFile(providerPath, "utf8");
|
|
736
|
+
const actionBindingCount = (providerSource.match(/\bapp\.actions\s*\(/gu) || []).length;
|
|
737
|
+
const serviceBindingCount = (providerSource.match(/\bapp\.(?:service|singleton)\s*\(/gu) || []).length;
|
|
738
|
+
const routeRegistrationCount = (providerSource.match(/\brouter\.register\s*\(/gu) || []).length;
|
|
739
|
+
|
|
740
|
+
if (actionBindingCount > 0) {
|
|
741
|
+
reasons.push("MainServiceProvider registers actions directly");
|
|
742
|
+
}
|
|
743
|
+
if (serviceBindingCount > 1) {
|
|
744
|
+
reasons.push(`MainServiceProvider registers ${serviceBindingCount} service/singleton bindings directly`);
|
|
745
|
+
}
|
|
746
|
+
if (routeRegistrationCount > 0) {
|
|
747
|
+
reasons.push("MainServiceProvider registers HTTP routes directly");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (reasons.length > 0) {
|
|
752
|
+
warnings.push(
|
|
753
|
+
`packages/main: [feature-lane:main-glue-only] packages/main should stay composition/glue. Found ${reasons.join("; ")}. Move substantial server feature logic into a dedicated package generated with feature-server-generator scaffold.`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function collectHandmadeFeatureLaneWarnings({ appLocalRegistry, warnings }) {
|
|
759
|
+
const packageEntries = sortStrings([...appLocalRegistry.keys()])
|
|
760
|
+
.map((packageId) => appLocalRegistry.get(packageId))
|
|
761
|
+
.filter(Boolean);
|
|
762
|
+
|
|
763
|
+
for (const packageEntry of packageEntries) {
|
|
764
|
+
const relativeDir = String(packageEntry?.relativeDir || "").trim();
|
|
765
|
+
if (relativeDir === "packages/main") {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const rootDir = String(packageEntry?.rootDir || "").trim();
|
|
770
|
+
if (!rootDir) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const metadata = normalizeFeatureLaneMetadata(packageEntry?.descriptor);
|
|
775
|
+
if (metadata.scaffoldShape === FEATURE_SERVER_SCAFFOLD_SHAPE) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
780
|
+
const capabilities = ensureObject(descriptor.capabilities);
|
|
781
|
+
const providedCapabilities = ensureArray(capabilities.provides)
|
|
782
|
+
.map((value) => String(value || "").trim())
|
|
783
|
+
.filter(Boolean);
|
|
784
|
+
const dependsOn = ensureArray(descriptor.dependsOn)
|
|
785
|
+
.map((value) => String(value || "").trim())
|
|
786
|
+
.filter(Boolean);
|
|
787
|
+
if (
|
|
788
|
+
providedCapabilities.some((value) => value.startsWith("crud.")) ||
|
|
789
|
+
dependsOn.includes("@jskit-ai/crud-core")
|
|
790
|
+
) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const serverProviders = ensureArray(ensureObject(ensureObject(descriptor.runtime).server).providers);
|
|
795
|
+
const hasDirectServerProvider = serverProviders.some((rawProvider) => {
|
|
796
|
+
const provider = ensureObject(rawProvider);
|
|
797
|
+
const entrypoint = String(provider.entrypoint || "").trim();
|
|
798
|
+
return entrypoint.startsWith("src/server/") && /Provider\.[A-Za-z0-9]+$/u.test(entrypoint);
|
|
799
|
+
});
|
|
800
|
+
if (!hasDirectServerProvider) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const markerPaths = [];
|
|
805
|
+
for (const relativePath of FEATURE_SERVER_COMPLEX_MARKER_RELATIVE_PATHS) {
|
|
806
|
+
if (await fileExists(path.join(rootDir, relativePath))) {
|
|
807
|
+
markerPaths.push(relativePath);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const looksLikeFeatureCapability = providedCapabilities.some((value) => value.startsWith("feature."));
|
|
812
|
+
if (!(markerPaths.length >= 3 || (looksLikeFeatureCapability && markerPaths.length >= 2))) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
warnings.push(
|
|
817
|
+
`${relativeDir || String(packageEntry?.packageId || "").trim()}: [feature-lane:handmade-feature] package looks like a substantial non-CRUD server feature (${markerPaths.join(", ")}) but is missing metadata.jskit.scaffoldShape="${FEATURE_SERVER_SCAFFOLD_SHAPE}". Start from jskit generate feature-server-generator scaffold <feature-name> for this lane instead of hand-making the topology.`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function collectFeatureLaneDoctorIssues({ appRoot, appLocalRegistry, issues, warnings }) {
|
|
823
|
+
const packageEntries = sortStrings([...appLocalRegistry.keys()])
|
|
824
|
+
.map((packageId) => appLocalRegistry.get(packageId))
|
|
825
|
+
.filter(Boolean);
|
|
826
|
+
|
|
827
|
+
for (const packageEntry of packageEntries) {
|
|
828
|
+
await collectFeatureLaneRuleIssuesForPackage({
|
|
829
|
+
appRoot,
|
|
830
|
+
packageEntry,
|
|
831
|
+
issues
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
await collectMainPackageFeatureLaneWarnings({
|
|
836
|
+
appRoot,
|
|
837
|
+
appLocalRegistry,
|
|
838
|
+
warnings
|
|
839
|
+
});
|
|
840
|
+
await collectHandmadeFeatureLaneWarnings({
|
|
841
|
+
appLocalRegistry,
|
|
842
|
+
warnings
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
493
846
|
function hasTopLevelObjectProperty(sourceText = "", propertyName = "") {
|
|
494
847
|
const normalizedPropertyName = String(propertyName || "").trim();
|
|
495
848
|
const normalizedSourceText = String(sourceText || "").trim();
|
|
@@ -801,20 +1154,32 @@ function createHealthCommands(ctx = {}) {
|
|
|
801
1154
|
}
|
|
802
1155
|
}
|
|
803
1156
|
|
|
804
|
-
async function collectUiVerificationDoctorIssues({ appRoot, issues }) {
|
|
1157
|
+
async function collectUiVerificationDoctorIssues({ appRoot, issues, against = "" }) {
|
|
805
1158
|
if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
|
|
806
1159
|
return;
|
|
807
1160
|
}
|
|
808
1161
|
|
|
809
|
-
const
|
|
810
|
-
|
|
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) {
|
|
811
1175
|
return;
|
|
812
1176
|
}
|
|
813
1177
|
|
|
814
1178
|
const receiptPath = path.join(appRoot, UI_VERIFICATION_RECEIPT_RELATIVE_PATH);
|
|
815
1179
|
if (!(await fileExists(receiptPath))) {
|
|
1180
|
+
const againstSegment = normalizedAgainst ? ` --against ${JSON.stringify(normalizedAgainst)}` : "";
|
|
816
1181
|
issues.push(
|
|
817
|
-
`[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(", ")}`
|
|
818
1183
|
);
|
|
819
1184
|
return;
|
|
820
1185
|
}
|
|
@@ -837,6 +1202,13 @@ function createHealthCommands(ctx = {}) {
|
|
|
837
1202
|
return;
|
|
838
1203
|
}
|
|
839
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
|
+
|
|
840
1212
|
if (JSON.stringify(receipt.changedUiFiles) !== JSON.stringify(changedUiState.paths)) {
|
|
841
1213
|
issues.push(
|
|
842
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(", ")}`
|
|
@@ -898,11 +1270,13 @@ function createHealthCommands(ctx = {}) {
|
|
|
898
1270
|
|
|
899
1271
|
async function commandDoctor({ cwd, options, stdout }) {
|
|
900
1272
|
const appRoot = await resolveAppRootFromCwd(cwd);
|
|
1273
|
+
const against = String(options?.inlineOptions?.against || "").trim();
|
|
901
1274
|
const { lock } = await loadLockFile(appRoot);
|
|
902
1275
|
const packageRegistry = await loadPackageRegistry();
|
|
903
1276
|
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
904
1277
|
const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
|
|
905
1278
|
const issues = [];
|
|
1279
|
+
const warnings = [];
|
|
906
1280
|
const installed = ensureObject(lock.installedPackages);
|
|
907
1281
|
await hydratePackageRegistryFromInstalledNodeModules({
|
|
908
1282
|
appRoot,
|
|
@@ -938,14 +1312,22 @@ function createHealthCommands(ctx = {}) {
|
|
|
938
1312
|
});
|
|
939
1313
|
await collectUiVerificationDoctorIssues({
|
|
940
1314
|
appRoot,
|
|
941
|
-
issues
|
|
1315
|
+
issues,
|
|
1316
|
+
against
|
|
1317
|
+
});
|
|
1318
|
+
await collectFeatureLaneDoctorIssues({
|
|
1319
|
+
appRoot,
|
|
1320
|
+
appLocalRegistry,
|
|
1321
|
+
issues,
|
|
1322
|
+
warnings
|
|
942
1323
|
});
|
|
943
1324
|
|
|
944
1325
|
const payload = {
|
|
945
1326
|
appRoot,
|
|
946
1327
|
lockVersion: lock.lockVersion,
|
|
947
1328
|
installedPackages: sortStrings(Object.keys(installed)),
|
|
948
|
-
issues
|
|
1329
|
+
issues,
|
|
1330
|
+
warnings: sortStrings(warnings)
|
|
949
1331
|
};
|
|
950
1332
|
|
|
951
1333
|
if (options.json) {
|
|
@@ -953,13 +1335,21 @@ function createHealthCommands(ctx = {}) {
|
|
|
953
1335
|
} else {
|
|
954
1336
|
stdout.write(`App root: ${appRoot}\n`);
|
|
955
1337
|
stdout.write(`Installed packages: ${payload.installedPackages.length}\n`);
|
|
956
|
-
if (issues.length === 0) {
|
|
1338
|
+
if (issues.length === 0 && payload.warnings.length === 0) {
|
|
957
1339
|
stdout.write("Doctor status: healthy\n");
|
|
1340
|
+
} else if (issues.length === 0) {
|
|
1341
|
+
stdout.write(`Doctor status: warnings (${payload.warnings.length} warning(s))\n`);
|
|
1342
|
+
for (const warning of payload.warnings) {
|
|
1343
|
+
stdout.write(`! ${warning}\n`);
|
|
1344
|
+
}
|
|
958
1345
|
} else {
|
|
959
1346
|
stdout.write(`Doctor status: unhealthy (${issues.length} issue(s))\n`);
|
|
960
1347
|
for (const issue of issues) {
|
|
961
1348
|
stdout.write(`- ${issue}\n`);
|
|
962
1349
|
}
|
|
1350
|
+
for (const warning of payload.warnings) {
|
|
1351
|
+
stdout.write(`! ${warning}\n`);
|
|
1352
|
+
}
|
|
963
1353
|
}
|
|
964
1354
|
}
|
|
965
1355
|
|
|
@@ -12,6 +12,8 @@ const MAIN_CLIENT_PROVIDERS_RELATIVE_PATH = "packages/main/src/client/providers"
|
|
|
12
12
|
const COMPONENT_TOKEN_PATTERN = /\bcomponentToken\s*:\s*["']([^"']+)["']/g;
|
|
13
13
|
const REGISTER_MAIN_CLIENT_COMPONENT_PATTERN = /registerMainClientComponent\(\s*["']([^"']+)["']\s*,/g;
|
|
14
14
|
const LINK_ITEM_TOKEN_SUFFIX = "link-item";
|
|
15
|
+
const JSKIT_SCOPE_PREFIX = "@jskit-ai/";
|
|
16
|
+
const FEATURE_SERVER_GENERATOR_PACKAGE_ID = "@jskit-ai/feature-server-generator";
|
|
15
17
|
const PROVIDER_SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
16
18
|
const READ_FILE_IGNORE_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EISDIR", "EACCES", "EPERM"]);
|
|
17
19
|
const READ_DIRECTORY_IGNORE_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
|
|
@@ -41,6 +43,41 @@ function appendTokenSource(map, token = "", source = "") {
|
|
|
41
43
|
map.set(normalizedToken, existingSources);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function toShortPackageId(packageId = "") {
|
|
47
|
+
const normalizedPackageId = String(packageId || "").trim();
|
|
48
|
+
if (!normalizedPackageId.startsWith(JSKIT_SCOPE_PREFIX)) {
|
|
49
|
+
return normalizedPackageId;
|
|
50
|
+
}
|
|
51
|
+
return normalizedPackageId.slice(JSKIT_SCOPE_PREFIX.length);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveGeneratorPrimarySubcommand(packageEntry = {}) {
|
|
55
|
+
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
56
|
+
const metadata = ensureObject(descriptor.metadata);
|
|
57
|
+
return String(metadata.generatorPrimarySubcommand || descriptor.generatorPrimarySubcommand || "").trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveGeneratorDescription(packageEntry = {}) {
|
|
61
|
+
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
62
|
+
return String(descriptor.description || "").trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveGeneratorQuickStartRows(packageEntry = {}, { limit = 3 } = {}) {
|
|
66
|
+
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
67
|
+
const metadata = ensureObject(descriptor.metadata);
|
|
68
|
+
const subcommands = ensureObject(metadata.generatorSubcommands || descriptor.generatorSubcommands);
|
|
69
|
+
const primarySubcommandName = resolveGeneratorPrimarySubcommand(packageEntry);
|
|
70
|
+
const primarySubcommand = ensureObject(subcommands[primarySubcommandName]);
|
|
71
|
+
const examples = ensureArray(primarySubcommand.examples);
|
|
72
|
+
return examples
|
|
73
|
+
.slice(0, Math.max(0, Number(limit) || 0))
|
|
74
|
+
.map((example) => ({
|
|
75
|
+
label: String(ensureObject(example).label || "").trim(),
|
|
76
|
+
lines: ensureArray(ensureObject(example).lines).map((value) => String(value || "").trim()).filter(Boolean)
|
|
77
|
+
}))
|
|
78
|
+
.filter((example) => example.lines.length > 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
function isLinkItemToken(token = "") {
|
|
45
82
|
return String(token || "").trim().toLowerCase().endsWith(LINK_ITEM_TOKEN_SUFFIX);
|
|
46
83
|
}
|
|
@@ -265,6 +302,21 @@ function createListCommands(ctx = {}) {
|
|
|
265
302
|
if (lines.length > 0) {
|
|
266
303
|
lines.push("");
|
|
267
304
|
}
|
|
305
|
+
const featureServerEntry = packageRegistry.get(FEATURE_SERVER_GENERATOR_PACKAGE_ID);
|
|
306
|
+
const recommendedQuickStarts = resolveGeneratorQuickStartRows(featureServerEntry, { limit: 3 });
|
|
307
|
+
if (recommendedQuickStarts.length > 0) {
|
|
308
|
+
lines.push(color.heading(`Recommended non-CRUD server starts (${recommendedQuickStarts.length}):`));
|
|
309
|
+
for (const example of recommendedQuickStarts) {
|
|
310
|
+
const label = String(example.label || "").trim();
|
|
311
|
+
if (label) {
|
|
312
|
+
lines.push(`- ${color.item(label)}`);
|
|
313
|
+
}
|
|
314
|
+
for (const commandLine of ensureArray(example.lines)) {
|
|
315
|
+
lines.push(` ${commandLine}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
lines.push("");
|
|
319
|
+
}
|
|
268
320
|
lines.push(color.heading("Available generators:"));
|
|
269
321
|
const packageIds = sortStrings([...packageRegistry.keys()].filter((packageId) => {
|
|
270
322
|
const packageEntry = packageRegistry.get(packageId);
|
|
@@ -273,8 +325,14 @@ function createListCommands(ctx = {}) {
|
|
|
273
325
|
for (const packageId of packageIds) {
|
|
274
326
|
const packageEntry = packageRegistry.get(packageId);
|
|
275
327
|
const installedLabel = installedPackages.has(packageId) ? " (installed)" : "";
|
|
328
|
+
const shortId = toShortPackageId(packageId);
|
|
329
|
+
const shortIdPrefix = shortId && shortId !== packageId ? `${color.item(shortId)} ` : "";
|
|
330
|
+
const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
|
|
331
|
+
const primarySuffix = primarySubcommand ? ` ${color.dim(`[primary:${primarySubcommand}]`)}` : "";
|
|
332
|
+
const description = resolveGeneratorDescription(packageEntry);
|
|
333
|
+
const descriptionSuffix = description ? `: ${description}` : "";
|
|
276
334
|
lines.push(
|
|
277
|
-
`- ${color.item(packageId)} ${color.version(`(${packageEntry.version})`)}${installedLabel ? color.installed(installedLabel) : ""}
|
|
335
|
+
`- ${shortIdPrefix}${color.item(packageId)} ${color.version(`(${packageEntry.version})`)}${primarySuffix}${installedLabel ? color.installed(installedLabel) : ""}${descriptionSuffix}`.trim()
|
|
278
336
|
);
|
|
279
337
|
}
|
|
280
338
|
}
|
|
@@ -328,8 +386,12 @@ function createListCommands(ctx = {}) {
|
|
|
328
386
|
}
|
|
329
387
|
return {
|
|
330
388
|
packageId,
|
|
389
|
+
shortId: toShortPackageId(packageId),
|
|
331
390
|
version: packageEntry.version,
|
|
332
|
-
installed: installedPackages.has(packageId)
|
|
391
|
+
installed: installedPackages.has(packageId),
|
|
392
|
+
description: resolveGeneratorDescription(packageEntry),
|
|
393
|
+
primarySubcommand: resolveGeneratorPrimarySubcommand(packageEntry),
|
|
394
|
+
quickStarts: resolveGeneratorQuickStartRows(packageEntry, { limit: 3 })
|
|
333
395
|
};
|
|
334
396
|
}).filter(Boolean)
|
|
335
397
|
: [],
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../../shared/outputFormatting.js";
|
|
11
11
|
|
|
12
12
|
const JSKIT_SCOPE_PREFIX = "@jskit-ai/";
|
|
13
|
+
const FEATURE_SERVER_GENERATOR_PACKAGE_ID = "@jskit-ai/feature-server-generator";
|
|
13
14
|
const HELP_TEXT_BY_KEY = Object.freeze({
|
|
14
15
|
"page-target-file":
|
|
15
16
|
"Vue page file relative to src/pages/. It must resolve to a configured surface.",
|
|
@@ -223,7 +224,8 @@ function appendHelpExamples(lines = [], exampleRows = [], { color = null } = {})
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
lines.push("");
|
|
226
|
-
|
|
227
|
+
const headingLabel = color ? color.heading(`Examples (${examples.length}):`) : `Examples (${examples.length}):`;
|
|
228
|
+
lines.push(headingLabel);
|
|
227
229
|
appendSeparatedBlocks(
|
|
228
230
|
lines,
|
|
229
231
|
examples.map((example) => {
|
|
@@ -250,6 +252,31 @@ function appendHelpExamples(lines = [], exampleRows = [], { color = null } = {})
|
|
|
250
252
|
);
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
function appendHelpExamplesWithHeading(lines = [], exampleRows = [], heading = "", { color = null } = {}) {
|
|
256
|
+
const examples = ensureArray(exampleRows);
|
|
257
|
+
const headingText = String(heading || "").trim();
|
|
258
|
+
if (examples.length < 1 || !headingText) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push(color ? color.heading(headingText) : headingText);
|
|
264
|
+
appendSeparatedBlocks(
|
|
265
|
+
lines,
|
|
266
|
+
examples.map((example) => {
|
|
267
|
+
const block = [];
|
|
268
|
+
const label = String(example?.label || "").trim();
|
|
269
|
+
if (label) {
|
|
270
|
+
block.push(`- ${color ? color.item(label) : label}`);
|
|
271
|
+
}
|
|
272
|
+
for (const commandLine of ensureArray(example?.lines)) {
|
|
273
|
+
block.push(` ${commandLine}`);
|
|
274
|
+
}
|
|
275
|
+
return block;
|
|
276
|
+
}).filter((block) => block.length > 0)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
253
280
|
function appendHelpNotes(lines = [], rawNotes = [], { color = null } = {}) {
|
|
254
281
|
const notes = normalizeHelpNoteRows(rawNotes);
|
|
255
282
|
if (notes.length < 1) {
|
|
@@ -420,6 +447,23 @@ function buildSubcommandOptionRows(optionRows = [], subcommandRow = {}) {
|
|
|
420
447
|
return rows;
|
|
421
448
|
}
|
|
422
449
|
|
|
450
|
+
function resolvePrimaryGeneratorQuickStartRows(packageEntry = {}, { limit = 3 } = {}) {
|
|
451
|
+
const metadata = resolveGeneratorSubcommandMetadata(packageEntry);
|
|
452
|
+
const subcommands = ensureArray(metadata.subcommands);
|
|
453
|
+
const primarySubcommand = subcommands.find((row) => row.primary) || subcommands[0] || null;
|
|
454
|
+
if (!primarySubcommand) {
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return ensureArray(primarySubcommand.examples)
|
|
459
|
+
.slice(0, Math.max(0, Number(limit) || 0))
|
|
460
|
+
.map((example) => Object.freeze({
|
|
461
|
+
label: String(example?.label || "").trim(),
|
|
462
|
+
lines: ensureArray(example?.lines).map((value) => String(value || "").trim()).filter(Boolean)
|
|
463
|
+
}))
|
|
464
|
+
.filter((example) => example.lines.length > 0);
|
|
465
|
+
}
|
|
466
|
+
|
|
423
467
|
function renderGenerateCatalogHelp({
|
|
424
468
|
io,
|
|
425
469
|
packageRegistry,
|
|
@@ -438,11 +482,14 @@ function renderGenerateCatalogHelp({
|
|
|
438
482
|
description: resolvePackageSummary(entry)
|
|
439
483
|
});
|
|
440
484
|
});
|
|
485
|
+
const featureServerPackageEntry = packageRegistry.get(FEATURE_SERVER_GENERATOR_PACKAGE_ID);
|
|
486
|
+
const recommendedQuickStarts = resolvePrimaryGeneratorQuickStartRows(featureServerPackageEntry, { limit: 3 });
|
|
441
487
|
|
|
442
488
|
if (json) {
|
|
443
489
|
io.stdout.write(`${JSON.stringify({
|
|
444
490
|
command: "generate",
|
|
445
491
|
generators,
|
|
492
|
+
recommendedQuickStarts,
|
|
446
493
|
usage: [
|
|
447
494
|
"jskit generate <generatorId> help",
|
|
448
495
|
"jskit generate <generatorId> [subcommand] [subcommand args...] [--<option> <value>...]",
|
|
@@ -471,6 +518,12 @@ function renderGenerateCatalogHelp({
|
|
|
471
518
|
lines.push("- jskit generate <generatorId> help");
|
|
472
519
|
lines.push("- jskit generate <generatorId> [subcommand] [subcommand args...] [--<option> <value>...]");
|
|
473
520
|
lines.push("- jskit list generators");
|
|
521
|
+
appendHelpExamplesWithHeading(
|
|
522
|
+
lines,
|
|
523
|
+
recommendedQuickStarts,
|
|
524
|
+
`Recommended non-CRUD server starts (${recommendedQuickStarts.length}):`,
|
|
525
|
+
{ color }
|
|
526
|
+
);
|
|
474
527
|
writeWrappedLines({
|
|
475
528
|
stdout: io.stdout,
|
|
476
529
|
lines
|
|
@@ -619,6 +672,12 @@ function renderGeneratePackageHelp({
|
|
|
619
672
|
const descriptionSuffix = subcommand.description ? `: ${subcommand.description}` : "";
|
|
620
673
|
lines.push(`- ${color.item(subcommand.name)}${primarySuffix}${descriptionSuffix}`);
|
|
621
674
|
}
|
|
675
|
+
appendHelpExamplesWithHeading(
|
|
676
|
+
lines,
|
|
677
|
+
resolvePrimaryGeneratorQuickStartRows(packageEntry, { limit: 3 }),
|
|
678
|
+
`Primary quick starts (${resolvePrimaryGeneratorQuickStartRows(packageEntry, { limit: 3 }).length}):`,
|
|
679
|
+
{ color }
|
|
680
|
+
);
|
|
622
681
|
lines.push("");
|
|
623
682
|
lines.push("Use subcommand help for positional args, options, notes, and examples:");
|
|
624
683
|
lines.push(` ${color.item("jskit generate <generatorId> <subcommand> help")}`);
|
|
@@ -240,7 +240,7 @@ async function runPackageGenerateCommand(
|
|
|
240
240
|
});
|
|
241
241
|
if (!resolvedPackageId) {
|
|
242
242
|
throw createCliError(
|
|
243
|
-
`Unknown package: ${packageIdInput}.
|
|
243
|
+
`Unknown package: ${packageIdInput}. Use jskit list generators to discover available generators. If you want to run generator subcommands from node_modules, install the generator first (npm install ${packageIdInput}). For a substantial non-CRUD server feature, start with: jskit generate feature-server-generator scaffold <feature-name>.`
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -255,7 +255,7 @@ async function runPackageGenerateCommand(
|
|
|
255
255
|
}
|
|
256
256
|
if (resolvePackageKind(packageEntry) !== "generator") {
|
|
257
257
|
throw createCliError(
|
|
258
|
-
`Package ${resolvedPackageId} is a runtime package. Use: jskit add package ${resolvedPackageId}
|
|
258
|
+
`Package ${resolvedPackageId} is a runtime package. Use: jskit add package ${resolvedPackageId}. For a new substantial non-CRUD server feature, start with: jskit generate feature-server-generator scaffold <feature-name>.`
|
|
259
259
|
);
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -23,7 +23,9 @@ async function runPackageUpdateCommand(
|
|
|
23
23
|
const installedPackages = ensureObject(lock.installedPackages);
|
|
24
24
|
const resolvedTargetId = resolveInstalledPackageIdInput(targetId, installedPackages);
|
|
25
25
|
if (!resolvedTargetId) {
|
|
26
|
-
throw createCliError(
|
|
26
|
+
throw createCliError(
|
|
27
|
+
`Package is not installed: ${targetId}. update package only reapplies packages already recorded in .jskit/lock.json. If you meant to install it, run: jskit add package ${targetId}`
|
|
28
|
+
);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
return runCommandAdd({
|
|
@@ -6,6 +6,35 @@ import { createShowRenderHelpers } from "./renderHelpers.js";
|
|
|
6
6
|
import { writePackageExportsSection } from "./renderPackageExports.js";
|
|
7
7
|
import { writeCapabilitiesSections } from "./renderPackageCapabilities.js";
|
|
8
8
|
|
|
9
|
+
function resolveGeneratorSubcommandRows(payload = {}) {
|
|
10
|
+
const metadata = ensureObject(payload.metadata);
|
|
11
|
+
const generatorSubcommands = ensureObject(metadata.generatorSubcommands);
|
|
12
|
+
const primarySubcommand = String(metadata.generatorPrimarySubcommand || "").trim();
|
|
13
|
+
return Object.keys(generatorSubcommands)
|
|
14
|
+
.sort((left, right) => left.localeCompare(right))
|
|
15
|
+
.map((subcommandName) => {
|
|
16
|
+
const definition = ensureObject(generatorSubcommands[subcommandName]);
|
|
17
|
+
return {
|
|
18
|
+
name: subcommandName,
|
|
19
|
+
primary: primarySubcommand === subcommandName,
|
|
20
|
+
description: String(definition.description || "").trim(),
|
|
21
|
+
examples: ensureArray(definition.examples)
|
|
22
|
+
.map((example) => {
|
|
23
|
+
const record = ensureObject(example);
|
|
24
|
+
return {
|
|
25
|
+
label: String(record.label || "").trim(),
|
|
26
|
+
lines: ensureArray(record.lines).map((value) => String(value || "").trim()).filter(Boolean)
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.filter((example) => example.lines.length > 0)
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveOwnershipGuidance(payload = {}) {
|
|
35
|
+
return ensureObject(ensureObject(ensureObject(payload.metadata).jskit).ownershipGuidance);
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
function renderPackagePayloadText({
|
|
10
39
|
payload,
|
|
11
40
|
provides,
|
|
@@ -70,6 +99,8 @@ function renderPackagePayloadText({
|
|
|
70
99
|
const bindingSections = ensureObject(payload.containerBindings);
|
|
71
100
|
const serverBindings = ensureArray(bindingSections.server);
|
|
72
101
|
const clientBindings = ensureArray(bindingSections.client);
|
|
102
|
+
const generatorSubcommands = resolveGeneratorSubcommandRows(payload);
|
|
103
|
+
const ownershipGuidance = resolveOwnershipGuidance(payload);
|
|
73
104
|
|
|
74
105
|
stdout.write(`${color.heading("Information")}\n`);
|
|
75
106
|
writeField("Package", payload.packageId, color.item);
|
|
@@ -98,6 +129,50 @@ function renderPackagePayloadText({
|
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
131
|
|
|
132
|
+
if (generatorSubcommands.length > 0) {
|
|
133
|
+
stdout.write(`${color.heading(`Generator commands (${generatorSubcommands.length}):`)}\n`);
|
|
134
|
+
for (const subcommand of generatorSubcommands) {
|
|
135
|
+
const primarySuffix = subcommand.primary ? ` ${color.installed("[primary]")}` : "";
|
|
136
|
+
const descriptionSuffix = subcommand.description ? `: ${subcommand.description}` : "";
|
|
137
|
+
stdout.write(`- ${color.item(subcommand.name)}${primarySuffix}${descriptionSuffix}\n`);
|
|
138
|
+
if (options.details && subcommand.examples.length > 0) {
|
|
139
|
+
for (const example of subcommand.examples.slice(0, 2)) {
|
|
140
|
+
const exampleLabel = String(example.label || "").trim();
|
|
141
|
+
if (exampleLabel) {
|
|
142
|
+
stdout.write(` ${color.dim(`example: ${exampleLabel}`)}\n`);
|
|
143
|
+
}
|
|
144
|
+
for (const commandLine of ensureArray(example.lines)) {
|
|
145
|
+
stdout.write(` ${commandLine}\n`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (options.details && Object.keys(ownershipGuidance).length > 0) {
|
|
153
|
+
const title = String(ownershipGuidance.title || "Ownership guidance").trim() || "Ownership guidance";
|
|
154
|
+
const summary = String(ownershipGuidance.summary || "").trim();
|
|
155
|
+
const responsibilities = ensureArray(ownershipGuidance.responsibilities)
|
|
156
|
+
.map((value) => String(value || "").trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
const examples = ensureArray(ownershipGuidance.examples)
|
|
159
|
+
.map((value) => String(value || "").trim())
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
stdout.write(`${color.heading(title)}\n`);
|
|
162
|
+
if (summary) {
|
|
163
|
+
stdout.write(`- ${summary}\n`);
|
|
164
|
+
}
|
|
165
|
+
for (const responsibility of responsibilities) {
|
|
166
|
+
stdout.write(`- ${responsibility}\n`);
|
|
167
|
+
}
|
|
168
|
+
if (examples.length > 0) {
|
|
169
|
+
stdout.write(`- ${color.dim("quick starts:")}\n`);
|
|
170
|
+
for (const example of examples) {
|
|
171
|
+
stdout.write(` ${example}\n`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
101
176
|
if (placementOutlets.length > 0) {
|
|
102
177
|
stdout.write(`${color.heading(`Placement outlets (${placementOutlets.length}):`)}\n`);
|
|
103
178
|
for (const outlet of placementOutlets) {
|
|
@@ -223,22 +223,23 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
223
223
|
"Short ids resolve to @jskit-ai/<id> when available.",
|
|
224
224
|
"Running without args lists available generators.",
|
|
225
225
|
"Running with only <generatorId> shows generator help.",
|
|
226
|
+
"For substantial non-CRUD server features, start with feature-server-generator scaffold.",
|
|
226
227
|
"Use jskit generate <generatorId> <subcommand> help for subcommand-specific usage."
|
|
227
228
|
]),
|
|
228
229
|
examples: Object.freeze([
|
|
229
230
|
Object.freeze({
|
|
230
231
|
label: "Common usage",
|
|
231
232
|
lines: Object.freeze([
|
|
232
|
-
"npx jskit generate
|
|
233
|
-
"
|
|
233
|
+
"npx jskit generate feature-server-generator scaffold \\",
|
|
234
|
+
" booking-engine"
|
|
234
235
|
])
|
|
235
236
|
}),
|
|
236
237
|
Object.freeze({
|
|
237
238
|
label: "More advanced usage",
|
|
238
239
|
lines: Object.freeze([
|
|
239
|
-
"npx jskit generate
|
|
240
|
-
"
|
|
241
|
-
" --
|
|
240
|
+
"npx jskit generate feature-server-generator scaffold \\",
|
|
241
|
+
" availability-engine \\",
|
|
242
|
+
" --mode orchestrator"
|
|
242
243
|
])
|
|
243
244
|
})
|
|
244
245
|
]),
|
|
@@ -265,6 +266,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
265
266
|
]),
|
|
266
267
|
defaults: Object.freeze([
|
|
267
268
|
"Without mode, list prints bundles + runtime packages + generators.",
|
|
269
|
+
"Use list generators when you want the standard non-CRUD server lane command examples.",
|
|
268
270
|
"placements are listed by the dedicated list-placements command.",
|
|
269
271
|
"--full and --expanded only affect bundle/package listing views."
|
|
270
272
|
]),
|
|
@@ -338,6 +340,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
338
340
|
]),
|
|
339
341
|
defaults: Object.freeze([
|
|
340
342
|
"Basic output is compact; --details expands capability and runtime sections.",
|
|
343
|
+
"Generator packages may include lane ownership guidance and subcommand examples under --details.",
|
|
341
344
|
"--debug-exports implies --details."
|
|
342
345
|
]),
|
|
343
346
|
fullUse: "jskit show <id> [--details] [--debug-exports] [--json]",
|
|
@@ -472,14 +475,15 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
472
475
|
defaults: Object.freeze([
|
|
473
476
|
"Validates lock entries, managed files, and registry visibility.",
|
|
474
477
|
"Reports issues as plain text by default.",
|
|
475
|
-
"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."
|
|
476
480
|
]),
|
|
477
|
-
fullUse: "jskit doctor [--json]",
|
|
481
|
+
fullUse: "jskit doctor [--against <base-ref>] [--json]",
|
|
478
482
|
showHelpOnBareInvocation: false,
|
|
479
483
|
handlerName: "commandDoctor",
|
|
480
484
|
allowedFlagKeys: Object.freeze(["json"]),
|
|
481
|
-
inlineOptionMode: "
|
|
482
|
-
allowedValueOptionNames: Object.freeze([])
|
|
485
|
+
inlineOptionMode: "enumerated",
|
|
486
|
+
allowedValueOptionNames: Object.freeze(["against"])
|
|
483
487
|
}),
|
|
484
488
|
"lint-descriptors": Object.freeze({
|
|
485
489
|
command: "lint-descriptors",
|
|
@@ -39,6 +39,12 @@ function printTopLevelHelp(stream = process.stderr) {
|
|
|
39
39
|
for (const entry of listOverviewCommandDescriptors()) {
|
|
40
40
|
lines.push(` ${color.item(entry.command.padEnd(16, " "))} ${entry.summary}`);
|
|
41
41
|
}
|
|
42
|
+
lines.push("");
|
|
43
|
+
lines.push(color.heading("Generator quick starts:"));
|
|
44
|
+
lines.push(" substantial non-CRUD server feature:");
|
|
45
|
+
lines.push(` ${color.item("jskit generate feature-server-generator scaffold booking-engine")}`);
|
|
46
|
+
lines.push(" non-persistent workflow/orchestrator:");
|
|
47
|
+
lines.push(` ${color.item("jskit generate feature-server-generator scaffold availability-engine --mode orchestrator")}`);
|
|
42
48
|
writeHelpLines(stream, lines);
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -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
|
};
|