@jskit-ai/jskit-cli 0.2.67 → 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 +23 -7
- package/src/server/commandHandlers/health.js +371 -2
- 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;
|
|
@@ -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,
|
|
@@ -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();
|
|
@@ -903,6 +1256,7 @@ function createHealthCommands(ctx = {}) {
|
|
|
903
1256
|
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
904
1257
|
const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
|
|
905
1258
|
const issues = [];
|
|
1259
|
+
const warnings = [];
|
|
906
1260
|
const installed = ensureObject(lock.installedPackages);
|
|
907
1261
|
await hydratePackageRegistryFromInstalledNodeModules({
|
|
908
1262
|
appRoot,
|
|
@@ -940,12 +1294,19 @@ function createHealthCommands(ctx = {}) {
|
|
|
940
1294
|
appRoot,
|
|
941
1295
|
issues
|
|
942
1296
|
});
|
|
1297
|
+
await collectFeatureLaneDoctorIssues({
|
|
1298
|
+
appRoot,
|
|
1299
|
+
appLocalRegistry,
|
|
1300
|
+
issues,
|
|
1301
|
+
warnings
|
|
1302
|
+
});
|
|
943
1303
|
|
|
944
1304
|
const payload = {
|
|
945
1305
|
appRoot,
|
|
946
1306
|
lockVersion: lock.lockVersion,
|
|
947
1307
|
installedPackages: sortStrings(Object.keys(installed)),
|
|
948
|
-
issues
|
|
1308
|
+
issues,
|
|
1309
|
+
warnings: sortStrings(warnings)
|
|
949
1310
|
};
|
|
950
1311
|
|
|
951
1312
|
if (options.json) {
|
|
@@ -953,13 +1314,21 @@ function createHealthCommands(ctx = {}) {
|
|
|
953
1314
|
} else {
|
|
954
1315
|
stdout.write(`App root: ${appRoot}\n`);
|
|
955
1316
|
stdout.write(`Installed packages: ${payload.installedPackages.length}\n`);
|
|
956
|
-
if (issues.length === 0) {
|
|
1317
|
+
if (issues.length === 0 && payload.warnings.length === 0) {
|
|
957
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
|
+
}
|
|
958
1324
|
} else {
|
|
959
1325
|
stdout.write(`Doctor status: unhealthy (${issues.length} issue(s))\n`);
|
|
960
1326
|
for (const issue of issues) {
|
|
961
1327
|
stdout.write(`- ${issue}\n`);
|
|
962
1328
|
}
|
|
1329
|
+
for (const warning of payload.warnings) {
|
|
1330
|
+
stdout.write(`! ${warning}\n`);
|
|
1331
|
+
}
|
|
963
1332
|
}
|
|
964
1333
|
}
|
|
965
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
|
|