@jskit-ai/jskit-cli 0.2.4

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.
@@ -0,0 +1,408 @@
1
+ import process from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { Writable } from "node:stream";
4
+ import { createCliError } from "./cliError.js";
5
+ import { ensureArray } from "./collectionUtils.js";
6
+
7
+ const OPTION_INTERPOLATION_PATTERN = /\$\{option:([a-z][a-z0-9-]*)(\|[^}]*)?\}/gi;
8
+
9
+ function normalizeSnippet(value) {
10
+ return String(value || "")
11
+ .replace(/\r\n/g, "\n")
12
+ .replace(/\r/g, "\n")
13
+ .trimEnd();
14
+ }
15
+
16
+ function appendTextSnippet(content, snippet, position = "bottom") {
17
+ const normalizedContent = String(content || "")
18
+ .replace(/\r\n/g, "\n")
19
+ .replace(/\r/g, "\n");
20
+ const normalizedSnippet = normalizeSnippet(snippet);
21
+
22
+ if (!normalizedSnippet) {
23
+ return {
24
+ changed: false,
25
+ content: normalizedContent
26
+ };
27
+ }
28
+
29
+ if (normalizedContent.includes(normalizedSnippet)) {
30
+ return {
31
+ changed: false,
32
+ content: normalizedContent
33
+ };
34
+ }
35
+
36
+ if (!normalizedContent) {
37
+ return {
38
+ changed: true,
39
+ content: `${normalizedSnippet}\n`
40
+ };
41
+ }
42
+
43
+ if (position === "top") {
44
+ const nextContent = `${normalizedSnippet}\n\n${normalizedContent.replace(/^\n+/, "")}`.replace(/\n+$/, "\n");
45
+ return {
46
+ changed: true,
47
+ content: nextContent
48
+ };
49
+ }
50
+
51
+ const nextContent = `${normalizedContent.replace(/\n*$/, "\n\n")}${normalizedSnippet}\n`;
52
+ return {
53
+ changed: true,
54
+ content: nextContent
55
+ };
56
+ }
57
+
58
+ function normalizeSkipChecks(value) {
59
+ if (Array.isArray(value)) {
60
+ return value;
61
+ }
62
+
63
+ if (value == null) {
64
+ return [];
65
+ }
66
+
67
+ const one = String(value || "").trim();
68
+ return one ? [one] : [];
69
+ }
70
+
71
+ function splitTextIntoWords(value) {
72
+ const normalized = String(value || "")
73
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
74
+ .replace(/[^A-Za-z0-9]+/g, " ")
75
+ .trim();
76
+ if (!normalized) {
77
+ return [];
78
+ }
79
+ return normalized
80
+ .split(/\s+/)
81
+ .map((entry) => entry.toLowerCase())
82
+ .filter(Boolean);
83
+ }
84
+
85
+ function wordsToPascal(words) {
86
+ return ensureArray(words)
87
+ .map((entry) => {
88
+ const value = String(entry || "").toLowerCase();
89
+ if (!value) {
90
+ return "";
91
+ }
92
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
93
+ })
94
+ .join("");
95
+ }
96
+
97
+ function wordsToCamel(words) {
98
+ const pascal = wordsToPascal(words);
99
+ if (!pascal) {
100
+ return "";
101
+ }
102
+ return `${pascal.slice(0, 1).toLowerCase()}${pascal.slice(1)}`;
103
+ }
104
+
105
+ function wordsToSnake(words) {
106
+ return ensureArray(words)
107
+ .map((entry) => String(entry || "").toLowerCase())
108
+ .filter(Boolean)
109
+ .join("_");
110
+ }
111
+
112
+ function wordsToKebab(words) {
113
+ return ensureArray(words)
114
+ .map((entry) => String(entry || "").toLowerCase())
115
+ .filter(Boolean)
116
+ .join("-");
117
+ }
118
+
119
+ function toSingularForm(value) {
120
+ const words = splitTextIntoWords(value);
121
+ if (words.length < 1) {
122
+ return "";
123
+ }
124
+
125
+ const lastIndex = words.length - 1;
126
+ const last = words[lastIndex];
127
+ if (!last) {
128
+ return wordsToKebab(words);
129
+ }
130
+
131
+ if (last.endsWith("ies") && last.length > 3) {
132
+ words[lastIndex] = `${last.slice(0, -3)}y`;
133
+ return wordsToKebab(words);
134
+ }
135
+ if (last.endsWith("sses") && last.length > 4) {
136
+ words[lastIndex] = last.slice(0, -2);
137
+ return wordsToKebab(words);
138
+ }
139
+ if (last.endsWith("s") && !last.endsWith("ss") && last.length > 1) {
140
+ words[lastIndex] = last.slice(0, -1);
141
+ return wordsToKebab(words);
142
+ }
143
+
144
+ return wordsToKebab(words);
145
+ }
146
+
147
+ function toPluralForm(value) {
148
+ const words = splitTextIntoWords(value);
149
+ if (words.length < 1) {
150
+ return "";
151
+ }
152
+
153
+ const lastIndex = words.length - 1;
154
+ const last = words[lastIndex];
155
+ if (!last) {
156
+ return wordsToKebab(words);
157
+ }
158
+
159
+ if (last.endsWith("s")) {
160
+ return wordsToKebab(words);
161
+ }
162
+ if (/(x|z|ch|sh)$/i.test(last)) {
163
+ words[lastIndex] = `${last}es`;
164
+ return wordsToKebab(words);
165
+ }
166
+ if (last.endsWith("y") && !/[aeiou]y$/i.test(last)) {
167
+ words[lastIndex] = `${last.slice(0, -1)}ies`;
168
+ return wordsToKebab(words);
169
+ }
170
+
171
+ words[lastIndex] = `${last}s`;
172
+ return wordsToKebab(words);
173
+ }
174
+
175
+ function normalizePathValue(value) {
176
+ return String(value || "")
177
+ .split("/")
178
+ .map((segment) => wordsToKebab(splitTextIntoWords(segment)))
179
+ .filter(Boolean)
180
+ .join("/");
181
+ }
182
+
183
+ function parseTransformSpec(transform) {
184
+ const normalized = String(transform || "").trim();
185
+ if (!normalized) {
186
+ return {
187
+ name: "",
188
+ args: []
189
+ };
190
+ }
191
+
192
+ const match = /^([a-z][a-z0-9-]*)(?:\((.*)\))?$/i.exec(normalized);
193
+ if (!match) {
194
+ return {
195
+ name: "",
196
+ args: []
197
+ };
198
+ }
199
+
200
+ const name = String(match[1] || "").trim().toLowerCase();
201
+ const rawArgs = String(match[2] || "").trim();
202
+ const args = rawArgs
203
+ ? rawArgs.split(",").map((entry) => String(entry || "").trim())
204
+ : [];
205
+
206
+ return {
207
+ name,
208
+ args
209
+ };
210
+ }
211
+
212
+ function applyOptionTransform(value, transform, ownerId, key, optionName) {
213
+ const spec = parseTransformSpec(transform);
214
+ const name = spec.name;
215
+ if (!name) {
216
+ return value;
217
+ }
218
+
219
+ if (name === "trim") {
220
+ return String(value || "").trim();
221
+ }
222
+ if (name === "lower") {
223
+ return String(value || "").toLowerCase();
224
+ }
225
+ if (name === "upper") {
226
+ return String(value || "").toUpperCase();
227
+ }
228
+ if (name === "kebab") {
229
+ return wordsToKebab(splitTextIntoWords(value));
230
+ }
231
+ if (name === "snake") {
232
+ return wordsToSnake(splitTextIntoWords(value));
233
+ }
234
+ if (name === "pascal") {
235
+ return wordsToPascal(splitTextIntoWords(value));
236
+ }
237
+ if (name === "camel") {
238
+ return wordsToCamel(splitTextIntoWords(value));
239
+ }
240
+ if (name === "singular") {
241
+ return toSingularForm(value);
242
+ }
243
+ if (name === "plural") {
244
+ return toPluralForm(value);
245
+ }
246
+ if (name === "path") {
247
+ return normalizePathValue(value);
248
+ }
249
+ if (name === "pathprefix") {
250
+ const normalizedPath = normalizePathValue(value);
251
+ return normalizedPath ? `${normalizedPath}/` : "";
252
+ }
253
+ if (name === "default") {
254
+ const fallback = String(spec.args[0] || "");
255
+ const normalized = String(value || "").trim();
256
+ return normalized ? value : fallback;
257
+ }
258
+ if (name === "prefix") {
259
+ const prefix = String(spec.args[0] || "");
260
+ return `${prefix}${String(value || "")}`;
261
+ }
262
+ if (name === "suffix") {
263
+ const suffix = String(spec.args[0] || "");
264
+ return `${String(value || "")}${suffix}`;
265
+ }
266
+
267
+ throw createCliError(
268
+ `Unknown option transform "${name}" while applying ${ownerId} mutation ${key} (option: ${optionName}).`
269
+ );
270
+ }
271
+
272
+ function applyOptionTransformPipeline(rawValue, rawPipeline, ownerId, key, optionName) {
273
+ let value = String(rawValue || "");
274
+ const pipeline = String(rawPipeline || "")
275
+ .split("|")
276
+ .map((entry) => String(entry || "").trim())
277
+ .filter(Boolean);
278
+
279
+ for (const transform of pipeline) {
280
+ value = applyOptionTransform(value, transform, ownerId, key, optionName);
281
+ }
282
+
283
+ return value;
284
+ }
285
+
286
+ function interpolateOptionValue(rawValue, options, ownerId, key) {
287
+ return String(rawValue || "").replace(OPTION_INTERPOLATION_PATTERN, (_, optionName, rawPipeline = "") => {
288
+ if (Object.prototype.hasOwnProperty.call(options, optionName)) {
289
+ return applyOptionTransformPipeline(String(options[optionName]), rawPipeline, ownerId, key, optionName);
290
+ }
291
+ throw createCliError(
292
+ `Missing required option ${optionName} while applying ${ownerId} mutation ${key}.`
293
+ );
294
+ });
295
+ }
296
+
297
+ function escapeRegExp(value) {
298
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
299
+ }
300
+
301
+ function isSecretOptionInput(optionSchema) {
302
+ const inputType = String(optionSchema?.inputType || "").trim().toLowerCase();
303
+ if (inputType === "password" || inputType === "secret" || inputType === "hidden") {
304
+ return true;
305
+ }
306
+ return optionSchema?.sensitive === true || optionSchema?.secret === true;
307
+ }
308
+
309
+ function createMutedReadlineOutput(stdout) {
310
+ let muted = false;
311
+ const output = new Writable({
312
+ write(chunk, encoding, callback) {
313
+ if (!muted) {
314
+ stdout.write(chunk, encoding);
315
+ }
316
+ callback();
317
+ }
318
+ });
319
+
320
+ return {
321
+ output,
322
+ setMuted(nextMuted) {
323
+ muted = Boolean(nextMuted);
324
+ }
325
+ };
326
+ }
327
+
328
+ async function promptForRequiredOption({
329
+ ownerType,
330
+ ownerId,
331
+ optionName,
332
+ optionSchema,
333
+ stdin = process.stdin,
334
+ stdout = process.stdout
335
+ }) {
336
+ const defaultValue = String(optionSchema?.defaultValue || "").trim();
337
+ const promptLabel = String(optionSchema?.promptLabel || "").trim();
338
+ const promptHint = String(optionSchema?.promptHint || "").trim();
339
+ const required = Boolean(optionSchema?.required);
340
+ const allowEmpty = optionSchema?.allowEmpty === true;
341
+
342
+ if (!stdin?.isTTY || !stdout?.isTTY) {
343
+ if (defaultValue) {
344
+ return defaultValue;
345
+ }
346
+ if (required) {
347
+ if (allowEmpty) {
348
+ return "";
349
+ }
350
+ throw createCliError(
351
+ `${ownerType} ${ownerId} requires option ${optionName}. Non-interactive mode requires --${optionName} <value>.`
352
+ );
353
+ }
354
+ return "";
355
+ }
356
+
357
+ const label = promptLabel || `Select ${optionName} for ${ownerType} ${ownerId}`;
358
+ const defaultHint = defaultValue ? ` [default: ${defaultValue}]` : "";
359
+ const hintSuffix = promptHint ? ` ${promptHint}` : "";
360
+ const promptText = `${label}${defaultHint}${hintSuffix}: `;
361
+
362
+ let answer = "";
363
+
364
+ if (isSecretOptionInput(optionSchema)) {
365
+ const outputController = createMutedReadlineOutput(stdout);
366
+ const rl = createInterface({
367
+ input: stdin,
368
+ output: outputController.output
369
+ });
370
+
371
+ try {
372
+ stdout.write(promptText);
373
+ outputController.setMuted(true);
374
+ answer = String(await rl.question("")).trim();
375
+ } finally {
376
+ outputController.setMuted(false);
377
+ stdout.write("\n");
378
+ rl.close();
379
+ }
380
+ } else {
381
+ const rl = createInterface({
382
+ input: stdin,
383
+ output: stdout
384
+ });
385
+
386
+ try {
387
+ answer = String(await rl.question(promptText)).trim();
388
+ } finally {
389
+ rl.close();
390
+ }
391
+ }
392
+
393
+ if (!answer && defaultValue) {
394
+ return defaultValue;
395
+ }
396
+ if (!answer && required && !allowEmpty) {
397
+ throw createCliError(`${ownerType} ${ownerId} requires option ${optionName}.`);
398
+ }
399
+ return answer || "";
400
+ }
401
+
402
+ export {
403
+ appendTextSnippet,
404
+ escapeRegExp,
405
+ interpolateOptionValue,
406
+ normalizeSkipChecks,
407
+ promptForRequiredOption
408
+ };
@@ -0,0 +1,103 @@
1
+ import { ensureArray, ensureObject } from "./collectionUtils.js";
2
+
3
+ const ANSI_RESET = "\u001b[0m";
4
+ const ANSI_BOLD = "\u001b[1m";
5
+ const ANSI_DIM = "\u001b[2m";
6
+ const ANSI_CYAN = "\u001b[36m";
7
+ const ANSI_GREEN = "\u001b[32m";
8
+ const ANSI_YELLOW = "\u001b[33m";
9
+ const ANSI_WHITE = "\u001b[97m";
10
+
11
+ function createColorFormatter(stream) {
12
+ const noColor = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
13
+ const term = String(process.env.TERM || "").toLowerCase();
14
+ const forceColor = String(process.env.FORCE_COLOR || "").trim();
15
+ const enableColor = (() => {
16
+ if (forceColor === "0") {
17
+ return false;
18
+ }
19
+ if (forceColor) {
20
+ return true;
21
+ }
22
+ if (noColor || term === "dumb") {
23
+ return false;
24
+ }
25
+ return Boolean(stream && stream.isTTY);
26
+ })();
27
+
28
+ const paint = (text, sequence) => {
29
+ const value = String(text);
30
+ if (!enableColor) {
31
+ return value;
32
+ }
33
+ return `${sequence}${value}${ANSI_RESET}`;
34
+ };
35
+
36
+ return Object.freeze({
37
+ heading: (text) => paint(text, `${ANSI_BOLD}${ANSI_WHITE}`),
38
+ emphasis: (text) => paint(text, `${ANSI_BOLD}${ANSI_CYAN}`),
39
+ white: (text) => paint(text, ANSI_WHITE),
40
+ item: (text) => paint(text, ANSI_CYAN),
41
+ version: (text) => paint(text, ANSI_DIM),
42
+ installed: (text) => paint(text, ANSI_GREEN),
43
+ provider: (text) => paint(text, ANSI_YELLOW),
44
+ dim: (text) => paint(text, ANSI_DIM)
45
+ });
46
+ }
47
+
48
+ function resolveWrapWidth(stream, fallbackWidth = 80) {
49
+ const parsedFallback = Number(fallbackWidth);
50
+ const fallback = Number.isFinite(parsedFallback) ? Math.max(20, Math.floor(parsedFallback)) : 80;
51
+ const columns = Number(stream?.columns);
52
+ if (!Number.isFinite(columns) || columns < 20) {
53
+ return fallback;
54
+ }
55
+ return Math.floor(columns);
56
+ }
57
+
58
+ function writeWrappedItems({ stdout, heading, items, lineIndent = " ", wrapWidth = 80 }) {
59
+ const records = ensureArray(items)
60
+ .map((entry) => {
61
+ const normalized = ensureObject(entry);
62
+ const text = String(normalized.text || "").trim();
63
+ const rendered = String(normalized.rendered || text);
64
+ if (!text) {
65
+ return null;
66
+ }
67
+ return { text, rendered };
68
+ })
69
+ .filter(Boolean);
70
+
71
+ if (records.length === 0) {
72
+ return;
73
+ }
74
+
75
+ stdout.write(`${heading}\n`);
76
+ const width = Math.max(20, Number(wrapWidth) || 80);
77
+ let line = lineIndent;
78
+ let lineLength = lineIndent.length;
79
+
80
+ for (const record of records) {
81
+ const separator = lineLength > lineIndent.length ? " " : "";
82
+ const addedLength = separator.length + record.text.length;
83
+ if (lineLength > lineIndent.length && lineLength + addedLength > width) {
84
+ stdout.write(`${line}\n`);
85
+ line = `${lineIndent}${record.rendered}`;
86
+ lineLength = lineIndent.length + record.text.length;
87
+ continue;
88
+ }
89
+
90
+ line = `${line}${separator}${record.rendered}`;
91
+ lineLength += addedLength;
92
+ }
93
+
94
+ if (lineLength > lineIndent.length) {
95
+ stdout.write(`${line}\n`);
96
+ }
97
+ }
98
+
99
+ export {
100
+ createColorFormatter,
101
+ resolveWrapWidth,
102
+ writeWrappedItems
103
+ };
@@ -0,0 +1,106 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createCliError } from "./cliError.js";
7
+
8
+ const CLI_PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
+ const require = createRequire(import.meta.url);
10
+
11
+ function isWorkspaceRoot(candidateRoot) {
12
+ if (!candidateRoot) {
13
+ return false;
14
+ }
15
+ return (
16
+ existsSync(path.join(candidateRoot, "packages")) &&
17
+ existsSync(path.join(candidateRoot, "packages", "kernel")) &&
18
+ existsSync(path.join(candidateRoot, "tooling", "jskit-cli"))
19
+ );
20
+ }
21
+
22
+ function collectAncestorDirectories(startDirectory) {
23
+ const ancestors = [];
24
+ let current = path.resolve(startDirectory);
25
+ while (true) {
26
+ ancestors.push(current);
27
+ const parent = path.dirname(current);
28
+ if (parent === current) {
29
+ break;
30
+ }
31
+ current = parent;
32
+ }
33
+ return ancestors;
34
+ }
35
+
36
+ function resolveWorkspaceRoot() {
37
+ const candidates = [];
38
+ const seen = new Set();
39
+ const appendCandidate = (candidatePath) => {
40
+ const raw = String(candidatePath || "").trim();
41
+ if (!raw) {
42
+ return;
43
+ }
44
+ const absolute = path.resolve(raw);
45
+ if (seen.has(absolute)) {
46
+ return;
47
+ }
48
+ seen.add(absolute);
49
+ candidates.push(absolute);
50
+ };
51
+
52
+ appendCandidate(process.env.JSKIT_REPO_ROOT);
53
+ appendCandidate(path.resolve(CLI_PACKAGE_ROOT, "../.."));
54
+ appendCandidate(CLI_PACKAGE_ROOT);
55
+
56
+ const cwdAncestors = collectAncestorDirectories(process.cwd());
57
+ for (const ancestor of cwdAncestors) {
58
+ appendCandidate(ancestor);
59
+ appendCandidate(path.join(ancestor, "jskit-ai"));
60
+ }
61
+
62
+ for (const candidate of candidates) {
63
+ if (isWorkspaceRoot(candidate)) {
64
+ return candidate;
65
+ }
66
+ }
67
+
68
+ return "";
69
+ }
70
+
71
+ function resolveCatalogPackagesPath() {
72
+ const explicitPath = String(process.env.JSKIT_CATALOG_PACKAGES_PATH || "").trim();
73
+ if (explicitPath) {
74
+ return path.resolve(explicitPath);
75
+ }
76
+
77
+ let catalogPackageJsonPath = "";
78
+ try {
79
+ catalogPackageJsonPath = require.resolve("@jskit-ai/jskit-catalog/package.json");
80
+ } catch {}
81
+ if (catalogPackageJsonPath) {
82
+ return path.join(path.dirname(catalogPackageJsonPath), "catalog", "packages.json");
83
+ }
84
+
85
+ const workspaceCatalogPath = path.resolve(CLI_PACKAGE_ROOT, "../jskit-catalog/catalog/packages.json");
86
+ if (existsSync(workspaceCatalogPath)) {
87
+ return workspaceCatalogPath;
88
+ }
89
+
90
+ throw createCliError(
91
+ "Unable to resolve @jskit-ai/jskit-catalog. Install it alongside @jskit-ai/jskit-cli or set JSKIT_CATALOG_PACKAGES_PATH."
92
+ );
93
+ }
94
+
95
+ const WORKSPACE_ROOT = resolveWorkspaceRoot();
96
+ const MODULES_ROOT = WORKSPACE_ROOT ? path.join(WORKSPACE_ROOT, "packages") : "";
97
+ const BUNDLES_ROOT = path.join(CLI_PACKAGE_ROOT, "bundles");
98
+ const CATALOG_PACKAGES_PATH = resolveCatalogPackagesPath();
99
+
100
+ export {
101
+ CLI_PACKAGE_ROOT,
102
+ WORKSPACE_ROOT,
103
+ MODULES_ROOT,
104
+ BUNDLES_ROOT,
105
+ CATALOG_PACKAGES_PATH
106
+ };