@kosdev-code/kos-ui-cli 0.1.0-dev.5053
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/README.md +7 -0
- package/package.json +30 -0
- package/src/index.d.ts +2 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -0
- package/src/lib/cli.mjs +803 -0
- package/src/lib/generators/cache/index.mjs +18 -0
- package/src/lib/generators/component/index.mjs +55 -0
- package/src/lib/generators/env/index.mjs +42 -0
- package/src/lib/generators/i18n/namespace.mjs +61 -0
- package/src/lib/generators/kab/index.mjs +82 -0
- package/src/lib/generators/metadata.json +341 -0
- package/src/lib/generators/model/add-future.mjs +96 -0
- package/src/lib/generators/model/companion.mjs +117 -0
- package/src/lib/generators/model/container.mjs +96 -0
- package/src/lib/generators/model/context.mjs +77 -0
- package/src/lib/generators/model/hook.mjs +77 -0
- package/src/lib/generators/model/model.mjs +79 -0
- package/src/lib/generators/plugin/index.mjs +195 -0
- package/src/lib/generators/project/app.mjs +39 -0
- package/src/lib/generators/project/content.mjs +41 -0
- package/src/lib/generators/project/i18n.mjs +38 -0
- package/src/lib/generators/project/plugin.mjs +38 -0
- package/src/lib/generators/project/splash.mjs +39 -0
- package/src/lib/generators/project/theme.mjs +38 -0
- package/src/lib/generators/serve/index.mjs +74 -0
- package/src/lib/generators/version/index.mjs +182 -0
- package/src/lib/generators/workspace/index.mjs +40 -0
- package/src/lib/generators/workspace/list-models.mjs +64 -0
- package/src/lib/generators/workspace/list-projects.mjs +167 -0
- package/src/lib/plopfile.mjs +67 -0
- package/src/lib/routing-plopfile.mjs +53 -0
- package/src/lib/scripts/generate-metadata.mjs +39 -0
- package/src/lib/utils/action-factory.mjs +9 -0
- package/src/lib/utils/cache.mjs +128 -0
- package/src/lib/utils/command-builder.mjs +94 -0
- package/src/lib/utils/exec.mjs +18 -0
- package/src/lib/utils/generator-loader.mjs +65 -0
- package/src/lib/utils/index.mjs +1 -0
- package/src/lib/utils/java-home.mjs +55 -0
- package/src/lib/utils/logger.mjs +0 -0
- package/src/lib/utils/nx-context.mjs +395 -0
- package/src/lib/utils/prompts.mjs +75 -0
- package/src/lib/utils/studio-home.mjs +12 -0
- package/src/lib/utils/utils.mjs +126 -0
- package/src/lib/utils/validators.mjs +10 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAllProjects,
|
|
3
|
+
getLibraryProjects,
|
|
4
|
+
getModelProjects,
|
|
5
|
+
getModelComponentProjects,
|
|
6
|
+
getUIProjects,
|
|
7
|
+
getI18nProjects,
|
|
8
|
+
getPluginProjects,
|
|
9
|
+
getModelProjectsWithFallback,
|
|
10
|
+
getModelComponentProjectsWithFallback,
|
|
11
|
+
getI18nProjectsWithFallback,
|
|
12
|
+
getPluginProjectsWithFallback,
|
|
13
|
+
} from "../../utils/nx-context.mjs";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generator for listing all projects in a workspace
|
|
17
|
+
*/
|
|
18
|
+
export default async function register(plop) {
|
|
19
|
+
plop.setGenerator("workspace:list-projects", {
|
|
20
|
+
description: "List all available projects in the workspace",
|
|
21
|
+
prompts: [
|
|
22
|
+
{
|
|
23
|
+
type: "list",
|
|
24
|
+
name: "projectType",
|
|
25
|
+
message: "Project type to filter by",
|
|
26
|
+
choices: [
|
|
27
|
+
{ name: "All projects", value: "all" },
|
|
28
|
+
{ name: "Model projects", value: "model" },
|
|
29
|
+
{ name: "Model component projects", value: "model-component" },
|
|
30
|
+
{ name: "UI projects", value: "ui" },
|
|
31
|
+
{ name: "I18n projects", value: "i18n" },
|
|
32
|
+
{ name: "Plugin projects", value: "plugin" },
|
|
33
|
+
{ name: "Library projects", value: "library" },
|
|
34
|
+
],
|
|
35
|
+
default: "all",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "list",
|
|
39
|
+
name: "outputFormat",
|
|
40
|
+
message: "Output format",
|
|
41
|
+
choices: ["json", "table", "simple"],
|
|
42
|
+
default: "json",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "confirm",
|
|
46
|
+
name: "includeFallback",
|
|
47
|
+
message: "Include fallback projects if no projects of specified type found?",
|
|
48
|
+
default: true,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
actions: function (answers) {
|
|
52
|
+
return [
|
|
53
|
+
async function listProjects() {
|
|
54
|
+
const includeFallback = answers.projectType === "all" ? false : (answers.includeFallback ?? true);
|
|
55
|
+
const projects = await getProjectsByType(
|
|
56
|
+
answers.projectType,
|
|
57
|
+
includeFallback
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
formatProjectOutput(projects, answers.outputFormat, answers.projectType);
|
|
61
|
+
|
|
62
|
+
return `[ok] Found ${projects.length} ${answers.projectType} projects`;
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Zone 1: High-level orchestration
|
|
70
|
+
async function getProjectsByType(projectType, includeFallback) {
|
|
71
|
+
switch (projectType) {
|
|
72
|
+
case "all":
|
|
73
|
+
return await getAllProjects();
|
|
74
|
+
case "model":
|
|
75
|
+
return includeFallback
|
|
76
|
+
? await getModelProjectsWithFallback()
|
|
77
|
+
: await getModelProjects();
|
|
78
|
+
case "model-component":
|
|
79
|
+
return includeFallback
|
|
80
|
+
? await getModelComponentProjectsWithFallback()
|
|
81
|
+
: await getModelComponentProjects();
|
|
82
|
+
case "ui":
|
|
83
|
+
return await getUIProjects();
|
|
84
|
+
case "i18n":
|
|
85
|
+
return includeFallback
|
|
86
|
+
? await getI18nProjectsWithFallback()
|
|
87
|
+
: await getI18nProjects();
|
|
88
|
+
case "plugin":
|
|
89
|
+
return includeFallback
|
|
90
|
+
? await getPluginProjectsWithFallback()
|
|
91
|
+
: await getPluginProjects();
|
|
92
|
+
case "library":
|
|
93
|
+
return await getLibraryProjects();
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unknown project type: ${projectType}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Zone 3: Output formatting
|
|
100
|
+
function formatProjectOutput(projects, format, projectType) {
|
|
101
|
+
const enrichedProjects = enrichProjectData(projects, projectType);
|
|
102
|
+
|
|
103
|
+
switch (format) {
|
|
104
|
+
case "json":
|
|
105
|
+
console.log(JSON.stringify(enrichedProjects, null, 2));
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case "table":
|
|
109
|
+
console.table(
|
|
110
|
+
enrichedProjects.map((p) => ({
|
|
111
|
+
Name: p.name,
|
|
112
|
+
Type: p.type || "unknown",
|
|
113
|
+
}))
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case "simple":
|
|
118
|
+
for (const project of enrichedProjects) {
|
|
119
|
+
console.log(project.name);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function enrichProjectData(projects, requestedType) {
|
|
126
|
+
return projects.map((projectName) => ({
|
|
127
|
+
name: projectName,
|
|
128
|
+
type: requestedType === "all" ? inferProjectType(projectName) : requestedType,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function inferProjectType(projectName) {
|
|
133
|
+
const name = projectName.toLowerCase();
|
|
134
|
+
|
|
135
|
+
if (name.includes("model") && !name.includes("component")) {
|
|
136
|
+
return "model";
|
|
137
|
+
}
|
|
138
|
+
if (name.includes("model") && name.includes("component")) {
|
|
139
|
+
return "model-component";
|
|
140
|
+
}
|
|
141
|
+
if (name.includes("ui") || name.includes("app")) {
|
|
142
|
+
return "ui";
|
|
143
|
+
}
|
|
144
|
+
if (name.includes("i18n") || name.includes("localization")) {
|
|
145
|
+
return "i18n";
|
|
146
|
+
}
|
|
147
|
+
if (name.includes("plugin") || name.includes("extension")) {
|
|
148
|
+
return "plugin";
|
|
149
|
+
}
|
|
150
|
+
if (name.includes("lib")) {
|
|
151
|
+
return "library";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return "unknown";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Export metadata for CLI integration
|
|
158
|
+
export const metadata = {
|
|
159
|
+
key: "workspace:list-projects",
|
|
160
|
+
name: "List all available projects in the workspace",
|
|
161
|
+
description: "List all available projects in the workspace with optional type filtering",
|
|
162
|
+
namedArguments: {
|
|
163
|
+
type: "projectType",
|
|
164
|
+
format: "outputFormat",
|
|
165
|
+
fallback: "includeFallback",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// plopfile.mjs
|
|
2
|
+
import { detectWorkspace } from "./utils/nx-context.mjs";
|
|
3
|
+
|
|
4
|
+
// Generator registration
|
|
5
|
+
import registerAddFuture from "./generators/model/add-future.mjs";
|
|
6
|
+
import registerCompanion from "./generators/model/companion.mjs";
|
|
7
|
+
import registerContainer from "./generators/model/container.mjs";
|
|
8
|
+
import registerContext from "./generators/model/context.mjs";
|
|
9
|
+
import registerHook from "./generators/model/hook.mjs";
|
|
10
|
+
import registerKosModel from "./generators/model/model.mjs";
|
|
11
|
+
|
|
12
|
+
import registerComponent from "./generators/component/index.mjs";
|
|
13
|
+
import registerPluginComponent from "./generators/plugin/index.mjs";
|
|
14
|
+
|
|
15
|
+
import registerCacheGenerators from "./generators/cache/index.mjs";
|
|
16
|
+
import registerEnv from "./generators/env/index.mjs";
|
|
17
|
+
import registerKab from "./generators/kab/index.mjs";
|
|
18
|
+
import registerServe from "./generators/serve/index.mjs";
|
|
19
|
+
import registerVersion from "./generators/version/index.mjs";
|
|
20
|
+
import registerI18nNamespace from "./generators/i18n/namespace.mjs";
|
|
21
|
+
import registerUiProject from "./generators/project/app.mjs";
|
|
22
|
+
import registerContentProject from "./generators/project/content.mjs";
|
|
23
|
+
import registerI18n from "./generators/project/i18n.mjs";
|
|
24
|
+
import registerPluginProject from "./generators/project/plugin.mjs";
|
|
25
|
+
import registerSplashProject from "./generators/project/splash.mjs";
|
|
26
|
+
import registerTheme from "./generators/project/theme.mjs";
|
|
27
|
+
import registerWorkspace from "./generators/workspace/index.mjs";
|
|
28
|
+
|
|
29
|
+
import { clearCache } from "./utils/cache.mjs";
|
|
30
|
+
|
|
31
|
+
export default async function (plop) {
|
|
32
|
+
plop.setActionType("clearCache", (answers, config, plop) => {
|
|
33
|
+
clearCache();
|
|
34
|
+
return "[ok] CLI cache cleared successfully.";
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await registerWorkspace(plop);
|
|
38
|
+
|
|
39
|
+
const isWorkspace = await detectWorkspace();
|
|
40
|
+
if (!isWorkspace) {
|
|
41
|
+
console.warn(
|
|
42
|
+
"[kos-cli] Not inside an Nx workspace. Only a subset of generators are available."
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await registerKosModel(plop);
|
|
48
|
+
await registerAddFuture(plop);
|
|
49
|
+
await registerHook(plop);
|
|
50
|
+
await registerCompanion(plop);
|
|
51
|
+
await registerContainer(plop);
|
|
52
|
+
await registerContext(plop);
|
|
53
|
+
await registerComponent(plop);
|
|
54
|
+
await registerPluginComponent(plop);
|
|
55
|
+
await registerTheme(plop);
|
|
56
|
+
await registerContentProject(plop);
|
|
57
|
+
await registerUiProject(plop);
|
|
58
|
+
await registerPluginProject(plop);
|
|
59
|
+
await registerSplashProject(plop);
|
|
60
|
+
await registerI18n(plop);
|
|
61
|
+
await registerI18nNamespace(plop);
|
|
62
|
+
await registerEnv(plop);
|
|
63
|
+
await registerKab(plop);
|
|
64
|
+
await registerServe(plop);
|
|
65
|
+
await registerVersion(plop);
|
|
66
|
+
await registerCacheGenerators(plop);
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import minimist from "minimist";
|
|
4
|
+
import { getAllGeneratorMetadata } from "./utils/generator-loader.mjs";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const argv = minimist(args);
|
|
8
|
+
|
|
9
|
+
export default async function (plop) {
|
|
10
|
+
const metadata = await getAllGeneratorMetadata();
|
|
11
|
+
|
|
12
|
+
const sorted = Object.entries(metadata).sort(([aKey, a], [bKey, b]) => {
|
|
13
|
+
if (a.category === b.category) return a.name.localeCompare(b.name);
|
|
14
|
+
return a.category.localeCompare(b.category);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const choices = sorted.map(([key, meta]) => ({
|
|
18
|
+
name: `[${meta.category}] ${meta.name}`,
|
|
19
|
+
value: `${key}`,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
plop.setActionType("execCLI", function (answers) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const command = `kosui ${answers.template}`;
|
|
25
|
+
const child = spawn("npx", ["kosui", answers.template], {
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
shell: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
child.on("exit", (code) => {
|
|
31
|
+
if (code === 0) resolve(`Executed kosui ${answers.template}`);
|
|
32
|
+
else reject(new Error(`Failed with code ${code}`));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
plop.setGenerator("help", {
|
|
38
|
+
description: "Select a task to run",
|
|
39
|
+
prompts: [
|
|
40
|
+
{
|
|
41
|
+
type: "list",
|
|
42
|
+
name: "template",
|
|
43
|
+
message: "Choose a task to run:",
|
|
44
|
+
choices,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
actions: [
|
|
48
|
+
{
|
|
49
|
+
type: "execCLI",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const generatorsRoot = path.join(__dirname, "..", "generators");
|
|
7
|
+
const outputPath = path.join(generatorsRoot, "metadata.json");
|
|
8
|
+
|
|
9
|
+
async function discoverGenerators() {
|
|
10
|
+
const categories = await fs.readdir(generatorsRoot, { withFileTypes: true });
|
|
11
|
+
const entries = [];
|
|
12
|
+
|
|
13
|
+
for (const category of categories) {
|
|
14
|
+
if (!category.isDirectory()) continue;
|
|
15
|
+
|
|
16
|
+
const categoryPath = path.join(generatorsRoot, category.name);
|
|
17
|
+
const files = await fs.readdir(categoryPath);
|
|
18
|
+
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
if (!file.endsWith(".mjs")) continue;
|
|
21
|
+
|
|
22
|
+
const fullPath = path.join(categoryPath, file);
|
|
23
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
24
|
+
|
|
25
|
+
if (!module.metadata) continue;
|
|
26
|
+
|
|
27
|
+
entries.push({
|
|
28
|
+
category: category.name,
|
|
29
|
+
file,
|
|
30
|
+
metadata: module.metadata,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await fs.writeFile(outputPath, JSON.stringify(entries, null, 2));
|
|
36
|
+
console.log(`Generated metadata.json with ${entries.length} entries.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await discoverGenerators();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// utils/cache.mjs
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
// Default to project-local cache; can swap to global using `getGlobalCachePath()`
|
|
8
|
+
const CACHE_PATH = path.resolve(".nx/cli-cache.json");
|
|
9
|
+
const CACHE_TTL = 600 * 1000;
|
|
10
|
+
const ARGS = process.argv;
|
|
11
|
+
const DISABLE_CACHE =
|
|
12
|
+
process.env.DISABLE_CACHE === "true" || process.env.REFRESH === "true";
|
|
13
|
+
|
|
14
|
+
let _cache = {}; // in-memory cache
|
|
15
|
+
let _loaded = false;
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TRACKED_FILES = [
|
|
18
|
+
"workspace.json",
|
|
19
|
+
"nx.json",
|
|
20
|
+
"project.json",
|
|
21
|
+
"tsconfig.base.json",
|
|
22
|
+
"package.json",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function ensureCacheDir() {
|
|
26
|
+
const dir = path.dirname(CACHE_PATH);
|
|
27
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadCacheFromDisk() {
|
|
31
|
+
if (_loaded) return;
|
|
32
|
+
_loaded = true;
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(CACHE_PATH)) {
|
|
35
|
+
const data = fs.readFileSync(CACHE_PATH, "utf-8");
|
|
36
|
+
_cache = JSON.parse(data);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn("Failed to load CLI cache:", err);
|
|
40
|
+
_cache = {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveCacheToDisk() {
|
|
45
|
+
try {
|
|
46
|
+
ensureCacheDir();
|
|
47
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(_cache, null, 2));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn("Failed to save CLI cache:", err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isFresh(entry, ttl = CACHE_TTL) {
|
|
54
|
+
if (!entry || !entry.timestamp) return false;
|
|
55
|
+
return Date.now() - entry.timestamp < ttl;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getCached(key, ttl = CACHE_TTL) {
|
|
59
|
+
if (DISABLE_CACHE) return null;
|
|
60
|
+
loadCacheFromDisk();
|
|
61
|
+
const entry = _cache[key];
|
|
62
|
+
if (isFresh(entry, ttl)) return entry.data;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function setCached(key, data) {
|
|
67
|
+
loadCacheFromDisk();
|
|
68
|
+
_cache[key] = {
|
|
69
|
+
data,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
};
|
|
72
|
+
saveCacheToDisk();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearCache(key) {
|
|
76
|
+
loadCacheFromDisk();
|
|
77
|
+
if (key) {
|
|
78
|
+
delete _cache[key];
|
|
79
|
+
} else {
|
|
80
|
+
_cache = {};
|
|
81
|
+
}
|
|
82
|
+
saveCacheToDisk();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getGlobalCachePath() {
|
|
86
|
+
return path.join(os.homedir(), ".kos-cli-cache.json");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function hashFiles(filePaths = DEFAULT_TRACKED_FILES) {
|
|
90
|
+
const hash = crypto.createHash("sha256");
|
|
91
|
+
for (const file of filePaths) {
|
|
92
|
+
if (fs.existsSync(file)) {
|
|
93
|
+
hash.update(fs.readFileSync(file));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return hash.digest("hex");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getHashedCache(key, filePaths = DEFAULT_TRACKED_FILES) {
|
|
100
|
+
if (DISABLE_CACHE) return null;
|
|
101
|
+
loadCacheFromDisk();
|
|
102
|
+
const entry = _cache[key];
|
|
103
|
+
const currentHash = hashFiles(filePaths);
|
|
104
|
+
if (entry && entry.hash === currentHash) {
|
|
105
|
+
return entry.data;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function setHashedCache(key, data, filePaths = DEFAULT_TRACKED_FILES) {
|
|
111
|
+
loadCacheFromDisk();
|
|
112
|
+
const hash = hashFiles(filePaths);
|
|
113
|
+
_cache[key] = { data, hash, timestamp: Date.now() };
|
|
114
|
+
saveCacheToDisk();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function markCacheDirty(reason = "manual") {
|
|
118
|
+
_loaded = false;
|
|
119
|
+
_cache = {};
|
|
120
|
+
if (fs.existsSync(CACHE_PATH)) {
|
|
121
|
+
fs.unlinkSync(CACHE_PATH);
|
|
122
|
+
console.debug(`Cache invalidated due to ${reason}: ${CACHE_PATH} deleted`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function shouldInvalidateFromMeta(metadata = {}) {
|
|
127
|
+
return metadata?.invalidateCache === true;
|
|
128
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a non-interactive command string from interactive session answers
|
|
3
|
+
* This helps users understand how to run the same command without prompts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts interactive prompt answers back to CLI command arguments
|
|
8
|
+
* @param {string} command - The generator command name
|
|
9
|
+
* @param {Object} answers - The answers collected from prompts
|
|
10
|
+
* @param {Object} meta - Generator metadata including namedArguments mapping
|
|
11
|
+
* @returns {string|null} The equivalent CLI command string, or null if cannot be built
|
|
12
|
+
*/
|
|
13
|
+
export function buildNonInteractiveCommand(command, answers, meta) {
|
|
14
|
+
if (!meta?.namedArguments) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const args = [`kosui ${command}`];
|
|
19
|
+
const reverseMap = {};
|
|
20
|
+
|
|
21
|
+
// Create reverse mapping from prompt names to CLI arguments
|
|
22
|
+
Object.entries(meta.namedArguments).forEach(([cliArg, promptName]) => {
|
|
23
|
+
// Skip special flags that don't map to prompts
|
|
24
|
+
if (cliArg !== "interactive" && cliArg !== "dryRun") {
|
|
25
|
+
reverseMap[promptName] = cliArg;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Build command arguments from answers
|
|
30
|
+
Object.entries(answers).forEach(([promptName, value]) => {
|
|
31
|
+
const cliArg = reverseMap[promptName];
|
|
32
|
+
if (cliArg && value !== undefined && value !== null && value !== "") {
|
|
33
|
+
args.push(formatArgument(cliArg, value));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Add dryRun if it was provided
|
|
38
|
+
if (answers.dryRun) {
|
|
39
|
+
args.push("--dryRun");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return args.join(" ");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats a single CLI argument based on its value type
|
|
47
|
+
* @param {string} argName - The CLI argument name
|
|
48
|
+
* @param {any} value - The value for the argument
|
|
49
|
+
* @returns {string} The formatted CLI argument string
|
|
50
|
+
*/
|
|
51
|
+
function formatArgument(argName, value) {
|
|
52
|
+
// Handle boolean values
|
|
53
|
+
if (typeof value === "boolean") {
|
|
54
|
+
if (value) {
|
|
55
|
+
return `--${argName}`;
|
|
56
|
+
} else {
|
|
57
|
+
// For false booleans, explicitly set to false
|
|
58
|
+
// This ensures the command can be replicated exactly
|
|
59
|
+
return `--${argName}=false`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle string/number values
|
|
64
|
+
const stringValue = String(value);
|
|
65
|
+
|
|
66
|
+
// Check if value needs escaping
|
|
67
|
+
if (needsEscaping(stringValue)) {
|
|
68
|
+
// Use double quotes and escape internal quotes
|
|
69
|
+
return `--${argName}="${stringValue.replace(/"/g, '\\"')}"`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `--${argName}=${stringValue}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determines if a string value needs shell escaping
|
|
77
|
+
* @param {string} value - The value to check
|
|
78
|
+
* @returns {boolean} True if the value needs escaping
|
|
79
|
+
*/
|
|
80
|
+
function needsEscaping(value) {
|
|
81
|
+
// Check for characters that need escaping in shell
|
|
82
|
+
return /[\s'"$`\\!]/.test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determines if the CLI is running in interactive mode
|
|
87
|
+
* @param {Object} parsedArgs - Parsed command line arguments
|
|
88
|
+
* @param {boolean} hasAnyAnswers - Whether any answers were provided via CLI
|
|
89
|
+
* @returns {boolean} True if running in interactive mode
|
|
90
|
+
*/
|
|
91
|
+
export function isRunningInteractively(parsedArgs, hasAnyAnswers) {
|
|
92
|
+
const forceInteractive = parsedArgs.interactive || parsedArgs.i;
|
|
93
|
+
return forceInteractive || (!hasAnyAnswers && !parsedArgs.help);
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// utils/exec.mjs
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Executes a shell command and returns a Promise.
|
|
6
|
+
* @param {string} command
|
|
7
|
+
* @returns {Promise<void>}
|
|
8
|
+
*/
|
|
9
|
+
export function execute(command) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = exec(command, { shell: true }, (error, stdout, stderr) => {
|
|
12
|
+
if (stdout) process.stdout.write(stdout);
|
|
13
|
+
if (stderr) process.stderr.write(stderr);
|
|
14
|
+
if (error) return reject(error);
|
|
15
|
+
resolve();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// utils/generator-loader.mjs
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const GENERATOR_ROOT = path.join(__dirname, "../generators");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively finds generator metadata across all generator folders.
|
|
11
|
+
* @returns {Promise<Record<string, any>>} Mapping of generator key -> metadata
|
|
12
|
+
*/
|
|
13
|
+
export async function getAllGeneratorMetadata() {
|
|
14
|
+
const metadataMap = {};
|
|
15
|
+
|
|
16
|
+
const categories = await fs.readdir(GENERATOR_ROOT, { withFileTypes: true });
|
|
17
|
+
for (const category of categories) {
|
|
18
|
+
if (!category.isDirectory()) continue;
|
|
19
|
+
|
|
20
|
+
const categoryPath = path.join(GENERATOR_ROOT, category.name);
|
|
21
|
+
const files = await fs.readdir(categoryPath);
|
|
22
|
+
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
if (!file.endsWith(".mjs")) continue;
|
|
25
|
+
|
|
26
|
+
const modulePath = path.join(categoryPath, file);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const module = await import(modulePath);
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(module.metadata)) {
|
|
32
|
+
for (const meta of module.metadata) {
|
|
33
|
+
if (meta?.key) {
|
|
34
|
+
metadataMap[meta.key] = {
|
|
35
|
+
...meta,
|
|
36
|
+
path: modulePath,
|
|
37
|
+
category: category.name,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else if (module.metadata?.key) {
|
|
42
|
+
metadataMap[module.metadata.key] = {
|
|
43
|
+
...module.metadata,
|
|
44
|
+
path: modulePath,
|
|
45
|
+
category: category.name,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.warn(`Failed to load metadata from ${modulePath}`, e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return metadataMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get metadata for a specific generator by key.
|
|
59
|
+
* @param {string} key
|
|
60
|
+
* @returns {Promise<any>}
|
|
61
|
+
*/
|
|
62
|
+
export async function getGeneratorMetadata(key) {
|
|
63
|
+
const all = await getAllGeneratorMetadata();
|
|
64
|
+
return all[key] || {};
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./utils.mjs";
|