@towles/tool 0.0.20 → 0.0.48

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 (45) hide show
  1. package/{LICENSE.md → LICENSE} +1 -1
  2. package/README.md +86 -85
  3. package/bin/run.ts +5 -0
  4. package/package.json +84 -64
  5. package/patches/prompts.patch +34 -0
  6. package/src/commands/base.ts +27 -0
  7. package/src/commands/config.test.ts +15 -0
  8. package/src/commands/config.ts +44 -0
  9. package/src/commands/doctor.ts +136 -0
  10. package/src/commands/gh/branch-clean.ts +116 -0
  11. package/src/commands/gh/branch.test.ts +124 -0
  12. package/src/commands/gh/branch.ts +135 -0
  13. package/src/commands/gh/pr.ts +175 -0
  14. package/src/commands/graph-template.html +1214 -0
  15. package/src/commands/graph.test.ts +176 -0
  16. package/src/commands/graph.ts +970 -0
  17. package/src/commands/install.ts +154 -0
  18. package/src/commands/journal/daily-notes.ts +70 -0
  19. package/src/commands/journal/meeting.ts +89 -0
  20. package/src/commands/journal/note.ts +89 -0
  21. package/src/commands/ralph/plan/add.ts +75 -0
  22. package/src/commands/ralph/plan/done.ts +82 -0
  23. package/src/commands/ralph/plan/list.test.ts +48 -0
  24. package/src/commands/ralph/plan/list.ts +99 -0
  25. package/src/commands/ralph/plan/remove.ts +71 -0
  26. package/src/commands/ralph/run.test.ts +521 -0
  27. package/src/commands/ralph/run.ts +345 -0
  28. package/src/commands/ralph/show.ts +88 -0
  29. package/src/config/settings.ts +136 -0
  30. package/src/lib/journal/utils.ts +399 -0
  31. package/src/lib/ralph/execution.ts +292 -0
  32. package/src/lib/ralph/formatter.ts +238 -0
  33. package/src/lib/ralph/index.ts +4 -0
  34. package/src/lib/ralph/state.ts +166 -0
  35. package/src/types/journal.ts +16 -0
  36. package/src/utils/date-utils.test.ts +97 -0
  37. package/src/utils/date-utils.ts +54 -0
  38. package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
  39. package/src/utils/git/gh-cli-wrapper.ts +54 -0
  40. package/src/utils/git/git-wrapper.test.ts +26 -0
  41. package/src/utils/git/git-wrapper.ts +15 -0
  42. package/src/utils/render.test.ts +71 -0
  43. package/src/utils/render.ts +34 -0
  44. package/dist/index.d.mts +0 -1
  45. package/dist/index.mjs +0 -805
@@ -0,0 +1,345 @@
1
+ import * as fs from "node:fs";
2
+ import { Flags } from "@oclif/core";
3
+ import consola from "consola";
4
+ import { colors } from "consola/utils";
5
+ import { BaseCommand } from "../base.js";
6
+ import {
7
+ DEFAULT_STATE_FILE,
8
+ DEFAULT_LOG_FILE,
9
+ DEFAULT_MAX_ITERATIONS,
10
+ DEFAULT_COMPLETION_MARKER,
11
+ CLAUDE_DEFAULT_ARGS,
12
+ loadState,
13
+ saveState,
14
+ appendHistory,
15
+ resolveRalphPath,
16
+ getRalphPaths,
17
+ } from "../../lib/ralph/state.js";
18
+ import {
19
+ buildIterationPrompt,
20
+ formatDuration,
21
+ extractOutputSummary,
22
+ detectCompletionMarker,
23
+ } from "../../lib/ralph/formatter.js";
24
+ import { checkClaudeCli, runIteration } from "../../lib/ralph/execution.js";
25
+ import type { RalphPlan } from "../../lib/ralph/state.js";
26
+
27
+ /** Get the plan to work on: focused plan or first incomplete */
28
+ function getCurrentPlan(plans: RalphPlan[], focusedPlanId: number | null): RalphPlan | undefined {
29
+ if (focusedPlanId !== null) {
30
+ return plans.find((p) => p.id === focusedPlanId);
31
+ }
32
+ return plans.find((p) => p.status !== "done");
33
+ }
34
+
35
+ /**
36
+ * Run the autonomous ralph loop
37
+ */
38
+ export default class Run extends BaseCommand {
39
+ static override description = "Start the autonomous ralph loop";
40
+
41
+ static override examples = [
42
+ { description: "Start the autonomous loop", command: "<%= config.bin %> <%= command.id %>" },
43
+ {
44
+ description: "Limit to 20 iterations",
45
+ command: "<%= config.bin %> <%= command.id %> --maxIterations 20",
46
+ },
47
+ {
48
+ description: "Focus on specific plan",
49
+ command: "<%= config.bin %> <%= command.id %> --planId 5",
50
+ },
51
+ {
52
+ description: "Run without auto-committing",
53
+ command: "<%= config.bin %> <%= command.id %> --no-autoCommit",
54
+ },
55
+ {
56
+ description: "Preview config without executing",
57
+ command: "<%= config.bin %> <%= command.id %> --dryRun",
58
+ },
59
+ ];
60
+
61
+ static override flags = {
62
+ ...BaseCommand.baseFlags,
63
+ stateFile: Flags.string({
64
+ char: "s",
65
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
66
+ }),
67
+ planId: Flags.integer({
68
+ char: "p",
69
+ description: "Focus on specific plan ID",
70
+ }),
71
+ maxIterations: Flags.integer({
72
+ char: "m",
73
+ description: "Max iterations",
74
+ default: DEFAULT_MAX_ITERATIONS,
75
+ }),
76
+ autoCommit: Flags.boolean({
77
+ description: "Auto-commit after each completed plan",
78
+ default: true,
79
+ allowNo: true,
80
+ }),
81
+ dryRun: Flags.boolean({
82
+ char: "n",
83
+ description: "Show config without executing",
84
+ default: false,
85
+ }),
86
+ claudeArgs: Flags.string({
87
+ description: "Extra args to pass to claude CLI (space-separated)",
88
+ }),
89
+ logFile: Flags.string({
90
+ description: `Log file path (default: ${DEFAULT_LOG_FILE})`,
91
+ }),
92
+ completionMarker: Flags.string({
93
+ description: "Completion marker",
94
+ default: DEFAULT_COMPLETION_MARKER,
95
+ }),
96
+ };
97
+
98
+ async run(): Promise<void> {
99
+ const { flags } = await this.parse(Run);
100
+ const ralphSettings = this.settings.settings.ralphSettings;
101
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
102
+ const logFile = resolveRalphPath(flags.logFile, "logFile", ralphSettings);
103
+ const ralphPaths = getRalphPaths(ralphSettings);
104
+
105
+ const maxIterations = flags.maxIterations;
106
+ const extraClaudeArgs = flags.claudeArgs?.split(" ").filter(Boolean) || [];
107
+ const focusedPlanId = flags.planId ?? null;
108
+
109
+ // Load existing state
110
+ let state = loadState(stateFile);
111
+
112
+ if (!state) {
113
+ this.error(`No state file found at: ${stateFile}\nUse: tt ralph plan add --file path.md`);
114
+ }
115
+
116
+ const remainingPlans = state.plans.filter((t) => t.status !== "done");
117
+ if (remainingPlans.length === 0) {
118
+ consola.log(colors.green("✅ All plans are done!"));
119
+ return;
120
+ }
121
+
122
+ // Validate focused plan if specified
123
+ if (focusedPlanId !== null) {
124
+ const focusedPlan = state.plans.find((t) => t.id === focusedPlanId);
125
+ if (!focusedPlan) {
126
+ this.error(`Plan #${focusedPlanId} not found. Use: tt ralph plan list`);
127
+ }
128
+ if (focusedPlan.status === "done") {
129
+ consola.log(colors.yellow(`Plan #${focusedPlanId} is already done.`));
130
+ return;
131
+ }
132
+ }
133
+
134
+ // Get current plan to work on
135
+ const currentPlan = getCurrentPlan(state.plans, focusedPlanId);
136
+ if (!currentPlan) {
137
+ consola.log(colors.green("✅ All plans are done!"));
138
+ return;
139
+ }
140
+
141
+ // Dry run mode
142
+ if (flags.dryRun) {
143
+ consola.log(colors.bold("\n=== DRY RUN ===\n"));
144
+ consola.log(colors.cyan("Config:"));
145
+ consola.log(` Max iterations: ${maxIterations}`);
146
+ consola.log(` State file: ${stateFile}`);
147
+ consola.log(` Log file: ${logFile}`);
148
+ consola.log(` Completion marker: ${flags.completionMarker}`);
149
+ consola.log(` Auto-commit: ${flags.autoCommit}`);
150
+ consola.log(` Claude args: ${[...CLAUDE_DEFAULT_ARGS, ...extraClaudeArgs].join(" ")}`);
151
+ consola.log(` Remaining plans: ${remainingPlans.length}`);
152
+
153
+ consola.log(colors.cyan("\nCurrent plan:"));
154
+ consola.log(` #${currentPlan.id}: ${currentPlan.description}`);
155
+
156
+ // Show prompt preview
157
+ const prompt = buildIterationPrompt({
158
+ completionMarker: flags.completionMarker,
159
+ plan: currentPlan,
160
+ skipCommit: !flags.autoCommit,
161
+ });
162
+ consola.log(colors.dim("─".repeat(60)));
163
+ consola.log(colors.bold("Prompt Preview"));
164
+ consola.log(colors.dim("─".repeat(60)));
165
+ consola.log(prompt);
166
+ consola.log(colors.dim("─".repeat(60)));
167
+
168
+ consola.log(colors.bold("\n=== END DRY RUN ===\n"));
169
+ return;
170
+ }
171
+
172
+ // Check claude CLI is available
173
+ if (!(await checkClaudeCli())) {
174
+ this.error(
175
+ "claude CLI not found in PATH\nInstall Claude Code: https://docs.anthropic.com/en/docs/claude-code",
176
+ );
177
+ }
178
+
179
+ // Update state for this run
180
+ state.status = "running";
181
+
182
+ // Create log stream (append mode)
183
+ const logStream = fs.createWriteStream(logFile, { flags: "a" });
184
+
185
+ const ready = state.plans.filter((t) => t.status === "ready").length;
186
+ const done = state.plans.filter((t) => t.status === "done").length;
187
+
188
+ logStream.write(`\n${"=".repeat(60)}\n`);
189
+ logStream.write(`Ralph Loop Started: ${new Date().toISOString()}\n`);
190
+ logStream.write(`${"=".repeat(60)}\n\n`);
191
+
192
+ consola.log(colors.bold(colors.blue("\nRalph Loop Starting\n")));
193
+ consola.log(colors.dim(`Focus: ${focusedPlanId ? `Plan #${focusedPlanId}` : "Ralph picks"}`));
194
+ consola.log(colors.dim(`Max iterations: ${maxIterations}`));
195
+ consola.log(colors.dim(`Log file: ${logFile}`));
196
+ consola.log(colors.dim(`Auto-commit: ${flags.autoCommit}`));
197
+ consola.log(colors.dim(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)`));
198
+ consola.log("");
199
+
200
+ logStream.write(`Focus: ${focusedPlanId ? `Plan #${focusedPlanId}` : "Ralph picks"}\n`);
201
+ logStream.write(`Max iterations: ${maxIterations}\n`);
202
+ logStream.write(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)\n\n`);
203
+
204
+ // Handle SIGINT gracefully
205
+ let interrupted = false;
206
+ process.on("SIGINT", () => {
207
+ if (interrupted) {
208
+ logStream.end();
209
+ process.exit(130);
210
+ }
211
+ interrupted = true;
212
+ const msg = "\n\nInterrupted. Press Ctrl+C again to force exit.\n";
213
+ consola.log(colors.yellow(msg));
214
+ logStream.write(msg);
215
+ state.status = "error";
216
+ saveState(state, stateFile);
217
+ });
218
+
219
+ // Main loop
220
+ let completed = false;
221
+ let iteration = 0;
222
+
223
+ while (iteration < maxIterations && !interrupted && !completed) {
224
+ iteration++;
225
+
226
+ const iterHeader = `Iteration ${iteration}/${maxIterations}`;
227
+ logStream.write(`\n━━━ ${iterHeader} ━━━\n`);
228
+
229
+ const iterationStart = new Date().toISOString();
230
+ // Get current plan for this iteration
231
+ const plan = getCurrentPlan(state.plans, focusedPlanId);
232
+ if (!plan) {
233
+ completed = true;
234
+ state.status = "completed";
235
+ saveState(state, stateFile);
236
+ consola.log(
237
+ colors.bold(colors.green(`\n✅ All plans completed after ${iteration} iteration(s)`)),
238
+ );
239
+ logStream.write(`\n✅ All plans completed after ${iteration} iteration(s)\n`);
240
+ break;
241
+ }
242
+ const prompt = buildIterationPrompt({
243
+ completionMarker: flags.completionMarker,
244
+ plan: plan,
245
+ skipCommit: !flags.autoCommit,
246
+ });
247
+
248
+ // Log the prompt
249
+ logStream.write(`\n--- Prompt ---\n${prompt}\n--- End Prompt ---\n\n`);
250
+
251
+ // Build claude args
252
+ const iterClaudeArgs = [...extraClaudeArgs];
253
+
254
+ // Print iteration header
255
+ consola.log("");
256
+ consola.log(colors.bold(colors.blue(`━━━ ${iterHeader} ━━━`)));
257
+ consola.log(colors.dim("─".repeat(60)));
258
+ consola.log(colors.bold("Prompt"));
259
+ consola.log(colors.dim("─".repeat(60)));
260
+ consola.log(prompt);
261
+ consola.log(colors.dim("─".repeat(60)));
262
+
263
+ // Run iteration - output goes directly to stdout
264
+ const iterResult = await runIteration(prompt, iterClaudeArgs, logStream);
265
+
266
+ // Reload state from disk to pick up changes made by child claude process
267
+ const freshState = loadState(stateFile);
268
+ if (freshState) {
269
+ Object.assign(state, freshState);
270
+ }
271
+
272
+ const iterationEnd = new Date().toISOString();
273
+ const markerFound = detectCompletionMarker(iterResult.output, flags.completionMarker);
274
+
275
+ // Calculate duration
276
+ const startTime = new Date(iterationStart).getTime();
277
+ const endTime = new Date(iterationEnd).getTime();
278
+ const durationMs = endTime - startTime;
279
+ const durationHuman = formatDuration(durationMs);
280
+
281
+ // Record history
282
+ appendHistory(
283
+ {
284
+ iteration,
285
+ startedAt: iterationStart,
286
+ completedAt: iterationEnd,
287
+ durationMs,
288
+ durationHuman,
289
+ outputSummary: extractOutputSummary(iterResult.output),
290
+ markerFound,
291
+ contextUsedPercent: iterResult.contextUsedPercent,
292
+ },
293
+ ralphPaths.historyFile,
294
+ );
295
+
296
+ // Save state
297
+ saveState(state, stateFile);
298
+
299
+ // Log summary
300
+ const contextInfo =
301
+ iterResult.contextUsedPercent !== undefined
302
+ ? ` | Context: ${iterResult.contextUsedPercent}%`
303
+ : "";
304
+ logStream.write(
305
+ `\n━━━ Iteration ${iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
306
+ );
307
+ consola.log(
308
+ colors.dim(
309
+ `Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ? colors.green("yes") : colors.yellow("no")}`,
310
+ ),
311
+ );
312
+
313
+ // Check completion
314
+ if (markerFound) {
315
+ completed = true;
316
+ state.status = "completed";
317
+ saveState(state, stateFile);
318
+ consola.log(
319
+ colors.bold(colors.green(`\n✅ Plan completed after ${iteration} iteration(s)`)),
320
+ );
321
+ logStream.write(`\n✅ Plan completed after ${iteration} iteration(s)\n`);
322
+ }
323
+ }
324
+
325
+ logStream.end();
326
+
327
+ // Final status
328
+ if (completed) {
329
+ return;
330
+ }
331
+
332
+ if (!interrupted && iteration >= maxIterations) {
333
+ state.status = "max_iterations_reached";
334
+ saveState(state, stateFile);
335
+ consola.log(
336
+ colors.bold(
337
+ colors.yellow(`\n⚠️ Max iterations (${maxIterations}) reached without completion`),
338
+ ),
339
+ );
340
+ consola.log(colors.dim(`State saved to: ${stateFile}`));
341
+ logStream.write(`\n⚠️ Max iterations (${maxIterations}) reached without completion\n`);
342
+ this.exit(1);
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,88 @@
1
+ import { Flags } from "@oclif/core";
2
+ import consola from "consola";
3
+ import { colors } from "consola/utils";
4
+ import { BaseCommand } from "../base.js";
5
+ import { DEFAULT_STATE_FILE, loadState, resolveRalphPath } from "../../lib/ralph/state.js";
6
+ import {
7
+ formatPlanAsMarkdown,
8
+ formatPlanAsJson,
9
+ copyToClipboard,
10
+ } from "../../lib/ralph/formatter.js";
11
+
12
+ /**
13
+ * Show plan summary with status, tasks, and mermaid graph
14
+ */
15
+ export default class Show extends BaseCommand {
16
+ static override description = "Show plan summary with status, tasks, and mermaid graph";
17
+
18
+ static override examples = [
19
+ {
20
+ description: "Show plan summary with mermaid graph",
21
+ command: "<%= config.bin %> <%= command.id %>",
22
+ },
23
+ {
24
+ description: "Output plan as JSON",
25
+ command: "<%= config.bin %> <%= command.id %> --format json",
26
+ },
27
+ {
28
+ description: "Copy plan output to clipboard",
29
+ command: "<%= config.bin %> <%= command.id %> --copy",
30
+ },
31
+ ];
32
+
33
+ static override flags = {
34
+ ...BaseCommand.baseFlags,
35
+ stateFile: Flags.string({
36
+ char: "s",
37
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
38
+ }),
39
+ format: Flags.string({
40
+ char: "f",
41
+ description: "Output format",
42
+ default: "default",
43
+ options: ["default", "markdown", "json"],
44
+ }),
45
+ copy: Flags.boolean({
46
+ char: "c",
47
+ description: "Copy output to clipboard",
48
+ default: false,
49
+ }),
50
+ };
51
+
52
+ async run(): Promise<void> {
53
+ const { flags } = await this.parse(Show);
54
+ const ralphSettings = this.settings.settings.ralphSettings;
55
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
56
+
57
+ const state = loadState(stateFile);
58
+
59
+ if (!state) {
60
+ consola.log(colors.yellow(`No state file found at: ${stateFile}`));
61
+ return;
62
+ }
63
+
64
+ if (state.plans.length === 0) {
65
+ consola.log(colors.yellow("No tasks in state file."));
66
+ consola.log(colors.dim('Use: tt ralph plan add "description"'));
67
+ return;
68
+ }
69
+
70
+ let output: string;
71
+
72
+ if (flags.format === "json") {
73
+ output = formatPlanAsJson(state.plans, state);
74
+ } else {
75
+ output = formatPlanAsMarkdown(state.plans, state);
76
+ }
77
+
78
+ consola.log(output);
79
+
80
+ if (flags.copy) {
81
+ if (copyToClipboard(output)) {
82
+ consola.log(colors.green("✓ Copied to clipboard"));
83
+ } else {
84
+ consola.log(colors.yellow("⚠ Could not copy to clipboard (xclip/xsel not installed?)"));
85
+ }
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,136 @@
1
+ import { z } from "zod/v4";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { homedir } from "node:os";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+
8
+ const TOOL_NAME = "towles-tool";
9
+
10
+ /** Default config directory */
11
+ export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config", TOOL_NAME);
12
+
13
+ /** User settings file path */
14
+ export const USER_SETTINGS_PATH = path.join(DEFAULT_CONFIG_DIR, `${TOOL_NAME}.settings.json`);
15
+
16
+ export const RalphSettingsSchema = z.object({
17
+ // Base directory for ralph files (relative to cwd or absolute)
18
+ stateDir: z.string().default("./.claude/.ralph"),
19
+ });
20
+
21
+ export type RalphSettings = z.infer<typeof RalphSettingsSchema>;
22
+
23
+ export const JournalSettingsSchema = z.object({
24
+ // Base folder where all journal files are stored
25
+ baseFolder: z.string().default(path.join(homedir())),
26
+ // https://moment.github.io/luxon/#/formatting?id=table-of-tokens
27
+ dailyPathTemplate: z
28
+ .string()
29
+ .default(
30
+ path.join(
31
+ "journal/{monday:yyyy}/{monday:MM}/daily-notes/{monday:yyyy}-{monday:MM}-{monday:dd}-daily-notes.md",
32
+ ),
33
+ ),
34
+ meetingPathTemplate: z
35
+ .string()
36
+ .default(path.join("journal/{yyyy}/{MM}/meetings/{yyyy}-{MM}-{dd}-{title}.md")),
37
+ notePathTemplate: z
38
+ .string()
39
+ .default(path.join("journal/{yyyy}/{MM}/notes/{yyyy}-{MM}-{dd}-{title}.md")),
40
+ // Directory for external templates (fallback to hardcoded if not found)
41
+ templateDir: z.string().default(path.join(homedir(), ".config", TOOL_NAME, "templates")),
42
+ });
43
+
44
+ export type JournalSettings = z.infer<typeof JournalSettingsSchema>;
45
+
46
+ export const UserSettingsSchema = z.object({
47
+ preferredEditor: z.string().default("code"),
48
+ journalSettings: JournalSettingsSchema.optional().transform(
49
+ (v) => v ?? JournalSettingsSchema.parse({}),
50
+ ),
51
+ ralphSettings: RalphSettingsSchema.optional().transform(
52
+ (v) => v ?? RalphSettingsSchema.parse({}),
53
+ ),
54
+ });
55
+
56
+ type UserSettings = z.infer<typeof UserSettingsSchema>;
57
+
58
+ export interface SettingsFile {
59
+ settings: UserSettings;
60
+ path: string;
61
+ }
62
+
63
+ function createDefaultSettings(): UserSettings {
64
+ return UserSettingsSchema.parse({});
65
+ }
66
+
67
+ function createAndSaveDefaultSettings(): UserSettings {
68
+ const userSettings = createDefaultSettings();
69
+ saveSettings({
70
+ path: USER_SETTINGS_PATH,
71
+ settings: userSettings,
72
+ });
73
+ return userSettings;
74
+ }
75
+
76
+ export function saveSettings(settingsFile: SettingsFile): void {
77
+ try {
78
+ // Ensure the directory exists
79
+ const dirPath = path.dirname(settingsFile.path);
80
+ if (!fs.existsSync(dirPath)) {
81
+ fs.mkdirSync(dirPath, { recursive: true });
82
+ }
83
+
84
+ fs.writeFileSync(settingsFile.path, JSON.stringify(settingsFile.settings, null, 2), "utf-8");
85
+ } catch (error) {
86
+ consola.error("Error saving user settings file:", error);
87
+ }
88
+ }
89
+
90
+ export async function loadSettings(): Promise<SettingsFile> {
91
+ let userSettings: UserSettings | null = null;
92
+
93
+ // Load user settings
94
+ if (fs.existsSync(USER_SETTINGS_PATH)) {
95
+ const userContent = fs.readFileSync(USER_SETTINGS_PATH, "utf-8");
96
+ const parsedUserSettings: unknown = JSON.parse(userContent);
97
+
98
+ userSettings = UserSettingsSchema.parse(parsedUserSettings);
99
+ // made add a save here if the default values differ from the current values
100
+ if (JSON.stringify(parsedUserSettings) !== JSON.stringify(userSettings)) {
101
+ consola.warn(`Settings file ${USER_SETTINGS_PATH} has been updated with default values.`);
102
+ const tempSettingsFile: SettingsFile = {
103
+ path: USER_SETTINGS_PATH,
104
+ settings: userSettings,
105
+ };
106
+
107
+ saveSettings(tempSettingsFile);
108
+ }
109
+ } else {
110
+ // Settings file doesn't exist
111
+ const isNonInteractive = process.env.CI || !process.stdout.isTTY;
112
+
113
+ if (isNonInteractive) {
114
+ // Auto-create in CI/non-TTY environments
115
+ consola.info(`Creating settings file: ${USER_SETTINGS_PATH}`);
116
+ userSettings = createAndSaveDefaultSettings();
117
+ } else {
118
+ // Interactive: ask user if they want to create it
119
+ const confirmed = await consola.prompt(
120
+ `Settings file not found. Create ${colors.cyan(USER_SETTINGS_PATH)}?`,
121
+ {
122
+ type: "confirm",
123
+ },
124
+ );
125
+ if (!confirmed) {
126
+ throw new Error(`Settings file not found and user chose not to create it.`);
127
+ }
128
+ userSettings = createAndSaveDefaultSettings();
129
+ }
130
+ }
131
+
132
+ return {
133
+ path: USER_SETTINGS_PATH,
134
+ settings: userSettings!,
135
+ };
136
+ }