@jskit-ai/jskit-cli 0.2.66 → 0.2.68
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/appCommands/linkLocalPackages.js +137 -3
- package/src/server/commandHandlers/health.js +545 -3
- 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 +8 -5
- package/src/server/core/usageHelp.js +6 -0
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.68",
|
|
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.67",
|
|
24
|
+
"@jskit-ai/kernel": "0.1.59",
|
|
25
|
+
"@jskit-ai/shell-web": "0.1.58"
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { mkdir, rm, symlink } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readFile, readlink, rm, symlink } from "node:fs/promises";
|
|
3
3
|
import {
|
|
4
4
|
discoverLocalPackageMap,
|
|
5
5
|
fileExists,
|
|
@@ -8,6 +8,132 @@ import {
|
|
|
8
8
|
resolveSymlinkType
|
|
9
9
|
} from "./shared.js";
|
|
10
10
|
|
|
11
|
+
const COMPANION_PACKAGES = Object.freeze([
|
|
12
|
+
Object.freeze({
|
|
13
|
+
packageName: "json-rest-schema",
|
|
14
|
+
repoDirName: "json-rest-schema"
|
|
15
|
+
}),
|
|
16
|
+
Object.freeze({
|
|
17
|
+
packageName: "json-rest-stores",
|
|
18
|
+
repoDirName: "json-rest-stores"
|
|
19
|
+
})
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function collectDeclaredPackageNames(packageJson = {}) {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
const sections = [
|
|
25
|
+
packageJson?.dependencies,
|
|
26
|
+
packageJson?.devDependencies,
|
|
27
|
+
packageJson?.optionalDependencies,
|
|
28
|
+
packageJson?.peerDependencies
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const section of sections) {
|
|
32
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const packageName of Object.keys(section)) {
|
|
37
|
+
const normalizedPackageName = String(packageName || "").trim();
|
|
38
|
+
if (normalizedPackageName) {
|
|
39
|
+
names.add(normalizedPackageName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function verifySymlinkTarget(targetPath = "", sourceDir = "", {
|
|
48
|
+
packageName = ""
|
|
49
|
+
} = {}) {
|
|
50
|
+
const stats = await lstat(targetPath);
|
|
51
|
+
if (!stats.isSymbolicLink()) {
|
|
52
|
+
throw new Error(`[link-local] expected ${packageName || targetPath} to be a symlink after linking.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const linkedTarget = await readlink(targetPath);
|
|
56
|
+
if (linkedTarget !== sourceDir) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[link-local] expected ${packageName || targetPath} to link to ${sourceDir}, got ${linkedTarget}.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
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
|
+
|
|
74
|
+
async function maybeLinkCompanionPackages({
|
|
75
|
+
appRoot = "",
|
|
76
|
+
repoRoot = "",
|
|
77
|
+
stdout,
|
|
78
|
+
createCliError
|
|
79
|
+
}) {
|
|
80
|
+
const companionRoot = path.dirname(repoRoot);
|
|
81
|
+
const appPackageJsonPath = path.join(appRoot, "package.json");
|
|
82
|
+
let appPackageJson = {};
|
|
83
|
+
try {
|
|
84
|
+
appPackageJson = JSON.parse(await readFile(appPackageJsonPath, "utf8"));
|
|
85
|
+
} catch {
|
|
86
|
+
appPackageJson = {};
|
|
87
|
+
}
|
|
88
|
+
const declaredPackageNames = collectDeclaredPackageNames(appPackageJson);
|
|
89
|
+
let linkedCount = 0;
|
|
90
|
+
|
|
91
|
+
for (const companion of COMPANION_PACKAGES) {
|
|
92
|
+
if (!declaredPackageNames.has(companion.packageName)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sourceDir = path.join(companionRoot, companion.repoDirName);
|
|
97
|
+
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
98
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
99
|
+
throw createCliError(
|
|
100
|
+
`[link-local] companion package ${companion.packageName} is declared by the app but local source was not found at ${sourceDir}.`,
|
|
101
|
+
{ exitCode: 1 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let packageJson = {};
|
|
106
|
+
try {
|
|
107
|
+
packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (String(packageJson?.name || "").trim() !== companion.packageName) {
|
|
113
|
+
throw createCliError(
|
|
114
|
+
`[link-local] companion source at ${sourceDir} does not match expected package ${companion.packageName}.`,
|
|
115
|
+
{ exitCode: 1 }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const appTargetPath = path.join(appRoot, "node_modules", companion.packageName);
|
|
120
|
+
await replaceWithSymlink(appTargetPath, sourceDir, {
|
|
121
|
+
packageName: companion.packageName
|
|
122
|
+
});
|
|
123
|
+
stdout.write(`[link-local] linked ${companion.packageName} -> ${sourceDir}\n`);
|
|
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;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return linkedCount;
|
|
135
|
+
}
|
|
136
|
+
|
|
11
137
|
async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout }) {
|
|
12
138
|
const { createCliError } = ctx;
|
|
13
139
|
const explicitRepoRoot = String(options?.inlineOptions?.["repo-root"] || "").trim();
|
|
@@ -39,8 +165,9 @@ async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options
|
|
|
39
165
|
|
|
40
166
|
for (const [packageDirName, sourceDir] of [...packageMap.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
41
167
|
const targetPath = path.join(scopeDirectory, packageDirName);
|
|
42
|
-
await
|
|
43
|
-
|
|
168
|
+
await replaceWithSymlink(targetPath, sourceDir, {
|
|
169
|
+
packageName: `@jskit-ai/${packageDirName}`
|
|
170
|
+
});
|
|
44
171
|
stdout.write(`[link-local] linked @jskit-ai/${packageDirName} -> ${sourceDir}\n`);
|
|
45
172
|
await linkPackageBinEntries({
|
|
46
173
|
appRoot,
|
|
@@ -51,6 +178,13 @@ async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options
|
|
|
51
178
|
linkedCount += 1;
|
|
52
179
|
}
|
|
53
180
|
|
|
181
|
+
linkedCount += await maybeLinkCompanionPackages({
|
|
182
|
+
appRoot,
|
|
183
|
+
repoRoot,
|
|
184
|
+
stdout,
|
|
185
|
+
createCliError
|
|
186
|
+
});
|
|
187
|
+
|
|
54
188
|
if (await fileExists(viteCacheDirectory)) {
|
|
55
189
|
await rm(viteCacheDirectory, { recursive: true, force: true });
|
|
56
190
|
stdout.write(`[link-local] cleared Vite cache at ${viteCacheDirectory}\n`);
|
|
@@ -75,6 +75,39 @@ function createHealthCommands(ctx = {}) {
|
|
|
75
75
|
"createCrudListFilters",
|
|
76
76
|
"useCrudListFilters"
|
|
77
77
|
]);
|
|
78
|
+
const CRUD_TRANSPORT_RUNTIME_CALLEES = Object.freeze([
|
|
79
|
+
"useCrudList",
|
|
80
|
+
"useCrudView",
|
|
81
|
+
"useCrudAddEdit"
|
|
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;
|
|
78
111
|
|
|
79
112
|
function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
|
|
80
113
|
const declaredTokens = new Set();
|
|
@@ -485,6 +518,471 @@ function createHealthCommands(ctx = {}) {
|
|
|
485
518
|
return bindings;
|
|
486
519
|
}
|
|
487
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
|
+
|
|
846
|
+
function hasTopLevelObjectProperty(sourceText = "", propertyName = "") {
|
|
847
|
+
const normalizedPropertyName = String(propertyName || "").trim();
|
|
848
|
+
const normalizedSourceText = String(sourceText || "").trim();
|
|
849
|
+
if (!normalizedPropertyName || !normalizedSourceText.startsWith("{")) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
let parenDepth = 0;
|
|
854
|
+
let braceDepth = 0;
|
|
855
|
+
let bracketDepth = 0;
|
|
856
|
+
let inLineComment = false;
|
|
857
|
+
let inBlockComment = false;
|
|
858
|
+
|
|
859
|
+
for (let index = 0; index < normalizedSourceText.length; index += 1) {
|
|
860
|
+
const character = normalizedSourceText[index];
|
|
861
|
+
const nextCharacter = normalizedSourceText[index + 1] || "";
|
|
862
|
+
|
|
863
|
+
if (inLineComment) {
|
|
864
|
+
if (character === "\n") {
|
|
865
|
+
inLineComment = false;
|
|
866
|
+
}
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (inBlockComment) {
|
|
871
|
+
if (character === "*" && nextCharacter === "/") {
|
|
872
|
+
inBlockComment = false;
|
|
873
|
+
index += 1;
|
|
874
|
+
}
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (character === "/" && nextCharacter === "/") {
|
|
879
|
+
inLineComment = true;
|
|
880
|
+
index += 1;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (character === "/" && nextCharacter === "*") {
|
|
885
|
+
inBlockComment = true;
|
|
886
|
+
index += 1;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (character === "'" || character === "\"") {
|
|
891
|
+
const quote = character;
|
|
892
|
+
const stringStart = index + 1;
|
|
893
|
+
let stringEnd = stringStart;
|
|
894
|
+
for (; stringEnd < normalizedSourceText.length; stringEnd += 1) {
|
|
895
|
+
if (
|
|
896
|
+
normalizedSourceText[stringEnd] === quote &&
|
|
897
|
+
!isEscapedCharacter(normalizedSourceText, stringEnd)
|
|
898
|
+
) {
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const stringValue = normalizedSourceText.slice(stringStart, stringEnd);
|
|
904
|
+
index = stringEnd;
|
|
905
|
+
if (braceDepth === 1 && parenDepth === 0 && bracketDepth === 0 && stringValue === normalizedPropertyName) {
|
|
906
|
+
let cursor = index + 1;
|
|
907
|
+
while (/\s/u.test(normalizedSourceText[cursor] || "")) {
|
|
908
|
+
cursor += 1;
|
|
909
|
+
}
|
|
910
|
+
if (normalizedSourceText[cursor] === ":") {
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (character === "`") {
|
|
918
|
+
for (index += 1; index < normalizedSourceText.length; index += 1) {
|
|
919
|
+
if (
|
|
920
|
+
normalizedSourceText[index] === "`" &&
|
|
921
|
+
!isEscapedCharacter(normalizedSourceText, index)
|
|
922
|
+
) {
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (character === "(") {
|
|
930
|
+
parenDepth += 1;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (character === ")") {
|
|
934
|
+
parenDepth -= 1;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (character === "{") {
|
|
938
|
+
braceDepth += 1;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (character === "}") {
|
|
942
|
+
braceDepth -= 1;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (character === "[") {
|
|
946
|
+
bracketDepth += 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (character === "]") {
|
|
950
|
+
bracketDepth -= 1;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (
|
|
955
|
+
braceDepth === 1 &&
|
|
956
|
+
parenDepth === 0 &&
|
|
957
|
+
bracketDepth === 0 &&
|
|
958
|
+
/[A-Za-z_$]/u.test(character)
|
|
959
|
+
) {
|
|
960
|
+
const identifierStart = index;
|
|
961
|
+
for (index += 1; index < normalizedSourceText.length; index += 1) {
|
|
962
|
+
if (!/[\w$]/u.test(normalizedSourceText[index] || "")) {
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const identifier = normalizedSourceText.slice(identifierStart, index);
|
|
968
|
+
index -= 1;
|
|
969
|
+
if (identifier !== normalizedPropertyName) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
let cursor = index + 1;
|
|
974
|
+
while (/\s/u.test(normalizedSourceText[cursor] || "")) {
|
|
975
|
+
cursor += 1;
|
|
976
|
+
}
|
|
977
|
+
if (normalizedSourceText[cursor] === ":") {
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
|
|
488
986
|
function isSharedListFiltersImportSource(sourcePath = "") {
|
|
489
987
|
return /(^|\/)shared\/[^/'"]*ListFilters(?:\.[A-Za-z0-9]+)?$/u.test(String(sourcePath || "").trim());
|
|
490
988
|
}
|
|
@@ -570,6 +1068,26 @@ function createHealthCommands(ctx = {}) {
|
|
|
570
1068
|
}
|
|
571
1069
|
}
|
|
572
1070
|
|
|
1071
|
+
function collectCrudTransportOwnershipIssues({
|
|
1072
|
+
sourceText = "",
|
|
1073
|
+
relativePath = "",
|
|
1074
|
+
issues = []
|
|
1075
|
+
}) {
|
|
1076
|
+
for (const calleeName of CRUD_TRANSPORT_RUNTIME_CALLEES) {
|
|
1077
|
+
for (const callSite of findCallSites(sourceText, calleeName)) {
|
|
1078
|
+
const firstArgument = extractFirstArgumentText(callSite.argsText).trim();
|
|
1079
|
+
if (!hasTopLevelObjectProperty(firstArgument, "transport")) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const lineNumber = resolveLineNumberFromIndex(sourceText, callSite.index);
|
|
1084
|
+
issues.push(
|
|
1085
|
+
`${relativePath}:${lineNumber}: [crud:transport-derived] do not pass explicit transport to ${calleeName}(...). Let the shared CRUD resource derive JSON:API transport automatically, or drop to useList/useView/useAddEdit/usersWebHttpClient.request(...) for custom transport behavior.`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
573
1091
|
async function collectMdiSvgDoctorIssues({ appRoot, issues }) {
|
|
574
1092
|
if (!(await appUsesVuetifyMdiSvg(appRoot))) {
|
|
575
1093
|
return;
|
|
@@ -609,7 +1127,10 @@ function createHealthCommands(ctx = {}) {
|
|
|
609
1127
|
if (
|
|
610
1128
|
!sourceText.includes("useCrudListFilters") &&
|
|
611
1129
|
!sourceText.includes("createCrudListFilters") &&
|
|
612
|
-
!sourceText.includes("createQueryValidator")
|
|
1130
|
+
!sourceText.includes("createQueryValidator") &&
|
|
1131
|
+
!sourceText.includes("useCrudList") &&
|
|
1132
|
+
!sourceText.includes("useCrudView") &&
|
|
1133
|
+
!sourceText.includes("useCrudAddEdit")
|
|
613
1134
|
) {
|
|
614
1135
|
continue;
|
|
615
1136
|
}
|
|
@@ -625,6 +1146,11 @@ function createHealthCommands(ctx = {}) {
|
|
|
625
1146
|
relativePath,
|
|
626
1147
|
issues
|
|
627
1148
|
});
|
|
1149
|
+
collectCrudTransportOwnershipIssues({
|
|
1150
|
+
sourceText,
|
|
1151
|
+
relativePath,
|
|
1152
|
+
issues
|
|
1153
|
+
});
|
|
628
1154
|
}
|
|
629
1155
|
}
|
|
630
1156
|
|
|
@@ -730,6 +1256,7 @@ function createHealthCommands(ctx = {}) {
|
|
|
730
1256
|
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
731
1257
|
const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
|
|
732
1258
|
const issues = [];
|
|
1259
|
+
const warnings = [];
|
|
733
1260
|
const installed = ensureObject(lock.installedPackages);
|
|
734
1261
|
await hydratePackageRegistryFromInstalledNodeModules({
|
|
735
1262
|
appRoot,
|
|
@@ -767,12 +1294,19 @@ function createHealthCommands(ctx = {}) {
|
|
|
767
1294
|
appRoot,
|
|
768
1295
|
issues
|
|
769
1296
|
});
|
|
1297
|
+
await collectFeatureLaneDoctorIssues({
|
|
1298
|
+
appRoot,
|
|
1299
|
+
appLocalRegistry,
|
|
1300
|
+
issues,
|
|
1301
|
+
warnings
|
|
1302
|
+
});
|
|
770
1303
|
|
|
771
1304
|
const payload = {
|
|
772
1305
|
appRoot,
|
|
773
1306
|
lockVersion: lock.lockVersion,
|
|
774
1307
|
installedPackages: sortStrings(Object.keys(installed)),
|
|
775
|
-
issues
|
|
1308
|
+
issues,
|
|
1309
|
+
warnings: sortStrings(warnings)
|
|
776
1310
|
};
|
|
777
1311
|
|
|
778
1312
|
if (options.json) {
|
|
@@ -780,13 +1314,21 @@ function createHealthCommands(ctx = {}) {
|
|
|
780
1314
|
} else {
|
|
781
1315
|
stdout.write(`App root: ${appRoot}\n`);
|
|
782
1316
|
stdout.write(`Installed packages: ${payload.installedPackages.length}\n`);
|
|
783
|
-
if (issues.length === 0) {
|
|
1317
|
+
if (issues.length === 0 && payload.warnings.length === 0) {
|
|
784
1318
|
stdout.write("Doctor status: healthy\n");
|
|
1319
|
+
} else if (issues.length === 0) {
|
|
1320
|
+
stdout.write(`Doctor status: warnings (${payload.warnings.length} warning(s))\n`);
|
|
1321
|
+
for (const warning of payload.warnings) {
|
|
1322
|
+
stdout.write(`! ${warning}\n`);
|
|
1323
|
+
}
|
|
785
1324
|
} else {
|
|
786
1325
|
stdout.write(`Doctor status: unhealthy (${issues.length} issue(s))\n`);
|
|
787
1326
|
for (const issue of issues) {
|
|
788
1327
|
stdout.write(`- ${issue}\n`);
|
|
789
1328
|
}
|
|
1329
|
+
for (const warning of payload.warnings) {
|
|
1330
|
+
stdout.write(`! ${warning}\n`);
|
|
1331
|
+
}
|
|
790
1332
|
}
|
|
791
1333
|
}
|
|
792
1334
|
|
|
@@ -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]",
|
|
@@ -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
|
|