@jskit-ai/jskit-cli 0.2.41 → 0.2.43
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 -3
- package/src/server/cliRuntime/completion.js +1177 -0
- package/src/server/cliRuntime/descriptorValidation.js +18 -3
- package/src/server/cliRuntime/ioAndMigrations.js +2 -2
- package/src/server/cliRuntime/mutationApplication.js +1 -1
- package/src/server/cliRuntime/mutationWhen.js +2 -0
- package/src/server/cliRuntime/mutations/fileMutations.js +188 -143
- package/src/server/cliRuntime/mutations/installMigrationMutation.js +11 -38
- package/src/server/cliRuntime/mutations/templateContext.js +8 -14
- package/src/server/cliRuntime/mutations/textMutations.js +11 -6
- package/src/server/cliRuntime/packageInstallFlow.js +36 -21
- package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +13 -22
- package/src/server/cliRuntime/packageOptions.js +149 -3
- package/src/server/cliRuntime/packageRegistries.js +3 -2
- package/src/server/commandHandlers/completion.js +129 -0
- package/src/server/commandHandlers/list.js +4 -6
- package/src/server/commandHandlers/packageCommands/add.js +31 -11
- package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +10 -2
- package/src/server/commandHandlers/packageCommands/generate.js +29 -31
- package/src/server/commandHandlers/packageCommands/tabLinkItemProvisioning.js +123 -164
- package/src/server/commandHandlers/shared.js +23 -3
- package/src/server/commandHandlers/show/renderPackageText.js +3 -3
- package/src/server/core/argParser.js +12 -2
- package/src/server/core/commandCatalog.js +36 -13
- package/src/server/core/createCommandHandlers.js +3 -0
- package/src/server/shared/optionInterpolation.js +93 -0
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
COMMAND_IDS,
|
|
6
|
+
isKnownCommandName,
|
|
7
|
+
resolveCommandAlias,
|
|
8
|
+
resolveCommandDescriptor
|
|
9
|
+
} from "../core/commandCatalog.js";
|
|
10
|
+
|
|
11
|
+
const WRAPPER_COMMANDS = new Set(["npx", "jsx"]);
|
|
12
|
+
const KNOWN_GENERATE_FLAG_OPTIONS = Object.freeze(["dry-run", "run-npm-install", "json", "verbose"]);
|
|
13
|
+
const BOOLEAN_OPTION_NAMES = new Set([
|
|
14
|
+
"dry-run",
|
|
15
|
+
"run-npm-install",
|
|
16
|
+
"full",
|
|
17
|
+
"expanded",
|
|
18
|
+
"details",
|
|
19
|
+
"debug-exports",
|
|
20
|
+
"check-di-labels",
|
|
21
|
+
"verbose",
|
|
22
|
+
"json",
|
|
23
|
+
"all",
|
|
24
|
+
"help",
|
|
25
|
+
"force"
|
|
26
|
+
]);
|
|
27
|
+
const LIST_MODES = Object.freeze(["bundles", "packages", "generators"]);
|
|
28
|
+
const ADD_TARGET_TYPES = Object.freeze(["package", "bundle"]);
|
|
29
|
+
const POSITION_TARGET_TYPES = Object.freeze(["element"]);
|
|
30
|
+
const UPDATE_TARGET_TYPES = Object.freeze(["package"]);
|
|
31
|
+
const REMOVE_TARGET_TYPES = Object.freeze(["package"]);
|
|
32
|
+
const MIGRATION_SCOPES = Object.freeze(["all", "changed", "package"]);
|
|
33
|
+
|
|
34
|
+
function normalizeText(value = "") {
|
|
35
|
+
return String(value || "").trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toPosix(value = "") {
|
|
39
|
+
return String(value || "").replaceAll(path.sep, "/");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function uniqueSorted(values = []) {
|
|
43
|
+
return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].sort((left, right) =>
|
|
44
|
+
String(left).localeCompare(String(right))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function filterByPrefix(values = [], prefix = "") {
|
|
49
|
+
const normalizedPrefix = String(prefix || "");
|
|
50
|
+
if (!normalizedPrefix) {
|
|
51
|
+
return uniqueSorted(values);
|
|
52
|
+
}
|
|
53
|
+
return uniqueSorted(values).filter((value) => String(value || "").startsWith(normalizedPrefix));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveAllowedValues(schema = {}) {
|
|
57
|
+
const values = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
for (const rawValue of Array.isArray(schema?.allowedValues) ? schema.allowedValues : []) {
|
|
60
|
+
const value = normalizeText(typeof rawValue === "string" ? rawValue : rawValue?.value);
|
|
61
|
+
if (!value) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const normalizedKey = value.toLowerCase();
|
|
65
|
+
if (seen.has(normalizedKey)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
seen.add(normalizedKey);
|
|
69
|
+
values.push(value);
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze(values);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureTrailingSlash(value = "") {
|
|
75
|
+
const normalized = toPosix(value).replace(/\/+$/g, "");
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
return `${normalized}/`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function pathExists(targetPath) {
|
|
83
|
+
try {
|
|
84
|
+
await access(targetPath);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readJsonFile(filePath) {
|
|
92
|
+
const source = await readFile(filePath, "utf8");
|
|
93
|
+
return JSON.parse(source);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function safeReaddir(directoryPath) {
|
|
97
|
+
try {
|
|
98
|
+
return await readdir(directoryPath, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function walkDirectory(rootPath, { includeDirectories = false, includeFiles = true, fileFilter = null } = {}) {
|
|
105
|
+
const entries = [];
|
|
106
|
+
|
|
107
|
+
async function visit(directoryPath) {
|
|
108
|
+
const children = await safeReaddir(directoryPath);
|
|
109
|
+
for (const child of children) {
|
|
110
|
+
if (child.name.startsWith(".")) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const absolutePath = path.join(directoryPath, child.name);
|
|
114
|
+
if (child.isDirectory()) {
|
|
115
|
+
if (includeDirectories) {
|
|
116
|
+
entries.push(absolutePath);
|
|
117
|
+
}
|
|
118
|
+
await visit(absolutePath);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!includeFiles) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (typeof fileFilter === "function" && fileFilter(absolutePath) !== true) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
entries.push(absolutePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (await pathExists(rootPath)) {
|
|
132
|
+
await visit(rootPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function importDefaultModule(modulePath) {
|
|
139
|
+
const moduleUrl = `${pathToFileURL(modulePath).href}?mtime=${Date.now()}`;
|
|
140
|
+
const imported = await import(moduleUrl);
|
|
141
|
+
return imported?.default;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function loadCommandCatalog() {
|
|
145
|
+
return {
|
|
146
|
+
COMMAND_IDS,
|
|
147
|
+
isKnownCommandName,
|
|
148
|
+
resolveCommandAlias,
|
|
149
|
+
resolveCommandDescriptor
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isDirectoryLike(entry) {
|
|
154
|
+
return Boolean(entry && (entry.isDirectory() || entry.isSymbolicLink()));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function discoverDescriptorPackages(appRoot) {
|
|
158
|
+
const packageDirs = [];
|
|
159
|
+
|
|
160
|
+
for (const entry of await safeReaddir(path.join(appRoot, "packages"))) {
|
|
161
|
+
if (isDirectoryLike(entry)) {
|
|
162
|
+
packageDirs.push(path.join(appRoot, "packages", entry.name));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const entry of await safeReaddir(path.join(appRoot, "node_modules", "@jskit-ai"))) {
|
|
167
|
+
if (isDirectoryLike(entry)) {
|
|
168
|
+
packageDirs.push(path.join(appRoot, "node_modules", "@jskit-ai", entry.name));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const discovered = [];
|
|
173
|
+
for (const packageDir of packageDirs) {
|
|
174
|
+
const descriptorPath = path.join(packageDir, "package.descriptor.mjs");
|
|
175
|
+
if (!(await pathExists(descriptorPath))) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const descriptor = await importDefaultModule(descriptorPath);
|
|
180
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
181
|
+
const packageJson = (await pathExists(packageJsonPath)) ? await readJsonFile(packageJsonPath) : {};
|
|
182
|
+
const packageId = normalizeText(descriptor?.packageId || packageJson?.name || path.basename(packageDir));
|
|
183
|
+
if (!packageId) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
discovered.push(
|
|
187
|
+
Object.freeze({
|
|
188
|
+
packageDir,
|
|
189
|
+
packageId,
|
|
190
|
+
descriptor: descriptor && typeof descriptor === "object" ? descriptor : {}
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
} catch {
|
|
194
|
+
// Ignore malformed descriptors during completion discovery.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return uniqueSorted(discovered.map((entry) => entry.packageId)).map((packageId) =>
|
|
199
|
+
discovered.find((entry) => entry.packageId === packageId)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function discoverBundleIds(appRoot) {
|
|
204
|
+
const bundleDescriptorPaths = [];
|
|
205
|
+
|
|
206
|
+
for (const packageDir of [
|
|
207
|
+
...((await safeReaddir(path.join(appRoot, "packages"))).filter((entry) => isDirectoryLike(entry)).map((entry) =>
|
|
208
|
+
path.join(appRoot, "packages", entry.name)
|
|
209
|
+
)),
|
|
210
|
+
...((await safeReaddir(path.join(appRoot, "node_modules", "@jskit-ai"))).filter((entry) => isDirectoryLike(entry)).map((entry) =>
|
|
211
|
+
path.join(appRoot, "node_modules", "@jskit-ai", entry.name)
|
|
212
|
+
))
|
|
213
|
+
]) {
|
|
214
|
+
const bundlesDir = path.join(packageDir, "bundles");
|
|
215
|
+
for (const entry of await safeReaddir(bundlesDir)) {
|
|
216
|
+
if (!entry.isDirectory()) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const descriptorPath = path.join(bundlesDir, entry.name, "bundle.descriptor.mjs");
|
|
220
|
+
if (await pathExists(descriptorPath)) {
|
|
221
|
+
bundleDescriptorPaths.push(descriptorPath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const bundleIds = [];
|
|
227
|
+
for (const descriptorPath of bundleDescriptorPaths) {
|
|
228
|
+
try {
|
|
229
|
+
const descriptor = await importDefaultModule(descriptorPath);
|
|
230
|
+
const bundleId = normalizeText(descriptor?.bundleId);
|
|
231
|
+
if (bundleId) {
|
|
232
|
+
bundleIds.push(bundleId);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Ignore malformed bundle descriptors.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return uniqueSorted(bundleIds);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toShortPackageId(packageId = "") {
|
|
243
|
+
const normalized = normalizeText(packageId);
|
|
244
|
+
if (!normalized.startsWith("@jskit-ai/")) {
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
return normalized.slice("@jskit-ai/".length);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function discoverGenerators(appRoot) {
|
|
251
|
+
const packages = await discoverDescriptorPackages(appRoot);
|
|
252
|
+
const generators = [];
|
|
253
|
+
for (const entry of packages) {
|
|
254
|
+
if (normalizeText(entry?.descriptor?.kind) !== "generator") {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const shortId = toShortPackageId(entry.packageId);
|
|
258
|
+
generators.push(
|
|
259
|
+
Object.freeze({
|
|
260
|
+
packageId: entry.packageId,
|
|
261
|
+
shortId,
|
|
262
|
+
descriptor: entry.descriptor
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return generators.sort((left, right) => left.shortId.localeCompare(right.shortId));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function discoverRuntimePackages(appRoot) {
|
|
270
|
+
const packages = await discoverDescriptorPackages(appRoot);
|
|
271
|
+
const runtimeIds = [];
|
|
272
|
+
for (const entry of packages) {
|
|
273
|
+
if (normalizeText(entry?.descriptor?.kind) === "generator") {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
runtimeIds.push(entry.packageId);
|
|
277
|
+
if (entry.packageId.startsWith("@jskit-ai/")) {
|
|
278
|
+
runtimeIds.push(toShortPackageId(entry.packageId));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return uniqueSorted(runtimeIds);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function discoverResourceFiles(appRoot) {
|
|
285
|
+
const sharedFiles = [];
|
|
286
|
+
for (const packageDir of await safeReaddir(path.join(appRoot, "packages"))) {
|
|
287
|
+
if (!packageDir.isDirectory()) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const sharedDir = path.join(appRoot, "packages", packageDir.name, "src", "shared");
|
|
291
|
+
for (const fileEntry of await safeReaddir(sharedDir)) {
|
|
292
|
+
if (!fileEntry.isFile()) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!/Resource\.js$/u.test(fileEntry.name)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
sharedFiles.push(toPosix(path.relative(appRoot, path.join(sharedDir, fileEntry.name))));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return uniqueSorted(sharedFiles);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function discoverSurfaces(appRoot) {
|
|
305
|
+
const pagesRoot = path.join(appRoot, "src", "pages");
|
|
306
|
+
const surfaces = [];
|
|
307
|
+
for (const entry of await safeReaddir(pagesRoot)) {
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
surfaces.push(entry.name);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (entry.isFile() && entry.name.endsWith(".vue")) {
|
|
313
|
+
surfaces.push(entry.name.slice(0, -".vue".length));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return uniqueSorted(surfaces);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function extractMatches(source = "", patterns = []) {
|
|
320
|
+
const values = [];
|
|
321
|
+
for (const pattern of patterns) {
|
|
322
|
+
for (const match of String(source || "").matchAll(pattern)) {
|
|
323
|
+
const value = normalizeText(match?.[1]);
|
|
324
|
+
if (value) {
|
|
325
|
+
values.push(value);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return values;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function discoverPlacementTargets(appRoot) {
|
|
333
|
+
const placementValues = [];
|
|
334
|
+
const placementSourcePath = path.join(appRoot, "src", "placement.js");
|
|
335
|
+
if (await pathExists(placementSourcePath)) {
|
|
336
|
+
const source = await readFile(placementSourcePath, "utf8");
|
|
337
|
+
placementValues.push(...extractMatches(source, [/\btarget\s*:\s*["']([^"']+)["']/g]));
|
|
338
|
+
for (const match of source.matchAll(/\bhost\s*:\s*["']([^"']+)["']\s*,\s*\n?\s*position\s*:\s*["']([^"']+)["']/g)) {
|
|
339
|
+
const host = normalizeText(match?.[1]);
|
|
340
|
+
const position = normalizeText(match?.[2]);
|
|
341
|
+
if (host && position) {
|
|
342
|
+
placementValues.push(`${host}:${position}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const sourceFiles = await walkDirectory(path.join(appRoot, "src"), {
|
|
348
|
+
includeFiles: true,
|
|
349
|
+
includeDirectories: false,
|
|
350
|
+
fileFilter: (filePath) => /\.(vue|js|mjs)$/u.test(filePath)
|
|
351
|
+
});
|
|
352
|
+
for (const filePath of sourceFiles) {
|
|
353
|
+
const source = await readFile(filePath, "utf8");
|
|
354
|
+
placementValues.push(...extractMatches(source, [/\btarget\s*=\s*["']([^"']+)["']/g]));
|
|
355
|
+
for (const match of source.matchAll(/\bhost\s*=\s*["']([^"']+)["']\s+position\s*=\s*["']([^"']+)["']/g)) {
|
|
356
|
+
const host = normalizeText(match?.[1]);
|
|
357
|
+
const position = normalizeText(match?.[2]);
|
|
358
|
+
if (host && position) {
|
|
359
|
+
placementValues.push(`${host}:${position}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return uniqueSorted(placementValues);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function discoverComponentTokens(appRoot) {
|
|
368
|
+
const tokens = [];
|
|
369
|
+
for (const filePath of [
|
|
370
|
+
path.join(appRoot, "src", "placement.js"),
|
|
371
|
+
...((await walkDirectory(path.join(appRoot, "src"), {
|
|
372
|
+
includeFiles: true,
|
|
373
|
+
includeDirectories: false,
|
|
374
|
+
fileFilter: (candidate) => /\.(vue|js|mjs)$/u.test(candidate)
|
|
375
|
+
})) || []),
|
|
376
|
+
...((await walkDirectory(path.join(appRoot, "packages"), {
|
|
377
|
+
includeFiles: true,
|
|
378
|
+
includeDirectories: false,
|
|
379
|
+
fileFilter: (candidate) => /\.(vue|js|mjs)$/u.test(candidate)
|
|
380
|
+
})) || [])
|
|
381
|
+
]) {
|
|
382
|
+
if (!(await pathExists(filePath))) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const source = await readFile(filePath, "utf8");
|
|
386
|
+
tokens.push(...extractMatches(source, [
|
|
387
|
+
/\bcomponentToken\s*:\s*["']([^"']+)["']/g,
|
|
388
|
+
/\bdefault-link-component-token\s*=\s*["']([^"']+)["']/g,
|
|
389
|
+
/registerMainClientComponent\(\s*["']([^"']+)["']/g
|
|
390
|
+
]));
|
|
391
|
+
}
|
|
392
|
+
return uniqueSorted(tokens);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isRouteLikeRelativePath(relativePath = "") {
|
|
396
|
+
return !toPosix(relativePath)
|
|
397
|
+
.split("/")
|
|
398
|
+
.filter(Boolean)
|
|
399
|
+
.some((segment) => segment.startsWith("_"));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function discoverPagesRelativeDirectories(appRoot) {
|
|
403
|
+
const pagesRoot = path.join(appRoot, "src", "pages");
|
|
404
|
+
const directories = await walkDirectory(pagesRoot, {
|
|
405
|
+
includeDirectories: true,
|
|
406
|
+
includeFiles: false
|
|
407
|
+
});
|
|
408
|
+
return uniqueSorted(
|
|
409
|
+
directories
|
|
410
|
+
.map((directoryPath) => path.relative(pagesRoot, directoryPath))
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.filter((relativePath) => isRouteLikeRelativePath(relativePath))
|
|
413
|
+
.map((relativePath) => ensureTrailingSlash(relativePath))
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function discoverPagesRelativeFiles(appRoot) {
|
|
418
|
+
const pagesRoot = path.join(appRoot, "src", "pages");
|
|
419
|
+
const files = await walkDirectory(pagesRoot, {
|
|
420
|
+
includeDirectories: false,
|
|
421
|
+
includeFiles: true,
|
|
422
|
+
fileFilter: (filePath) => filePath.endsWith(".vue")
|
|
423
|
+
});
|
|
424
|
+
return uniqueSorted(
|
|
425
|
+
files
|
|
426
|
+
.map((filePath) => path.relative(pagesRoot, filePath))
|
|
427
|
+
.filter((relativePath) => isRouteLikeRelativePath(relativePath))
|
|
428
|
+
.map((relativePath) => toPosix(relativePath))
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function discoverAppVueFiles(appRoot) {
|
|
433
|
+
const srcRoot = path.join(appRoot, "src");
|
|
434
|
+
const files = await walkDirectory(srcRoot, {
|
|
435
|
+
includeDirectories: false,
|
|
436
|
+
includeFiles: true,
|
|
437
|
+
fileFilter: (filePath) => filePath.endsWith(".vue")
|
|
438
|
+
});
|
|
439
|
+
return uniqueSorted(files.map((filePath) => toPosix(path.relative(appRoot, filePath))));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildOptionSuggestions(optionNames = [], { current = "", seenOptionNames = new Set() } = {}) {
|
|
443
|
+
const suggestions = [];
|
|
444
|
+
for (const optionName of optionNames) {
|
|
445
|
+
if (!optionName) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (seenOptionNames.has(optionName) && !BOOLEAN_OPTION_NAMES.has(optionName)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
suggestions.push(`--${optionName}`);
|
|
452
|
+
}
|
|
453
|
+
return filterByPrefix(suggestions, current);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function parseOptionToken(token = "") {
|
|
457
|
+
const normalized = String(token || "");
|
|
458
|
+
if (!normalized.startsWith("--")) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const withoutPrefix = normalized.slice(2);
|
|
462
|
+
if (!withoutPrefix) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const equalsIndex = withoutPrefix.indexOf("=");
|
|
466
|
+
if (equalsIndex < 0) {
|
|
467
|
+
return {
|
|
468
|
+
name: withoutPrefix,
|
|
469
|
+
value: "",
|
|
470
|
+
hasInlineValue: false
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
name: withoutPrefix.slice(0, equalsIndex),
|
|
475
|
+
value: withoutPrefix.slice(equalsIndex + 1),
|
|
476
|
+
hasInlineValue: true
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function parseContextTokens(tokens = [], optionMeta = {}) {
|
|
481
|
+
const positionals = [];
|
|
482
|
+
const seenOptionNames = new Set();
|
|
483
|
+
const optionValues = new Map();
|
|
484
|
+
let expectValueFor = "";
|
|
485
|
+
|
|
486
|
+
for (const token of Array.isArray(tokens) ? tokens : []) {
|
|
487
|
+
if (expectValueFor) {
|
|
488
|
+
optionValues.set(expectValueFor, token);
|
|
489
|
+
seenOptionNames.add(expectValueFor);
|
|
490
|
+
expectValueFor = "";
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const parsedOption = parseOptionToken(token);
|
|
495
|
+
if (parsedOption) {
|
|
496
|
+
const optionName = normalizeText(parsedOption.name);
|
|
497
|
+
if (!optionName) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const meta = optionMeta[optionName] || {};
|
|
501
|
+
seenOptionNames.add(optionName);
|
|
502
|
+
if (parsedOption.hasInlineValue) {
|
|
503
|
+
optionValues.set(optionName, parsedOption.value);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (normalizeText(meta.inputType) === "flag" || BOOLEAN_OPTION_NAMES.has(optionName)) {
|
|
507
|
+
optionValues.set(optionName, "true");
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
expectValueFor = optionName;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
positionals.push(token);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return Object.freeze({
|
|
518
|
+
positionals: Object.freeze(positionals),
|
|
519
|
+
optionValues,
|
|
520
|
+
seenOptionNames,
|
|
521
|
+
expectValueFor
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function resolveCurrentOptionValueRequest({ currentToken = "", previousToken = "", optionMeta = {} } = {}) {
|
|
526
|
+
const previousOption = parseOptionToken(previousToken);
|
|
527
|
+
if (previousOption && !previousOption.hasInlineValue) {
|
|
528
|
+
const meta = optionMeta[previousOption.name] || {};
|
|
529
|
+
if (normalizeText(meta.inputType) !== "flag" && !BOOLEAN_OPTION_NAMES.has(previousOption.name)) {
|
|
530
|
+
return {
|
|
531
|
+
optionName: previousOption.name,
|
|
532
|
+
valuePrefix: currentToken,
|
|
533
|
+
includeOptionPrefix: false
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const currentOption = parseOptionToken(currentToken);
|
|
539
|
+
if (currentOption && currentOption.hasInlineValue) {
|
|
540
|
+
const meta = optionMeta[currentOption.name] || {};
|
|
541
|
+
if (normalizeText(meta.inputType) !== "flag" && !BOOLEAN_OPTION_NAMES.has(currentOption.name)) {
|
|
542
|
+
return {
|
|
543
|
+
optionName: currentOption.name,
|
|
544
|
+
valuePrefix: currentOption.value,
|
|
545
|
+
includeOptionPrefix: true
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function completeCommaSeparatedValues({ currentValue = "", allowedValues = [] } = {}) {
|
|
554
|
+
const source = String(currentValue || "");
|
|
555
|
+
const segments = source.split(",");
|
|
556
|
+
segments.pop();
|
|
557
|
+
const used = new Set(segments.map((entry) => normalizeText(entry)).filter(Boolean));
|
|
558
|
+
const prefix = segments.length > 0 ? `${segments.join(",")},` : "";
|
|
559
|
+
|
|
560
|
+
return filterByPrefix(
|
|
561
|
+
allowedValues.filter((value) => !used.has(value)).map((value) => `${prefix}${value}`),
|
|
562
|
+
source
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function extractBracketParams(value = "") {
|
|
567
|
+
return [...String(value || "").matchAll(/\[([^\]/]+)\]/g)].map((match) => normalizeText(match?.[1])).filter(Boolean);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function completeOptionValue({
|
|
571
|
+
appRoot,
|
|
572
|
+
optionName = "",
|
|
573
|
+
valuePrefix = "",
|
|
574
|
+
includeOptionPrefix = false,
|
|
575
|
+
parseState,
|
|
576
|
+
currentGenerator = "",
|
|
577
|
+
optionMeta = {}
|
|
578
|
+
} = {}) {
|
|
579
|
+
const normalizedOptionName = normalizeText(optionName);
|
|
580
|
+
const descriptorOption = optionMeta?.descriptorOption || {};
|
|
581
|
+
const validationType = normalizeText(descriptorOption?.validationType).toLowerCase();
|
|
582
|
+
const allowedValues = resolveAllowedValues(descriptorOption);
|
|
583
|
+
let suggestions = [];
|
|
584
|
+
|
|
585
|
+
if (normalizedOptionName === "resource-file") {
|
|
586
|
+
suggestions = await discoverResourceFiles(appRoot);
|
|
587
|
+
} else if (["link-placement", "placement", "target"].includes(normalizedOptionName)) {
|
|
588
|
+
suggestions = await discoverPlacementTargets(appRoot);
|
|
589
|
+
} else if (normalizedOptionName === "link-component-token") {
|
|
590
|
+
suggestions = await discoverComponentTokens(appRoot);
|
|
591
|
+
} else if (normalizedOptionName === "surface") {
|
|
592
|
+
suggestions = await discoverSurfaces(appRoot);
|
|
593
|
+
} else if (validationType === "csv-enum" && allowedValues.length > 0) {
|
|
594
|
+
suggestions = completeCommaSeparatedValues({
|
|
595
|
+
currentValue: valuePrefix,
|
|
596
|
+
allowedValues
|
|
597
|
+
});
|
|
598
|
+
} else if (validationType === "enum" && allowedValues.length > 0) {
|
|
599
|
+
suggestions = allowedValues;
|
|
600
|
+
} else if (normalizedOptionName === "display-fields") {
|
|
601
|
+
const resourceFile = normalizeText(parseState?.optionValues?.get("resource-file"));
|
|
602
|
+
const fields = await discoverResourceDisplayFields(appRoot, resourceFile);
|
|
603
|
+
suggestions = completeCommaSeparatedValues({
|
|
604
|
+
currentValue: valuePrefix,
|
|
605
|
+
allowedValues: fields
|
|
606
|
+
});
|
|
607
|
+
} else if (normalizedOptionName === "prefix") {
|
|
608
|
+
const tokens = await discoverComponentTokens(appRoot);
|
|
609
|
+
const prefixes = new Set();
|
|
610
|
+
for (const token of tokens) {
|
|
611
|
+
const segments = String(token || "").split(".").filter(Boolean);
|
|
612
|
+
let currentPrefix = "";
|
|
613
|
+
for (const segment of segments.slice(0, -1)) {
|
|
614
|
+
currentPrefix = currentPrefix ? `${currentPrefix}.${segment}` : segment;
|
|
615
|
+
prefixes.add(`${currentPrefix}.`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
suggestions = [...prefixes];
|
|
619
|
+
} else if (normalizedOptionName === "id-param") {
|
|
620
|
+
const dynamicParams = new Set(["recordId"]);
|
|
621
|
+
for (const positional of parseState?.positionals || []) {
|
|
622
|
+
for (const param of extractBracketParams(positional)) {
|
|
623
|
+
dynamicParams.add(param);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
suggestions = [...dynamicParams];
|
|
627
|
+
} else if (normalizedOptionName === "table-name" && currentGenerator === "crud-server-generator") {
|
|
628
|
+
suggestions = [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
suggestions = filterByPrefix(suggestions, valuePrefix);
|
|
632
|
+
if (!includeOptionPrefix) {
|
|
633
|
+
return suggestions;
|
|
634
|
+
}
|
|
635
|
+
return suggestions.map((value) => `--${normalizedOptionName}=${value}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function discoverResourceDisplayFields(appRoot, resourceFile = "") {
|
|
639
|
+
const normalizedPath = normalizeText(resourceFile);
|
|
640
|
+
if (!normalizedPath) {
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const absolutePath = path.resolve(appRoot, normalizedPath);
|
|
645
|
+
if (!absolutePath.startsWith(`${appRoot}${path.sep}`) && absolutePath !== appRoot) {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
if (!(await pathExists(absolutePath))) {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
const imported = await import(`${pathToFileURL(absolutePath).href}?mtime=${Date.now()}`);
|
|
654
|
+
const resource = imported?.resource;
|
|
655
|
+
if (!resource || typeof resource !== "object") {
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
const fieldKeys = new Set();
|
|
659
|
+
|
|
660
|
+
const outputSchemaProperties = resource?.operations?.view?.outputValidator?.schema?.properties;
|
|
661
|
+
if (outputSchemaProperties && typeof outputSchemaProperties === "object") {
|
|
662
|
+
for (const key of Object.keys(outputSchemaProperties)) {
|
|
663
|
+
if (key === resource?.contract?.lookup?.containerKey) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
fieldKeys.add(key);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for (const fieldMeta of Array.isArray(resource?.fieldMeta) ? resource.fieldMeta : []) {
|
|
671
|
+
const key = normalizeText(fieldMeta?.key);
|
|
672
|
+
if (key) {
|
|
673
|
+
fieldKeys.add(key);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return uniqueSorted([...fieldKeys]);
|
|
678
|
+
} catch {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function completeRelativeDirectoryRoot(appRoot, current = "") {
|
|
684
|
+
return filterByPrefix(await discoverPagesRelativeDirectories(appRoot), current);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function completeRelativePageTargetFile(appRoot, current = "") {
|
|
688
|
+
return filterByPrefix(
|
|
689
|
+
[...(await discoverPagesRelativeDirectories(appRoot)), ...(await discoverPagesRelativeFiles(appRoot))],
|
|
690
|
+
current
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function completeAppVueFile(appRoot, current = "") {
|
|
695
|
+
return filterByPrefix(await discoverAppVueFiles(appRoot), current);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizeCompletionInvocation(words = [], cword = 0) {
|
|
699
|
+
const rawWords = (Array.isArray(words) ? words : []).map((value) => String(value ?? ""));
|
|
700
|
+
let currentIndex = Number(cword);
|
|
701
|
+
if (!Number.isInteger(currentIndex)) {
|
|
702
|
+
currentIndex = Math.max(rawWords.length - 1, 0);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (rawWords.length < 1) {
|
|
706
|
+
return {
|
|
707
|
+
words: ["jskit"],
|
|
708
|
+
cword: 0,
|
|
709
|
+
wrapperOnly: false
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (WRAPPER_COMMANDS.has(rawWords[0])) {
|
|
714
|
+
const jskitIndex = rawWords.findIndex((token, index) => index > 0 && ["jskit", "@jskit-ai/jskit-cli"].includes(token));
|
|
715
|
+
if (jskitIndex < 0) {
|
|
716
|
+
return {
|
|
717
|
+
words: rawWords,
|
|
718
|
+
cword: currentIndex,
|
|
719
|
+
wrapperOnly: true
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const normalizedWords = rawWords.slice(jskitIndex);
|
|
723
|
+
return {
|
|
724
|
+
words: normalizedWords,
|
|
725
|
+
cword: Math.max(0, currentIndex - jskitIndex),
|
|
726
|
+
wrapperOnly: false
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
words: rawWords[0] === "@jskit-ai/jskit-cli" ? ["jskit", ...rawWords.slice(1)] : rawWords,
|
|
732
|
+
cword: currentIndex,
|
|
733
|
+
wrapperOnly: false
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function buildTopLevelCommandMetadata(catalogModule) {
|
|
738
|
+
const commands = new Set();
|
|
739
|
+
for (const commandId of catalogModule.COMMAND_IDS || []) {
|
|
740
|
+
const descriptor = catalogModule.resolveCommandDescriptor(commandId);
|
|
741
|
+
if (!descriptor) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
commands.add(descriptor.command);
|
|
745
|
+
for (const alias of Array.isArray(descriptor.aliases) ? descriptor.aliases : []) {
|
|
746
|
+
commands.add(alias);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
commands.add("help");
|
|
750
|
+
return uniqueSorted([...commands]);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function buildCommandOptionMeta(command = "", catalogModule) {
|
|
754
|
+
const descriptor = catalogModule.resolveCommandDescriptor(command);
|
|
755
|
+
if (!descriptor) {
|
|
756
|
+
return {};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const optionMeta = {};
|
|
760
|
+
for (const flagKey of Array.isArray(descriptor.allowedFlagKeys) ? descriptor.allowedFlagKeys : []) {
|
|
761
|
+
const labels = {
|
|
762
|
+
dryRun: "dry-run",
|
|
763
|
+
runNpmInstall: "run-npm-install",
|
|
764
|
+
full: "full",
|
|
765
|
+
expanded: "expanded",
|
|
766
|
+
details: "details",
|
|
767
|
+
debugExports: "debug-exports",
|
|
768
|
+
checkDiLabels: "check-di-labels",
|
|
769
|
+
verbose: "verbose",
|
|
770
|
+
json: "json",
|
|
771
|
+
all: "all"
|
|
772
|
+
};
|
|
773
|
+
const optionName = labels[flagKey];
|
|
774
|
+
if (optionName) {
|
|
775
|
+
optionMeta[optionName] = { inputType: "flag" };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
for (const optionName of Array.isArray(descriptor.allowedValueOptionNames) ? descriptor.allowedValueOptionNames : []) {
|
|
779
|
+
optionMeta[optionName] = { inputType: "text" };
|
|
780
|
+
}
|
|
781
|
+
optionMeta.help = { inputType: "flag" };
|
|
782
|
+
return optionMeta;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function buildGeneratorLookup(generators = []) {
|
|
786
|
+
const lookup = new Map();
|
|
787
|
+
for (const generator of generators) {
|
|
788
|
+
lookup.set(generator.shortId, generator);
|
|
789
|
+
lookup.set(generator.packageId, generator);
|
|
790
|
+
}
|
|
791
|
+
return lookup;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function buildGeneratorOptionMeta(generator = null, subcommandName = "") {
|
|
795
|
+
const optionMeta = {};
|
|
796
|
+
for (const optionName of KNOWN_GENERATE_FLAG_OPTIONS) {
|
|
797
|
+
optionMeta[optionName] = { inputType: "flag" };
|
|
798
|
+
}
|
|
799
|
+
optionMeta.help = { inputType: "flag" };
|
|
800
|
+
|
|
801
|
+
const descriptorOptions = generator?.descriptor?.options && typeof generator.descriptor.options === "object"
|
|
802
|
+
? generator.descriptor.options
|
|
803
|
+
: {};
|
|
804
|
+
const subcommand = generator?.descriptor?.metadata?.generatorSubcommands?.[subcommandName] || {};
|
|
805
|
+
for (const optionName of Array.isArray(subcommand.optionNames) ? subcommand.optionNames : []) {
|
|
806
|
+
const descriptorOption = descriptorOptions?.[optionName] || {};
|
|
807
|
+
optionMeta[optionName] = {
|
|
808
|
+
inputType: normalizeText(descriptorOption.inputType) || (BOOLEAN_OPTION_NAMES.has(optionName) ? "flag" : "text"),
|
|
809
|
+
descriptorOption
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
return optionMeta;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function resolveImplicitGeneratorSubcommand(generator = null, tokensAfterGenerator = [], currentToken = "") {
|
|
816
|
+
const metadata = generator?.descriptor?.metadata || {};
|
|
817
|
+
const subcommands = metadata.generatorSubcommands && typeof metadata.generatorSubcommands === "object"
|
|
818
|
+
? metadata.generatorSubcommands
|
|
819
|
+
: {};
|
|
820
|
+
const tokenList = Array.isArray(tokensAfterGenerator) ? tokensAfterGenerator : [];
|
|
821
|
+
const firstToken = normalizeText(tokenList[0]);
|
|
822
|
+
|
|
823
|
+
if (firstToken && !firstToken.startsWith("-") && firstToken !== "help" && Object.hasOwn(subcommands, firstToken)) {
|
|
824
|
+
return {
|
|
825
|
+
subcommandName: firstToken,
|
|
826
|
+
explicit: true,
|
|
827
|
+
offset: 1
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const primarySubcommand = normalizeText(metadata.generatorPrimarySubcommand);
|
|
832
|
+
if (!primarySubcommand || !Object.hasOwn(subcommands, primarySubcommand)) {
|
|
833
|
+
return {
|
|
834
|
+
subcommandName: "",
|
|
835
|
+
explicit: false,
|
|
836
|
+
offset: 0
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const hasOptionUsage = tokenList.some((token) => String(token || "").startsWith("--")) || String(currentToken || "").startsWith("--");
|
|
841
|
+
if (!hasOptionUsage) {
|
|
842
|
+
return {
|
|
843
|
+
subcommandName: "",
|
|
844
|
+
explicit: false,
|
|
845
|
+
offset: 0
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
subcommandName: primarySubcommand,
|
|
851
|
+
explicit: false,
|
|
852
|
+
offset: 0
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function completeGenericContext({
|
|
857
|
+
appRoot,
|
|
858
|
+
currentToken = "",
|
|
859
|
+
previousToken = "",
|
|
860
|
+
optionMeta = {},
|
|
861
|
+
positionalArgs = [],
|
|
862
|
+
tokensBeforeCurrent = [],
|
|
863
|
+
optionNames = [],
|
|
864
|
+
positionalCompleter = null,
|
|
865
|
+
generatorName = "",
|
|
866
|
+
subcommandName = ""
|
|
867
|
+
} = {}) {
|
|
868
|
+
const parseState = parseContextTokens(tokensBeforeCurrent, optionMeta);
|
|
869
|
+
const optionValueRequest = resolveCurrentOptionValueRequest({
|
|
870
|
+
currentToken,
|
|
871
|
+
previousToken,
|
|
872
|
+
optionMeta
|
|
873
|
+
});
|
|
874
|
+
if (optionValueRequest) {
|
|
875
|
+
return completeOptionValue({
|
|
876
|
+
appRoot,
|
|
877
|
+
optionName: optionValueRequest.optionName,
|
|
878
|
+
valuePrefix: optionValueRequest.valuePrefix,
|
|
879
|
+
includeOptionPrefix: optionValueRequest.includeOptionPrefix,
|
|
880
|
+
parseState,
|
|
881
|
+
currentSubcommand: subcommandName,
|
|
882
|
+
currentGenerator: generatorName,
|
|
883
|
+
optionMeta: optionMeta[optionValueRequest.optionName] || {}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (String(currentToken || "").startsWith("--")) {
|
|
888
|
+
return buildOptionSuggestions(optionNames, {
|
|
889
|
+
current: currentToken,
|
|
890
|
+
seenOptionNames: parseState.seenOptionNames
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const positionalIndex = parseState.positionals.length;
|
|
895
|
+
const suggestions = [];
|
|
896
|
+
if (typeof positionalCompleter === "function") {
|
|
897
|
+
suggestions.push(
|
|
898
|
+
...(await positionalCompleter({
|
|
899
|
+
positionalIndex,
|
|
900
|
+
currentToken,
|
|
901
|
+
parseState,
|
|
902
|
+
positionalArgs,
|
|
903
|
+
appRoot,
|
|
904
|
+
generatorName,
|
|
905
|
+
subcommandName
|
|
906
|
+
}))
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!currentToken) {
|
|
911
|
+
suggestions.push(
|
|
912
|
+
...buildOptionSuggestions(optionNames, {
|
|
913
|
+
current: currentToken,
|
|
914
|
+
seenOptionNames: parseState.seenOptionNames
|
|
915
|
+
})
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return uniqueSorted(suggestions);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async function completeTopLevel({ currentToken = "", catalogModule }) {
|
|
923
|
+
const commandSuggestions = buildTopLevelCommandMetadata(catalogModule);
|
|
924
|
+
if (String(currentToken || "").startsWith("-")) {
|
|
925
|
+
return filterByPrefix(["--help"], currentToken);
|
|
926
|
+
}
|
|
927
|
+
return filterByPrefix([...commandSuggestions, "--help"], currentToken);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function completeGenerateCommand({ appRoot, words, cword, catalogModule }) {
|
|
931
|
+
const currentToken = words[cword] ?? "";
|
|
932
|
+
const previousToken = words[cword - 1] ?? "";
|
|
933
|
+
const generators = await discoverGenerators(appRoot);
|
|
934
|
+
const generatorLookup = buildGeneratorLookup(generators);
|
|
935
|
+
|
|
936
|
+
if (cword <= 2) {
|
|
937
|
+
const generatorIds = uniqueSorted(
|
|
938
|
+
generators.flatMap((generator) => [generator.shortId, generator.packageId])
|
|
939
|
+
);
|
|
940
|
+
if (String(currentToken || "").startsWith("-")) {
|
|
941
|
+
return buildOptionSuggestions(KNOWN_GENERATE_FLAG_OPTIONS, {
|
|
942
|
+
current: currentToken,
|
|
943
|
+
seenOptionNames: parseContextTokens(words.slice(2, cword), buildCommandOptionMeta("generate", catalogModule)).seenOptionNames
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
return filterByPrefix([...generatorIds, ...KNOWN_GENERATE_FLAG_OPTIONS.map((name) => `--${name}`)], currentToken);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const generatorToken = normalizeText(words[2]);
|
|
950
|
+
const generator = generatorLookup.get(generatorToken);
|
|
951
|
+
if (!generator) {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const generatorContext = resolveImplicitGeneratorSubcommand(generator, words.slice(3, cword), currentToken);
|
|
956
|
+
const metadata = generator?.descriptor?.metadata || {};
|
|
957
|
+
const subcommands = metadata.generatorSubcommands && typeof metadata.generatorSubcommands === "object"
|
|
958
|
+
? metadata.generatorSubcommands
|
|
959
|
+
: {};
|
|
960
|
+
|
|
961
|
+
if (!generatorContext.subcommandName && cword <= 3) {
|
|
962
|
+
const subcommandNames = uniqueSorted([...Object.keys(subcommands), "help"]);
|
|
963
|
+
if (String(currentToken || "").startsWith("-")) {
|
|
964
|
+
return buildOptionSuggestions(KNOWN_GENERATE_FLAG_OPTIONS, {
|
|
965
|
+
current: currentToken,
|
|
966
|
+
seenOptionNames: parseContextTokens(words.slice(3, cword), buildCommandOptionMeta("generate", catalogModule)).seenOptionNames
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return filterByPrefix([...subcommandNames, ...KNOWN_GENERATE_FLAG_OPTIONS.map((name) => `--${name}`)], currentToken);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (!generatorContext.subcommandName) {
|
|
973
|
+
return filterByPrefix(uniqueSorted([...Object.keys(subcommands), "help"]), currentToken);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const subcommandName = generatorContext.subcommandName;
|
|
977
|
+
const subcommand = subcommands[subcommandName] || {};
|
|
978
|
+
const optionMeta = buildGeneratorOptionMeta(generator, subcommandName);
|
|
979
|
+
const optionNames = uniqueSorted(Object.keys(optionMeta));
|
|
980
|
+
const tokensBeforeCurrent = words.slice(3 + generatorContext.offset, cword);
|
|
981
|
+
|
|
982
|
+
return completeGenericContext({
|
|
983
|
+
appRoot,
|
|
984
|
+
currentToken,
|
|
985
|
+
previousToken,
|
|
986
|
+
optionMeta,
|
|
987
|
+
positionalArgs: Array.isArray(subcommand.positionalArgs) ? subcommand.positionalArgs : [],
|
|
988
|
+
tokensBeforeCurrent,
|
|
989
|
+
optionNames,
|
|
990
|
+
generatorName: generator.shortId,
|
|
991
|
+
subcommandName,
|
|
992
|
+
positionalCompleter: async ({ positionalIndex, currentToken: currentPositional }) => {
|
|
993
|
+
if (currentToken === "help" || previousToken === "help") {
|
|
994
|
+
return [];
|
|
995
|
+
}
|
|
996
|
+
if (generator.shortId === "crud-ui-generator" && subcommandName === "crud" && positionalIndex === 0) {
|
|
997
|
+
return completeRelativeDirectoryRoot(appRoot, currentPositional);
|
|
998
|
+
}
|
|
999
|
+
if (generator.shortId === "crud-ui-generator" && subcommandName === "field") {
|
|
1000
|
+
if (positionalIndex === 1) {
|
|
1001
|
+
return filterByPrefix(await discoverResourceFiles(appRoot), currentPositional);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (generator.shortId === "crud-server-generator" && subcommandName === "scaffold-field" && positionalIndex === 1) {
|
|
1005
|
+
return filterByPrefix(await discoverResourceFiles(appRoot), currentPositional);
|
|
1006
|
+
}
|
|
1007
|
+
if (generator.shortId === "ui-generator" && ["page", "add-subpages"].includes(subcommandName) && positionalIndex === 0) {
|
|
1008
|
+
return completeRelativePageTargetFile(appRoot, currentPositional);
|
|
1009
|
+
}
|
|
1010
|
+
if (generator.shortId === "ui-generator" && subcommandName === "outlet" && positionalIndex === 0) {
|
|
1011
|
+
return completeAppVueFile(appRoot, currentPositional);
|
|
1012
|
+
}
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function completeCommand({ appRoot, words, cword, catalogModule }) {
|
|
1019
|
+
const currentToken = words[cword] ?? "";
|
|
1020
|
+
const commandToken = normalizeText(words[1]);
|
|
1021
|
+
const command = catalogModule.resolveCommandAlias(commandToken);
|
|
1022
|
+
const previousToken = words[cword - 1] ?? "";
|
|
1023
|
+
|
|
1024
|
+
if (!command || !catalogModule.isKnownCommandName(command)) {
|
|
1025
|
+
return completeTopLevel({ appRoot, currentToken, catalogModule });
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (command === "generate") {
|
|
1029
|
+
return completeGenerateCommand({ appRoot, words, cword, catalogModule });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const optionMeta = buildCommandOptionMeta(command, catalogModule);
|
|
1033
|
+
const optionNames = uniqueSorted(Object.keys(optionMeta));
|
|
1034
|
+
const tokensBeforeCurrent = words.slice(2, cword);
|
|
1035
|
+
|
|
1036
|
+
return completeGenericContext({
|
|
1037
|
+
appRoot,
|
|
1038
|
+
currentToken,
|
|
1039
|
+
previousToken,
|
|
1040
|
+
optionMeta,
|
|
1041
|
+
tokensBeforeCurrent,
|
|
1042
|
+
optionNames,
|
|
1043
|
+
positionalCompleter: async ({ positionalIndex, currentToken: positionalCurrent, parseState }) => {
|
|
1044
|
+
if (command === "help" && positionalIndex === 0) {
|
|
1045
|
+
return filterByPrefix(buildTopLevelCommandMetadata(catalogModule), positionalCurrent);
|
|
1046
|
+
}
|
|
1047
|
+
if (command === "create" && positionalIndex === 0) {
|
|
1048
|
+
return filterByPrefix(["package"], positionalCurrent);
|
|
1049
|
+
}
|
|
1050
|
+
if (command === "add") {
|
|
1051
|
+
if (positionalIndex === 0) {
|
|
1052
|
+
return filterByPrefix(ADD_TARGET_TYPES, positionalCurrent);
|
|
1053
|
+
}
|
|
1054
|
+
if (positionalIndex === 1) {
|
|
1055
|
+
if (parseState.positionals[0] === "package") {
|
|
1056
|
+
return filterByPrefix(await discoverRuntimePackages(appRoot), positionalCurrent);
|
|
1057
|
+
}
|
|
1058
|
+
if (parseState.positionals[0] === "bundle") {
|
|
1059
|
+
return filterByPrefix(await discoverBundleIds(appRoot), positionalCurrent);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (command === "list" && positionalIndex === 0) {
|
|
1064
|
+
return filterByPrefix(LIST_MODES, positionalCurrent);
|
|
1065
|
+
}
|
|
1066
|
+
if (command === "show" && positionalIndex === 0) {
|
|
1067
|
+
return filterByPrefix(
|
|
1068
|
+
[...(await discoverRuntimePackages(appRoot)), ...(await discoverBundleIds(appRoot))],
|
|
1069
|
+
positionalCurrent
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (command === "migrations") {
|
|
1073
|
+
if (positionalIndex === 0) {
|
|
1074
|
+
return filterByPrefix(MIGRATION_SCOPES, positionalCurrent);
|
|
1075
|
+
}
|
|
1076
|
+
if (positionalIndex === 1 && parseState.positionals[0] === "package") {
|
|
1077
|
+
return filterByPrefix(await discoverRuntimePackages(appRoot), positionalCurrent);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (command === "position") {
|
|
1081
|
+
if (positionalIndex === 0) {
|
|
1082
|
+
return filterByPrefix(POSITION_TARGET_TYPES, positionalCurrent);
|
|
1083
|
+
}
|
|
1084
|
+
if (positionalIndex === 1 && parseState.positionals[0] === "element") {
|
|
1085
|
+
return filterByPrefix(await discoverRuntimePackages(appRoot), positionalCurrent);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (command === "update") {
|
|
1089
|
+
if (positionalIndex === 0) {
|
|
1090
|
+
return filterByPrefix(UPDATE_TARGET_TYPES, positionalCurrent);
|
|
1091
|
+
}
|
|
1092
|
+
if (positionalIndex === 1 && parseState.positionals[0] === "package") {
|
|
1093
|
+
return filterByPrefix(await discoverRuntimePackages(appRoot), positionalCurrent);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (command === "remove") {
|
|
1097
|
+
if (positionalIndex === 0) {
|
|
1098
|
+
return filterByPrefix(REMOVE_TARGET_TYPES, positionalCurrent);
|
|
1099
|
+
}
|
|
1100
|
+
if (positionalIndex === 1 && parseState.positionals[0] === "package") {
|
|
1101
|
+
return filterByPrefix(await discoverRuntimePackages(appRoot), positionalCurrent);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function getCompletions({ appRoot = process.cwd(), words = [], cword = 0 } = {}) {
|
|
1110
|
+
const normalized = normalizeCompletionInvocation(words, cword);
|
|
1111
|
+
const currentToken = normalized.words[normalized.cword] ?? "";
|
|
1112
|
+
|
|
1113
|
+
if (normalized.wrapperOnly) {
|
|
1114
|
+
return filterByPrefix(["jskit"], currentToken);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (normalized.words[0] !== "jskit") {
|
|
1118
|
+
return filterByPrefix(["jskit"], currentToken);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const catalogModule = await loadCommandCatalog(appRoot);
|
|
1122
|
+
if (normalized.cword <= 1) {
|
|
1123
|
+
return completeTopLevel({ appRoot, currentToken, catalogModule });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return completeCommand({
|
|
1127
|
+
appRoot,
|
|
1128
|
+
words: normalized.words,
|
|
1129
|
+
cword: normalized.cword,
|
|
1130
|
+
catalogModule
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function renderBashCompletionScript() {
|
|
1135
|
+
return `# shellcheck shell=bash
|
|
1136
|
+
|
|
1137
|
+
_jskit_completion() {
|
|
1138
|
+
local first_word="\${COMP_WORDS[0]}"
|
|
1139
|
+
local second_word="\${COMP_WORDS[1]}"
|
|
1140
|
+
|
|
1141
|
+
if [[ "$first_word" == "npx" || "$first_word" == "jsx" ]]; then
|
|
1142
|
+
if (( COMP_CWORD == 1 )); then
|
|
1143
|
+
COMPREPLY=( $(compgen -W "jskit" -- "\${COMP_WORDS[COMP_CWORD]}") )
|
|
1144
|
+
return 0
|
|
1145
|
+
fi
|
|
1146
|
+
if [[ "$second_word" != "jskit" && "$second_word" != "@jskit-ai/jskit-cli" ]]; then
|
|
1147
|
+
return 1
|
|
1148
|
+
fi
|
|
1149
|
+
elif [[ "$first_word" != "jskit" ]]; then
|
|
1150
|
+
return 1
|
|
1151
|
+
fi
|
|
1152
|
+
|
|
1153
|
+
mapfile -t COMPREPLY < <(
|
|
1154
|
+
npx jskit completion bash __complete__ "$COMP_CWORD" -- "\${COMP_WORDS[@]}"
|
|
1155
|
+
) || return 1
|
|
1156
|
+
|
|
1157
|
+
if ((\${#COMPREPLY[@]} == 0)); then
|
|
1158
|
+
return 1
|
|
1159
|
+
fi
|
|
1160
|
+
|
|
1161
|
+
return 0
|
|
1162
|
+
}
|
|
1163
|
+
complete -o bashdefault -o default -F _jskit_completion npx
|
|
1164
|
+
complete -o bashdefault -o default -F _jskit_completion jsx
|
|
1165
|
+
complete -o bashdefault -o default -F _jskit_completion jskit
|
|
1166
|
+
`;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export {
|
|
1170
|
+
discoverPlacementTargets,
|
|
1171
|
+
discoverResourceDisplayFields,
|
|
1172
|
+
discoverResourceFiles,
|
|
1173
|
+
discoverSurfaces,
|
|
1174
|
+
getCompletions,
|
|
1175
|
+
normalizeCompletionInvocation,
|
|
1176
|
+
renderBashCompletionScript
|
|
1177
|
+
};
|