aiwcli 0.13.4 → 0.13.5

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.
@@ -31,12 +31,12 @@ export default class ClearCommand extends BaseCommand {
31
31
  */
32
32
  private cleanupBackupFiles;
33
33
  /**
34
- * Clean up gitignore entries and prune stale entries.
34
+ * Clean up git exclude entries and prune stale entries.
35
35
  *
36
36
  * @param targetDir - Project root directory
37
- * @returns True if gitignore was updated
37
+ * @returns True if git exclude was updated
38
38
  */
39
- private cleanupGitignore;
39
+ private cleanupGitExclude;
40
40
  /**
41
41
  * Display a list of folders to remove.
42
42
  *
@@ -103,7 +103,7 @@ export default class ClearCommand extends BaseCommand {
103
103
  */
104
104
  private findWorkflowFolders;
105
105
  /**
106
- * Perform all post-deletion cleanup: empty dir removal, gitignore, settings, IDE folders.
106
+ * Perform all post-deletion cleanup: empty dir removal, git exclude, settings, IDE folders.
107
107
  *
108
108
  * @param targetDir - Project root directory
109
109
  * @param methodsToRemove - Method names being removed
@@ -139,7 +139,7 @@ export default class ClearCommand extends BaseCommand {
139
139
  * @param deleteCounts.deletedOutput - Number of output folders deleted
140
140
  * @param deleteCounts.deletedIde - Number of IDE method folders deleted
141
141
  * @param cleanup - Cleanup operation results
142
- * @param cleanup.gitignoreUpdated - Whether gitignore was updated
142
+ * @param cleanup.gitExcludeUpdated - Whether git exclude was updated
143
143
  * @param cleanup.removedOutputDir - Whether _output dir was removed
144
144
  * @param cleanup.removedAiwcliContainer - Whether .aiwcli dir was removed
145
145
  * @param cleanup.removedClaudeDir - Whether .claude dir was removed
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import confirm from '@inquirer/confirm';
4
4
  import { Flags } from '@oclif/core';
5
5
  import BaseCommand from '../lib/base-command.js';
6
- import { computeGitignoreRemovals, pruneGitignoreStaleEntries, removeGitignoreEntries } from '../lib/gitignore-manager.js';
6
+ import { computeExcludeRemovals, pruneExcludeStaleEntries, removeExcludeEntries, resolveGitDir } from '../lib/git-exclude-manager.js';
7
7
  import { pathExists } from '../lib/paths.js';
8
8
  import { getSharedTemplatePath } from '../lib/template-resolver.js';
9
9
  import { reconstructIdeSettings } from '../lib/template-settings-reconstructor.js';
@@ -660,23 +660,26 @@ export default class ClearCommand extends BaseCommand {
660
660
  await Promise.all(cleanups);
661
661
  }
662
662
  /**
663
- * Clean up gitignore entries and prune stale entries.
663
+ * Clean up git exclude entries and prune stale entries.
664
664
  *
665
665
  * @param targetDir - Project root directory
666
- * @returns True if gitignore was updated
666
+ * @returns True if git exclude was updated
667
667
  */
668
- async cleanupGitignore(targetDir) {
669
- const { toRemove, toKeep } = await computeGitignoreRemovals(targetDir);
668
+ async cleanupGitExclude(targetDir) {
669
+ const gitDir = await resolveGitDir(targetDir);
670
+ if (!gitDir)
671
+ return false;
672
+ const { toRemove, toKeep } = await computeExcludeRemovals(gitDir, targetDir);
670
673
  for (const { entry, reason } of toKeep) {
671
- this.logDebug(`Keeping ${entry}/ in .gitignore (${reason})`);
674
+ this.logDebug(`Keeping ${entry}/ in git exclude (${reason})`);
672
675
  }
673
676
  if (toRemove.length > 0) {
674
- await removeGitignoreEntries(targetDir, toRemove);
675
- this.logDebug(`Removed from .gitignore: ${toRemove.join(', ')}`);
677
+ await removeExcludeEntries(gitDir, toRemove);
678
+ this.logDebug(`Removed from git exclude: ${toRemove.join(', ')}`);
676
679
  }
677
- const pruned = await pruneGitignoreStaleEntries(targetDir);
680
+ const pruned = await pruneExcludeStaleEntries(gitDir, targetDir);
678
681
  if (pruned) {
679
- this.logDebug('Pruned stale .gitignore entries');
682
+ this.logDebug('Pruned stale git exclude entries');
680
683
  }
681
684
  return toRemove.length > 0 || pruned;
682
685
  }
@@ -736,14 +739,15 @@ export default class ClearCommand extends BaseCommand {
736
739
  this.logInfo(`${IDE_FOLDERS.windsurf.root}/ folder will be removed (will be empty)`);
737
740
  this.log('');
738
741
  }
739
- // Compute gitignore changes for dry-run display
740
- const gitignoreSimulation = await computeGitignoreRemovals(targetDir);
741
- if (gitignoreSimulation.toRemove.length > 0 || gitignoreSimulation.toKeep.length > 0) {
742
- this.logInfo('Gitignore changes:');
743
- for (const { entry, reason } of gitignoreSimulation.toKeep) {
742
+ // Compute git exclude changes for dry-run display
743
+ const gitDir = await resolveGitDir(targetDir);
744
+ const excludeSimulation = gitDir ? await computeExcludeRemovals(gitDir, targetDir) : { toRemove: [], toKeep: [] };
745
+ if (excludeSimulation.toRemove.length > 0 || excludeSimulation.toKeep.length > 0) {
746
+ this.logInfo('Git exclude changes:');
747
+ for (const { entry, reason } of excludeSimulation.toKeep) {
744
748
  this.log(` keep ${entry}/ (${reason})`);
745
749
  }
746
- for (const entry of gitignoreSimulation.toRemove) {
750
+ for (const entry of excludeSimulation.toRemove) {
747
751
  this.log(` remove ${entry}/`);
748
752
  }
749
753
  this.log('');
@@ -923,7 +927,7 @@ export default class ClearCommand extends BaseCommand {
923
927
  return foundFolders;
924
928
  }
925
929
  /**
926
- * Perform all post-deletion cleanup: empty dir removal, gitignore, settings, IDE folders.
930
+ * Perform all post-deletion cleanup: empty dir removal, git exclude, settings, IDE folders.
927
931
  *
928
932
  * @param targetDir - Project root directory
929
933
  * @param methodsToRemove - Method names being removed
@@ -942,8 +946,8 @@ export default class ClearCommand extends BaseCommand {
942
946
  if (removedAiwcliContainer) {
943
947
  this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/ folder`);
944
948
  }
945
- // Smart gitignore removal
946
- const gitignoreUpdated = await this.cleanupGitignore(targetDir);
949
+ // Smart git exclude removal
950
+ const gitExcludeUpdated = await this.cleanupGitExclude(targetDir);
947
951
  // Reconstruct IDE settings
948
952
  let { updatedClaudeSettings, updatedWindsurfSettings } = await this.reconstructSettingsAfterRemoval(targetDir, methodsToRemove);
949
953
  // Clean up backup files
@@ -957,7 +961,7 @@ export default class ClearCommand extends BaseCommand {
957
961
  updatedWindsurfSettings = false;
958
962
  return {
959
963
  removedOutputDir, removedAiwcliContainer, removedClaudeDir, removedWindsurfDir,
960
- updatedClaudeSettings, updatedWindsurfSettings, gitignoreUpdated,
964
+ updatedClaudeSettings, updatedWindsurfSettings, gitExcludeUpdated,
961
965
  };
962
966
  }
963
967
  /**
@@ -1056,7 +1060,7 @@ export default class ClearCommand extends BaseCommand {
1056
1060
  * @param deleteCounts.deletedOutput - Number of output folders deleted
1057
1061
  * @param deleteCounts.deletedIde - Number of IDE method folders deleted
1058
1062
  * @param cleanup - Cleanup operation results
1059
- * @param cleanup.gitignoreUpdated - Whether gitignore was updated
1063
+ * @param cleanup.gitExcludeUpdated - Whether git exclude was updated
1060
1064
  * @param cleanup.removedOutputDir - Whether _output dir was removed
1061
1065
  * @param cleanup.removedAiwcliContainer - Whether .aiwcli dir was removed
1062
1066
  * @param cleanup.removedClaudeDir - Whether .claude dir was removed
@@ -1082,8 +1086,8 @@ export default class ClearCommand extends BaseCommand {
1082
1086
  if (cleanup.removedWindsurfDir)
1083
1087
  parts.push(`${IDE_FOLDERS.windsurf.root}/ folder`);
1084
1088
  this.logSuccess(`Cleared: ${parts.join(', ')}.`);
1085
- if (cleanup.gitignoreUpdated) {
1086
- this.logSuccess('Updated .gitignore.');
1089
+ if (cleanup.gitExcludeUpdated) {
1090
+ this.logSuccess('Updated git exclude.');
1087
1091
  }
1088
1092
  if (cleanup.updatedClaudeSettings) {
1089
1093
  this.logSuccess('Updated .claude/settings.json (backup: settings.json.backup).');
@@ -41,7 +41,7 @@ export default class Init extends BaseCommand {
41
41
  * Perform minimal installation (_shared folder only, no template method).
42
42
  *
43
43
  * @param targetDir - Target directory for installation
44
- * @param hasGit - Whether git repository exists
44
+ * @param gitDir - Resolved git directory path, or null if not a git repo
45
45
  */
46
46
  private performMinimalInstall;
47
47
  /**
@@ -56,8 +56,8 @@ export default class Init extends BaseCommand {
56
56
  * @param config.targetDir - Project directory
57
57
  * @param config.method - Method name that was installed
58
58
  * @param config.ides - IDEs that were configured
59
- * @param config.hasGit - Whether git repository exists
60
- * @param config.foldersForGitignore - Folders to add to .gitignore
59
+ * @param config.gitDir - Resolved git directory path, or null if not a git repo
60
+ * @param config.foldersForExclude - Folders to add to git exclude
61
61
  */
62
62
  private performPostInstallActions;
63
63
  /**
@@ -8,7 +8,7 @@ import input from '@inquirer/input';
8
8
  import select from '@inquirer/select';
9
9
  import { Flags } from '@oclif/core';
10
10
  import BaseCommand from '../../lib/base-command.js';
11
- import { AIW_GITIGNORE_ENTRIES, updateGitignore } from '../../lib/gitignore-manager.js';
11
+ import { AIW_EXCLUDE_ENTRIES, resolveGitDir, updateGitExclude } from '../../lib/git-exclude-manager.js';
12
12
  import { IdePathResolver } from '../../lib/ide-path-resolver.js';
13
13
  import { pathExists } from '../../lib/paths.js';
14
14
  import { getTargetSettingsFile, readClaudeSettings, writeClaudeSettings } from '../../lib/settings-hierarchy.js';
@@ -24,20 +24,7 @@ const AVAILABLE_IDES = [
24
24
  { value: 'claude', name: 'Claude Code', description: 'Anthropic Claude Code CLI' },
25
25
  { value: 'windsurf', name: 'Windsurf', description: 'Codeium Windsurf IDE' },
26
26
  ];
27
- /**
28
- * Detect if current directory is a git repository.
29
- * Checks for .git directory existence.
30
- */
31
- async function detectGitRepository(targetDir) {
32
- try {
33
- const gitPath = join(targetDir, '.git');
34
- await fs.access(gitPath);
35
- return true;
36
- }
37
- catch {
38
- return false;
39
- }
40
- }
27
+ // detectGitRepository replaced by resolveGitDir from git-exclude-manager
41
28
  /**
42
29
  * Extract project name from directory path.
43
30
  * Returns the basename of the given directory.
@@ -82,12 +69,12 @@ export default class Init extends BaseCommand {
82
69
  // Get available templates for validation
83
70
  const availableTemplates = await getAvailableTemplates();
84
71
  // Check git repository early (needed by both install paths)
85
- const hasGit = await detectGitRepository(targetDir);
72
+ const gitDir = await resolveGitDir(targetDir);
86
73
  // Resolve installation configuration from flags or interactive wizard
87
74
  const config = await this.resolveInstallationConfig(flags, targetDir, availableTemplates);
88
75
  // If config is null, perform minimal install (shared folder only)
89
76
  if (!config) {
90
- await this.performMinimalInstall(targetDir, hasGit);
77
+ await this.performMinimalInstall(targetDir, gitDir);
91
78
  return;
92
79
  }
93
80
  const { method, ides, username, projectName } = config;
@@ -144,22 +131,22 @@ export default class Init extends BaseCommand {
144
131
  projectName,
145
132
  templatePath,
146
133
  });
147
- // Collect all folders that need gitignore entries
134
+ // Collect all folders that need exclude entries
148
135
  // The .aiwcli/ container holds all template infrastructure and runtime data
149
- const foldersForGitignore = [...AIW_GITIGNORE_ENTRIES];
136
+ const foldersForExclude = [...AIW_EXCLUDE_ENTRIES];
150
137
  // Report installation results
151
138
  if (result.installedFolders.length > 0) {
152
139
  this.logSuccess(`✓ Installed: ${result.installedFolders.join(', ')}`);
153
140
  }
154
141
  // Install global resolver for cwd-drift-proof hook/status line commands
155
142
  await this.installGlobalResolver();
156
- // Perform post-installation actions (settings tracking, hook merging, gitignore updates)
143
+ // Perform post-installation actions (settings tracking, hook merging, git exclude updates)
157
144
  await this.performPostInstallActions({
158
145
  targetDir,
159
146
  method,
160
147
  ides,
161
- hasGit,
162
- foldersForGitignore,
148
+ gitDir,
149
+ foldersForExclude,
163
150
  });
164
151
  this.log('');
165
152
  this.logSuccess(`✓ ${method} initialized successfully`);
@@ -261,9 +248,9 @@ export default class Init extends BaseCommand {
261
248
  * Perform minimal installation (_shared folder only, no template method).
262
249
  *
263
250
  * @param targetDir - Target directory for installation
264
- * @param hasGit - Whether git repository exists
251
+ * @param gitDir - Resolved git directory path, or null if not a git repo
265
252
  */
266
- async performMinimalInstall(targetDir, hasGit) {
253
+ async performMinimalInstall(targetDir, gitDir) {
267
254
  this.logInfo('Performing minimal installation (_shared folder only)...');
268
255
  this.log('');
269
256
  // Create .aiwcli container and install _shared
@@ -292,10 +279,10 @@ export default class Init extends BaseCommand {
292
279
  await this.installGlobalResolver();
293
280
  // Reconstruct settings from _shared template
294
281
  await reconstructIdeSettings(targetDir, [], ['claude']);
295
- // Update .gitignore if git repository exists
296
- if (hasGit) {
297
- await updateGitignore(targetDir, [...AIW_GITIGNORE_ENTRIES]);
298
- this.logSuccess('✓ .gitignore updated');
282
+ // Update git exclude if git repository exists
283
+ if (gitDir) {
284
+ await updateGitExclude(gitDir, [...AIW_EXCLUDE_ENTRIES]);
285
+ this.logSuccess('✓ git exclude updated');
299
286
  }
300
287
  this.log('');
301
288
  this.logSuccess('✓ Minimal installation completed successfully');
@@ -316,11 +303,11 @@ export default class Init extends BaseCommand {
316
303
  * @param config.targetDir - Project directory
317
304
  * @param config.method - Method name that was installed
318
305
  * @param config.ides - IDEs that were configured
319
- * @param config.hasGit - Whether git repository exists
320
- * @param config.foldersForGitignore - Folders to add to .gitignore
306
+ * @param config.gitDir - Resolved git directory path, or null if not a git repo
307
+ * @param config.foldersForExclude - Folders to add to git exclude
321
308
  */
322
309
  async performPostInstallActions(config) {
323
- const { targetDir, method, ides, hasGit, foldersForGitignore } = config;
310
+ const { targetDir, method, ides, gitDir, foldersForExclude } = config;
324
311
  // Track method installation in settings.json first (so reconstructor can read methods list)
325
312
  await this.trackMethodInstallation(targetDir, method, ides);
326
313
  // Read installed methods to build active templates list
@@ -330,10 +317,10 @@ export default class Init extends BaseCommand {
330
317
  // Reconstruct IDE settings from all active templates
331
318
  await reconstructIdeSettings(targetDir, activeTemplates, ides);
332
319
  this.logSuccess('✓ Reconstructed IDE settings from active templates');
333
- // Update .gitignore if git repository exists
334
- if (hasGit) {
335
- await updateGitignore(targetDir, foldersForGitignore);
336
- this.logSuccess('✓ .gitignore updated');
320
+ // Update git exclude if git repository exists
321
+ if (gitDir) {
322
+ await updateGitExclude(gitDir, foldersForExclude);
323
+ this.logSuccess('✓ git exclude updated');
337
324
  }
338
325
  }
339
326
  /**
@@ -0,0 +1,59 @@
1
+ /** Standard exclude entries managed by AIW */
2
+ export declare const AIW_EXCLUDE_ENTRIES: string[];
3
+ /**
4
+ * Resolve the git directory for a given target directory.
5
+ * Handles normal repos, worktrees, and submodules via `git rev-parse --git-dir`.
6
+ *
7
+ * @param targetDir - Directory to resolve git dir for
8
+ * @returns Absolute path to the git directory, or null if not a git repo
9
+ */
10
+ export declare function resolveGitDir(targetDir: string): Promise<string | null>;
11
+ /**
12
+ * Prune stale entries from the AIW Installation section in git exclude file.
13
+ * Checks each entry against disk existence and removes entries whose paths don't exist.
14
+ * Removes the entire section if no entries remain after pruning.
15
+ *
16
+ * @param gitDir - Git directory (contains info/exclude)
17
+ * @param targetDir - Project root directory (for disk-existence checks)
18
+ * @returns True if any entries were pruned
19
+ */
20
+ export declare function pruneExcludeStaleEntries(gitDir: string, targetDir: string): Promise<boolean>;
21
+ /**
22
+ * Update git exclude file with patterns for installed folders.
23
+ *
24
+ * Creates exclude file if it doesn't exist, or appends to existing file.
25
+ * Prevents duplicate patterns by checking each pattern individually.
26
+ *
27
+ * @param gitDir - Git directory (contains info/exclude)
28
+ * @param folders - List of folder names to add as exclude patterns (e.g., ['.aiwcli', '.claude'])
29
+ */
30
+ export declare function updateGitExclude(gitDir: string, folders: string[]): Promise<void>;
31
+ /**
32
+ * Compute which AIW exclude entries should be removed during clear.
33
+ * Returns a simulation result — the caller decides whether to apply.
34
+ *
35
+ * Logic per entry:
36
+ * - If in permanentEntries → keep (reason: "permanent")
37
+ * - If directory exists and is non-empty → keep (reason: "directory has content")
38
+ * - Otherwise → mark for removal
39
+ *
40
+ * @param gitDir - Git directory (contains info/exclude)
41
+ * @param targetDir - Project root directory (for disk-existence checks)
42
+ * @param permanentEntries - Entries that should never be removed (defaults to AIW_PERMANENT_ENTRIES)
43
+ * @returns Lists of entries to remove and entries to keep with reasons
44
+ */
45
+ export declare function computeExcludeRemovals(gitDir: string, targetDir: string, permanentEntries?: string[]): Promise<{
46
+ toKeep: Array<{
47
+ entry: string;
48
+ reason: string;
49
+ }>;
50
+ toRemove: string[];
51
+ }>;
52
+ /**
53
+ * Remove specific entries from the AIW section in git exclude file.
54
+ * Cleans up the section header if no entries remain.
55
+ *
56
+ * @param gitDir - Git directory (contains info/exclude)
57
+ * @param entriesToRemove - Entry names to remove (without trailing slash)
58
+ */
59
+ export declare function removeExcludeEntries(gitDir: string, entriesToRemove: string[]): Promise<void>;
@@ -1,27 +1,60 @@
1
+ import { execFile } from 'node:child_process';
1
2
  import { promises as fs } from 'node:fs';
2
- import { join } from 'node:path';
3
+ import { isAbsolute, join } from 'node:path';
4
+ import { promisify } from 'node:util';
3
5
  import { pathExists } from './paths.js';
6
+ const execFileAsync = promisify(execFile);
4
7
  /**
5
- * AIW gitignore section header marker
8
+ * AIW exclude section header marker
6
9
  */
7
- const AIW_GITIGNORE_HEADER = '# AIW Installation';
8
- /** Standard gitignore entries managed by AIW */
9
- export const AIW_GITIGNORE_ENTRIES = ['.aiwcli', '_output', '.claude', '.windsurf'];
10
- /** Entries that should NEVER be removed from gitignore, even on clear */
10
+ const AIW_EXCLUDE_HEADER = '# AIW Installation';
11
+ /** Standard exclude entries managed by AIW */
12
+ export const AIW_EXCLUDE_ENTRIES = ['.aiwcli', '_output', '.claude', '.windsurf'];
13
+ /** Entries that should NEVER be removed from exclude, even on clear */
11
14
  const AIW_PERMANENT_ENTRIES = ['_output'];
12
15
  /**
13
- * Prune stale entries from the AIW Installation section in .gitignore.
16
+ * Resolve the git directory for a given target directory.
17
+ * Handles normal repos, worktrees, and submodules via `git rev-parse --git-dir`.
18
+ *
19
+ * @param targetDir - Directory to resolve git dir for
20
+ * @returns Absolute path to the git directory, or null if not a git repo
21
+ */
22
+ export async function resolveGitDir(targetDir) {
23
+ try {
24
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: targetDir });
25
+ const gitDir = stdout.trim();
26
+ return isAbsolute(gitDir) ? gitDir : join(targetDir, gitDir);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ /**
33
+ * Get the path to the exclude file within the git directory.
34
+ */
35
+ function getExcludePath(gitDir) {
36
+ return join(gitDir, 'info', 'exclude');
37
+ }
38
+ /**
39
+ * Ensure the info/ directory exists within the git directory.
40
+ */
41
+ async function ensureInfoDir(gitDir) {
42
+ await fs.mkdir(join(gitDir, 'info'), { recursive: true });
43
+ }
44
+ /**
45
+ * Prune stale entries from the AIW Installation section in git exclude file.
14
46
  * Checks each entry against disk existence and removes entries whose paths don't exist.
15
47
  * Removes the entire section if no entries remain after pruning.
16
48
  *
17
- * @param targetDir - Directory containing .gitignore
49
+ * @param gitDir - Git directory (contains info/exclude)
50
+ * @param targetDir - Project root directory (for disk-existence checks)
18
51
  * @returns True if any entries were pruned
19
52
  */
20
- export async function pruneGitignoreStaleEntries(targetDir) {
21
- const gitignorePath = join(targetDir, '.gitignore');
53
+ export async function pruneExcludeStaleEntries(gitDir, targetDir) {
54
+ const excludePath = getExcludePath(gitDir);
22
55
  try {
23
- const content = await fs.readFile(gitignorePath, 'utf8');
24
- if (!content.includes(AIW_GITIGNORE_HEADER)) {
56
+ const content = await fs.readFile(excludePath, 'utf8');
57
+ if (!content.includes(AIW_EXCLUDE_HEADER)) {
25
58
  return false;
26
59
  }
27
60
  const lines = content.split('\n');
@@ -30,7 +63,7 @@ export async function pruneGitignoreStaleEntries(targetDir) {
30
63
  const aiwSectionLines = [];
31
64
  let pruned = false;
32
65
  for (const line of lines) {
33
- if (line === AIW_GITIGNORE_HEADER) {
66
+ if (line === AIW_EXCLUDE_HEADER) {
34
67
  inAiwSection = true;
35
68
  aiwSectionLines.push(line);
36
69
  continue;
@@ -40,7 +73,7 @@ export async function pruneGitignoreStaleEntries(targetDir) {
40
73
  continue;
41
74
  }
42
75
  // AIW section ends at empty line or another comment header
43
- if (line === '' || (line.startsWith('#') && line !== AIW_GITIGNORE_HEADER)) {
76
+ if (line === '' || (line.startsWith('#') && line !== AIW_EXCLUDE_HEADER)) {
44
77
  inAiwSection = false;
45
78
  const { lines: filtered, pruned: sectionPruned } = await pruneSection(aiwSectionLines, targetDir); // eslint-disable-line no-await-in-loop
46
79
  if (sectionPruned)
@@ -68,7 +101,7 @@ export async function pruneGitignoreStaleEntries(targetDir) {
68
101
  if (result.trim() === '') {
69
102
  result = '';
70
103
  }
71
- await fs.writeFile(gitignorePath, result, 'utf8');
104
+ await fs.writeFile(excludePath, result, 'utf8');
72
105
  return true;
73
106
  }
74
107
  catch {
@@ -77,14 +110,14 @@ export async function pruneGitignoreStaleEntries(targetDir) {
77
110
  }
78
111
  /**
79
112
  * Prune stale entries from a parsed AIW section.
80
- * Checks each gitignore pattern against disk existence.
113
+ * Checks each exclude pattern against disk existence.
81
114
  */
82
115
  async function pruneSection(sectionLines, targetDir) {
83
116
  let pruned = false;
84
117
  const filtered = [];
85
118
  for (const line of sectionLines) {
86
119
  // Always keep the header
87
- if (line === AIW_GITIGNORE_HEADER) {
120
+ if (line === AIW_EXCLUDE_HEADER) {
88
121
  filtered.push(line);
89
122
  continue;
90
123
  }
@@ -108,7 +141,7 @@ function cleanupEmptySections(content) {
108
141
  const newLines = [];
109
142
  for (let i = 0; i < lines.length; i++) {
110
143
  const line = lines[i];
111
- if (line === AIW_GITIGNORE_HEADER) {
144
+ if (line === AIW_EXCLUDE_HEADER) {
112
145
  // Look ahead to see if there are any patterns
113
146
  const nextLine = lines[i + 1];
114
147
  if (nextLine === undefined || nextLine === '' || nextLine.startsWith('#')) {
@@ -125,20 +158,21 @@ function cleanupEmptySections(content) {
125
158
  return newLines.join('\n');
126
159
  }
127
160
  /**
128
- * Update .gitignore with patterns for installed folders.
161
+ * Update git exclude file with patterns for installed folders.
129
162
  *
130
- * Creates .gitignore if it doesn't exist, or appends to existing file.
163
+ * Creates exclude file if it doesn't exist, or appends to existing file.
131
164
  * Prevents duplicate patterns by checking each pattern individually.
132
165
  *
133
- * @param targetDir - Directory containing .gitignore file
134
- * @param folders - List of folder names to add as gitignore patterns (e.g., ['_bmad', '.claude'])
166
+ * @param gitDir - Git directory (contains info/exclude)
167
+ * @param folders - List of folder names to add as exclude patterns (e.g., ['.aiwcli', '.claude'])
135
168
  */
136
- export async function updateGitignore(targetDir, folders) {
137
- const gitignorePath = join(targetDir, '.gitignore');
169
+ export async function updateGitExclude(gitDir, folders) {
170
+ await ensureInfoDir(gitDir);
171
+ const excludePath = getExcludePath(gitDir);
138
172
  try {
139
- // Try to read existing .gitignore
140
- const existing = await fs.readFile(gitignorePath, 'utf8');
141
- // Filter out patterns that already exist in .gitignore
173
+ // Try to read existing exclude file
174
+ const existing = await fs.readFile(excludePath, 'utf8');
175
+ // Filter out patterns that already exist in exclude file
142
176
  const newPatterns = folders.filter((folder) => {
143
177
  const pattern = `${folder}/`;
144
178
  // Check if this exact pattern exists in the file
@@ -173,17 +207,17 @@ export async function updateGitignore(targetDir, folders) {
173
207
  const separator = existing.length > 0 && existing.endsWith('\n') ? '\n' : existing.length > 0 ? '\n\n' : '';
174
208
  updatedContent = existing + separator + `# AIW Installation\n${patterns}\n`;
175
209
  }
176
- await fs.writeFile(gitignorePath, updatedContent, 'utf8');
210
+ await fs.writeFile(excludePath, updatedContent, 'utf8');
177
211
  }
178
212
  catch {
179
- // .gitignore doesn't exist, create it
213
+ // Exclude file doesn't exist, create it
180
214
  const patterns = folders.map((folder) => `${folder}/`).join('\n');
181
215
  const patternsBlock = `# AIW Installation\n${patterns}`;
182
- await fs.writeFile(gitignorePath, patternsBlock + '\n', 'utf8');
216
+ await fs.writeFile(excludePath, patternsBlock + '\n', 'utf8');
183
217
  }
184
218
  }
185
219
  /**
186
- * Compute which AIW gitignore entries should be removed during clear.
220
+ * Compute which AIW exclude entries should be removed during clear.
187
221
  * Returns a simulation result — the caller decides whether to apply.
188
222
  *
189
223
  * Logic per entry:
@@ -191,23 +225,24 @@ export async function updateGitignore(targetDir, folders) {
191
225
  * - If directory exists and is non-empty → keep (reason: "directory has content")
192
226
  * - Otherwise → mark for removal
193
227
  *
194
- * @param targetDir - Directory containing .gitignore
228
+ * @param gitDir - Git directory (contains info/exclude)
229
+ * @param targetDir - Project root directory (for disk-existence checks)
195
230
  * @param permanentEntries - Entries that should never be removed (defaults to AIW_PERMANENT_ENTRIES)
196
231
  * @returns Lists of entries to remove and entries to keep with reasons
197
232
  */
198
- export async function computeGitignoreRemovals(targetDir, permanentEntries = AIW_PERMANENT_ENTRIES) {
199
- const gitignorePath = join(targetDir, '.gitignore');
233
+ export async function computeExcludeRemovals(gitDir, targetDir, permanentEntries = AIW_PERMANENT_ENTRIES) {
234
+ const excludePath = getExcludePath(gitDir);
200
235
  const toRemove = [];
201
236
  const toKeep = [];
202
- // Read AIW section entries from .gitignore
237
+ // Read AIW section entries from exclude file
203
238
  let content;
204
239
  try {
205
- content = await fs.readFile(gitignorePath, 'utf8');
240
+ content = await fs.readFile(excludePath, 'utf8');
206
241
  }
207
242
  catch {
208
243
  return { toRemove, toKeep };
209
244
  }
210
- if (!content.includes(AIW_GITIGNORE_HEADER)) {
245
+ if (!content.includes(AIW_EXCLUDE_HEADER)) {
211
246
  return { toRemove, toKeep };
212
247
  }
213
248
  // Parse entries from the AIW section
@@ -215,12 +250,12 @@ export async function computeGitignoreRemovals(targetDir, permanentEntries = AIW
215
250
  let inAiwSection = false;
216
251
  const aiwEntries = [];
217
252
  for (const line of lines) {
218
- if (line === AIW_GITIGNORE_HEADER) {
253
+ if (line === AIW_EXCLUDE_HEADER) {
219
254
  inAiwSection = true;
220
255
  continue;
221
256
  }
222
257
  if (inAiwSection) {
223
- if (line === '' || (line.startsWith('#') && line !== AIW_GITIGNORE_HEADER)) {
258
+ if (line === '' || (line.startsWith('#') && line !== AIW_EXCLUDE_HEADER)) {
224
259
  inAiwSection = false;
225
260
  }
226
261
  else {
@@ -261,17 +296,17 @@ export async function computeGitignoreRemovals(targetDir, permanentEntries = AIW
261
296
  return { toRemove, toKeep };
262
297
  }
263
298
  /**
264
- * Remove specific entries from the AIW section in .gitignore.
299
+ * Remove specific entries from the AIW section in git exclude file.
265
300
  * Cleans up the section header if no entries remain.
266
301
  *
267
- * @param targetDir - Directory containing .gitignore
302
+ * @param gitDir - Git directory (contains info/exclude)
268
303
  * @param entriesToRemove - Entry names to remove (without trailing slash)
269
304
  */
270
- export async function removeGitignoreEntries(targetDir, entriesToRemove) {
271
- const gitignorePath = join(targetDir, '.gitignore');
305
+ export async function removeExcludeEntries(gitDir, entriesToRemove) {
306
+ const excludePath = getExcludePath(gitDir);
272
307
  try {
273
- const content = await fs.readFile(gitignorePath, 'utf8');
274
- if (!content.includes(AIW_GITIGNORE_HEADER)) {
308
+ const content = await fs.readFile(excludePath, 'utf8');
309
+ if (!content.includes(AIW_EXCLUDE_HEADER)) {
275
310
  return;
276
311
  }
277
312
  const patternsToRemove = new Set(entriesToRemove.map((e) => `${e}/`));
@@ -280,16 +315,16 @@ export async function removeGitignoreEntries(targetDir, entriesToRemove) {
280
315
  let inAiwSection = false;
281
316
  const aiwSectionLines = [];
282
317
  for (const line of lines) {
283
- if (line === AIW_GITIGNORE_HEADER) {
318
+ if (line === AIW_EXCLUDE_HEADER) {
284
319
  inAiwSection = true;
285
320
  aiwSectionLines.push(line);
286
321
  continue;
287
322
  }
288
323
  if (inAiwSection) {
289
- if (line === '' || (line.startsWith('#') && line !== AIW_GITIGNORE_HEADER)) {
324
+ if (line === '' || (line.startsWith('#') && line !== AIW_EXCLUDE_HEADER)) {
290
325
  inAiwSection = false;
291
326
  // Filter the AIW section
292
- const filtered = aiwSectionLines.filter((l) => l === AIW_GITIGNORE_HEADER || !patternsToRemove.has(l));
327
+ const filtered = aiwSectionLines.filter((l) => l === AIW_EXCLUDE_HEADER || !patternsToRemove.has(l));
293
328
  newLines.push(...filtered, line);
294
329
  }
295
330
  else {
@@ -302,7 +337,7 @@ export async function removeGitignoreEntries(targetDir, entriesToRemove) {
302
337
  }
303
338
  // Handle AIW section at end of file
304
339
  if (inAiwSection) {
305
- const filtered = aiwSectionLines.filter((l) => l === AIW_GITIGNORE_HEADER || !patternsToRemove.has(l));
340
+ const filtered = aiwSectionLines.filter((l) => l === AIW_EXCLUDE_HEADER || !patternsToRemove.has(l));
306
341
  newLines.push(...filtered);
307
342
  }
308
343
  // Clean up empty AIW section
@@ -311,9 +346,9 @@ export async function removeGitignoreEntries(targetDir, entriesToRemove) {
311
346
  if (result.trim() === '') {
312
347
  result = '';
313
348
  }
314
- await fs.writeFile(gitignorePath, result, 'utf8');
349
+ await fs.writeFile(excludePath, result, 'utf8');
315
350
  }
316
351
  catch {
317
- // .gitignore doesn't exist or can't be read
352
+ // Exclude file doesn't exist or can't be read
318
353
  }
319
354
  }
@@ -138,6 +138,17 @@ export function isExecSyncError(e: unknown): e is ExecSyncError {
138
138
  * On non-Windows platforms, returns the string unchanged (execFile
139
139
  * handles quoting automatically without shell).
140
140
  */
141
+ /**
142
+ * Normalize a file path for CLI subprocess usage.
143
+ * Replaces backslashes with forward slashes on Windows to avoid
144
+ * cmd.exe escape interpretation issues. No-op on other platforms.
145
+ * Forward slashes are valid path separators on Windows in all modern CLIs.
146
+ */
147
+ export function normalizePathForCli(p: string): string {
148
+ if (process.platform !== "win32") return p;
149
+ return p.replaceAll("\\", "/");
150
+ }
151
+
141
152
  export function shellQuoteWin(arg: string): string {
142
153
  if (process.platform !== "win32") return arg;
143
154
  return '"' + arg.replaceAll('"', '""') + '"';
@@ -199,6 +199,15 @@ function isWideChar(cp: number): boolean {
199
199
  );
200
200
  }
201
201
 
202
+ function measureWidth(str: string): number {
203
+ const stripped = str.replace(ANSI_RE, "");
204
+ let w = 0;
205
+ for (const ch of stripped) {
206
+ w += isWideChar(ch.codePointAt(0) ?? 0) ? 2 : 1;
207
+ }
208
+ return w;
209
+ }
210
+
202
211
  function truncateToWidth(str: string, maxWidth: number): string {
203
212
  // Walk through string preserving ANSI codes (zero-width) and measuring visible chars
204
213
  let w = 0;
@@ -244,9 +253,8 @@ function truncateToWidth(str: string, maxWidth: number): string {
244
253
  // Separator (dynamic width)
245
254
  // ---------------------------------------------------------------------------
246
255
 
247
- function makeSeparator(termWidth: number): string {
248
- const w = Math.min(termWidth, 120);
249
- return `${SLATE_600}${"─".repeat(w)}${RESET}`;
256
+ function makeSeparator(width: number): string {
257
+ return `${SLATE_600}${"─".repeat(Math.max(1, width))}${RESET}`;
250
258
  }
251
259
 
252
260
  // ---------------------------------------------------------------------------
@@ -276,7 +284,7 @@ function renderContext(
276
284
  contextK: number,
277
285
  maxK: number,
278
286
  modelName: string,
279
- ): void {
287
+ ): string {
280
288
  let pctColor: string;
281
289
  if (contextPct <= 33) pctColor = EMERALD;
282
290
  else if (contextPct <= 66) pctColor = AMBER;
@@ -342,8 +350,7 @@ function renderContext(
342
350
  }
343
351
  }
344
352
 
345
- console.log(truncateToWidth(line, termWidth));
346
- console.log(makeSeparator(termWidth));
353
+ return truncateToWidth(line, termWidth);
347
354
  }
348
355
 
349
356
  // ---------------------------------------------------------------------------
@@ -461,14 +468,16 @@ function renderGit(
461
468
  termWidth: number,
462
469
  git: GitStatus | null,
463
470
  dirName: string,
464
- ): void {
471
+ ): string {
465
472
  const totalChanged = git ? git.modified + git.staged : 0;
466
473
  const statusIcon =
467
474
  git && (totalChanged > 0 || git.untracked > 0) ? "*" : "\u2713";
468
475
 
476
+ let line: string;
477
+
469
478
  switch (mode) {
470
479
  case "micro": {
471
- let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
480
+ line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
472
481
  if (git) {
473
482
  line += ` ${GIT_VALUE}${git.branch}${RESET}`;
474
483
  if (git.age_display) {
@@ -481,12 +490,10 @@ function renderGit(
481
490
  line += `${GIT_MODIFIED}${statusIcon}${totalChanged}${RESET}`;
482
491
  }
483
492
  }
484
- console.log(truncateToWidth(line, termWidth));
485
-
486
493
  break;
487
494
  }
488
495
  case "mini": {
489
- let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
496
+ line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
490
497
  if (git) {
491
498
  line += ` ${SLATE_600}\u2502${RESET} ${GIT_VALUE}${git.branch}${RESET}`;
492
499
  if (git.age_display) {
@@ -502,12 +509,10 @@ function renderGit(
502
509
  }
503
510
  }
504
511
  }
505
- console.log(truncateToWidth(line, termWidth));
506
-
507
512
  break;
508
513
  }
509
514
  case "nano": {
510
- let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
515
+ line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_DIR}${dirName}${RESET}`;
511
516
  if (git) {
512
517
  line += ` ${GIT_VALUE}${git.branch}${RESET} `;
513
518
  if (statusIcon === "\u2713") {
@@ -516,12 +521,10 @@ function renderGit(
516
521
  line += `${GIT_MODIFIED}*${totalChanged}${RESET}`;
517
522
  }
518
523
  }
519
- console.log(truncateToWidth(line, termWidth));
520
-
521
524
  break;
522
525
  }
523
526
  default: {
524
- let line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_PRIMARY}PWD:${RESET} ${GIT_DIR}${dirName}${RESET}`;
527
+ line = `${GIT_PRIMARY}\u25C8${RESET} ${GIT_PRIMARY}PWD:${RESET} ${GIT_DIR}${dirName}${RESET}`;
525
528
  if (git) {
526
529
  line +=
527
530
  ` ${SLATE_600}\u2502${RESET} ` +
@@ -556,9 +559,10 @@ function renderGit(
556
559
  }
557
560
  }
558
561
  }
559
- console.log(truncateToWidth(line, termWidth));
560
562
  }
561
563
  }
564
+
565
+ return truncateToWidth(line, termWidth);
562
566
  }
563
567
 
564
568
  // ---------------------------------------------------------------------------
@@ -587,7 +591,7 @@ function renderContextManager(
587
591
  mode: string,
588
592
  contextId: string,
589
593
  contextState: Record<string, unknown> | null,
590
- ): void {
594
+ ): string {
591
595
  // Strip YYMMDD-HHMM- timestamp prefix from context ID for display
592
596
  let displayId = contextId.replace(/^\d{6}-\d{4}-/, "");
593
597
  if (!displayId) displayId = contextId;
@@ -650,47 +654,24 @@ function renderContextManager(
650
654
  planPart = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Plan:${RESET} ${SLATE_300}${truncatedPlan}${RESET}`;
651
655
  }
652
656
 
657
+ // Note: termWidth not available here — caller truncates via main()
653
658
  switch (mode) {
654
- case "micro": {
655
- console.log(
656
- `${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`,
657
- );
658
-
659
- break;
660
- }
661
- case "mini": {
662
- console.log(
663
- `${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}` +
664
- `${modeBadge}${planPart}`,
665
- );
666
-
667
- break;
668
- }
669
- case "nano": {
670
- console.log(
671
- `${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`,
672
- );
673
-
674
- break;
675
- }
676
- default: {
677
- console.log(
678
- `${CTX_ACCENT}\u25C6${RESET} ${CTX_SECONDARY}Context:${RESET} ${SLATE_300}${truncatedId}${RESET}` +
679
- `${modeBadge}${planPart}`,
680
- );
681
- }
659
+ case "micro":
660
+ case "nano":
661
+ return `${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}`;
662
+ case "mini":
663
+ return `${CTX_ACCENT}\u25C6${RESET} ${SLATE_400}${truncatedId}${RESET}${modeBadge}${planPart}`;
664
+ default:
665
+ return `${CTX_ACCENT}\u25C6${RESET} ${CTX_SECONDARY}Context:${RESET} ${SLATE_300}${truncatedId}${RESET}${modeBadge}${planPart}`;
682
666
  }
683
667
  }
684
668
 
685
- function renderNoContext(mode: string): void {
669
+ function renderNoContext(mode: string): string {
686
670
  const warn = `${ROSE}\u26A0 ${RESET}`;
687
671
  if (mode === "normal") {
688
- console.log(
689
- `${warn} ${ROSE}NO CONTEXT${RESET} ${SLATE_500}\u2014 type ^ for context manager${RESET}`,
690
- );
691
- } else {
692
- console.log(`${warn} ${ROSE}NO CONTEXT${RESET}`);
672
+ return `${warn} ${ROSE}NO CONTEXT${RESET} ${SLATE_500}\u2014 type ^ for context manager${RESET}`;
693
673
  }
674
+ return `${warn} ${ROSE}NO CONTEXT${RESET}`;
694
675
  }
695
676
 
696
677
  // ---------------------------------------------------------------------------
@@ -826,22 +807,35 @@ function main(): void {
826
807
  // Resolve context ID for display and persistence
827
808
  const contextId = resolveContextId(sessionId);
828
809
 
829
- // Render context section
830
- renderContext(mode, termWidth, contextPct, contextK, maxK, modelName);
831
-
832
- // Render PWD + git section (PWD always shown, git stats only when in a repo)
810
+ // Render content lines first, truncate all to terminal width, then size separators
811
+ const contextLine = renderContext(mode, termWidth, contextPct, contextK, maxK, modelName);
833
812
  const git = getGitStatus(currentDir);
834
- renderGit(mode, termWidth, git, dirName);
813
+ const gitLine = renderGit(mode, termWidth, git, dirName);
835
814
 
836
- // Render context manager line (line 3) with separator
837
- console.log(makeSeparator(termWidth));
815
+ let ctxMgrLine: string;
838
816
  if (contextId) {
839
817
  const contextState = loadContextState(contextId);
840
- renderContextManager(mode, contextId, contextState);
818
+ ctxMgrLine = truncateToWidth(renderContextManager(mode, contextId, contextState), termWidth);
841
819
  } else {
842
- renderNoContext(mode);
820
+ ctxMgrLine = truncateToWidth(renderNoContext(mode), termWidth);
843
821
  }
844
822
 
823
+ // Measure visible width of each content line, cap separator at terminal width
824
+ const maxContentWidth = Math.min(
825
+ termWidth,
826
+ Math.max(
827
+ measureWidth(contextLine),
828
+ measureWidth(gitLine),
829
+ measureWidth(ctxMgrLine),
830
+ ),
831
+ );
832
+ const sep = makeSeparator(maxContentWidth);
833
+
834
+ const lines = [contextLine, sep, gitLine, sep, ctxMgrLine];
835
+
836
+ // Single atomic write — prevents partial output if process is interrupted
837
+ process.stdout.write(lines.join("\n") + "\n");
838
+
845
839
  // Persist context_window to state.json
846
840
  if (contextId) {
847
841
  writeContextWindow(contextId, {
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "codex": {
9
9
  "enabled": true,
10
- "models": ["gpt-5.1-codex-mini"]
10
+ "models": ["codex-mini-latest"]
11
11
  }
12
12
  }
13
13
  },
@@ -71,7 +71,7 @@ export const DEFAULT_COMPLEXITY_CATEGORIES = ["code", "infrastructure", "documen
71
71
  export const DEFAULT_MODELS_CONFIG: ModelsConfig = {
72
72
  providers: {
73
73
  claude: { enabled: true, models: ["sonnet"] },
74
- codex: { enabled: true, models: ["gpt-5.1-codex-mini"] },
74
+ codex: { enabled: true, models: ["codex-mini-latest"] },
75
75
  },
76
76
  };
77
77
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { logDebug, logInfo, logWarn, logError } from "../../../../../_shared/lib-ts/base/logger.js";
8
- import { getInternalSubprocessEnv, findExecutable, execFileAsync } from "../../../../../_shared/lib-ts/base/subprocess-utils.js";
8
+ import { getInternalSubprocessEnv, findExecutable, execFileAsync, normalizePathForCli } from "../../../../../_shared/lib-ts/base/subprocess-utils.js";
9
9
  import { debugLog, debugRaw } from "../../../../lib-ts/debug.js";
10
10
  import type { AgentConfig } from "../../../../lib-ts/types.js";
11
11
 
@@ -176,7 +176,8 @@ export abstract class BaseCliAgent<T> {
176
176
 
177
177
  // 3. Execute subprocess
178
178
  const env = getInternalSubprocessEnv();
179
- const result = await execFileAsync(cliPath, args, {
179
+ const normalizedCliPath = normalizePathForCli(cliPath);
180
+ const result = await execFileAsync(normalizedCliPath, args, {
180
181
  input: prompt,
181
182
  timeout: this.timeout * 1000,
182
183
  env: env as Record<string, string>,
@@ -8,7 +8,7 @@ import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
 
10
10
  import { logDebug, logWarn } from "../../../../../_shared/lib-ts/base/logger.js";
11
- import { getInternalSubprocessEnv, execFileAsync } from "../../../../../_shared/lib-ts/base/subprocess-utils.js";
11
+ import { getInternalSubprocessEnv, execFileAsync, normalizePathForCli, shellQuoteWin } from "../../../../../_shared/lib-ts/base/subprocess-utils.js";
12
12
  import { debugLog, debugRaw } from "../../../../lib-ts/debug.js";
13
13
  import { parseJsonMaybe, coerceToReview } from "../../../../lib-ts/json-parser.js";
14
14
  import type { ReviewerResult } from "../../../../lib-ts/types.js";
@@ -35,9 +35,12 @@ export class CodexAgent extends BaseCliAgent<ReviewerResult> {
35
35
  const outPath = path.join(this.tempDir, "output.json");
36
36
  fs.writeFileSync(schemaPath, JSON.stringify(this.schema, null, 2), "utf-8");
37
37
 
38
- const cmdArgs = ["exec", "--sandbox", "read-only"];
38
+ const normalizedSchema = shellQuoteWin(normalizePathForCli(schemaPath));
39
+ const normalizedOut = shellQuoteWin(normalizePathForCli(outPath));
40
+
41
+ const cmdArgs = ["exec", "--sandbox", "read-only", "--reasoning", "medium"];
39
42
  if (this.agent.model) cmdArgs.push("--model", this.agent.model);
40
- cmdArgs.push("--output-schema", schemaPath, "-o", outPath, "-");
43
+ cmdArgs.push("--output-schema", normalizedSchema, "-o", normalizedOut, "-");
41
44
 
42
45
  return cmdArgs;
43
46
  }
@@ -416,5 +416,5 @@
416
416
  ]
417
417
  }
418
418
  },
419
- "version": "0.13.4"
419
+ "version": "0.13.5"
420
420
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiwcli",
3
3
  "description": "AI Workflow CLI - Command-line interface for AI-powered workflows",
4
- "version": "0.13.4",
4
+ "version": "0.13.5",
5
5
  "author": "jofu-tofu",
6
6
  "bin": {
7
7
  "aiw": "bin/run.js"
@@ -1,49 +0,0 @@
1
- /** Standard gitignore entries managed by AIW */
2
- export declare const AIW_GITIGNORE_ENTRIES: string[];
3
- /**
4
- * Prune stale entries from the AIW Installation section in .gitignore.
5
- * Checks each entry against disk existence and removes entries whose paths don't exist.
6
- * Removes the entire section if no entries remain after pruning.
7
- *
8
- * @param targetDir - Directory containing .gitignore
9
- * @returns True if any entries were pruned
10
- */
11
- export declare function pruneGitignoreStaleEntries(targetDir: string): Promise<boolean>;
12
- /**
13
- * Update .gitignore with patterns for installed folders.
14
- *
15
- * Creates .gitignore if it doesn't exist, or appends to existing file.
16
- * Prevents duplicate patterns by checking each pattern individually.
17
- *
18
- * @param targetDir - Directory containing .gitignore file
19
- * @param folders - List of folder names to add as gitignore patterns (e.g., ['_bmad', '.claude'])
20
- */
21
- export declare function updateGitignore(targetDir: string, folders: string[]): Promise<void>;
22
- /**
23
- * Compute which AIW gitignore entries should be removed during clear.
24
- * Returns a simulation result — the caller decides whether to apply.
25
- *
26
- * Logic per entry:
27
- * - If in permanentEntries → keep (reason: "permanent")
28
- * - If directory exists and is non-empty → keep (reason: "directory has content")
29
- * - Otherwise → mark for removal
30
- *
31
- * @param targetDir - Directory containing .gitignore
32
- * @param permanentEntries - Entries that should never be removed (defaults to AIW_PERMANENT_ENTRIES)
33
- * @returns Lists of entries to remove and entries to keep with reasons
34
- */
35
- export declare function computeGitignoreRemovals(targetDir: string, permanentEntries?: string[]): Promise<{
36
- toKeep: Array<{
37
- entry: string;
38
- reason: string;
39
- }>;
40
- toRemove: string[];
41
- }>;
42
- /**
43
- * Remove specific entries from the AIW section in .gitignore.
44
- * Cleans up the section header if no entries remain.
45
- *
46
- * @param targetDir - Directory containing .gitignore
47
- * @param entriesToRemove - Entry names to remove (without trailing slash)
48
- */
49
- export declare function removeGitignoreEntries(targetDir: string, entriesToRemove: string[]): Promise<void>;