@kosdev-code/kos-ui-cli 2.0.8 → 2.0.9

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.
@@ -1,65 +1,37 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "child_process";
2
3
  import minimist from "minimist";
3
- import { dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
4
+ import { getAllGeneratorMetadata } from "./utils/generator-loader.mjs";
5
5
 
6
6
  const args = process.argv.slice(2);
7
7
  const argv = minimist(args);
8
8
 
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
9
  export default async function (plop) {
11
- const generators = {
12
- model: { name: "KOS Model", path: "plopfile.mjs" },
13
- "model:companion": {
14
- name: "KOS Companion Model",
15
- path: "model-aware-plopfile.mjs",
16
- },
17
- hook: { name: "KOS Model Hook and HoC", path: "model-aware-plopfile.mjs" },
18
- context: { name: "KOS Model Context", path: "model-aware-plopfile.mjs" },
19
- container: {
20
- name: "KOS Model Container",
21
- path: "model-aware-plopfile.mjs",
22
- },
10
+ const metadata = await getAllGeneratorMetadata();
23
11
 
24
- workspace: {
25
- name: "KOS Development Workspace",
26
- path: "plopfile.mjs",
27
- },
28
- project: { name: "KOS UI App Project", path: "model-aware-plopfile.mjs" },
29
- plugin: { name: "KOS UI Plugin Project", path: "plopfile.mjs" },
30
- "plugin:utility": { name: "KOS UI Plugin Utility", path: "plopfile.mjs" },
31
- "plugin:setup": { name: "KOS UI Plugin Setup Step", path: "plopfile.mjs" },
32
- "plugin:cui": {
33
- name: "KOS UI Plugin CUI Configuration",
34
- path: "plopfile.mjs",
35
- },
36
- "plugin:setup": { name: "KOS UI Plugin Setup Step", path: "plopfile.mjs" },
37
- "plugin:nav": {
38
- name: "KOS UI Plugin Page Navigation",
39
- path: "plopfile.mjs",
40
- },
41
- component: { name: "KOS UI Component", path: "plopfile.mjs" },
42
- pluginComponent: { name: "KOS UI Plugin Component", path: "plopfile.mjs" },
43
- i18n: {
44
- name: "KOS Localization Project",
45
- path: "plopfile.mjs",
46
- },
47
- "i18n:namespace": {
48
- name: "KOS Localization Namespace",
49
- path: "plopfile.mjs",
50
- },
51
- theme: { name: "KOS Theme Project", path: "plopfile.mjs" },
52
- env: {
53
- name: "Discover and Set Studio Environment Variables",
54
- path: "env-plopfile.mjs",
55
- },
56
- };
57
- plop.setActionType("execute", async (answers, config) => {
58
- const template = answers.template;
59
- argv._ = [template];
60
- const pathName = generators[template].path;
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
+ });
61
29
 
62
- console.warn(`--------- Please run kosui ${template} ---------`);
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
+ });
63
35
  });
64
36
 
65
37
  plop.setGenerator("help", {
@@ -69,17 +41,13 @@ export default async function (plop) {
69
41
  type: "list",
70
42
  name: "template",
71
43
  message: "Choose a task to run:",
72
- choices: Object.keys(generators).map((key) => ({
73
- name: generators[key].name,
74
- value: key,
75
- })),
44
+ choices,
45
+ },
46
+ ],
47
+ actions: [
48
+ {
49
+ type: "execCLI",
76
50
  },
77
51
  ],
78
- actions: function (data) {
79
- const action = {
80
- type: "execute",
81
- };
82
- return [action];
83
- },
84
52
  });
85
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 = 60 * 1000; // 1 minute
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,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
+ }
File without changes
@@ -0,0 +1,97 @@
1
+ // utils/nx-context.mjs
2
+ import { execSync } from "child_process";
3
+ import { existsSync } from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { getCached, setCached } from "./cache.mjs";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ function execJson(cmd) {
11
+ const output = execSync(cmd, { encoding: "utf-8" });
12
+ return JSON.parse(output);
13
+ }
14
+
15
+ let _workspaceDetected;
16
+
17
+ export async function detectWorkspace(startDir = process.cwd()) {
18
+ if (_workspaceDetected !== undefined) return _workspaceDetected;
19
+
20
+ let dir = startDir;
21
+ while (dir !== path.dirname(dir)) {
22
+ if (existsSync(path.join(dir, "nx.json"))) {
23
+ _workspaceDetected = true;
24
+ return true;
25
+ }
26
+ dir = path.dirname(dir);
27
+ }
28
+
29
+ _workspaceDetected = false;
30
+ return false;
31
+ }
32
+ export async function getAllProjects() {
33
+ const cached = getCached("allProjects");
34
+ if (cached) return cached;
35
+
36
+ const projects = execJson("npx nx show projects --all --json").map((p) => p);
37
+ setCached("allProjects", projects);
38
+ return projects;
39
+ }
40
+
41
+ export async function getLibraryProjects() {
42
+ const cached = getCached("libraryProjects");
43
+ if (cached) return cached;
44
+
45
+ const projects = execJson("npx nx show projects --type=lib --json").map(
46
+ (p) => p
47
+ );
48
+ setCached("libraryProjects", projects);
49
+ return projects;
50
+ }
51
+
52
+ export async function getPluginProjects() {
53
+ const cached = getCached("pluginProjects");
54
+ if (cached) return cached;
55
+
56
+ const all = await getAllProjects();
57
+ const filtered = all.filter(
58
+ (p) => p.includes("plugin") || p.includes("extension")
59
+ );
60
+ setCached("pluginProjects", filtered);
61
+ return filtered;
62
+ }
63
+
64
+ export async function getAllModels() {
65
+ const cached = getCached("allModels");
66
+ if (cached) return cached;
67
+
68
+ const json = execJson("npx nx show projects --all --json");
69
+ const allModels = [];
70
+
71
+ for (const proj of json) {
72
+ const modelPath = path.join(__dirname, `../../${proj.root}/models`);
73
+ if (existsSync(modelPath)) {
74
+ const models = execSync(`ls ${modelPath}`, { encoding: "utf-8" })
75
+ .split("\n")
76
+ .filter(Boolean)
77
+ .map((file) => ({
78
+ model: file.replace(/\.ts$/, ""),
79
+ project: proj.name,
80
+ }));
81
+ allModels.push(...models);
82
+ }
83
+ }
84
+
85
+ setCached("allModels", allModels);
86
+ return allModels;
87
+ }
88
+
89
+ export async function getProjectDetails(projectName) {
90
+ const cacheKey = `projectDetails:${projectName}`;
91
+ const cached = getCached(cacheKey);
92
+ if (cached) return cached;
93
+
94
+ const details = execJson(`npx nx show project ${projectName} --json`);
95
+ setCached(cacheKey, details);
96
+ return details;
97
+ }
@@ -0,0 +1,46 @@
1
+ // utils/prompts.mjs
2
+
3
+ export const DEFAULT_PROMPTS = [
4
+ {
5
+ type: "confirm",
6
+ name: "interactive",
7
+ message: "Use interactive mode?",
8
+ default: false,
9
+ when: false,
10
+ },
11
+ {
12
+ type: "confirm",
13
+ name: "dryRun",
14
+ message: "Dry run the command?",
15
+ default: false,
16
+ when: false,
17
+ },
18
+ ];
19
+
20
+ export const MODEL_PROMPTS = [
21
+ {
22
+ type: "confirm",
23
+ name: "container",
24
+ message: "Requires container model?",
25
+ default: false,
26
+ },
27
+ {
28
+ type: "confirm",
29
+ name: "parentAware",
30
+ message: "Aware of parent container?",
31
+ default: false,
32
+ when: (answers) => !!answers.container,
33
+ },
34
+ {
35
+ type: "confirm",
36
+ name: "singleton",
37
+ message: "Is singleton?",
38
+ default: false,
39
+ },
40
+ {
41
+ type: "confirm",
42
+ name: "dataServices",
43
+ message: "Create data services?",
44
+ default: true,
45
+ },
46
+ ];
@@ -0,0 +1,10 @@
1
+ // utils/validators.mjs
2
+
3
+ /**
4
+ * Validator that ensures a value is not empty.
5
+ * @param {string} value
6
+ * @returns {true|string}
7
+ */
8
+ export function required(value) {
9
+ return value && value.trim() !== "" ? true : "This field is required.";
10
+ }