@jskit-ai/jskit-cli 0.2.30 → 0.2.32
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 +3 -3
- package/src/server/cliRuntime/mutationWhen.js +25 -7
- package/src/server/commandHandlers/health.js +140 -0
- package/src/server/commandHandlers/list.js +268 -1
- package/src/server/commandHandlers/packageCommands/add.js +113 -0
- package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +649 -0
- package/src/server/commandHandlers/packageCommands/generate.js +151 -10
- package/src/server/commandHandlers/packageCommands/tabLinkItemProvisioning.js +326 -0
- package/src/server/core/commandCatalog.js +4 -1
- package/src/server/core/createCommandHandlers.js +2 -1
- package/src/server/core/dispatchCli.js +3 -0
- package/src/server/core/usageHelp.js +32 -5
- package/src/server/shared/optionInterpolation.js +6 -0
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isHelpToken,
|
|
3
|
+
renderGenerateCatalogHelp,
|
|
4
|
+
renderGeneratePackageHelp,
|
|
5
|
+
renderGenerateSubcommandHelp
|
|
6
|
+
} from "./discoverabilityHelp.js";
|
|
7
|
+
|
|
8
|
+
function resolveGeneratorSubcommandDefinitionMetadata(packageEntry = {}, subcommandName = "") {
|
|
9
|
+
const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
|
|
10
|
+
? packageEntry.descriptor
|
|
11
|
+
: {};
|
|
12
|
+
const metadata = descriptor?.metadata && typeof descriptor.metadata === "object"
|
|
13
|
+
? descriptor.metadata
|
|
14
|
+
: {};
|
|
15
|
+
const subcommands = metadata?.generatorSubcommands && typeof metadata.generatorSubcommands === "object"
|
|
16
|
+
? metadata.generatorSubcommands
|
|
17
|
+
: descriptor?.generatorSubcommands && typeof descriptor.generatorSubcommands === "object"
|
|
18
|
+
? descriptor.generatorSubcommands
|
|
19
|
+
: {};
|
|
20
|
+
const normalizedSubcommandName = String(subcommandName || "").trim();
|
|
21
|
+
if (!normalizedSubcommandName) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const definition = subcommands[normalizedSubcommandName];
|
|
25
|
+
return definition && typeof definition === "object" ? definition : {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveSubcommandRequiresInput(packageEntry = {}, subcommandName = "") {
|
|
29
|
+
const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
|
|
30
|
+
? packageEntry.descriptor
|
|
31
|
+
: {};
|
|
32
|
+
const optionSchemas = descriptor?.options && typeof descriptor.options === "object"
|
|
33
|
+
? descriptor.options
|
|
34
|
+
: {};
|
|
35
|
+
const subcommandDefinition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
|
|
36
|
+
const positionalArgs = Array.isArray(subcommandDefinition?.positionalArgs)
|
|
37
|
+
? subcommandDefinition.positionalArgs
|
|
38
|
+
: [];
|
|
39
|
+
if (positionalArgs.length > 0) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const requiredOptionNames = Array.isArray(subcommandDefinition?.requiredOptionNames)
|
|
43
|
+
? subcommandDefinition.requiredOptionNames
|
|
44
|
+
: [];
|
|
45
|
+
if (requiredOptionNames.some((optionName) => String(optionName || "").trim().length > 0)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const optionNames = Array.isArray(subcommandDefinition?.optionNames) && subcommandDefinition.optionNames.length > 0
|
|
50
|
+
? subcommandDefinition.optionNames
|
|
51
|
+
: Object.keys(optionSchemas);
|
|
52
|
+
for (const optionName of optionNames) {
|
|
53
|
+
const normalizedOptionName = String(optionName || "").trim();
|
|
54
|
+
if (!normalizedOptionName) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const schema = optionSchemas[normalizedOptionName];
|
|
58
|
+
if (schema && typeof schema === "object" && schema.required === true) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
1
65
|
async function runPackageGenerateCommand(
|
|
2
66
|
ctx = {},
|
|
3
67
|
{ positional, options, cwd, io },
|
|
@@ -28,13 +92,8 @@ async function runPackageGenerateCommand(
|
|
|
28
92
|
const targetId = firstToken === "package" ? secondToken : firstToken;
|
|
29
93
|
const subcommandName = firstToken === "package" ? thirdToken : secondToken;
|
|
30
94
|
const subcommandArgs = firstToken === "package" ? positional.slice(3) : positional.slice(2);
|
|
31
|
-
if (!targetId) {
|
|
32
|
-
throw createCliError("generate requires a package id (generate <packageId>).", {
|
|
33
|
-
showUsage: true
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
95
|
|
|
37
|
-
|
|
96
|
+
async function resolveGeneratorPackageEntry(packageIdInput = "") {
|
|
38
97
|
const appRoot = await resolveAppRootFromCwd(cwd);
|
|
39
98
|
const packageRegistry = await loadPackageRegistry();
|
|
40
99
|
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
@@ -43,11 +102,11 @@ async function runPackageGenerateCommand(
|
|
|
43
102
|
const resolvedPackageId = await resolvePackageIdFromRegistryOrNodeModules({
|
|
44
103
|
appRoot,
|
|
45
104
|
packageRegistry: combinedPackageRegistry,
|
|
46
|
-
packageIdInput
|
|
105
|
+
packageIdInput
|
|
47
106
|
});
|
|
48
107
|
if (!resolvedPackageId) {
|
|
49
108
|
throw createCliError(
|
|
50
|
-
`Unknown package: ${
|
|
109
|
+
`Unknown package: ${packageIdInput}. Install it first (npm install ${packageIdInput}) if you want to run generator subcommands from node_modules.`
|
|
51
110
|
);
|
|
52
111
|
}
|
|
53
112
|
|
|
@@ -58,15 +117,97 @@ async function runPackageGenerateCommand(
|
|
|
58
117
|
});
|
|
59
118
|
const packageEntry = combinedPackageRegistry.get(resolvedPackageId);
|
|
60
119
|
if (!packageEntry) {
|
|
61
|
-
throw createCliError(`Unknown package: ${
|
|
120
|
+
throw createCliError(`Unknown package: ${packageIdInput}`);
|
|
62
121
|
}
|
|
63
|
-
|
|
64
122
|
if (resolvePackageKind(packageEntry) !== "generator") {
|
|
65
123
|
throw createCliError(
|
|
66
124
|
`Package ${resolvedPackageId} is a runtime package. Use: jskit add package ${resolvedPackageId}`
|
|
67
125
|
);
|
|
68
126
|
}
|
|
69
127
|
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
appRoot,
|
|
130
|
+
packageEntry,
|
|
131
|
+
resolvedPackageId
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!targetId) {
|
|
136
|
+
const packageRegistry = await loadPackageRegistry();
|
|
137
|
+
renderGenerateCatalogHelp({
|
|
138
|
+
io,
|
|
139
|
+
packageRegistry,
|
|
140
|
+
resolvePackageKind,
|
|
141
|
+
json: options.json
|
|
142
|
+
});
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isHelpToken(subcommandName)) {
|
|
147
|
+
const helpSubcommandName = String(subcommandArgs[0] || "").trim();
|
|
148
|
+
if (subcommandArgs.length > 1) {
|
|
149
|
+
throw createCliError("generate help accepts at most one subcommand name.");
|
|
150
|
+
}
|
|
151
|
+
const { packageEntry } = await resolveGeneratorPackageEntry(targetId);
|
|
152
|
+
if (helpSubcommandName) {
|
|
153
|
+
const rendered = renderGenerateSubcommandHelp({
|
|
154
|
+
io,
|
|
155
|
+
packageEntry,
|
|
156
|
+
packageIdInput: targetId,
|
|
157
|
+
subcommandName: helpSubcommandName,
|
|
158
|
+
json: options.json
|
|
159
|
+
});
|
|
160
|
+
if (!rendered) {
|
|
161
|
+
throw createCliError(
|
|
162
|
+
`Unknown generator subcommand "${helpSubcommandName}" for ${String(packageEntry?.packageId || targetId)}.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
renderGeneratePackageHelp({
|
|
169
|
+
io,
|
|
170
|
+
packageEntry,
|
|
171
|
+
packageIdInput: targetId,
|
|
172
|
+
json: options.json
|
|
173
|
+
});
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (subcommandName) {
|
|
178
|
+
const {
|
|
179
|
+
appRoot,
|
|
180
|
+
packageEntry,
|
|
181
|
+
resolvedPackageId
|
|
182
|
+
} = await resolveGeneratorPackageEntry(targetId);
|
|
183
|
+
const hasInlineOptions = Object.keys(options?.inlineOptions || {}).length > 0;
|
|
184
|
+
const hasSubcommandArgs = subcommandArgs.length > 0;
|
|
185
|
+
if (!hasInlineOptions && !hasSubcommandArgs && resolveSubcommandRequiresInput(packageEntry, subcommandName)) {
|
|
186
|
+
const rendered = renderGenerateSubcommandHelp({
|
|
187
|
+
io,
|
|
188
|
+
packageEntry,
|
|
189
|
+
packageIdInput: targetId,
|
|
190
|
+
subcommandName,
|
|
191
|
+
json: options.json
|
|
192
|
+
});
|
|
193
|
+
if (rendered) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (subcommandArgs.length === 1 && isHelpToken(subcommandArgs[0])) {
|
|
198
|
+
const rendered = renderGenerateSubcommandHelp({
|
|
199
|
+
io,
|
|
200
|
+
packageEntry,
|
|
201
|
+
packageIdInput: targetId,
|
|
202
|
+
subcommandName,
|
|
203
|
+
json: options.json
|
|
204
|
+
});
|
|
205
|
+
if (!rendered) {
|
|
206
|
+
throw createCliError(`Unknown generator subcommand "${subcommandName}" for ${resolvedPackageId}.`);
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
70
211
|
const normalizedSubcommandName = String(subcommandName || "").trim().toLowerCase();
|
|
71
212
|
const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
|
|
72
213
|
if (
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const MAIN_CLIENT_PROVIDER_FILE = "packages/main/src/client/providers/MainClientProvider.js";
|
|
5
|
+
const TAB_LINK_COMPONENT_FILE = "src/components/TabLinkItem.vue";
|
|
6
|
+
const TAB_LINK_COMPONENT_NAME = "TabLinkItem";
|
|
7
|
+
const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
|
|
8
|
+
|
|
9
|
+
function toPosixPath(value = "") {
|
|
10
|
+
return String(value || "").replaceAll("\\", "/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ensureTrailingNewline(value = "") {
|
|
14
|
+
const source = String(value || "");
|
|
15
|
+
return source.endsWith("\n") ? source : `${source}\n`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function insertImportIfMissing(source = "", importLine = "") {
|
|
19
|
+
const normalizedImportLine = String(importLine || "").trim();
|
|
20
|
+
const sourceText = String(source || "");
|
|
21
|
+
if (!normalizedImportLine || sourceText.includes(normalizedImportLine)) {
|
|
22
|
+
return {
|
|
23
|
+
changed: false,
|
|
24
|
+
content: sourceText
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const importPattern = /^import\s+[^;]+;\s*$/gm;
|
|
29
|
+
let match = null;
|
|
30
|
+
let insertionIndex = 0;
|
|
31
|
+
while ((match = importPattern.exec(sourceText)) !== null) {
|
|
32
|
+
insertionIndex = match.index + match[0].length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (insertionIndex > 0) {
|
|
36
|
+
return {
|
|
37
|
+
changed: true,
|
|
38
|
+
content: `${sourceText.slice(0, insertionIndex)}\n${normalizedImportLine}${sourceText.slice(insertionIndex)}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
changed: true,
|
|
44
|
+
content: `${normalizedImportLine}\n${sourceText}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function insertBeforeClassDeclaration(source = "", line = "", { className = "", contextFile = "" } = {}) {
|
|
49
|
+
const normalizedLine = String(line || "").trim();
|
|
50
|
+
const sourceText = String(source || "");
|
|
51
|
+
if (!normalizedLine || sourceText.includes(normalizedLine)) {
|
|
52
|
+
return {
|
|
53
|
+
changed: false,
|
|
54
|
+
content: sourceText
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const classPattern = new RegExp(`^class\\s+${String(className || "").trim()}\\b`, "m");
|
|
59
|
+
const classMatch = classPattern.exec(sourceText);
|
|
60
|
+
if (!classMatch) {
|
|
61
|
+
throw new Error(`crud-ui-generator could not find ${className} class declaration in ${contextFile || "target file"}.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
changed: true,
|
|
66
|
+
content: `${sourceText.slice(0, classMatch.index)}${normalizedLine}\n\n${sourceText.slice(classMatch.index)}`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderTabLinkItemSource() {
|
|
71
|
+
return `<script setup>
|
|
72
|
+
import { computed } from "vue";
|
|
73
|
+
import { useRoute } from "vue-router";
|
|
74
|
+
import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
|
|
75
|
+
import { useWorkspaceRouteContext } from "@jskit-ai/users-web/client/composables/useWorkspaceRouteContext";
|
|
76
|
+
|
|
77
|
+
const props = defineProps({
|
|
78
|
+
label: {
|
|
79
|
+
type: String,
|
|
80
|
+
default: ""
|
|
81
|
+
},
|
|
82
|
+
to: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: ""
|
|
85
|
+
},
|
|
86
|
+
surface: {
|
|
87
|
+
type: String,
|
|
88
|
+
default: ""
|
|
89
|
+
},
|
|
90
|
+
workspaceSuffix: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: "/"
|
|
93
|
+
},
|
|
94
|
+
nonWorkspaceSuffix: {
|
|
95
|
+
type: String,
|
|
96
|
+
default: "/"
|
|
97
|
+
},
|
|
98
|
+
disabled: {
|
|
99
|
+
type: Boolean,
|
|
100
|
+
default: false
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const route = useRoute();
|
|
105
|
+
const paths = usePaths();
|
|
106
|
+
const { currentSurfaceId, workspaceSlugFromRoute } = useWorkspaceRouteContext();
|
|
107
|
+
|
|
108
|
+
function normalizePathname(pathname = "") {
|
|
109
|
+
const source = String(pathname || "").trim();
|
|
110
|
+
if (!source) {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const queryIndex = source.indexOf("?");
|
|
115
|
+
const hashIndex = source.indexOf("#");
|
|
116
|
+
const cutoff =
|
|
117
|
+
queryIndex < 0
|
|
118
|
+
? hashIndex
|
|
119
|
+
: hashIndex < 0
|
|
120
|
+
? queryIndex
|
|
121
|
+
: Math.min(queryIndex, hashIndex);
|
|
122
|
+
return cutoff < 0 ? source : source.slice(0, cutoff);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function interpolateBracketParams(pathTemplate = "", params = {}) {
|
|
126
|
+
const source = String(pathTemplate || "").trim();
|
|
127
|
+
if (!source) {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return source.replace(/\\[([^\\]]+)\\]/g, (_match, rawKey) => {
|
|
132
|
+
const key = String(rawKey || "").trim();
|
|
133
|
+
if (!key) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
const value = params?.[key];
|
|
137
|
+
return value == null ? "[" + key + "]" : encodeURIComponent(String(value));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const targetSurfaceId = computed(() => {
|
|
142
|
+
const explicitSurface = String(props.surface || "").trim().toLowerCase();
|
|
143
|
+
if (explicitSurface && explicitSurface !== "*") {
|
|
144
|
+
return explicitSurface;
|
|
145
|
+
}
|
|
146
|
+
return String(currentSurfaceId.value || paths.currentSurfaceId.value || "").trim().toLowerCase();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const resolvedTo = computed(() => {
|
|
150
|
+
const explicitTo = String(props.to || "").trim();
|
|
151
|
+
if (explicitTo) {
|
|
152
|
+
if (explicitTo.startsWith("./")) {
|
|
153
|
+
const workspaceSlug = String(workspaceSlugFromRoute.value || "").trim();
|
|
154
|
+
const suffixTemplate = workspaceSlug ? props.workspaceSuffix : props.nonWorkspaceSuffix;
|
|
155
|
+
const interpolatedSuffix = interpolateBracketParams(suffixTemplate, route.params || {});
|
|
156
|
+
if (interpolatedSuffix && !interpolatedSuffix.includes("[")) {
|
|
157
|
+
return paths.page(interpolatedSuffix, {
|
|
158
|
+
surface: targetSurfaceId.value,
|
|
159
|
+
mode: "auto"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return explicitTo;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const workspaceSlug = String(workspaceSlugFromRoute.value || "").trim();
|
|
167
|
+
const suffix = workspaceSlug ? props.workspaceSuffix : props.nonWorkspaceSuffix;
|
|
168
|
+
const normalizedSuffix = String(suffix || "/").trim() || "/";
|
|
169
|
+
return paths.page(normalizedSuffix, {
|
|
170
|
+
surface: targetSurfaceId.value,
|
|
171
|
+
mode: "auto"
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const isActive = computed(() => {
|
|
176
|
+
const targetPath = normalizePathname(resolvedTo.value);
|
|
177
|
+
const currentPath = normalizePathname(route.fullPath || route.path || "");
|
|
178
|
+
if (!targetPath || !currentPath) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return currentPath === targetPath || currentPath.startsWith(\`\${targetPath}/\`);
|
|
182
|
+
});
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<v-btn
|
|
187
|
+
class="tab-link-item"
|
|
188
|
+
variant="text"
|
|
189
|
+
size="small"
|
|
190
|
+
:to="resolvedTo"
|
|
191
|
+
:active="isActive"
|
|
192
|
+
:disabled="disabled"
|
|
193
|
+
color="primary"
|
|
194
|
+
>
|
|
195
|
+
{{ label || "Tab" }}
|
|
196
|
+
</v-btn>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<style scoped>
|
|
200
|
+
.tab-link-item {
|
|
201
|
+
text-transform: none;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
border-radius: 999px;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function readUtf8FileIfExists(absolutePath = "") {
|
|
210
|
+
try {
|
|
211
|
+
return await readFile(absolutePath, "utf8");
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error && error.code === "ENOENT") {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function ensureTabLinkItemComponentFile({ appRoot = "", dryRun = false, touchedFiles = new Set() } = {}) {
|
|
221
|
+
const componentRelativePath = TAB_LINK_COMPONENT_FILE;
|
|
222
|
+
const componentAbsolutePath = path.join(appRoot, componentRelativePath);
|
|
223
|
+
const existingComponentSource = await readUtf8FileIfExists(componentAbsolutePath);
|
|
224
|
+
if (existingComponentSource) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (dryRun !== true) {
|
|
229
|
+
await mkdir(path.dirname(componentAbsolutePath), { recursive: true });
|
|
230
|
+
await writeFile(componentAbsolutePath, renderTabLinkItemSource(), "utf8");
|
|
231
|
+
}
|
|
232
|
+
touchedFiles.add(toPosixPath(componentRelativePath));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasTabLinkItemTokenRegistration(providerSource = "") {
|
|
236
|
+
const tokenPattern = TAB_LINK_COMPONENT_TOKEN.replaceAll(".", "\\.");
|
|
237
|
+
const pattern = new RegExp(`registerMainClientComponent\\(\\s*"${tokenPattern}"\\s*,`, "m");
|
|
238
|
+
return pattern.test(String(providerSource || ""));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function loadMainClientProviderSource({ appRoot = "", createCliError } = {}) {
|
|
242
|
+
const providerAbsolutePath = path.join(appRoot, MAIN_CLIENT_PROVIDER_FILE);
|
|
243
|
+
let providerSource = "";
|
|
244
|
+
try {
|
|
245
|
+
providerSource = await readFile(providerAbsolutePath, "utf8");
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error && error.code === "ENOENT") {
|
|
248
|
+
throw createCliError(
|
|
249
|
+
`crud-ui-generator placement component token "${TAB_LINK_COMPONENT_TOKEN}" requires ${MAIN_CLIENT_PROVIDER_FILE}.`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
|
|
256
|
+
throw createCliError(
|
|
257
|
+
`crud-ui-generator placement component token "${TAB_LINK_COMPONENT_TOKEN}" could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return providerSource;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function ensureTabLinkItemProviderRegistration({
|
|
265
|
+
appRoot = "",
|
|
266
|
+
createCliError,
|
|
267
|
+
dryRun = false,
|
|
268
|
+
touchedFiles = new Set()
|
|
269
|
+
} = {}) {
|
|
270
|
+
const providerRelativePath = MAIN_CLIENT_PROVIDER_FILE;
|
|
271
|
+
const providerAbsolutePath = path.join(appRoot, providerRelativePath);
|
|
272
|
+
const providerSource = await loadMainClientProviderSource({
|
|
273
|
+
appRoot,
|
|
274
|
+
createCliError
|
|
275
|
+
});
|
|
276
|
+
if (hasTabLinkItemTokenRegistration(providerSource)) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const importLine = `import ${TAB_LINK_COMPONENT_NAME} from "/${toPosixPath(TAB_LINK_COMPONENT_FILE)}";`;
|
|
281
|
+
const registerLine = `registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT_NAME});`;
|
|
282
|
+
|
|
283
|
+
const importApplied = insertImportIfMissing(providerSource, importLine);
|
|
284
|
+
const registerApplied = insertBeforeClassDeclaration(importApplied.content, registerLine, {
|
|
285
|
+
className: "MainClientProvider",
|
|
286
|
+
contextFile: MAIN_CLIENT_PROVIDER_FILE
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!importApplied.changed && !registerApplied.changed) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (dryRun !== true) {
|
|
294
|
+
await writeFile(providerAbsolutePath, ensureTrailingNewline(registerApplied.content), "utf8");
|
|
295
|
+
}
|
|
296
|
+
touchedFiles.add(toPosixPath(providerRelativePath));
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function ensureLocalMainTabLinkItemProvisioning({
|
|
301
|
+
appRoot = "",
|
|
302
|
+
createCliError,
|
|
303
|
+
dryRun = false,
|
|
304
|
+
touchedFiles = new Set()
|
|
305
|
+
} = {}) {
|
|
306
|
+
const providerSource = await loadMainClientProviderSource({
|
|
307
|
+
appRoot,
|
|
308
|
+
createCliError
|
|
309
|
+
});
|
|
310
|
+
if (hasTabLinkItemTokenRegistration(providerSource)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await ensureTabLinkItemComponentFile({ appRoot, dryRun, touchedFiles });
|
|
315
|
+
await ensureTabLinkItemProviderRegistration({
|
|
316
|
+
appRoot,
|
|
317
|
+
createCliError,
|
|
318
|
+
dryRun,
|
|
319
|
+
touchedFiles
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export {
|
|
324
|
+
TAB_LINK_COMPONENT_TOKEN,
|
|
325
|
+
ensureLocalMainTabLinkItemProvisioning
|
|
326
|
+
};
|
|
@@ -4,6 +4,7 @@ const KNOWN_COMMAND_IDS = Object.freeze([
|
|
|
4
4
|
"generate",
|
|
5
5
|
"list",
|
|
6
6
|
"list-placements",
|
|
7
|
+
"list-link-items",
|
|
7
8
|
"show",
|
|
8
9
|
"view",
|
|
9
10
|
"migrations",
|
|
@@ -21,7 +22,9 @@ const COMMAND_ALIASES = Object.freeze({
|
|
|
21
22
|
view: "show",
|
|
22
23
|
ls: "list",
|
|
23
24
|
gen: "generate",
|
|
24
|
-
lp: "list-placements"
|
|
25
|
+
lp: "list-placements",
|
|
26
|
+
lpct: "list-link-items",
|
|
27
|
+
"list-placement-component-tokens": "list-link-items"
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
function resolveCommandAlias(rawCommand) {
|
|
@@ -11,7 +11,7 @@ function createCommandHandlers(deps = {}) {
|
|
|
11
11
|
...shared
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const { commandList, commandListPlacements } = createListCommands(commandContext);
|
|
14
|
+
const { commandList, commandListPlacements, commandListLinkItems } = createListCommands(commandContext);
|
|
15
15
|
const { commandShow } = createShowCommand(commandContext);
|
|
16
16
|
const {
|
|
17
17
|
commandCreate,
|
|
@@ -27,6 +27,7 @@ function createCommandHandlers(deps = {}) {
|
|
|
27
27
|
return {
|
|
28
28
|
commandList,
|
|
29
29
|
commandListPlacements,
|
|
30
|
+
commandListLinkItems,
|
|
30
31
|
commandShow,
|
|
31
32
|
commandCreate,
|
|
32
33
|
commandAdd,
|
|
@@ -58,6 +58,9 @@ function createRunCli({
|
|
|
58
58
|
if (command === "list-placements") {
|
|
59
59
|
return await commandHandlers.commandListPlacements({ options, cwd, stdout });
|
|
60
60
|
}
|
|
61
|
+
if (command === "list-link-items") {
|
|
62
|
+
return await commandHandlers.commandListLinkItems({ options, cwd, stdout });
|
|
63
|
+
}
|
|
61
64
|
if (command === "show") {
|
|
62
65
|
return await commandHandlers.commandShow({ positional, options, stdout });
|
|
63
66
|
}
|
|
@@ -21,6 +21,10 @@ const COMMAND_OVERVIEW = Object.freeze([
|
|
|
21
21
|
command: "list-placements",
|
|
22
22
|
summary: "List discovered UI placement targets."
|
|
23
23
|
}),
|
|
24
|
+
Object.freeze({
|
|
25
|
+
command: "list-link-items",
|
|
26
|
+
summary: "List available placement link-item component tokens."
|
|
27
|
+
}),
|
|
24
28
|
Object.freeze({
|
|
25
29
|
command: "show",
|
|
26
30
|
summary: "Show detailed metadata for a bundle or package."
|
|
@@ -84,6 +88,7 @@ const COMMAND_HELP = Object.freeze({
|
|
|
84
88
|
defaults: Object.freeze([
|
|
85
89
|
"No npm install runs unless --run-npm-install is passed.",
|
|
86
90
|
"Short ids resolve to @jskit-ai/<id> when available.",
|
|
91
|
+
"Running without args lists bundles and runtime packages.",
|
|
87
92
|
"Existing matching version is skipped unless options force reapply."
|
|
88
93
|
]),
|
|
89
94
|
fullUse: "jskit add <package|bundle> <id> [--<option> <value>...] [--dry-run] [--run-npm-install] [--json] [--verbose]"
|
|
@@ -98,7 +103,7 @@ const COMMAND_HELP = Object.freeze({
|
|
|
98
103
|
}),
|
|
99
104
|
Object.freeze({
|
|
100
105
|
name: "[subcommand]",
|
|
101
|
-
description: "Optional generator subcommand (for example:
|
|
106
|
+
description: "Optional generator subcommand (for example: scaffold or scaffold-field)."
|
|
102
107
|
}),
|
|
103
108
|
Object.freeze({
|
|
104
109
|
name: "[subcommand args...]",
|
|
@@ -108,7 +113,9 @@ const COMMAND_HELP = Object.freeze({
|
|
|
108
113
|
defaults: Object.freeze([
|
|
109
114
|
"No npm install runs unless --run-npm-install is passed.",
|
|
110
115
|
"Short ids resolve to @jskit-ai/<id> when available.",
|
|
111
|
-
"
|
|
116
|
+
"Running without args lists available generators.",
|
|
117
|
+
"If no subcommand is provided, the generator primary command runs.",
|
|
118
|
+
"Use jskit generate <generatorId> <subcommand> help for subcommand-specific usage."
|
|
112
119
|
]),
|
|
113
120
|
fullUse: "jskit generate <generatorId> [subcommand] [subcommand args...] [--<option> <value>...] [--dry-run] [--run-npm-install] [--json] [--verbose]"
|
|
114
121
|
}),
|
|
@@ -133,12 +140,34 @@ const COMMAND_HELP = Object.freeze({
|
|
|
133
140
|
minimalUse: "jskit list-placements",
|
|
134
141
|
parameters: Object.freeze([]),
|
|
135
142
|
defaults: Object.freeze([
|
|
136
|
-
"Discovers
|
|
143
|
+
"Discovers placement outlets from app Vue ShellOutlet tags and route meta.",
|
|
137
144
|
"Includes placement outlets contributed by installed package metadata.",
|
|
138
145
|
"Shows plain text by default; use --json for structured output."
|
|
139
146
|
]),
|
|
140
147
|
fullUse: "jskit list-placements [--json]"
|
|
141
148
|
}),
|
|
149
|
+
"list-link-items": Object.freeze({
|
|
150
|
+
title: "list-link-items",
|
|
151
|
+
minimalUse: "jskit list-link-items",
|
|
152
|
+
parameters: Object.freeze([
|
|
153
|
+
Object.freeze({
|
|
154
|
+
name: "[--prefix <value>]",
|
|
155
|
+
description: "Optional token prefix filter (example: local.main. or users.web.shell.)."
|
|
156
|
+
}),
|
|
157
|
+
Object.freeze({
|
|
158
|
+
name: "[--all]",
|
|
159
|
+
description: "Include all discovered tokens (including non-link-item and client container/runtime tokens)."
|
|
160
|
+
})
|
|
161
|
+
]),
|
|
162
|
+
defaults: Object.freeze([
|
|
163
|
+
"Default output shows link-item tokens only (token names ending with link-item).",
|
|
164
|
+
"Default includes app and installed-package placement-linked token sources.",
|
|
165
|
+
"Use --prefix to narrow quickly (recommended: --prefix local.main.).",
|
|
166
|
+
"Use --all when you want the full discovered token set.",
|
|
167
|
+
"Shows plain text by default; use --json for structured output."
|
|
168
|
+
]),
|
|
169
|
+
fullUse: "jskit list-link-items [--prefix <value>] [--all] [--json]"
|
|
170
|
+
}),
|
|
142
171
|
show: Object.freeze({
|
|
143
172
|
title: "show",
|
|
144
173
|
minimalUse: "jskit show <id>",
|
|
@@ -261,8 +290,6 @@ const COMMAND_HELP = Object.freeze({
|
|
|
261
290
|
|
|
262
291
|
const BARE_COMMAND_HELP = new Set([
|
|
263
292
|
"create",
|
|
264
|
-
"add",
|
|
265
|
-
"generate",
|
|
266
293
|
"show",
|
|
267
294
|
"migrations",
|
|
268
295
|
"position",
|
|
@@ -197,6 +197,12 @@ function normalizePathValue(value) {
|
|
|
197
197
|
return normalizedSegment;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
const routeGroupMatch = /^\(([^()]+)\)$/.exec(normalizedSegment);
|
|
201
|
+
if (routeGroupMatch) {
|
|
202
|
+
const routeGroupName = wordsToKebab(splitTextIntoWords(routeGroupMatch[1]));
|
|
203
|
+
return routeGroupName ? `(${routeGroupName})` : "";
|
|
204
|
+
}
|
|
205
|
+
|
|
200
206
|
return wordsToKebab(splitTextIntoWords(normalizedSegment));
|
|
201
207
|
})
|
|
202
208
|
.filter(Boolean)
|