product-discovery-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "product-discovery-cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI to consume the Product Discovery Agent API",
5
+ "license": "Apache-2.0",
6
+ "author": "AuronForge",
7
+ "keywords": [
8
+ "product-discovery",
9
+ "ai-agent",
10
+ "copilot",
11
+ "cli"
12
+ ],
13
+ "bin": {
14
+ "product-discovery": "src/index.js"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "package.json"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "start": "node src/index.js",
25
+ "test": "jest",
26
+ "test:coverage": "jest --coverage",
27
+ "postinstall": "npm cache verify"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "dependencies": {
33
+ "boxen": "^5.1.2",
34
+ "chalk": "^4.1.2",
35
+ "commander": "^11.1.0",
36
+ "inquirer": "^8.2.6",
37
+ "ora": "^5.4.1"
38
+ },
39
+ "devDependencies": {
40
+ "jest": "^29.7.0"
41
+ }
42
+ }
@@ -0,0 +1,82 @@
1
+ const { DiscoverySession } = require("../domain/DiscoverySession");
2
+
3
+ class RunDiscoveryFlow {
4
+ constructor({ prompt, apiClient, storage, presenter }) {
5
+ this.prompt = prompt;
6
+ this.apiClient = apiClient;
7
+ this.storage = storage;
8
+ this.presenter = presenter;
9
+ }
10
+
11
+ async execute({ apiUrl, saveDefaults }) {
12
+ this.presenter.printHeader();
13
+
14
+ let continueOuter = true;
15
+
16
+ while (continueOuter) {
17
+ const session = new DiscoverySession();
18
+ let shouldImprove = true;
19
+
20
+ while (shouldImprove) {
21
+ this.presenter.info("What do you want to run discovery for?");
22
+ const idea = await this.prompt.askInput("Describe your idea/problem/application/pain:", {
23
+ required: true
24
+ });
25
+
26
+ const attemptText = session.addIdea(idea);
27
+
28
+ const spinner = this.presenter.spinner("Calling the discovery API...");
29
+ let result;
30
+ try {
31
+ result = await this.apiClient.runDiscovery(attemptText, apiUrl);
32
+ spinner.succeed("Discovery completed.");
33
+ } catch (error) {
34
+ spinner.fail("Discovery failed.");
35
+ this.presenter.error(error.message);
36
+ const retry = await this.prompt.askYesNo("Do you want to try again?");
37
+ if (!retry) {
38
+ return;
39
+ }
40
+ continue;
41
+ }
42
+
43
+ this.presenter.json(result);
44
+
45
+ if (saveDefaults.autoSave === false) {
46
+ const improve = await this.prompt.askYesNo("Do you want to improve the result?");
47
+ if (!improve) {
48
+ shouldImprove = false;
49
+ continueOuter = false;
50
+ break;
51
+ }
52
+ continue;
53
+ }
54
+
55
+ const saveInfo = await this.storage.saveJson(result, saveDefaults);
56
+ if (saveInfo.saved) {
57
+ this.presenter.success(`Saved to: ${saveInfo.fullPath}`);
58
+ shouldImprove = false;
59
+ continue;
60
+ }
61
+
62
+ const improve = await this.prompt.askYesNo("Do you want to improve the result?");
63
+ if (!improve) {
64
+ shouldImprove = false;
65
+ continueOuter = false;
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (!continueOuter) {
71
+ break;
72
+ }
73
+
74
+ const another = await this.prompt.askYesNo("Do you want to run discovery for another idea?");
75
+ if (!another) {
76
+ continueOuter = false;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ module.exports = { RunDiscoveryFlow };
@@ -0,0 +1,19 @@
1
+ const { Command } = require("commander");
2
+
3
+ function buildOptions(defaultApiUrl) {
4
+ const program = new Command();
5
+ program
6
+ .name("product-discovery")
7
+ .description("Generate a structured product discovery using the Product Discovery Agent API")
8
+ .option("-u, --api-url <url>", "API URL", defaultApiUrl)
9
+ .option("-c, --config <path>", "Path to JSON config file")
10
+ .option("-s, --save", "Auto-save the JSON result without prompting")
11
+ .option("-o, --output <dir>", "Default output directory")
12
+ .option("-f, --file <name>", "Default output filename")
13
+ .option("--no-save", "Disable saving prompt");
14
+
15
+ program.parse(process.argv);
16
+ return program.opts();
17
+ }
18
+
19
+ module.exports = { buildOptions };
@@ -0,0 +1,15 @@
1
+ class DiscoverySession {
2
+ constructor() {
3
+ this.baseIdea = "";
4
+ }
5
+
6
+ addIdea(text) {
7
+ if (!this.baseIdea) {
8
+ this.baseIdea = text;
9
+ return this.baseIdea;
10
+ }
11
+ return `${this.baseIdea}\n\nAdditional details: ${text}`;
12
+ }
13
+ }
14
+
15
+ module.exports = { DiscoverySession };
@@ -0,0 +1,22 @@
1
+ function sanitizeForFolderName(value) {
2
+ return (
3
+ value
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/gi, "-")
6
+ .replace(/^-+|-+$/g, "")
7
+ .slice(0, 60) || "product"
8
+ );
9
+ }
10
+
11
+ function timestampForPath() {
12
+ return new Date().toISOString().replace(/[:.]/g, "-");
13
+ }
14
+
15
+ function ensureJsonExtension(filename) {
16
+ if (!filename.toLowerCase().endsWith(".json")) {
17
+ return `${filename}.json`;
18
+ }
19
+ return filename;
20
+ }
21
+
22
+ module.exports = { sanitizeForFolderName, timestampForPath, ensureJsonExtension };
package/src/index.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { CliController } = require("./presentation/CliController");
4
+ const { PromptService } = require("./infrastructure/PromptService");
5
+ const { ConsolePresenter } = require("./infrastructure/ConsolePresenter");
6
+ const { ProductDiscoveryApi } = require("./infrastructure/ProductDiscoveryApi");
7
+ const { JsonFileStorage } = require("./infrastructure/JsonFileStorage");
8
+ const { RunDiscoveryFlow } = require("./application/RunDiscoveryFlow");
9
+
10
+ const DEFAULT_API_URL = process.env.API_URL || "http://localhost:3000/api/v1/discovery";
11
+
12
+ const prompt = new PromptService();
13
+ const presenter = new ConsolePresenter();
14
+ const apiClient = new ProductDiscoveryApi();
15
+ const storage = new JsonFileStorage();
16
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
17
+
18
+ const controller = new CliController({
19
+ defaultApiUrl: DEFAULT_API_URL,
20
+ prompt,
21
+ presenter,
22
+ apiClient,
23
+ storage,
24
+ useCase
25
+ });
26
+
27
+ controller.start().catch((error) => {
28
+ presenter.error(`Unexpected error: ${error.message}`);
29
+ });
@@ -0,0 +1,24 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ class ConfigLoader {
5
+ load(configPath) {
6
+ if (!configPath) return {};
7
+ const absolutePath = path.isAbsolute(configPath)
8
+ ? configPath
9
+ : path.join(process.cwd(), configPath);
10
+
11
+ if (!fs.existsSync(absolutePath)) {
12
+ throw new Error(`Config file not found: ${absolutePath}`);
13
+ }
14
+
15
+ const raw = fs.readFileSync(absolutePath, "utf-8");
16
+ try {
17
+ return JSON.parse(raw);
18
+ } catch (error) {
19
+ throw new Error(`Invalid JSON in config file: ${error.message}`);
20
+ }
21
+ }
22
+ }
23
+
24
+ module.exports = { ConfigLoader };
@@ -0,0 +1,53 @@
1
+ const { stdout: output } = require("node:process");
2
+ const chalk = require("chalk");
3
+ const boxen = require("boxen");
4
+ const ora = require("ora");
5
+
6
+ class ConsolePresenter {
7
+ printHeader() {
8
+ const logo = chalk.cyan(
9
+ String.raw`
10
+ ____ _ _ ____ _
11
+ | _ \ _ __ ___ __| |_ _ ___| |_ | _ \(_)___ ___ _____ _____ _ __
12
+ | |_) | '__/ _ \ / _ | | | |/ __| __| | | | | / __|/ __/ _ \ \ / / _ \ '__|
13
+ | __/| | | (_) | (_| | |_| | (__| |_ | |_| | \__ \ (_| (_) \ V / __/ |
14
+ |_| |_| \___/ \__,_|\__,_|\___|\__| |____/|_|___/\___\___/ \_/ \___|_|
15
+ `
16
+ );
17
+ const title = chalk.bold.cyan("Product Discovery CLI");
18
+ const subtitle = chalk.gray("Generate a structured product discovery via the Product Discovery Agent API.");
19
+ const banner = `${logo}\n${title}\n${subtitle}`;
20
+ output.write(
21
+ boxen(banner, {
22
+ padding: 1,
23
+ margin: 1,
24
+ borderStyle: "round",
25
+ borderColor: "cyan"
26
+ })
27
+ );
28
+ output.write("\n");
29
+ }
30
+
31
+ info(message) {
32
+ output.write(`${chalk.bold(message)}\n`);
33
+ }
34
+
35
+ success(message) {
36
+ output.write(`${chalk.green(message)}\n\n`);
37
+ }
38
+
39
+ error(message) {
40
+ output.write(`${chalk.red("Error:")} ${message}\n\n`);
41
+ }
42
+
43
+ json(payload) {
44
+ output.write(`\n${chalk.bold("Generated discovery JSON:")}\n\n`);
45
+ output.write(`${chalk.gray(JSON.stringify(payload, null, 2))}\n\n`);
46
+ }
47
+
48
+ spinner(text) {
49
+ return ora(text).start();
50
+ }
51
+ }
52
+
53
+ module.exports = { ConsolePresenter };
@@ -0,0 +1,63 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const os = require("node:os");
4
+ const { sanitizeForFolderName, timestampForPath, ensureJsonExtension } = require("../domain/PathNaming");
5
+
6
+ class JsonFileStorage {
7
+ resolveDesktopPath() {
8
+ const home = os.homedir();
9
+ const desktop = path.join(home, "Desktop");
10
+ if (fs.existsSync(desktop)) return desktop;
11
+ return home;
12
+ }
13
+
14
+ resolveSaveTarget({ result, directoryInput, filenameInput, defaultDir, defaultFileName }) {
15
+ const productName = sanitizeForFolderName(result?.name || "product");
16
+ const defaultFolderName = `product-${productName}-${timestampForPath()}`;
17
+
18
+ let targetDir = "";
19
+ let targetFilename = "";
20
+
21
+ if (!directoryInput && !filenameInput) {
22
+ const baseDir = defaultDir || this.resolveDesktopPath();
23
+ targetDir = path.join(baseDir, defaultFolderName);
24
+ targetFilename = defaultFileName || "discovery.json";
25
+ } else if (directoryInput && !filenameInput) {
26
+ targetDir = path.join(directoryInput, defaultFolderName);
27
+ targetFilename = defaultFileName || "discovery.json";
28
+ } else if (!directoryInput && filenameInput) {
29
+ targetDir = defaultDir || this.resolveDesktopPath();
30
+ targetFilename = filenameInput;
31
+ } else {
32
+ targetDir = directoryInput;
33
+ targetFilename = filenameInput;
34
+ }
35
+
36
+ targetFilename = ensureJsonExtension(targetFilename);
37
+ return { targetDir, targetFilename };
38
+ }
39
+
40
+ async saveJson(result, options) {
41
+ const wantsSave = options.autoSave ?? (await options.prompt.askYesNo("Do you want to save the JSON file?"));
42
+ if (!wantsSave) return { saved: false };
43
+
44
+ const directoryInput = options.directory ?? (await options.prompt.askInput("Directory (optional, press Enter to skip):"));
45
+ const filenameInput = options.filename ?? (await options.prompt.askInput("Filename (optional, press Enter to skip):"));
46
+
47
+ const { targetDir, targetFilename } = this.resolveSaveTarget({
48
+ result,
49
+ directoryInput,
50
+ filenameInput,
51
+ defaultDir: options.defaultDir,
52
+ defaultFileName: options.defaultFileName
53
+ });
54
+
55
+ fs.mkdirSync(targetDir, { recursive: true });
56
+ const fullPath = path.join(targetDir, targetFilename);
57
+
58
+ fs.writeFileSync(fullPath, JSON.stringify(result, null, 2), "utf-8");
59
+ return { saved: true, fullPath };
60
+ }
61
+ }
62
+
63
+ module.exports = { JsonFileStorage };
@@ -0,0 +1,26 @@
1
+ class ProductDiscoveryApi {
2
+ async runDiscovery(problemText, apiUrl) {
3
+ const response = await fetch(apiUrl, {
4
+ method: "POST",
5
+ headers: { "Content-Type": "application/json" },
6
+ body: JSON.stringify({ problem: problemText })
7
+ });
8
+
9
+ const text = await response.text();
10
+ let payload = null;
11
+ try {
12
+ payload = text ? JSON.parse(text) : null;
13
+ } catch {
14
+ payload = text;
15
+ }
16
+
17
+ if (!response.ok) {
18
+ const message = payload?.message || response.statusText || "Unknown error";
19
+ throw new Error(`API error (${response.status}): ${message}`);
20
+ }
21
+
22
+ return payload;
23
+ }
24
+ }
25
+
26
+ module.exports = { ProductDiscoveryApi };
@@ -0,0 +1,31 @@
1
+ const inquirer = require("inquirer");
2
+
3
+ class PromptService {
4
+ async askYesNo(message) {
5
+ const { answer } = await inquirer.prompt([
6
+ {
7
+ type: "confirm",
8
+ name: "answer",
9
+ message,
10
+ default: false
11
+ }
12
+ ]);
13
+ return answer;
14
+ }
15
+
16
+ async askInput(message, { required = false } = {}) {
17
+ const { answer } = await inquirer.prompt([
18
+ {
19
+ type: "input",
20
+ name: "answer",
21
+ message,
22
+ validate: required
23
+ ? (value) => (value && value.trim() ? true : "This field is required.")
24
+ : () => true
25
+ }
26
+ ]);
27
+ return answer.trim();
28
+ }
29
+ }
30
+
31
+ module.exports = { PromptService };
@@ -0,0 +1,38 @@
1
+ const { buildOptions } = require("../config/cliOptions");
2
+ const { ConfigLoader } = require("../infrastructure/ConfigLoader");
3
+
4
+ class CliController {
5
+ constructor({ defaultApiUrl, prompt, presenter, apiClient, storage, useCase }) {
6
+ this.defaultApiUrl = defaultApiUrl;
7
+ this.prompt = prompt;
8
+ this.presenter = presenter;
9
+ this.apiClient = apiClient;
10
+ this.storage = storage;
11
+ this.useCase = useCase;
12
+ }
13
+
14
+ async start() {
15
+ const cliOptions = buildOptions(this.defaultApiUrl);
16
+ let config = {};
17
+ try {
18
+ config = new ConfigLoader().load(cliOptions.config);
19
+ } catch (error) {
20
+ this.presenter.error(`Config error: ${error.message}`);
21
+ return;
22
+ }
23
+
24
+ const apiUrl = cliOptions.apiUrl || config.apiUrl || process.env.PRODUCT_DISCOVERY_API_URL || this.defaultApiUrl;
25
+ const saveDefaults = {
26
+ autoSave: typeof cliOptions.save === "boolean" ? cliOptions.save : config.autoSave,
27
+ directory: cliOptions.output || config.defaultOutputDir,
28
+ filename: cliOptions.file || config.defaultFileName,
29
+ defaultDir: cliOptions.output || config.defaultOutputDir,
30
+ defaultFileName: cliOptions.file || config.defaultFileName,
31
+ prompt: this.prompt
32
+ };
33
+
34
+ await this.useCase.execute({ apiUrl, saveDefaults });
35
+ }
36
+ }
37
+
38
+ module.exports = { CliController };