@nexical/cli 0.11.18 → 0.11.20
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/dist/chunk-54HY52LH.js +38 -0
- package/dist/chunk-54HY52LH.js.map +1 -0
- package/dist/index.js +5 -20
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.js +7 -7
- package/dist/src/commands/module/add.js +5 -5
- package/dist/src/commands/prompt.js +1 -1
- package/dist/src/commands/prompt.js.map +1 -1
- package/dist/src/utils/filter.d.ts +9 -0
- package/dist/src/utils/filter.js +9 -0
- package/dist/src/utils/filter.js.map +1 -0
- package/index.ts +2 -31
- package/package.json +1 -1
- package/src/commands/prompt.ts +1 -1
- package/src/utils/filter.ts +47 -0
- package/test/integration/commands/prompt.integration.test.ts +110 -0
- package/test/unit/commands/prompt.test.ts +273 -0
- package/test/unit/utils/filter.test.ts +40 -0
- package/vitest.config.ts +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
init_esm_shims
|
|
4
|
+
} from "./chunk-OYFWMYPG.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/filter.ts
|
|
7
|
+
init_esm_shims();
|
|
8
|
+
import path from "path";
|
|
9
|
+
function filterCommandDirectories(additionalCommands, coreCommandsDir) {
|
|
10
|
+
return additionalCommands.filter((dir) => {
|
|
11
|
+
const resolvedDir = path.resolve(dir);
|
|
12
|
+
const resolvedCore = path.resolve(coreCommandsDir);
|
|
13
|
+
if (resolvedDir === resolvedCore) return false;
|
|
14
|
+
const coreSuffix = path.join("@nexical", "cli", "dist", "src", "commands");
|
|
15
|
+
const coreSuffixSrc = path.join("packages", "cli", "dist", "src", "commands");
|
|
16
|
+
const coreSuffixRawSrc = path.join("packages", "cli", "src", "commands");
|
|
17
|
+
if (resolvedDir.endsWith(coreSuffix) || resolvedDir.endsWith(coreSuffixSrc) || resolvedDir.endsWith(coreSuffixRawSrc)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const distSuffix = path.join("dist", "src", "commands");
|
|
21
|
+
const srcSuffix = path.join("src", "commands");
|
|
22
|
+
if (resolvedCore.endsWith(distSuffix)) {
|
|
23
|
+
const baseDir = resolvedCore.substring(0, resolvedCore.length - distSuffix.length);
|
|
24
|
+
const srcVersion = path.join(baseDir, srcSuffix);
|
|
25
|
+
const normalizedDir = path.normalize(resolvedDir);
|
|
26
|
+
const normalizedSrc = path.normalize(srcVersion);
|
|
27
|
+
if (normalizedDir === normalizedSrc) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
filterCommandDirectories
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=chunk-54HY52LH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/filter.ts"],"sourcesContent":["import path from 'node:path';\n\n/**\n * Filters out duplicate core commands and source versions from additional command directories.\n * @param additionalCommands List of discovered command directories.\n * @param coreCommandsDir The primary core commands directory.\n * @returns Filtered list of additional command directories.\n */\nexport function filterCommandDirectories(\n additionalCommands: string[],\n coreCommandsDir: string,\n): string[] {\n return additionalCommands.filter((dir) => {\n const resolvedDir = path.resolve(dir);\n const resolvedCore = path.resolve(coreCommandsDir);\n\n if (resolvedDir === resolvedCore) return false;\n\n // Check if this is another instance of the core CLI commands (by checking path suffix)\n const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');\n const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');\n const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');\n\n if (\n resolvedDir.endsWith(coreSuffix) ||\n resolvedDir.endsWith(coreSuffixSrc) ||\n resolvedDir.endsWith(coreSuffixRawSrc)\n ) {\n return false;\n }\n\n // Handle mismatch between dist/src and src/\n const distSuffix = path.join('dist', 'src', 'commands');\n const srcSuffix = path.join('src', 'commands');\n if (resolvedCore.endsWith(distSuffix)) {\n const baseDir = resolvedCore.substring(0, resolvedCore.length - distSuffix.length);\n const srcVersion = path.join(baseDir, srcSuffix);\n const normalizedDir = path.normalize(resolvedDir);\n const normalizedSrc = path.normalize(srcVersion);\n if (normalizedDir === normalizedSrc) {\n return false;\n }\n }\n\n return true;\n });\n}\n"],"mappings":";;;;;;AAAA;AAAA,OAAO,UAAU;AAQV,SAAS,yBACd,oBACA,iBACU;AACV,SAAO,mBAAmB,OAAO,CAAC,QAAQ;AACxC,UAAM,cAAc,KAAK,QAAQ,GAAG;AACpC,UAAM,eAAe,KAAK,QAAQ,eAAe;AAEjD,QAAI,gBAAgB,aAAc,QAAO;AAGzC,UAAM,aAAa,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AACzE,UAAM,gBAAgB,KAAK,KAAK,YAAY,OAAO,QAAQ,OAAO,UAAU;AAC5E,UAAM,mBAAmB,KAAK,KAAK,YAAY,OAAO,OAAO,UAAU;AAEvE,QACE,YAAY,SAAS,UAAU,KAC/B,YAAY,SAAS,aAAa,KAClC,YAAY,SAAS,gBAAgB,GACrC;AACA,aAAO;AAAA,IACT;AAGA,UAAM,aAAa,KAAK,KAAK,QAAQ,OAAO,UAAU;AACtD,UAAM,YAAY,KAAK,KAAK,OAAO,UAAU;AAC7C,QAAI,aAAa,SAAS,UAAU,GAAG;AACrC,YAAM,UAAU,aAAa,UAAU,GAAG,aAAa,SAAS,WAAW,MAAM;AACjF,YAAM,aAAa,KAAK,KAAK,SAAS,SAAS;AAC/C,YAAM,gBAAgB,KAAK,UAAU,WAAW;AAChD,YAAM,gBAAgB,KAAK,UAAU,UAAU;AAC/C,UAAI,kBAAkB,eAAe;AACnC,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT,CAAC;AACH;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@ import { createRequire } from "module"; const require = createRequire(import.met
|
|
|
3
3
|
import {
|
|
4
4
|
discoverCommandDirectories
|
|
5
5
|
} from "./chunk-AC4B3HPJ.js";
|
|
6
|
+
import {
|
|
7
|
+
filterCommandDirectories
|
|
8
|
+
} from "./chunk-54HY52LH.js";
|
|
6
9
|
import {
|
|
7
10
|
init_esm_shims
|
|
8
11
|
} from "./chunk-OYFWMYPG.js";
|
|
@@ -15,7 +18,7 @@ import { fileURLToPath } from "url";
|
|
|
15
18
|
// package.json
|
|
16
19
|
var package_default = {
|
|
17
20
|
name: "@nexical/cli",
|
|
18
|
-
version: "0.11.
|
|
21
|
+
version: "0.11.20",
|
|
19
22
|
license: "Apache-2.0",
|
|
20
23
|
type: "module",
|
|
21
24
|
bin: {
|
|
@@ -87,25 +90,7 @@ var commandName = "nexical";
|
|
|
87
90
|
var projectRoot = await findProjectRoot(commandName, process.cwd()) || process.cwd();
|
|
88
91
|
var coreCommandsDir = path.resolve(__dirname, "./src/commands");
|
|
89
92
|
var additionalCommands = discoverCommandDirectories(projectRoot);
|
|
90
|
-
var filteredAdditional = additionalCommands
|
|
91
|
-
const resolvedDir = path.resolve(dir);
|
|
92
|
-
const resolvedCore = path.resolve(coreCommandsDir);
|
|
93
|
-
if (resolvedDir === resolvedCore) return false;
|
|
94
|
-
const coreSuffix = path.join("@nexical", "cli", "dist", "src", "commands");
|
|
95
|
-
const coreSuffixSrc = path.join("packages", "cli", "dist", "src", "commands");
|
|
96
|
-
const coreSuffixRawSrc = path.join("packages", "cli", "src", "commands");
|
|
97
|
-
if (resolvedDir.endsWith(coreSuffix) || resolvedDir.endsWith(coreSuffixSrc) || resolvedDir.endsWith(coreSuffixRawSrc)) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
if (resolvedCore.includes(path.join(path.sep, "dist", "src", "commands"))) {
|
|
101
|
-
const srcVersion = resolvedCore.replace(
|
|
102
|
-
path.join(path.sep, "dist", "src", "commands"),
|
|
103
|
-
path.join(path.sep, "src", "commands")
|
|
104
|
-
);
|
|
105
|
-
if (resolvedDir === srcVersion) return false;
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
108
|
-
});
|
|
93
|
+
var filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);
|
|
109
94
|
var app = new CLI({
|
|
110
95
|
version: package_default.version,
|
|
111
96
|
commandName,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\
|
|
1
|
+
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\nimport { filterCommandDirectories } from './src/utils/filter.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\nconst filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.11.20\",\n \"license\": \"Apache-2.0\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"test\": \"npm run test:unit && npm run test:integration && npm run test:e2e\",\n \"test:unit\": \"vitest run --config vitest.config.ts --coverage\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"test:e2e\": \"npm run build && vitest run --config vitest.e2e.config.ts\",\n \"test:watch\": \"vitest\",\n \"format\": \"prettier --write .\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"prepare\": \"husky\"\n },\n \"lint-staged\": {\n \"**/*\": [\n \"prettier --write --ignore-unknown\"\n ],\n \"**/*.{js,jsx,ts,tsx,astro}\": [\n \"eslint --fix\"\n ]\n },\n \"dependencies\": {\n \"@nexical/ai\": \"^0.1.5\",\n \"@nexical/cli-core\": \"^0.1.16\",\n \"dotenv\": \"^17.3.1\",\n \"fast-glob\": \"^3.3.3\",\n \"glob\": \"^13.0.5\",\n \"jiti\": \"^2.6.1\",\n \"minimist\": \"^1.2.8\",\n \"yaml\": \"^2.8.2\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.2\",\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/minimist\": \"^1.2.5\",\n \"@types/node\": \"^25.3.0\",\n \"@types/nunjucks\": \"^3.2.6\",\n \"@vitest/coverage-v8\": \"^4.0.18\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.6.0\",\n \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.0.1\",\n \"execa\": \"^9.6.1\",\n \"fs-extra\": \"^11.3.3\",\n \"globals\": \"^17.3.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\",\n \"vitest\": \"^4.0.18\"\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,KAAO;AAAA,IACL,SAAW;AAAA,EACb;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,KAAO;AAAA,IACP,OAAS;AAAA,IACT,MAAQ;AAAA,IACR,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAU;AAAA,IACV,MAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAW;AAAA,EACb;AAAA,EACA,eAAe;AAAA,IACb,QAAQ;AAAA,MACN;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB,QAAU;AAAA,IACV,aAAa;AAAA,IACb,MAAQ;AAAA,IACR,MAAQ;AAAA,IACR,UAAY;AAAA,IACZ,MAAQ;AAAA,EACV;AAAA,EACA,iBAAmB;AAAA,IACjB,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,mBAAmB;AAAA,IACnB,uBAAuB;AAAA,IACvB,QAAU;AAAA,IACV,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,6BAA6B;AAAA,IAC7B,OAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAW;AAAA,IACX,OAAS;AAAA,IACT,eAAe;AAAA,IACf,UAAY;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,qBAAqB;AAAA,IACrB,QAAU;AAAA,EACZ;AACF;;;AD5DA,OAAO,UAAU;AAGjB,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAM,cAAc;AACpB,IAAM,cAAe,MAAM,gBAAgB,aAAa,QAAQ,IAAI,CAAC,KAAM,QAAQ,IAAI;AACvF,IAAM,kBAAkB,KAAK,QAAQ,WAAW,gBAAgB;AAChE,IAAM,qBAAqB,2BAA2B,WAAW;AAEjE,IAAM,qBAAqB,yBAAyB,oBAAoB,eAAe;AAEvF,IAAM,MAAM,IAAI,IAAI;AAAA,EAClB,SAAS,gBAAI;AAAA,EACb;AAAA,EACA,mBAAmB,CAAC,GAAG,oBAAI,IAAI,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC;AAC1E,CAAC;AACD,IAAI,MAAM;","names":[]}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
addAll,
|
|
4
|
+
clone,
|
|
5
|
+
commit,
|
|
6
|
+
renameRemote,
|
|
7
|
+
updateSubmodules
|
|
8
|
+
} from "../../chunk-GEESHGE4.js";
|
|
2
9
|
import {
|
|
3
10
|
resolveGitUrl
|
|
4
11
|
} from "../../chunk-PJIOCW2A.js";
|
|
@@ -8,13 +15,6 @@ import {
|
|
|
8
15
|
import {
|
|
9
16
|
require_lib
|
|
10
17
|
} from "../../chunk-OUGA4CB4.js";
|
|
11
|
-
import {
|
|
12
|
-
addAll,
|
|
13
|
-
clone,
|
|
14
|
-
commit,
|
|
15
|
-
renameRemote,
|
|
16
|
-
updateSubmodules
|
|
17
|
-
} from "../../chunk-GEESHGE4.js";
|
|
18
18
|
import {
|
|
19
19
|
__toESM,
|
|
20
20
|
init_esm_shims
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
addSubmodule,
|
|
4
|
+
clone,
|
|
5
|
+
getRemoteUrl
|
|
6
|
+
} from "../../../chunk-GEESHGE4.js";
|
|
2
7
|
import {
|
|
3
8
|
resolveGitUrl
|
|
4
9
|
} from "../../../chunk-PJIOCW2A.js";
|
|
5
10
|
import {
|
|
6
11
|
require_lib
|
|
7
12
|
} from "../../../chunk-OUGA4CB4.js";
|
|
8
|
-
import {
|
|
9
|
-
addSubmodule,
|
|
10
|
-
clone,
|
|
11
|
-
getRemoteUrl
|
|
12
|
-
} from "../../../chunk-GEESHGE4.js";
|
|
13
13
|
import {
|
|
14
14
|
__toESM,
|
|
15
15
|
init_esm_shims
|
|
@@ -49,7 +49,7 @@ var PromptCommand = class extends BaseCommand {
|
|
|
49
49
|
const projectRoot = this.projectRoot;
|
|
50
50
|
const promptName = options.promptName;
|
|
51
51
|
const argv = minimist(options.args || []);
|
|
52
|
-
const isInteractive = options.interactive || options.i || argv.interactive || argv.i;
|
|
52
|
+
const isInteractive = !!(options.interactive || options.i || argv.interactive || argv.i);
|
|
53
53
|
const moduleName = options.module || options.m || argv.module || argv.m;
|
|
54
54
|
const modelsArg = options.models || argv.models || "gemini-3-flash-preview,gemini-3-pro-preview";
|
|
55
55
|
const models = modelsArg.split(",").map((m) => m.trim()).filter(Boolean);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/prompt.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport minimist from 'minimist';\nimport YAML from 'yaml';\nimport { PromptRunner } from '@nexical/ai';\n\nexport default class PromptCommand extends BaseCommand {\n static usage = 'prompt <prompt-name> [args...]';\n static description = 'Run an AI prompt using templates from the prompts directory.';\n static requiresProject = true;\n\n static args: CommandDefinition = {\n args: [\n {\n name: 'promptName',\n required: true,\n description: 'The name of the markdown file in the prompts directory.',\n },\n {\n name: 'args...',\n required: false,\n description: 'Additional arguments for the template and command',\n },\n ],\n options: [\n {\n name: '--module, -m <module>',\n description:\n 'Target a specific module (searches apps/frontend/modules and apps/backend/modules)',\n },\n { name: '--interactive, -i', description: 'Run in interactive chat mode' },\n {\n name: '--models <models>',\n description: 'Comma-separated list of models to try',\n default: 'gemini-3-flash-preview,gemini-3-pro-preview',\n },\n ],\n };\n\n async run(options: {\n promptName: string;\n args?: string[];\n module?: string;\n m?: string;\n interactive?: boolean;\n i?: boolean;\n models?: string;\n }) {\n const projectRoot = this.projectRoot as string;\n const promptName = options.promptName;\n\n // Parse additional template flags\n const argv = minimist(options.args || []);\n const isInteractive = options.interactive || options.i || argv.interactive || argv.i;\n const moduleName = options.module || options.m || argv.module || argv.m;\n const modelsArg =\n options.models || argv.models || 'gemini-3-flash-preview,gemini-3-pro-preview';\n const models = modelsArg\n .split(',')\n .map((m: string) => m.trim())\n .filter(Boolean);\n\n const PROMPTS_DIRS = [path.join(projectRoot, 'prompts')];\n const generatorAgentsPrompts = path.join(projectRoot, 'packages/generator/prompts/agents');\n\n if (await fs.pathExists(generatorAgentsPrompts)) {\n PROMPTS_DIRS.push(generatorAgentsPrompts);\n }\n\n // Module Resolution Logic\n const contextVars = { ...argv };\n if (moduleName) {\n const frontendPath = path.join(projectRoot, 'apps/frontend/modules', moduleName);\n const backendPath = path.join(projectRoot, 'apps/backend/modules', moduleName);\n\n let moduleRoot: string | undefined;\n let moduleType: 'frontend' | 'backend' | undefined;\n\n if (await fs.pathExists(frontendPath)) {\n moduleRoot = frontendPath;\n moduleType = 'frontend';\n } else if (await fs.pathExists(backendPath)) {\n moduleRoot = backendPath;\n moduleType = 'backend';\n }\n\n if (!moduleRoot) {\n this.error(\n `Module '${moduleName}' not found in apps/frontend/modules or apps/backend/modules.`,\n );\n return;\n }\n\n logger.debug(`[Context] Targeting ${moduleType} module: ${moduleName}`);\n contextVars.module_root = moduleRoot;\n contextVars.module_name = moduleName;\n contextVars.module_type = moduleType;\n contextVars.root_path = moduleRoot + '/';\n } else {\n if (!contextVars.root_path) {\n contextVars.root_path = process.cwd() + '/';\n }\n }\n\n // Extract AI configuration from nexical.yaml\n const configPath = path.join(projectRoot, 'nexical.yaml');\n let aiConfig: Record<string, unknown> = {};\n if (await fs.pathExists(configPath)) {\n try {\n const content = await fs.readFile(configPath, 'utf8');\n const config = YAML.parse(content) || {};\n aiConfig = (config.ai as Record<string, unknown>) || {};\n } catch {\n logger.warn('Failed to parse nexical.yaml AI config, using defaults.');\n }\n }\n\n const finalCode = await PromptRunner.run({\n promptName,\n promptDirs: PROMPTS_DIRS,\n args: contextVars,\n aiConfig,\n models,\n interactive: isInteractive as boolean,\n });\n\n if (finalCode !== 0) {\n process.exit(finalCode);\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAAA;AACA,sBAAe;AADf,SAAiC,aAAa,cAAc;AAE5D,OAAO,UAAU;AACjB,OAAO,cAAc;AACrB,OAAO,UAAU;AACjB,SAAS,oBAAoB;AAE7B,IAAqB,gBAArB,cAA2C,YAAY;AAAA,EACrD,OAAO,QAAQ;AAAA,EACf,OAAO,cAAc;AAAA,EACrB,OAAO,kBAAkB;AAAA,EAEzB,OAAO,OAA0B;AAAA,IAC/B,MAAM;AAAA,MACJ;AAAA,QACE,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,aACE;AAAA,MACJ;AAAA,MACA,EAAE,MAAM,qBAAqB,aAAa,+BAA+B;AAAA,MACzE;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,QACb,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,SAQP;AACD,UAAM,cAAc,KAAK;AACzB,UAAM,aAAa,QAAQ;AAG3B,UAAM,OAAO,SAAS,QAAQ,QAAQ,CAAC,CAAC;AACxC,UAAM,gBAAgB,QAAQ,eAAe,QAAQ,KAAK,KAAK,eAAe,KAAK;
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/prompt.ts"],"sourcesContent":["import { type CommandDefinition, BaseCommand, logger } from '@nexical/cli-core';\nimport fs from 'fs-extra';\nimport path from 'path';\nimport minimist from 'minimist';\nimport YAML from 'yaml';\nimport { PromptRunner } from '@nexical/ai';\n\nexport default class PromptCommand extends BaseCommand {\n static usage = 'prompt <prompt-name> [args...]';\n static description = 'Run an AI prompt using templates from the prompts directory.';\n static requiresProject = true;\n\n static args: CommandDefinition = {\n args: [\n {\n name: 'promptName',\n required: true,\n description: 'The name of the markdown file in the prompts directory.',\n },\n {\n name: 'args...',\n required: false,\n description: 'Additional arguments for the template and command',\n },\n ],\n options: [\n {\n name: '--module, -m <module>',\n description:\n 'Target a specific module (searches apps/frontend/modules and apps/backend/modules)',\n },\n { name: '--interactive, -i', description: 'Run in interactive chat mode' },\n {\n name: '--models <models>',\n description: 'Comma-separated list of models to try',\n default: 'gemini-3-flash-preview,gemini-3-pro-preview',\n },\n ],\n };\n\n async run(options: {\n promptName: string;\n args?: string[];\n module?: string;\n m?: string;\n interactive?: boolean;\n i?: boolean;\n models?: string;\n }) {\n const projectRoot = this.projectRoot as string;\n const promptName = options.promptName;\n\n // Parse additional template flags\n const argv = minimist(options.args || []);\n const isInteractive = !!(options.interactive || options.i || argv.interactive || argv.i);\n const moduleName = options.module || options.m || argv.module || argv.m;\n const modelsArg =\n options.models || argv.models || 'gemini-3-flash-preview,gemini-3-pro-preview';\n const models = modelsArg\n .split(',')\n .map((m: string) => m.trim())\n .filter(Boolean);\n\n const PROMPTS_DIRS = [path.join(projectRoot, 'prompts')];\n const generatorAgentsPrompts = path.join(projectRoot, 'packages/generator/prompts/agents');\n\n if (await fs.pathExists(generatorAgentsPrompts)) {\n PROMPTS_DIRS.push(generatorAgentsPrompts);\n }\n\n // Module Resolution Logic\n const contextVars = { ...argv };\n if (moduleName) {\n const frontendPath = path.join(projectRoot, 'apps/frontend/modules', moduleName);\n const backendPath = path.join(projectRoot, 'apps/backend/modules', moduleName);\n\n let moduleRoot: string | undefined;\n let moduleType: 'frontend' | 'backend' | undefined;\n\n if (await fs.pathExists(frontendPath)) {\n moduleRoot = frontendPath;\n moduleType = 'frontend';\n } else if (await fs.pathExists(backendPath)) {\n moduleRoot = backendPath;\n moduleType = 'backend';\n }\n\n if (!moduleRoot) {\n this.error(\n `Module '${moduleName}' not found in apps/frontend/modules or apps/backend/modules.`,\n );\n return;\n }\n\n logger.debug(`[Context] Targeting ${moduleType} module: ${moduleName}`);\n contextVars.module_root = moduleRoot;\n contextVars.module_name = moduleName;\n contextVars.module_type = moduleType;\n contextVars.root_path = moduleRoot + '/';\n } else {\n if (!contextVars.root_path) {\n contextVars.root_path = process.cwd() + '/';\n }\n }\n\n // Extract AI configuration from nexical.yaml\n const configPath = path.join(projectRoot, 'nexical.yaml');\n let aiConfig: Record<string, unknown> = {};\n if (await fs.pathExists(configPath)) {\n try {\n const content = await fs.readFile(configPath, 'utf8');\n const config = YAML.parse(content) || {};\n aiConfig = (config.ai as Record<string, unknown>) || {};\n } catch {\n logger.warn('Failed to parse nexical.yaml AI config, using defaults.');\n }\n }\n\n const finalCode = await PromptRunner.run({\n promptName,\n promptDirs: PROMPTS_DIRS,\n args: contextVars,\n aiConfig,\n models,\n interactive: isInteractive as boolean,\n });\n\n if (finalCode !== 0) {\n process.exit(finalCode);\n }\n }\n}\n"],"mappings":"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","names":["fs"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters out duplicate core commands and source versions from additional command directories.
|
|
3
|
+
* @param additionalCommands List of discovered command directories.
|
|
4
|
+
* @param coreCommandsDir The primary core commands directory.
|
|
5
|
+
* @returns Filtered list of additional command directories.
|
|
6
|
+
*/
|
|
7
|
+
declare function filterCommandDirectories(additionalCommands: string[], coreCommandsDir: string): string[];
|
|
8
|
+
|
|
9
|
+
export { filterCommandDirectories };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
filterCommandDirectories
|
|
4
|
+
} from "../../chunk-54HY52LH.js";
|
|
5
|
+
import "../../chunk-OYFWMYPG.js";
|
|
6
|
+
export {
|
|
7
|
+
filterCommandDirectories
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { discoverCommandDirectories } from './src/utils/discovery.js';
|
|
5
5
|
import pkg from './package.json';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { filterCommandDirectories } from './src/utils/filter.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
@@ -12,37 +13,7 @@ const projectRoot = (await findProjectRoot(commandName, process.cwd())) || proce
|
|
|
12
13
|
const coreCommandsDir = path.resolve(__dirname, './src/commands');
|
|
13
14
|
const additionalCommands = discoverCommandDirectories(projectRoot);
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
const filteredAdditional = additionalCommands.filter((dir) => {
|
|
17
|
-
const resolvedDir = path.resolve(dir);
|
|
18
|
-
const resolvedCore = path.resolve(coreCommandsDir);
|
|
19
|
-
|
|
20
|
-
if (resolvedDir === resolvedCore) return false;
|
|
21
|
-
|
|
22
|
-
// Check if this is another instance of the core CLI commands (by checking path suffix)
|
|
23
|
-
const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');
|
|
24
|
-
const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');
|
|
25
|
-
const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
resolvedDir.endsWith(coreSuffix) ||
|
|
29
|
-
resolvedDir.endsWith(coreSuffixSrc) ||
|
|
30
|
-
resolvedDir.endsWith(coreSuffixRawSrc)
|
|
31
|
-
) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Handle mismatch between dist/src and src/
|
|
36
|
-
if (resolvedCore.includes(path.join(path.sep, 'dist', 'src', 'commands'))) {
|
|
37
|
-
const srcVersion = resolvedCore.replace(
|
|
38
|
-
path.join(path.sep, 'dist', 'src', 'commands'),
|
|
39
|
-
path.join(path.sep, 'src', 'commands'),
|
|
40
|
-
);
|
|
41
|
-
if (resolvedDir === srcVersion) return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return true;
|
|
45
|
-
});
|
|
16
|
+
const filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);
|
|
46
17
|
|
|
47
18
|
const app = new CLI({
|
|
48
19
|
version: pkg.version,
|
package/package.json
CHANGED
package/src/commands/prompt.ts
CHANGED
|
@@ -52,7 +52,7 @@ export default class PromptCommand extends BaseCommand {
|
|
|
52
52
|
|
|
53
53
|
// Parse additional template flags
|
|
54
54
|
const argv = minimist(options.args || []);
|
|
55
|
-
const isInteractive = options.interactive || options.i || argv.interactive || argv.i;
|
|
55
|
+
const isInteractive = !!(options.interactive || options.i || argv.interactive || argv.i);
|
|
56
56
|
const moduleName = options.module || options.m || argv.module || argv.m;
|
|
57
57
|
const modelsArg =
|
|
58
58
|
options.models || argv.models || 'gemini-3-flash-preview,gemini-3-pro-preview';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filters out duplicate core commands and source versions from additional command directories.
|
|
5
|
+
* @param additionalCommands List of discovered command directories.
|
|
6
|
+
* @param coreCommandsDir The primary core commands directory.
|
|
7
|
+
* @returns Filtered list of additional command directories.
|
|
8
|
+
*/
|
|
9
|
+
export function filterCommandDirectories(
|
|
10
|
+
additionalCommands: string[],
|
|
11
|
+
coreCommandsDir: string,
|
|
12
|
+
): string[] {
|
|
13
|
+
return additionalCommands.filter((dir) => {
|
|
14
|
+
const resolvedDir = path.resolve(dir);
|
|
15
|
+
const resolvedCore = path.resolve(coreCommandsDir);
|
|
16
|
+
|
|
17
|
+
if (resolvedDir === resolvedCore) return false;
|
|
18
|
+
|
|
19
|
+
// Check if this is another instance of the core CLI commands (by checking path suffix)
|
|
20
|
+
const coreSuffix = path.join('@nexical', 'cli', 'dist', 'src', 'commands');
|
|
21
|
+
const coreSuffixSrc = path.join('packages', 'cli', 'dist', 'src', 'commands');
|
|
22
|
+
const coreSuffixRawSrc = path.join('packages', 'cli', 'src', 'commands');
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
resolvedDir.endsWith(coreSuffix) ||
|
|
26
|
+
resolvedDir.endsWith(coreSuffixSrc) ||
|
|
27
|
+
resolvedDir.endsWith(coreSuffixRawSrc)
|
|
28
|
+
) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle mismatch between dist/src and src/
|
|
33
|
+
const distSuffix = path.join('dist', 'src', 'commands');
|
|
34
|
+
const srcSuffix = path.join('src', 'commands');
|
|
35
|
+
if (resolvedCore.endsWith(distSuffix)) {
|
|
36
|
+
const baseDir = resolvedCore.substring(0, resolvedCore.length - distSuffix.length);
|
|
37
|
+
const srcVersion = path.join(baseDir, srcSuffix);
|
|
38
|
+
const normalizedDir = path.normalize(resolvedDir);
|
|
39
|
+
const normalizedSrc = path.normalize(srcVersion);
|
|
40
|
+
if (normalizedDir === normalizedSrc) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import PromptCommand from '../../../src/commands/prompt.js';
|
|
3
|
+
import { createTempDir, createMockRepo, cleanupTestRoot } from '../../utils/integration-helpers.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { CLI } from '@nexical/cli-core';
|
|
7
|
+
import { PromptRunner } from '@nexical/ai';
|
|
8
|
+
|
|
9
|
+
vi.mock('@nexical/ai', () => ({
|
|
10
|
+
PromptRunner: {
|
|
11
|
+
run: vi.fn().mockResolvedValue(0),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('Prompt Command Integration', () => {
|
|
16
|
+
let projectDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
const temp = await createTempDir('prompt-integration-');
|
|
20
|
+
projectDir = await createMockRepo(temp, {
|
|
21
|
+
'package.json': '{"name": "prompt-project", "version": "1.0.0"}',
|
|
22
|
+
'nexical.yaml': 'ai:\n provider: vertex',
|
|
23
|
+
'prompts/test-prompt.md': 'Testing {{ name }}',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await cleanupTestRoot();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should resolve project config and call PromptRunner', async () => {
|
|
32
|
+
const originalCwd = process.cwd();
|
|
33
|
+
try {
|
|
34
|
+
process.chdir(projectDir);
|
|
35
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
36
|
+
const command = new PromptCommand(cli);
|
|
37
|
+
|
|
38
|
+
// We need to set projectRoot manually because BaseCommand detection
|
|
39
|
+
// might fail if not properly initialized in test env
|
|
40
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
41
|
+
|
|
42
|
+
await command.run({ promptName: 'test-prompt' });
|
|
43
|
+
|
|
44
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
promptName: 'test-prompt',
|
|
47
|
+
promptDirs: [path.join(projectDir, 'prompts')],
|
|
48
|
+
aiConfig: { provider: 'vertex' },
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
} finally {
|
|
52
|
+
process.chdir(originalCwd);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should resolve module context in integration', async () => {
|
|
57
|
+
const originalCwd = process.cwd();
|
|
58
|
+
try {
|
|
59
|
+
process.chdir(projectDir);
|
|
60
|
+
|
|
61
|
+
const moduleDir = path.join(projectDir, 'apps/frontend/modules/my-mod');
|
|
62
|
+
await fs.ensureDir(moduleDir);
|
|
63
|
+
|
|
64
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
65
|
+
const command = new PromptCommand(cli);
|
|
66
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
67
|
+
|
|
68
|
+
await command.run({ promptName: 'test-prompt', module: 'my-mod' });
|
|
69
|
+
|
|
70
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
args: expect.objectContaining({
|
|
73
|
+
module_name: 'my-mod',
|
|
74
|
+
module_type: 'frontend',
|
|
75
|
+
module_root: moduleDir,
|
|
76
|
+
}),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
process.chdir(originalCwd);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should include generator agents prompts if they exist in integration', async () => {
|
|
85
|
+
const originalCwd = process.cwd();
|
|
86
|
+
try {
|
|
87
|
+
process.chdir(projectDir);
|
|
88
|
+
|
|
89
|
+
const generatorPromptsDir = path.join(projectDir, 'packages/generator/prompts/agents');
|
|
90
|
+
await fs.ensureDir(generatorPromptsDir);
|
|
91
|
+
|
|
92
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
93
|
+
const command = new PromptCommand(cli);
|
|
94
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectDir;
|
|
95
|
+
|
|
96
|
+
await command.run({ promptName: 'test-prompt' });
|
|
97
|
+
|
|
98
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
99
|
+
expect.objectContaining({
|
|
100
|
+
promptDirs: expect.arrayContaining([
|
|
101
|
+
path.join(projectDir, 'prompts'),
|
|
102
|
+
generatorPromptsDir,
|
|
103
|
+
]),
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
process.chdir(originalCwd);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import PromptCommand from '../../../src/commands/prompt.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
import { PromptRunner } from '@nexical/ai';
|
|
7
|
+
import { logger } from '@nexical/cli-core';
|
|
8
|
+
|
|
9
|
+
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
10
|
+
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
11
|
+
return {
|
|
12
|
+
...mod,
|
|
13
|
+
logger: {
|
|
14
|
+
code: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
success: vi.fn(),
|
|
18
|
+
info: vi.fn(),
|
|
19
|
+
warn: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('fs-extra');
|
|
25
|
+
vi.mock('yaml');
|
|
26
|
+
vi.mock('@nexical/ai');
|
|
27
|
+
|
|
28
|
+
describe('PromptCommand', () => {
|
|
29
|
+
let command: PromptCommand;
|
|
30
|
+
const projectRoot = '/test/project';
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
command = new PromptCommand({});
|
|
35
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectRoot;
|
|
36
|
+
|
|
37
|
+
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
38
|
+
|
|
39
|
+
// Default fs mocks
|
|
40
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockResolvedValue(false);
|
|
41
|
+
vi.mocked(
|
|
42
|
+
fs.readFile as unknown as (p: string, e: string) => Promise<string>,
|
|
43
|
+
).mockResolvedValue('');
|
|
44
|
+
|
|
45
|
+
// Default PromptRunner mock
|
|
46
|
+
vi.mocked(PromptRunner.run).mockResolvedValue(0);
|
|
47
|
+
|
|
48
|
+
// Mock process.exit
|
|
49
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
50
|
+
throw new Error(`Process.exit(${code})`);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.resetAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should have correct metadata', () => {
|
|
59
|
+
expect(PromptCommand.usage).toBeDefined();
|
|
60
|
+
expect(PromptCommand.description).toBeDefined();
|
|
61
|
+
expect(PromptCommand.requiresProject).toBe(true);
|
|
62
|
+
expect(PromptCommand.args).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should run prompt with default options', async () => {
|
|
66
|
+
await command.run({ promptName: 'test-prompt' });
|
|
67
|
+
|
|
68
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
promptName: 'test-prompt',
|
|
71
|
+
models: ['gemini-3-flash-preview', 'gemini-3-pro-preview'],
|
|
72
|
+
interactive: false,
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle interactive flag', async () => {
|
|
78
|
+
await command.run({ promptName: 'test-prompt', interactive: true });
|
|
79
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
interactive: true,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
vi.clearAllMocks();
|
|
86
|
+
await command.run({ promptName: 'test-prompt', i: true });
|
|
87
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
interactive: true,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
vi.clearAllMocks();
|
|
94
|
+
await command.run({ promptName: 'test-prompt', args: ['--interactive'] });
|
|
95
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
interactive: true,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle custom models', async () => {
|
|
103
|
+
await command.run({ promptName: 'test-prompt', models: 'model1, model2 ' });
|
|
104
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
models: ['model1', 'model2'],
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should include generator agents prompts if they exist', async () => {
|
|
112
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
113
|
+
async (p: string | Buffer | URL) => {
|
|
114
|
+
return (p as string).includes('packages/generator/prompts/agents');
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
await command.run({ promptName: 'test-prompt' });
|
|
119
|
+
|
|
120
|
+
const call = vi.mocked(PromptRunner.run).mock.calls[0][0];
|
|
121
|
+
expect(call?.promptDirs).toHaveLength(2);
|
|
122
|
+
expect(call?.promptDirs).toContain(path.join(projectRoot, 'prompts'));
|
|
123
|
+
expect(call?.promptDirs).toContain(path.join(projectRoot, 'packages/generator/prompts/agents'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should resolve frontend module context', async () => {
|
|
127
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
128
|
+
async (p: string | Buffer | URL) => {
|
|
129
|
+
return (p as string).includes('apps/frontend/modules/test-module');
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await command.run({ promptName: 'test-prompt', module: 'test-module' });
|
|
134
|
+
|
|
135
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
136
|
+
expect.objectContaining({
|
|
137
|
+
args: expect.objectContaining({
|
|
138
|
+
module_name: 'test-module',
|
|
139
|
+
module_type: 'frontend',
|
|
140
|
+
module_root: path.join(projectRoot, 'apps/frontend/modules/test-module'),
|
|
141
|
+
}),
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should resolve backend module context', async () => {
|
|
147
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
148
|
+
async (p: string | Buffer | URL) => {
|
|
149
|
+
return (p as string).includes('apps/backend/modules/test-module');
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await command.run({ promptName: 'test-prompt', m: 'test-module' });
|
|
154
|
+
|
|
155
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
args: expect.objectContaining({
|
|
158
|
+
module_name: 'test-module',
|
|
159
|
+
module_type: 'backend',
|
|
160
|
+
module_root: path.join(projectRoot, 'apps/backend/modules/test-module'),
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should fail if module not found', async () => {
|
|
167
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockResolvedValue(false);
|
|
168
|
+
|
|
169
|
+
await command.run({ promptName: 'test-prompt', module: 'missing-module' });
|
|
170
|
+
|
|
171
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
172
|
+
expect.stringContaining("Module 'missing-module' not found"),
|
|
173
|
+
);
|
|
174
|
+
expect(PromptRunner.run).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should load AI config from nexical.yaml', async () => {
|
|
178
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
179
|
+
async (p: string | Buffer | URL) => (p as string).includes('nexical.yaml'),
|
|
180
|
+
);
|
|
181
|
+
vi.mocked(
|
|
182
|
+
fs.readFile as unknown as (p: string, e: string) => Promise<string>,
|
|
183
|
+
).mockResolvedValue('ai:\n provider: vertex');
|
|
184
|
+
vi.mocked(YAML.parse).mockReturnValue({ ai: { provider: 'vertex' } });
|
|
185
|
+
|
|
186
|
+
await command.run({ promptName: 'test-prompt' });
|
|
187
|
+
|
|
188
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
aiConfig: { provider: 'vertex' },
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle missing AI config in nexical.yaml', async () => {
|
|
196
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
197
|
+
async (p: string | Buffer | URL) => (p as string).includes('nexical.yaml'),
|
|
198
|
+
);
|
|
199
|
+
vi.mocked(
|
|
200
|
+
fs.readFile as unknown as (p: string, e: string) => Promise<string>,
|
|
201
|
+
).mockResolvedValue('name: my-project');
|
|
202
|
+
vi.mocked(YAML.parse).mockReturnValue({ name: 'my-project' });
|
|
203
|
+
|
|
204
|
+
await command.run({ promptName: 'test-prompt' });
|
|
205
|
+
|
|
206
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
aiConfig: {},
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle falsy YAML parse result', async () => {
|
|
214
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
215
|
+
async (p: string | Buffer | URL) => (p as string).includes('nexical.yaml'),
|
|
216
|
+
);
|
|
217
|
+
vi.mocked(
|
|
218
|
+
fs.readFile as unknown as (p: string, e: string) => Promise<string>,
|
|
219
|
+
).mockResolvedValue('');
|
|
220
|
+
vi.mocked(YAML.parse).mockReturnValue(null);
|
|
221
|
+
|
|
222
|
+
await command.run({ promptName: 'test-prompt' });
|
|
223
|
+
|
|
224
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
225
|
+
expect.objectContaining({
|
|
226
|
+
aiConfig: {},
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should handle nexical.yaml parse errors', async () => {
|
|
232
|
+
vi.mocked<(p: string) => Promise<boolean>>(fs.pathExists).mockImplementation(
|
|
233
|
+
async (p: string | Buffer | URL) => (p as string).includes('nexical.yaml'),
|
|
234
|
+
);
|
|
235
|
+
vi.mocked(
|
|
236
|
+
fs.readFile as unknown as (p: string, e: string) => Promise<string>,
|
|
237
|
+
).mockResolvedValue('invalid: yaml: :');
|
|
238
|
+
vi.mocked(YAML.parse).mockImplementation(() => {
|
|
239
|
+
throw new Error('parse error');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await command.run({ promptName: 'test-prompt' });
|
|
243
|
+
|
|
244
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
245
|
+
expect.stringContaining('Failed to parse nexical.yaml'),
|
|
246
|
+
);
|
|
247
|
+
expect(PromptRunner.run).toHaveBeenCalledWith(
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
aiConfig: {},
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should exit with error code if PromptRunner fails', async () => {
|
|
255
|
+
vi.mocked(PromptRunner.run).mockResolvedValue(1);
|
|
256
|
+
|
|
257
|
+
await expect(command.run({ promptName: 'test-prompt' })).rejects.toThrow('Process.exit(1)');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should set default root_path if not provided', async () => {
|
|
261
|
+
await command.run({ promptName: 'test-prompt' });
|
|
262
|
+
|
|
263
|
+
const call = vi.mocked(PromptRunner.run).mock.calls[0][0];
|
|
264
|
+
expect(call?.args?.root_path).toBe(process.cwd() + '/');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should use provided root_path from args', async () => {
|
|
268
|
+
await command.run({ promptName: 'test-prompt', args: ['--root_path', '/custom/path/'] });
|
|
269
|
+
|
|
270
|
+
const call = vi.mocked(PromptRunner.run).mock.calls[0][0];
|
|
271
|
+
expect(call?.args?.root_path).toBe('/custom/path/');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { filterCommandDirectories } from '../../../src/utils/filter.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
describe('filterCommandDirectories', () => {
|
|
6
|
+
const coreDir = path.resolve('/test/packages/cli/src/commands');
|
|
7
|
+
|
|
8
|
+
it('should filter out the core commands directory itself', () => {
|
|
9
|
+
const dirs = [coreDir, path.resolve('/test/other')];
|
|
10
|
+
const filtered = filterCommandDirectories(dirs, coreDir);
|
|
11
|
+
expect(filtered).toEqual([path.resolve('/test/other')]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should filter out default core suffixes', () => {
|
|
15
|
+
const dirs = [
|
|
16
|
+
path.join('/some/path', '@nexical', 'cli', 'dist', 'src', 'commands'),
|
|
17
|
+
path.join('/another/path', 'packages', 'cli', 'dist', 'src', 'commands'),
|
|
18
|
+
path.join('/yet/another', 'packages', 'cli', 'src', 'commands'),
|
|
19
|
+
path.resolve('/test/valid'),
|
|
20
|
+
];
|
|
21
|
+
const filtered = filterCommandDirectories(dirs, coreDir);
|
|
22
|
+
expect(filtered).toEqual([path.resolve('/test/valid')]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle dist/src mismatch and filter src version', () => {
|
|
26
|
+
const base = '/another/project';
|
|
27
|
+
const distCore = path.join(base, 'dist', 'src', 'commands');
|
|
28
|
+
const srcCore = path.join(base, 'src', 'commands');
|
|
29
|
+
|
|
30
|
+
const dirs = [srcCore, path.resolve('/test/other')];
|
|
31
|
+
const filtered = filterCommandDirectories(dirs, distCore);
|
|
32
|
+
expect(filtered).toEqual([path.resolve('/test/other')]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should not filter out non-matching directories', () => {
|
|
36
|
+
const dirs = [path.resolve('/test/modules/mod1/src/commands')];
|
|
37
|
+
const filtered = filterCommandDirectories(dirs, coreDir);
|
|
38
|
+
expect(filtered).toEqual(dirs);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/vitest.config.ts
CHANGED