@shahmarasy/prodo 0.1.0

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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/bin/prodo.cjs +6 -0
  4. package/dist/agent-command-installer.d.ts +4 -0
  5. package/dist/agent-command-installer.js +158 -0
  6. package/dist/agents.d.ts +15 -0
  7. package/dist/agents.js +47 -0
  8. package/dist/artifact-registry.d.ts +11 -0
  9. package/dist/artifact-registry.js +49 -0
  10. package/dist/artifacts.d.ts +9 -0
  11. package/dist/artifacts.js +514 -0
  12. package/dist/cli.d.ts +9 -0
  13. package/dist/cli.js +305 -0
  14. package/dist/consistency.d.ts +8 -0
  15. package/dist/consistency.js +268 -0
  16. package/dist/constants.d.ts +7 -0
  17. package/dist/constants.js +64 -0
  18. package/dist/doctor.d.ts +1 -0
  19. package/dist/doctor.js +123 -0
  20. package/dist/errors.d.ts +3 -0
  21. package/dist/errors.js +10 -0
  22. package/dist/hook-executor.d.ts +1 -0
  23. package/dist/hook-executor.js +175 -0
  24. package/dist/init-tui.d.ts +21 -0
  25. package/dist/init-tui.js +161 -0
  26. package/dist/init.d.ts +10 -0
  27. package/dist/init.js +307 -0
  28. package/dist/markdown.d.ts +11 -0
  29. package/dist/markdown.js +66 -0
  30. package/dist/normalize.d.ts +7 -0
  31. package/dist/normalize.js +73 -0
  32. package/dist/normalized-brief.d.ts +39 -0
  33. package/dist/normalized-brief.js +170 -0
  34. package/dist/output-index.d.ts +13 -0
  35. package/dist/output-index.js +55 -0
  36. package/dist/paths.d.ts +16 -0
  37. package/dist/paths.js +76 -0
  38. package/dist/preset-loader.d.ts +4 -0
  39. package/dist/preset-loader.js +210 -0
  40. package/dist/project-config.d.ts +14 -0
  41. package/dist/project-config.js +69 -0
  42. package/dist/providers/index.d.ts +2 -0
  43. package/dist/providers/index.js +12 -0
  44. package/dist/providers/mock-provider.d.ts +7 -0
  45. package/dist/providers/mock-provider.js +168 -0
  46. package/dist/providers/openai-provider.d.ts +11 -0
  47. package/dist/providers/openai-provider.js +69 -0
  48. package/dist/registry.d.ts +13 -0
  49. package/dist/registry.js +115 -0
  50. package/dist/settings.d.ts +6 -0
  51. package/dist/settings.js +34 -0
  52. package/dist/template-resolver.d.ts +11 -0
  53. package/dist/template-resolver.js +28 -0
  54. package/dist/templates.d.ts +33 -0
  55. package/dist/templates.js +428 -0
  56. package/dist/types.d.ts +35 -0
  57. package/dist/types.js +5 -0
  58. package/dist/utils.d.ts +6 -0
  59. package/dist/utils.js +53 -0
  60. package/dist/validate.d.ts +9 -0
  61. package/dist/validate.js +226 -0
  62. package/dist/validator.d.ts +5 -0
  63. package/dist/validator.js +80 -0
  64. package/dist/version.d.ts +1 -0
  65. package/dist/version.js +30 -0
  66. package/dist/workflow-commands.d.ts +7 -0
  67. package/dist/workflow-commands.js +28 -0
  68. package/package.json +45 -0
  69. package/presets/fintech/preset.json +1 -0
  70. package/presets/fintech/prompts/prd.md +3 -0
  71. package/presets/marketplace/preset.json +1 -0
  72. package/presets/marketplace/prompts/prd.md +3 -0
  73. package/presets/saas/preset.json +1 -0
  74. package/presets/saas/prompts/prd.md +3 -0
  75. package/src/agent-command-installer.ts +174 -0
  76. package/src/agents.ts +56 -0
  77. package/src/artifact-registry.ts +69 -0
  78. package/src/artifacts.ts +606 -0
  79. package/src/cli.ts +322 -0
  80. package/src/consistency.ts +303 -0
  81. package/src/constants.ts +72 -0
  82. package/src/doctor.ts +137 -0
  83. package/src/errors.ts +7 -0
  84. package/src/hook-executor.ts +196 -0
  85. package/src/init-tui.ts +193 -0
  86. package/src/init.ts +375 -0
  87. package/src/markdown.ts +73 -0
  88. package/src/normalize.ts +89 -0
  89. package/src/normalized-brief.ts +206 -0
  90. package/src/output-index.ts +59 -0
  91. package/src/paths.ts +72 -0
  92. package/src/preset-loader.ts +237 -0
  93. package/src/project-config.ts +78 -0
  94. package/src/providers/index.ts +12 -0
  95. package/src/providers/mock-provider.ts +188 -0
  96. package/src/providers/openai-provider.ts +87 -0
  97. package/src/registry.ts +119 -0
  98. package/src/settings.ts +34 -0
  99. package/src/template-resolver.ts +33 -0
  100. package/src/templates.ts +440 -0
  101. package/src/types.ts +46 -0
  102. package/src/utils.ts +50 -0
  103. package/src/validate.ts +246 -0
  104. package/src/validator.ts +96 -0
  105. package/src/version.ts +24 -0
  106. package/src/workflow-commands.ts +31 -0
  107. package/templates/artifacts/prd.md +219 -0
  108. package/templates/artifacts/stories.md +49 -0
  109. package/templates/artifacts/techspec.md +42 -0
  110. package/templates/artifacts/wireframe.html +260 -0
  111. package/templates/artifacts/wireframe.md +22 -0
  112. package/templates/artifacts/workflow.md +22 -0
  113. package/templates/artifacts/workflow.mmd +6 -0
  114. package/templates/commands/prodo-normalize.md +24 -0
  115. package/templates/commands/prodo-prd.md +24 -0
  116. package/templates/commands/prodo-stories.md +24 -0
  117. package/templates/commands/prodo-techspec.md +24 -0
  118. package/templates/commands/prodo-validate.md +24 -0
  119. package/templates/commands/prodo-wireframe.md +24 -0
  120. package/templates/commands/prodo-workflow.md +24 -0
package/src/doctor.ts ADDED
@@ -0,0 +1,137 @@
1
+ import path from "node:path";
2
+ import { spawn } from "node:child_process";
3
+ import { prodoPath } from "./paths";
4
+ import { fileExists } from "./utils";
5
+ import { readCliVersion } from "./version";
6
+
7
+ type RowStatus = "available" | "not_found" | "ide";
8
+
9
+ type DoctorRow = {
10
+ name: string;
11
+ status: RowStatus;
12
+ detail: string;
13
+ };
14
+
15
+ function termWidth(): number {
16
+ return Math.max(80, process.stdout.columns ?? 100);
17
+ }
18
+
19
+ function stripAnsi(input: string): string {
20
+ return input.replace(/\u001B\[[0-9;]*m/g, "");
21
+ }
22
+
23
+ function color(input: string, code: string): string {
24
+ if (!process.stdout.isTTY) return input;
25
+ return `${code}${input}\u001B[0m`;
26
+ }
27
+
28
+ function center(input: string, width: number): string {
29
+ const visible = stripAnsi(input).length;
30
+ const left = Math.max(0, Math.floor((width - visible) / 2));
31
+ return `${" ".repeat(left)}${input}`;
32
+ }
33
+
34
+ function iconFor(status: RowStatus): string {
35
+ if (status === "available") return color("✔", "\u001B[32m");
36
+ if (status === "not_found") return color("✖", "\u001B[31m");
37
+ return color("•", "\u001B[33m");
38
+ }
39
+
40
+ function labelFor(status: RowStatus): string {
41
+ if (status === "available") return color("available", "\u001B[32m");
42
+ if (status === "not_found") return color("not found", "\u001B[31m");
43
+ return color("IDE-based", "\u001B[2;33m");
44
+ }
45
+
46
+ function renderRows(rows: DoctorRow[]): string[] {
47
+ const leftWidth = Math.max(...rows.map((row) => row.name.length), 10);
48
+ return rows.map((row) => {
49
+ const left = row.name.padEnd(leftWidth, " ");
50
+ const status = `${labelFor(row.status)}${row.detail ? ` (${row.detail})` : ""}`;
51
+ return ` ${iconFor(row.status)} ${left} ${status}`;
52
+ });
53
+ }
54
+
55
+ async function commandExists(command: string): Promise<boolean> {
56
+ return new Promise((resolve) => {
57
+ const lookup = process.platform === "win32" ? "where" : "which";
58
+ const child = spawn(lookup, [command], { stdio: "ignore" });
59
+ child.on("error", () => resolve(false));
60
+ child.on("close", (code) => resolve(code === 0));
61
+ });
62
+ }
63
+
64
+ async function firstAvailable(commands: string[]): Promise<boolean> {
65
+ for (const command of commands) {
66
+ if (await commandExists(command)) return true;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ function renderLogo(width: number): string {
72
+ const cyan = "\u001B[38;5;45m";
73
+ const blue = "\u001B[38;5;39m";
74
+ const logo = [
75
+ "██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ",
76
+ "██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗",
77
+ "██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║",
78
+ "██╔═══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║",
79
+ "██║ ██║ ██║╚██████╔╝██████╔╝╚██████╔╝"
80
+ ];
81
+ const painted = logo.map((line, idx) => color(line, idx % 2 === 0 ? cyan : blue));
82
+ return painted.map((line) => center(line, width)).join("\n");
83
+ }
84
+
85
+ export async function runDoctor(cwd: string, out: (line: string) => void): Promise<void> {
86
+ const width = termWidth();
87
+ const version = await readCliVersion(cwd);
88
+ const isInitialized = await fileExists(prodoPath(cwd));
89
+
90
+ const codex = await firstAvailable(["codex"]);
91
+ const gemini = await firstAvailable(["gemini", "gemini-cli"]);
92
+ const claude = await firstAvailable(["claude", "claude-cli"]);
93
+
94
+ const git = await firstAvailable(["git"]);
95
+ const node = await firstAvailable(["node"]);
96
+ const vscode = await firstAvailable(["code"]);
97
+
98
+ const coreRows: DoctorRow[] = [
99
+ { name: "Prodo CLI", status: "available", detail: `v${version}` },
100
+ {
101
+ name: "Project initialized",
102
+ status: isInitialized ? "available" : "not_found",
103
+ detail: isInitialized ? ".prodo found" : ".prodo missing"
104
+ }
105
+ ];
106
+
107
+ const aiRows: DoctorRow[] = [
108
+ { name: "Codex CLI", status: codex ? "available" : "not_found", detail: codex ? "available" : "not found" },
109
+ { name: "Gemini CLI", status: gemini ? "available" : "not_found", detail: gemini ? "available" : "not found" },
110
+ { name: "Claude CLI", status: claude ? "available" : "not_found", detail: claude ? "available" : "not found" }
111
+ ];
112
+
113
+ const devRows: DoctorRow[] = [
114
+ { name: "Git", status: git ? "available" : "not_found", detail: git ? "available" : "not found" },
115
+ { name: "Node.js", status: node ? "available" : "not_found", detail: node ? "available" : "not found" },
116
+ { name: "Visual Studio Code", status: vscode ? "available" : "not_found", detail: vscode ? "available" : "not found" },
117
+ { name: "Cursor", status: "ide", detail: "IDE-based, no CLI check" }
118
+ ];
119
+
120
+ out("");
121
+ out(renderLogo(width));
122
+ out("");
123
+ out(center(color("Prodo — Product Artifact Toolkit", "\u001B[1;37m"), width));
124
+ out(center(color("Crafted by Codex, guided by Shahmarasy intelligence", "\u001B[2;37m"), width));
125
+ out("");
126
+ out("Checking environment...");
127
+ out("");
128
+ out(color("Core", "\u001B[1m"));
129
+ for (const line of renderRows(coreRows)) out(line);
130
+ out("");
131
+ out(color("AI / Agents", "\u001B[1m"));
132
+ for (const line of renderRows(aiRows)) out(line);
133
+ out("");
134
+ out(color("Dev Tools", "\u001B[1m"));
135
+ for (const line of renderRows(devRows)) out(line);
136
+ out("");
137
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,7 @@
1
+ export class UserError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "UserError";
5
+ }
6
+ }
7
+
@@ -0,0 +1,196 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import yaml from "js-yaml";
5
+ import { UserError } from "./errors";
6
+ import { fileExists } from "./utils";
7
+
8
+ type HookItem = {
9
+ command?: string;
10
+ optional?: boolean;
11
+ enabled?: boolean;
12
+ description?: string;
13
+ prompt?: string;
14
+ extension?: string;
15
+ condition?: string;
16
+ timeout_ms?: number;
17
+ retry?: number;
18
+ retry_delay_ms?: number;
19
+ };
20
+
21
+ type HooksConfig = {
22
+ hooks?: Record<string, HookItem[]>;
23
+ };
24
+
25
+ function hooksPath(cwd: string): string {
26
+ return path.join(cwd, ".prodo", "hooks.yml");
27
+ }
28
+
29
+ async function runShellCommand(
30
+ command: string,
31
+ cwd: string,
32
+ timeoutMs: number
33
+ ): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
34
+ const parsed = parseCommand(command);
35
+ if (!parsed) {
36
+ return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
37
+ }
38
+ return new Promise((resolve) => {
39
+ const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ let timedOut = false;
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ child.kill();
46
+ }, Math.max(1000, timeoutMs));
47
+
48
+ child.stdout.on("data", (chunk) => {
49
+ stdout += chunk.toString();
50
+ });
51
+ child.stderr.on("data", (chunk) => {
52
+ stderr += chunk.toString();
53
+ });
54
+ child.on("close", (code) => {
55
+ clearTimeout(timer);
56
+ resolve({ code: code ?? 1, stdout, stderr, timedOut });
57
+ });
58
+ });
59
+ }
60
+
61
+ function parseCommand(command: string): { bin: string; args: string[] } | null {
62
+ const src = command.trim();
63
+ if (!src) return null;
64
+ const out: string[] = [];
65
+ let current = "";
66
+ let quote: '"' | "'" | null = null;
67
+ let escaping = false;
68
+ for (let i = 0; i < src.length; i += 1) {
69
+ const ch = src[i];
70
+ if (escaping) {
71
+ current += ch;
72
+ escaping = false;
73
+ continue;
74
+ }
75
+ if (ch === "\\") {
76
+ escaping = true;
77
+ continue;
78
+ }
79
+ if (quote) {
80
+ if (ch === quote) {
81
+ quote = null;
82
+ } else {
83
+ current += ch;
84
+ }
85
+ continue;
86
+ }
87
+ if (ch === "'" || ch === '"') {
88
+ quote = ch;
89
+ continue;
90
+ }
91
+ if (/\s/.test(ch)) {
92
+ if (current.length > 0) {
93
+ out.push(current);
94
+ current = "";
95
+ }
96
+ continue;
97
+ }
98
+ current += ch;
99
+ }
100
+ if (escaping || quote) return null;
101
+ if (current.length > 0) out.push(current);
102
+ if (out.length === 0) return null;
103
+ return { bin: out[0], args: out.slice(1) };
104
+ }
105
+
106
+ function toPositiveInt(value: unknown, fallback: number): number {
107
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
108
+ const normalized = Math.floor(value);
109
+ return normalized > 0 ? normalized : fallback;
110
+ }
111
+
112
+ function toNonNegativeInt(value: unknown, fallback: number): number {
113
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
114
+ const normalized = Math.floor(value);
115
+ return normalized >= 0 ? normalized : fallback;
116
+ }
117
+
118
+ async function sleep(ms: number): Promise<void> {
119
+ await new Promise((resolve) => setTimeout(resolve, ms));
120
+ }
121
+
122
+ async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
123
+ const trimmed = condition.trim();
124
+ if (!trimmed) return true;
125
+ const result = await runShellCommand(trimmed, cwd, 10_000);
126
+ return !result.timedOut && result.code === 0;
127
+ }
128
+
129
+ async function readHooks(cwd: string): Promise<HooksConfig | null> {
130
+ const file = hooksPath(cwd);
131
+ if (!(await fileExists(file))) return null;
132
+ try {
133
+ const raw = await fs.readFile(file, "utf8");
134
+ const parsed = yaml.load(raw) as HooksConfig;
135
+ return parsed ?? null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ export async function runHookPhase(
142
+ cwd: string,
143
+ phaseKey: string,
144
+ log: (message: string) => void
145
+ ): Promise<void> {
146
+ const config = await readHooks(cwd);
147
+ const phaseHooks = config?.hooks?.[phaseKey];
148
+ if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
149
+
150
+ for (const hook of phaseHooks) {
151
+ if (hook?.enabled === false) continue;
152
+ const command = typeof hook?.command === "string" ? hook.command.trim() : "";
153
+ if (!command) continue;
154
+ if (typeof hook.condition === "string" && hook.condition.trim()) {
155
+ const pass = await evaluateCondition(hook.condition, cwd);
156
+ if (!pass) {
157
+ log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
158
+ continue;
159
+ }
160
+ }
161
+
162
+ const label = hook.extension || hook.description || command;
163
+ if (hook.optional) {
164
+ log(`[Hook:optional:${phaseKey}] ${label}`);
165
+ if (hook.prompt) log(` Prompt: ${hook.prompt}`);
166
+ log(` To run manually: ${command}`);
167
+ continue;
168
+ }
169
+
170
+ const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
171
+ const retries = toNonNegativeInt(hook.retry, 0);
172
+ const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
173
+ const attempts = 1 + retries;
174
+
175
+ let lastDetail = "";
176
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
177
+ log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
178
+ const result = await runShellCommand(command, cwd, timeoutMs);
179
+ if (!result.timedOut && result.code === 0) {
180
+ lastDetail = "";
181
+ break;
182
+ }
183
+
184
+ const stderr = result.stderr.trim();
185
+ const stdout = result.stdout.trim();
186
+ lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
187
+ if (attempt < attempts) {
188
+ await sleep(retryDelayMs);
189
+ }
190
+ }
191
+
192
+ if (lastDetail) {
193
+ throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,193 @@
1
+ import path from "node:path";
2
+ import { resolveAi, type SupportedAi } from "./agent-command-installer";
3
+ import { UserError } from "./errors";
4
+ import { fileExists } from "./utils";
5
+
6
+ export type InitSelections = {
7
+ ai?: SupportedAi;
8
+ script: "sh" | "ps";
9
+ lang: "tr" | "en";
10
+ interactive: boolean;
11
+ };
12
+
13
+ type GatherInitUiOptions = {
14
+ projectRoot: string;
15
+ aiInput?: string;
16
+ langInput?: string;
17
+ };
18
+
19
+ type ClackPrompts = typeof import("@clack/prompts");
20
+
21
+ const dynamicImport = new Function("specifier", "return import(specifier)") as (
22
+ specifier: string
23
+ ) => Promise<unknown>;
24
+
25
+ async function loadClack(): Promise<ClackPrompts> {
26
+ return (await dynamicImport("@clack/prompts")) as ClackPrompts;
27
+ }
28
+
29
+ function isInteractiveTerminal(): boolean {
30
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
31
+ }
32
+
33
+ function color(text: string, code: string): string {
34
+ return `${code}${text}\u001B[0m`;
35
+ }
36
+
37
+ function stripAnsi(input: string): string {
38
+ return input.replace(/\u001B\[[0-9;]*m/g, "");
39
+ }
40
+
41
+ function centerLine(line: string, width: number): string {
42
+ const visible = stripAnsi(line).length;
43
+ const left = Math.max(0, Math.floor((width - visible) / 2));
44
+ return `${" ".repeat(left)}${line}`;
45
+ }
46
+
47
+ function centerBlock(lines: string[], width: number): string {
48
+ return lines.map((line) => centerLine(line, width)).join("\n");
49
+ }
50
+
51
+ function terminalWidth(): number {
52
+ const columns = process.stdout.columns ?? 100;
53
+ return Math.max(80, columns);
54
+ }
55
+
56
+ function renderLogo(): string {
57
+ const cyan = "\u001B[38;5;45m";
58
+ const blue = "\u001B[38;5;39m";
59
+ const lines = [
60
+ "██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ",
61
+ "██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗",
62
+ "██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║",
63
+ "██╔═══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║",
64
+ "██║ ██║ ██║╚██████╔╝██████╔╝╚██████╔╝"
65
+ ];
66
+ return lines.map((line, idx) => color(line, idx % 2 === 0 ? cyan : blue)).join("\n");
67
+ }
68
+
69
+ function renderProjectBox(projectName: string, projectRoot: string): string {
70
+ const content = [`Project ${projectName}`, `Directory ${projectRoot}`];
71
+ const innerWidth = Math.max(...content.map((line) => line.length)) + 2;
72
+ const top = `┌${"─".repeat(innerWidth)}┐`;
73
+ const rows = content.map((line) => `│ ${line.padEnd(innerWidth - 1)}│`);
74
+ const bottom = `└${"─".repeat(innerWidth)}┘`;
75
+ return [top, ...rows, bottom].join("\n");
76
+ }
77
+
78
+ async function detectAi(projectRoot: string): Promise<SupportedAi | undefined> {
79
+ if (await fileExists(path.join(projectRoot, ".agents"))) return "codex";
80
+ if (await fileExists(path.join(projectRoot, ".gemini"))) return "gemini-cli";
81
+ if (await fileExists(path.join(projectRoot, ".claude"))) return "claude-cli";
82
+ return undefined;
83
+ }
84
+
85
+ function normalizeLang(lang?: string): "tr" | "en" {
86
+ if ((lang ?? "").trim().toLowerCase().startsWith("tr")) return "tr";
87
+ return "en";
88
+ }
89
+
90
+ function modelChoiceFromAi(ai?: SupportedAi): "codex" | "gemini" | "auto-detect" {
91
+ if (ai === "codex") return "codex";
92
+ if (ai === "gemini-cli") return "gemini";
93
+ return "auto-detect";
94
+ }
95
+
96
+ export async function gatherInitSelections(options: GatherInitUiOptions): Promise<InitSelections> {
97
+ const clack = await loadClack();
98
+ const defaultLang = normalizeLang(options.langInput);
99
+ const fallbackScript: "sh" | "ps" = process.platform === "win32" ? "ps" : "sh";
100
+ const parsedAi = resolveAi(options.aiInput);
101
+
102
+ if (!isInteractiveTerminal()) {
103
+ return {
104
+ ai: parsedAi,
105
+ script: fallbackScript,
106
+ lang: defaultLang,
107
+ interactive: false
108
+ };
109
+ }
110
+
111
+ const detectedAi = await detectAi(options.projectRoot);
112
+ const initialModel = modelChoiceFromAi(parsedAi ?? detectedAi);
113
+ const projectName = path.basename(options.projectRoot) || ".";
114
+ const width = terminalWidth();
115
+ const subtitle = color("Prodo — Product Artifact Toolkit", "\u001B[1;37m");
116
+ const signature = color("Crafted by Codex, guided by Shahmarasy intelligence", "\u001B[38;5;244m");
117
+ const hero = [
118
+ "",
119
+ centerBlock(renderLogo().split("\n"), width),
120
+ "",
121
+ centerLine(subtitle, width),
122
+ centerLine(signature, width),
123
+ ""
124
+ ].join("\n");
125
+
126
+ clack.intro(hero);
127
+ clack.note(renderProjectBox(projectName, options.projectRoot), "Project Setup");
128
+
129
+ const model = await clack.select({
130
+ message: "Select model",
131
+ initialValue: initialModel,
132
+ options: [
133
+ { value: "codex", label: "codex", hint: "Native Codex flow" },
134
+ { value: "gemini", label: "gemini", hint: "Gemini CLI command set" },
135
+ { value: "auto-detect", label: "auto-detect", hint: "Detect from local agent directories" }
136
+ ]
137
+ });
138
+ if (clack.isCancel(model)) {
139
+ clack.cancel("Initialization cancelled.");
140
+ throw new UserError("Initialization cancelled.");
141
+ }
142
+
143
+ const script = await clack.select({
144
+ message: "Select script type",
145
+ initialValue: fallbackScript,
146
+ options: [
147
+ { value: "sh", label: "sh", hint: "Command profile (metadata)" },
148
+ { value: "ps", label: "ps", hint: "Command profile (metadata)" }
149
+ ]
150
+ });
151
+ if (clack.isCancel(script)) {
152
+ clack.cancel("Initialization cancelled.");
153
+ throw new UserError("Initialization cancelled.");
154
+ }
155
+
156
+ const lang = await clack.select({
157
+ message: "Select language",
158
+ initialValue: defaultLang,
159
+ options: [
160
+ { value: "tr", label: "tr", hint: "Turkish" },
161
+ { value: "en", label: "en", hint: "English" }
162
+ ]
163
+ });
164
+ if (clack.isCancel(lang)) {
165
+ clack.cancel("Initialization cancelled.");
166
+ throw new UserError("Initialization cancelled.");
167
+ }
168
+
169
+ let selectedAi: SupportedAi | undefined;
170
+ if (model === "codex") selectedAi = "codex";
171
+ else if (model === "gemini") selectedAi = "gemini-cli";
172
+ else selectedAi = detectedAi;
173
+
174
+ return {
175
+ ai: selectedAi,
176
+ script,
177
+ lang,
178
+ interactive: true
179
+ };
180
+ }
181
+
182
+ export function finishInitInteractive(summary: {
183
+ projectRoot: string;
184
+ settingsPath: string;
185
+ ai?: SupportedAi;
186
+ script: "sh" | "ps";
187
+ lang: "tr" | "en";
188
+ }): Promise<void> {
189
+ const aiText = summary.ai ?? "none";
190
+ return loadClack().then((clack) => clack.outro(
191
+ `Scaffold complete.\nAI: ${aiText}\nScript: ${summary.script}\nLanguage: ${summary.lang}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`
192
+ ));
193
+ }