aiwcli 0.15.5 → 0.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/capabilities/branch/adapters.d.ts +2 -0
- package/dist/capabilities/branch/adapters.js +21 -0
- package/dist/capabilities/branch/contracts.d.ts +57 -0
- package/dist/capabilities/branch/contracts.js +1 -0
- package/dist/capabilities/branch/control-plane.d.ts +2 -0
- package/dist/capabilities/branch/control-plane.js +343 -0
- package/dist/capabilities/branch/runtime-core.d.ts +5 -0
- package/dist/capabilities/branch/runtime-core.js +36 -0
- package/dist/capabilities/installation/control-plane/clean-command.d.ts +41 -0
- package/dist/capabilities/installation/control-plane/clean-command.js +196 -0
- package/dist/capabilities/installation/control-plane/clear-command.d.ts +160 -0
- package/dist/capabilities/installation/control-plane/clear-command.js +1220 -0
- package/dist/capabilities/installation/control-plane/init-command.d.ts +81 -0
- package/dist/capabilities/installation/control-plane/init-command.js +449 -0
- package/dist/capabilities/launch/contracts.d.ts +51 -0
- package/dist/capabilities/launch/contracts.js +1 -0
- package/dist/capabilities/launch/control-plane/execute-launch.d.ts +2 -0
- package/dist/capabilities/launch/control-plane/execute-launch.js +222 -0
- package/dist/capabilities/launch/runtime-core/launch-options.d.ts +14 -0
- package/dist/capabilities/launch/runtime-core/launch-options.js +69 -0
- package/dist/cli/base-command.d.ts +18 -0
- package/dist/cli/base-command.js +55 -0
- package/dist/commands/branch.d.ts +0 -20
- package/dist/commands/branch.js +24 -416
- package/dist/commands/clean.d.ts +1 -41
- package/dist/commands/clean.js +1 -196
- package/dist/commands/clear.d.ts +1 -161
- package/dist/commands/clear.js +1 -1121
- package/dist/commands/init/index.d.ts +1 -98
- package/dist/commands/init/index.js +4 -478
- package/dist/commands/launch.d.ts +36 -11
- package/dist/commands/launch.js +135 -159
- package/dist/lib/base-command.d.ts +1 -114
- package/dist/lib/base-command.js +1 -153
- package/dist/lib/claude-settings-types.d.ts +31 -19
- package/dist/lib/context/context-formatter.d.ts +74 -0
- package/dist/lib/context/context-formatter.js +493 -0
- package/dist/lib/context/context-selector.d.ts +42 -0
- package/dist/lib/context/context-selector.js +451 -0
- package/dist/lib/context/context-store.d.ts +100 -0
- package/dist/lib/context/context-store.js +618 -0
- package/dist/lib/context/plan-manager.d.ts +54 -0
- package/dist/lib/context/plan-manager.js +282 -0
- package/dist/lib/context/task-tracker.d.ts +44 -0
- package/dist/lib/context/task-tracker.js +146 -0
- package/dist/lib/core-ide-base.d.ts +4 -0
- package/dist/lib/core-ide-base.js +77 -0
- package/dist/lib/core-installer.d.ts +5 -0
- package/dist/lib/core-installer.js +54 -0
- package/dist/lib/git-exclude-manager.d.ts +2 -2
- package/dist/lib/git-exclude-manager.js +3 -3
- package/dist/lib/hooks/hook-utils.d.ts +143 -0
- package/dist/lib/hooks/hook-utils.js +609 -0
- package/dist/lib/hooks/session-end-logic.d.ts +5 -0
- package/dist/lib/hooks/session-end-logic.js +63 -0
- package/dist/lib/hooks-merger.js +25 -19
- package/dist/lib/ide-path-resolver.d.ts +19 -7
- package/dist/lib/ide-path-resolver.js +25 -9
- package/dist/lib/install-state.d.ts +34 -0
- package/dist/lib/install-state.js +161 -0
- package/dist/lib/launch-options.d.ts +1 -0
- package/dist/lib/launch-options.js +1 -0
- package/dist/lib/lsp-patch.d.ts +12 -0
- package/dist/lib/lsp-patch.js +156 -0
- package/dist/lib/multiplexer.d.ts +57 -0
- package/dist/lib/multiplexer.js +19 -0
- package/dist/lib/multiplexers/psmux.d.ts +75 -0
- package/dist/lib/multiplexers/psmux.js +384 -0
- package/dist/lib/multiplexers/tmux.d.ts +44 -0
- package/dist/lib/multiplexers/tmux.js +262 -0
- package/dist/lib/mux-utils.d.ts +5 -0
- package/dist/lib/mux-utils.js +42 -0
- package/dist/lib/paths.d.ts +2 -2
- package/dist/lib/paths.js +2 -2
- package/dist/lib/platform-commands.d.ts +27 -0
- package/dist/lib/platform-commands.js +49 -0
- package/dist/lib/runtime/aiw-cli.d.ts +37 -0
- package/dist/lib/runtime/aiw-cli.js +74 -0
- package/dist/lib/runtime/atomic-write.d.ts +19 -0
- package/dist/lib/runtime/atomic-write.js +121 -0
- package/dist/lib/runtime/cli-args.d.ts +55 -0
- package/dist/lib/runtime/cli-args.js +185 -0
- package/dist/lib/runtime/constants.d.ts +56 -0
- package/dist/lib/runtime/constants.js +230 -0
- package/dist/lib/runtime/executable-policy.d.ts +16 -0
- package/dist/lib/runtime/executable-policy.js +57 -0
- package/dist/lib/runtime/git-state.d.ts +9 -0
- package/dist/lib/runtime/git-state.js +59 -0
- package/dist/lib/runtime/inference.d.ts +37 -0
- package/dist/lib/runtime/inference.js +262 -0
- package/dist/lib/runtime/lint-dispatch.d.ts +40 -0
- package/dist/lib/runtime/lint-dispatch.js +285 -0
- package/dist/lib/runtime/logger.d.ts +66 -0
- package/dist/lib/runtime/logger.js +201 -0
- package/dist/lib/runtime/models.d.ts +14 -0
- package/dist/lib/runtime/models.js +14 -0
- package/dist/lib/runtime/platform-adapter.d.ts +7 -0
- package/dist/lib/runtime/platform-adapter.js +21 -0
- package/dist/lib/runtime/preflight.d.ts +24 -0
- package/dist/lib/runtime/preflight.js +65 -0
- package/dist/lib/runtime/sentinel-ipc.d.ts +14 -0
- package/dist/lib/runtime/sentinel-ipc.js +67 -0
- package/dist/lib/runtime/state-io.d.ts +30 -0
- package/dist/lib/runtime/state-io.js +174 -0
- package/dist/lib/runtime/stop-words.d.ts +20 -0
- package/dist/lib/runtime/stop-words.js +150 -0
- package/dist/lib/runtime/subprocess-utils.d.ts +29 -0
- package/dist/lib/runtime/subprocess-utils.js +96 -0
- package/dist/lib/runtime/tmux-preflight.d.ts +13 -0
- package/dist/lib/runtime/tmux-preflight.js +78 -0
- package/dist/lib/runtime/utils.d.ts +54 -0
- package/dist/lib/runtime/utils.js +162 -0
- package/dist/lib/sentinel-wrapper.d.ts +9 -0
- package/dist/lib/sentinel-wrapper.js +20 -0
- package/dist/lib/shell-quoting.d.ts +5 -0
- package/dist/lib/shell-quoting.js +17 -0
- package/dist/lib/spawn-errors.d.ts +6 -0
- package/dist/lib/spawn-errors.js +15 -0
- package/dist/lib/spawn.js +5 -11
- package/dist/lib/template-installer.d.ts +4 -5
- package/dist/lib/template-installer.js +36 -34
- package/dist/lib/template-resolver.d.ts +6 -7
- package/dist/lib/template-resolver.js +26 -21
- package/dist/lib/template-settings-reconstructor.d.ts +7 -2
- package/dist/lib/template-settings-reconstructor.js +76 -45
- package/dist/lib/terminal-strategy.d.ts +11 -0
- package/dist/lib/terminal-strategy.js +49 -0
- package/dist/lib/terminal.d.ts +28 -0
- package/dist/lib/terminal.js +162 -112
- package/dist/lib/tmux-pane-placement.d.ts +17 -0
- package/dist/lib/tmux-pane-placement.js +58 -0
- package/dist/lib/tmux-primitives.d.ts +5 -0
- package/dist/lib/tmux-primitives.js +15 -0
- package/dist/lib/tmux-session.d.ts +32 -0
- package/dist/lib/tmux-session.js +86 -0
- package/dist/lib/tty-detection.js +1 -1
- package/dist/lib/types.d.ts +168 -0
- package/dist/lib/types.js +6 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/platform/launch.d.ts +10 -0
- package/dist/platform/launch.js +10 -0
- package/dist/templates/CLAUDE.md +31 -40
- package/dist/templates/cc-native/.claude/settings.json +27 -27
- package/dist/templates/cc-native/CC-NATIVE-README.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +10 -9
- package/dist/templates/cc-native/_cc-native/CLAUDE.md +18 -18
- package/dist/templates/cc-native/_cc-native/artifacts/CLAUDE.md +3 -3
- package/dist/templates/cc-native/_cc-native/artifacts/lib/format.ts +14 -14
- package/dist/templates/cc-native/_cc-native/artifacts/lib/tracker.ts +1 -1
- package/dist/templates/cc-native/_cc-native/artifacts/lib/write.ts +3 -3
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +3 -3
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +16 -15
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +3 -3
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +2 -2
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +2 -2
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +3 -3
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +2 -2
- package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +3 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/CLAUDE.md +8 -8
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +4 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +2 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +2 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +8 -8
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +3 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +2 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +3 -3
- package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +3 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/__tests__/agent-selection.test.ts +345 -0
- package/dist/templates/cc-native/_cc-native/plan-review/lib/__tests__/preflight.test.ts +344 -0
- package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +37 -15
- package/dist/templates/cc-native/_cc-native/plan-review/lib/corroboration.ts +16 -69
- package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/output-builder.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/plan-questions.ts +2 -2
- package/dist/templates/cc-native/_cc-native/plan-review/lib/preflight.ts +56 -26
- package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +7 -7
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/base/base-agent.ts +3 -3
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/index.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/claude-agent.ts +2 -2
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/codex-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/gemini-agent.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +5 -6
- package/dist/templates/core/.codex/workflows/codex.md +17 -0
- package/dist/templates/core/.codex/workflows/handoff.md +5 -0
- package/dist/templates/core/.codex/workflows/meta-plan.md +7 -0
- package/dist/templates/core/.cognition/AGENTS.md +5 -0
- package/dist/templates/core/.cognition/config.json +12 -0
- package/dist/templates/{_shared → core}/.windsurf/workflows/handoff.md +1 -1
- package/dist/templates/{_shared → core}/.windsurf/workflows/meta-plan.md +1 -1
- package/dist/templates/core/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/{_shared → core}/hooks-ts/archive_plan.ts +14 -23
- package/dist/templates/core/hooks-ts/codex_explorer.ts +160 -0
- package/dist/templates/{_shared → core}/hooks-ts/context_monitor.ts +23 -55
- package/dist/templates/{_shared → core}/hooks-ts/file-suggestion.ts +4 -3
- package/dist/templates/{_shared → core}/hooks-ts/lint_after_edit.ts +7 -9
- package/dist/templates/{_shared → core}/hooks-ts/pre_compact.ts +5 -5
- package/dist/templates/{_shared → core}/hooks-ts/session_end.ts +38 -78
- package/dist/templates/{_shared → core}/hooks-ts/session_start.ts +5 -5
- package/dist/templates/core/hooks-ts/task_create_capture.ts +32 -0
- package/dist/templates/{_shared → core}/hooks-ts/task_update_capture.ts +9 -24
- package/dist/templates/core/hooks-ts/user_prompt_submit.ts +46 -0
- package/dist/templates/{_shared → core}/lib-ts/CLAUDE.md +27 -16
- package/dist/templates/{_shared → core}/lib-ts/agent-exec/backends/headless.ts +3 -2
- package/dist/templates/{_shared → core}/lib-ts/agent-exec/backends/tmux.ts +44 -15
- package/dist/templates/{_shared → core}/lib-ts/agent-exec/base-agent.ts +6 -4
- package/dist/templates/{_shared → core}/lib-ts/agent-exec/execution-backend.ts +1 -1
- package/dist/templates/{_shared → core}/lib-ts/agent-exec/index.ts +2 -2
- package/dist/templates/{_shared → core}/lib-ts/agent-exec/structured-output.ts +4 -5
- package/dist/templates/{_shared → core}/lib-ts/context/CLAUDE.md +9 -6
- package/dist/templates/{_shared → core}/lib-ts/context/context-formatter.ts +16 -21
- package/dist/templates/{_shared → core}/lib-ts/context/context-selector.ts +8 -6
- package/dist/templates/{_shared → core}/lib-ts/context/context-store.ts +32 -20
- package/dist/templates/{_shared → core}/lib-ts/context/plan-manager.ts +19 -15
- package/dist/templates/{_shared → core}/lib-ts/context/task-tracker.ts +3 -3
- package/dist/templates/core/lib-ts/hooks/context-monitor-logic.ts +32 -0
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/hooks}/hook-utils.ts +168 -41
- package/dist/templates/core/lib-ts/hooks/prompt-binding-logic.ts +80 -0
- package/dist/templates/core/lib-ts/hooks/session-end-logic.ts +93 -0
- package/dist/templates/core/lib-ts/package.json +19 -0
- package/dist/templates/core/lib-ts/runtime/agent-launcher.ts +295 -0
- package/dist/templates/core/lib-ts/runtime/aiw-cli.ts +106 -0
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/atomic-write.ts +12 -7
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/cli-args.ts +8 -6
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/constants.ts +326 -324
- package/dist/templates/core/lib-ts/runtime/executable-policy.ts +89 -0
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/git-state.ts +6 -4
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/inference.ts +59 -10
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/lint-dispatch.ts +25 -23
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/logger.ts +32 -29
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/models.ts +2 -2
- package/dist/templates/core/lib-ts/runtime/platform-adapter.ts +33 -0
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/preflight.ts +4 -3
- package/dist/templates/core/lib-ts/runtime/sentinel-ipc.ts +91 -0
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/state-io.ts +11 -7
- package/dist/templates/core/lib-ts/runtime/stop-words.ts +185 -0
- package/dist/templates/core/lib-ts/runtime/subprocess-utils.ts +147 -0
- package/dist/templates/core/lib-ts/runtime/tmux-preflight.ts +93 -0
- package/dist/templates/{_shared/lib-ts/base → core/lib-ts/runtime}/utils.ts +4 -3
- package/dist/templates/{_shared → core}/lib-ts/templates/formatters.ts +7 -5
- package/dist/templates/{_shared → core}/lib-ts/templates/plan-context.ts +2 -1
- package/dist/templates/{_shared → core}/lib-ts/tsconfig.json +3 -1
- package/dist/templates/{_shared → core}/lib-ts/types.ts +78 -77
- package/dist/templates/core/scripts/resolve-run.ts +61 -0
- package/dist/templates/{_shared → core}/scripts/resolve_context.ts +3 -3
- package/dist/templates/{_shared → core}/scripts/status_line.ts +25 -20
- package/dist/templates/core/skills/codex/CLAUDE.md +78 -0
- package/dist/templates/{_shared → core}/skills/codex/SKILL.md +21 -18
- package/dist/templates/{_shared → core}/skills/codex/lib/codex-watcher.ts +76 -103
- package/dist/templates/{_shared → core}/skills/codex/scripts/launch-codex.ts +119 -133
- package/dist/templates/{_shared → core}/skills/codex/scripts/watch-codex.ts +6 -4
- package/dist/templates/core/skills/devin/CLAUDE.md +65 -0
- package/dist/templates/core/skills/devin/SKILL.md +73 -0
- package/dist/templates/core/skills/devin/lib/devin-watcher.ts +280 -0
- package/dist/templates/core/skills/devin/scripts/launch-devin.ts +257 -0
- package/dist/templates/{_shared → core}/skills/handoff-system/CLAUDE.md +436 -433
- package/dist/templates/{_shared → core}/skills/handoff-system/lib/document-generator.ts +9 -7
- package/dist/templates/{_shared → core}/skills/handoff-system/lib/handoff-reader.ts +6 -4
- package/dist/templates/{_shared → core}/skills/handoff-system/scripts/resume_handoff.ts +10 -8
- package/dist/templates/{_shared → core}/skills/handoff-system/scripts/save_handoff.ts +12 -10
- package/dist/templates/{_shared → core}/skills/handoff-system/workflows/handoff-resume.md +2 -2
- package/dist/templates/{_shared → core}/skills/handoff-system/workflows/handoff.md +6 -5
- package/dist/templates/{_shared → core}/skills/meta-plan/CLAUDE.md +2 -1
- package/dist/templates/{_shared → core}/skills/meta-plan/workflows/meta-plan.md +8 -7
- package/oclif.manifest.json +89 -13
- package/package.json +13 -12
- package/dist/templates/_shared/.claude/settings.json +0 -120
- package/dist/templates/_shared/.claude/skills/codex/SKILL.md +0 -35
- package/dist/templates/_shared/.claude/skills/handoff/SKILL.md +0 -13
- package/dist/templates/_shared/.claude/skills/handoff-resume/SKILL.md +0 -13
- package/dist/templates/_shared/.claude/skills/meta-plan/SKILL.md +0 -43
- package/dist/templates/_shared/.codex/workflows/codex.md +0 -11
- package/dist/templates/_shared/.codex/workflows/handoff.md +0 -226
- package/dist/templates/_shared/.codex/workflows/meta-plan.md +0 -347
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +0 -2
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +0 -48
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +0 -93
- package/dist/templates/_shared/lib-ts/base/launchers/tmux-launcher.ts +0 -173
- package/dist/templates/_shared/lib-ts/base/launchers/window-launcher.ts +0 -93
- package/dist/templates/_shared/lib-ts/base/launchers/wt-launcher.ts +0 -64
- package/dist/templates/_shared/lib-ts/base/pane-launcher.ts +0 -55
- package/dist/templates/_shared/lib-ts/base/sentinel-ipc.ts +0 -87
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +0 -184
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +0 -249
- package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +0 -341
- package/dist/templates/_shared/lib-ts/base/tmux-pane-placement.ts +0 -78
- package/dist/templates/_shared/lib-ts/package.json +0 -20
- package/dist/templates/_shared/scripts/resolve-run.ts +0 -62
- package/dist/templates/_shared/skills/codex/CLAUDE.md +0 -70
- /package/dist/templates/{_shared → core}/lib-ts/agent-exec/backends/index.ts +0 -0
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import confirm from '@inquirer/confirm';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import BaseCommand from '../../../cli/base-command.js';
|
|
6
|
+
import { computeExcludeRemovals, pruneExcludeStaleEntries, removeExcludeEntries, resolveGitDir } from '../../../lib/git-exclude-manager.js';
|
|
7
|
+
import { deleteInstallStateIfPresent, getInstalledMethods, markMethodRemoved, readInstallState } from '../../../lib/install-state.js';
|
|
8
|
+
import { pathExists } from '../../../lib/paths.js';
|
|
9
|
+
import { getTemplatePath } from '../../../lib/template-resolver.js';
|
|
10
|
+
import { reconstructIdeSettings } from '../../../lib/template-settings-reconstructor.js';
|
|
11
|
+
import { EXIT_CODES } from '../../../types/exit-codes.js';
|
|
12
|
+
/**
|
|
13
|
+
* Container folder for method-specific files
|
|
14
|
+
* This keeps template infrastructure separate from IDE config
|
|
15
|
+
*/
|
|
16
|
+
const AIWCLI_CONTAINER = '.aiwcli';
|
|
17
|
+
/**
|
|
18
|
+
* The output folder name that contains method subdirectories.
|
|
19
|
+
* Structure: _output/{method}/ (e.g., _output/bmad/, _output/gsd/)
|
|
20
|
+
*/
|
|
21
|
+
const OUTPUT_FOLDER_NAME = '_output';
|
|
22
|
+
const CORE_TEMPLATE_NAME = 'core';
|
|
23
|
+
const SETTINGS_FILES_TO_SKIP = new Set(['hooks.json', 'settings.json']);
|
|
24
|
+
const CORE_RUNTIME_FOLDERS = ['_core'];
|
|
25
|
+
/**
|
|
26
|
+
* IDE configuration folder names and settings file locations.
|
|
27
|
+
* Method subfolders are discovered dynamically via disk scanning.
|
|
28
|
+
*/
|
|
29
|
+
const IDE_FOLDERS = {
|
|
30
|
+
claude: {
|
|
31
|
+
root: '.claude',
|
|
32
|
+
settingsFile: 'settings.json',
|
|
33
|
+
},
|
|
34
|
+
codex: {
|
|
35
|
+
root: '.codex',
|
|
36
|
+
},
|
|
37
|
+
windsurf: {
|
|
38
|
+
root: '.windsurf',
|
|
39
|
+
settingsFile: 'hooks.json',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Check if a directory is empty.
|
|
44
|
+
*
|
|
45
|
+
* @param dir - Directory to check
|
|
46
|
+
* @returns True if directory is empty or doesn't exist
|
|
47
|
+
*/
|
|
48
|
+
async function isDirectoryEmpty(dir) {
|
|
49
|
+
try {
|
|
50
|
+
const entries = await fs.readdir(dir);
|
|
51
|
+
return entries.length === 0;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a JSON settings file is empty or effectively empty.
|
|
59
|
+
* Returns true if the file doesn't exist, can't be parsed, or contains an empty object.
|
|
60
|
+
*
|
|
61
|
+
* @param filePath - Path to the JSON settings file
|
|
62
|
+
* @returns True if file is empty or doesn't exist
|
|
63
|
+
*/
|
|
64
|
+
async function isSettingsFileEmpty(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
67
|
+
const trimmed = content.trim();
|
|
68
|
+
if (trimmed === '' || trimmed === '{}') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const parsed = JSON.parse(content);
|
|
72
|
+
// Check if it's an empty object
|
|
73
|
+
return typeof parsed === 'object' && parsed !== null && Object.keys(parsed).length === 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// File doesn't exist or can't be parsed - consider it empty
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if an IDE folder should be fully deleted.
|
|
82
|
+
* Returns true if:
|
|
83
|
+
* 1. The settings file is empty (or doesn't exist)
|
|
84
|
+
* 2. All subdirectories are empty (or don't exist)
|
|
85
|
+
* Backup files (e.g., settings.json.backup) are ignored.
|
|
86
|
+
*
|
|
87
|
+
* @param targetDir - Directory containing the IDE folder
|
|
88
|
+
* @param ideFolder - IDE folder configuration
|
|
89
|
+
* @param ideFolder.root - Root folder name (e.g., '.claude')
|
|
90
|
+
* @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
|
|
91
|
+
* @returns True if the IDE folder should be fully deleted
|
|
92
|
+
*/
|
|
93
|
+
async function shouldDeleteIdeFolder(targetDir, ideFolder) {
|
|
94
|
+
const ideFolderPath = join(targetDir, ideFolder.root);
|
|
95
|
+
// Check if IDE folder exists at all
|
|
96
|
+
try {
|
|
97
|
+
const stat = await fs.stat(ideFolderPath);
|
|
98
|
+
if (!stat.isDirectory()) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Folder doesn't exist - nothing to delete
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Check if settings file is empty (for IDEs that use one)
|
|
107
|
+
if (ideFolder.settingsFile) {
|
|
108
|
+
const settingsPath = join(ideFolderPath, ideFolder.settingsFile);
|
|
109
|
+
const settingsEmpty = await isSettingsFileEmpty(settingsPath);
|
|
110
|
+
if (!settingsEmpty) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Check the IDE folder itself - ignore backup files and check for other meaningful content
|
|
115
|
+
try {
|
|
116
|
+
const entries = await fs.readdir(ideFolderPath);
|
|
117
|
+
// Filter entries to check (skip backup files and settings file)
|
|
118
|
+
const entriesToCheck = entries.filter((entry) => {
|
|
119
|
+
if (entry.endsWith('.backup'))
|
|
120
|
+
return false;
|
|
121
|
+
if (ideFolder.settingsFile && entry === ideFolder.settingsFile)
|
|
122
|
+
return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
// Check all entries in parallel
|
|
126
|
+
const entryResults = await Promise.all(entriesToCheck.map(async (entry) => {
|
|
127
|
+
const entryPath = join(ideFolderPath, entry);
|
|
128
|
+
try {
|
|
129
|
+
const stat = await fs.stat(entryPath);
|
|
130
|
+
if (stat.isDirectory()) {
|
|
131
|
+
return isDirectoryEmpty(entryPath);
|
|
132
|
+
}
|
|
133
|
+
// Non-backup file exists - don't delete the folder
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Can't stat entry - be safe and don't delete
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}));
|
|
141
|
+
// If unknown entry is not empty (or is a non-backup file), don't delete
|
|
142
|
+
if (entryResults.some((result) => !result)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Remove a directory recursively.
|
|
153
|
+
*
|
|
154
|
+
* @param dir - Directory to remove
|
|
155
|
+
*/
|
|
156
|
+
async function removeDirectory(dir) {
|
|
157
|
+
await fs.rm(dir, { force: true, recursive: true });
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Try to remove a directory if it is empty.
|
|
161
|
+
*
|
|
162
|
+
* @param dir - Directory to check and potentially remove
|
|
163
|
+
* @returns True if the directory was removed
|
|
164
|
+
*/
|
|
165
|
+
async function tryRemoveEmptyDir(dir) {
|
|
166
|
+
try {
|
|
167
|
+
if (await isDirectoryEmpty(dir)) {
|
|
168
|
+
await removeDirectory(dir);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Directory doesn't exist or can't be accessed
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if an IDE folder will be empty after removing specified method folders.
|
|
179
|
+
* Counts method folders vs folders being deleted, then simulates settings cleanup.
|
|
180
|
+
*
|
|
181
|
+
* @param targetDir - Project root directory
|
|
182
|
+
* @param ideFolder - IDE folder configuration
|
|
183
|
+
* @param ideFolder.root - Root folder name (e.g., '.claude')
|
|
184
|
+
* @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
|
|
185
|
+
* @param ideMethodFolders - IDE method folders being deleted
|
|
186
|
+
* @returns True if the IDE folder will be empty after removal
|
|
187
|
+
*/
|
|
188
|
+
async function checkIdeRemovalEligibility(targetDir, ideFolder, ideMethodFolders) {
|
|
189
|
+
const idePath = join(targetDir, ideFolder.root);
|
|
190
|
+
try {
|
|
191
|
+
const stat = await fs.stat(idePath);
|
|
192
|
+
if (!stat.isDirectory())
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
// Count method folders vs folders being deleted
|
|
199
|
+
const counts = await countMethodFolderDeletions(idePath, ideMethodFolders);
|
|
200
|
+
if (counts.total === 0 || counts.total !== counts.deleted)
|
|
201
|
+
return false;
|
|
202
|
+
// IDEs without a settings file are eligible based on folder counts alone.
|
|
203
|
+
if (!ideFolder.settingsFile)
|
|
204
|
+
return true;
|
|
205
|
+
// Check if settings file would become empty after removing AIW-managed hooks.
|
|
206
|
+
return wouldSettingsBeEmpty(idePath, ideFolder.settingsFile);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Count total method folders and how many are being deleted in an IDE root.
|
|
210
|
+
*
|
|
211
|
+
* @param idePath - Path to IDE root folder
|
|
212
|
+
* @param ideMethodFolders - IDE method folders being deleted
|
|
213
|
+
* @returns Counts of total and deleted method folders
|
|
214
|
+
*/
|
|
215
|
+
async function countMethodFolderDeletions(idePath, ideMethodFolders) {
|
|
216
|
+
let total = 0;
|
|
217
|
+
let deleted = 0;
|
|
218
|
+
try {
|
|
219
|
+
const topEntries = await fs.readdir(idePath, { withFileTypes: true });
|
|
220
|
+
const subdirs = topEntries.filter((e) => e.isDirectory());
|
|
221
|
+
const subResults = await Promise.all(subdirs.map(async (subdir) => {
|
|
222
|
+
const subdirPath = join(idePath, subdir.name);
|
|
223
|
+
try {
|
|
224
|
+
const entries = await fs.readdir(subdirPath, { withFileTypes: true });
|
|
225
|
+
const methodDirs = entries.filter((e) => e.isDirectory());
|
|
226
|
+
const deletedCount = methodDirs.filter((entry) => ideMethodFolders.includes(join(subdirPath, entry.name))).length;
|
|
227
|
+
return { deleted: deletedCount, total: methodDirs.length };
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { deleted: 0, total: 0 };
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
for (const r of subResults) {
|
|
234
|
+
total += r.total;
|
|
235
|
+
deleted += r.deleted;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return { deleted: 0, total: 0 };
|
|
240
|
+
}
|
|
241
|
+
return { deleted, total };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if a settings file would be empty after removing AIW-managed hooks.
|
|
245
|
+
*
|
|
246
|
+
* @param idePath - Path to IDE root folder
|
|
247
|
+
* @param settingsFile - Settings file name
|
|
248
|
+
* @returns True if settings would be empty
|
|
249
|
+
*/
|
|
250
|
+
async function wouldSettingsBeEmpty(idePath, settingsFile) {
|
|
251
|
+
const settingsPath = join(idePath, settingsFile);
|
|
252
|
+
try {
|
|
253
|
+
const content = await fs.readFile(settingsPath, 'utf8');
|
|
254
|
+
const settings = JSON.parse(content);
|
|
255
|
+
if (settings.hooks && typeof settings.hooks === 'object') {
|
|
256
|
+
delete settings.hooks;
|
|
257
|
+
}
|
|
258
|
+
return Object.keys(settings).length === 0;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if a log file exceeds 1MB and needs rotation.
|
|
266
|
+
*
|
|
267
|
+
* @param logPath - Path to the log file
|
|
268
|
+
* @returns Log action info if rotation needed, null otherwise
|
|
269
|
+
*/
|
|
270
|
+
async function checkLogRotation(logPath) {
|
|
271
|
+
try {
|
|
272
|
+
const stat = await fs.stat(logPath);
|
|
273
|
+
if (stat.size > 1_048_576) {
|
|
274
|
+
return { path: logPath, sizeBytes: stat.size };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Can't stat log file
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if a contexts directory has a non-empty _archive/ subdirectory.
|
|
284
|
+
*
|
|
285
|
+
* @param contextsPath - Path to the contexts directory
|
|
286
|
+
* @returns Archive info if found, null otherwise
|
|
287
|
+
*/
|
|
288
|
+
async function checkArchiveDir(contextsPath) {
|
|
289
|
+
const archivePath = join(contextsPath, '_archive');
|
|
290
|
+
try {
|
|
291
|
+
const entries = await fs.readdir(archivePath);
|
|
292
|
+
if (entries.length > 0) {
|
|
293
|
+
return { path: archivePath, count: entries.length };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// No archive directory
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Recursively remove files from targetDir that have a matching file in sourceDir.
|
|
303
|
+
* This removes only AIW-managed template files and leaves unknown user files intact.
|
|
304
|
+
*
|
|
305
|
+
* @param sourceDir - Template source subtree
|
|
306
|
+
* @param targetDir - Target subtree in project
|
|
307
|
+
* @returns Number of files removed
|
|
308
|
+
*/
|
|
309
|
+
async function removeMatchingFiles(sourceDir, targetDir) {
|
|
310
|
+
let entries;
|
|
311
|
+
try {
|
|
312
|
+
entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
let removed = 0;
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
320
|
+
const targetPath = join(targetDir, entry.name);
|
|
321
|
+
if (entry.isDirectory()) {
|
|
322
|
+
// eslint-disable-next-line no-await-in-loop
|
|
323
|
+
removed += await removeMatchingFiles(sourcePath, targetPath);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!entry.isFile())
|
|
327
|
+
continue;
|
|
328
|
+
if (SETTINGS_FILES_TO_SKIP.has(entry.name))
|
|
329
|
+
continue;
|
|
330
|
+
try {
|
|
331
|
+
// eslint-disable-next-line no-await-in-loop
|
|
332
|
+
if (!(await pathExists(targetPath)))
|
|
333
|
+
continue;
|
|
334
|
+
// eslint-disable-next-line no-await-in-loop
|
|
335
|
+
await fs.rm(targetPath, { force: true });
|
|
336
|
+
removed++;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Best-effort deletion
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const leftovers = await fs.readdir(targetDir);
|
|
344
|
+
if (leftovers.length === 0) {
|
|
345
|
+
await fs.rmdir(targetDir);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Target dir doesn't exist or isn't empty
|
|
350
|
+
}
|
|
351
|
+
return removed;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Clear method runtime folders, output folders, IDE method folders, and update configurations.
|
|
355
|
+
*/
|
|
356
|
+
export default class ClearCommand extends BaseCommand {
|
|
357
|
+
static description = 'Clear method runtime folders, output folders, IDE method folders (.claude/.codex/.windsurf), and update configurations';
|
|
358
|
+
static examples = [
|
|
359
|
+
'<%= config.bin %> <%= command.id %>',
|
|
360
|
+
'<%= config.bin %> <%= command.id %> --template cc-native',
|
|
361
|
+
'<%= config.bin %> <%= command.id %> -t cc-native',
|
|
362
|
+
'<%= config.bin %> <%= command.id %> --dry-run',
|
|
363
|
+
'<%= config.bin %> <%= command.id %> --force',
|
|
364
|
+
'<%= config.bin %> <%= command.id %> --output',
|
|
365
|
+
'<%= config.bin %> <%= command.id %> --output --dry-run',
|
|
366
|
+
];
|
|
367
|
+
static flags = {
|
|
368
|
+
...BaseCommand.baseFlags,
|
|
369
|
+
'dry-run': Flags.boolean({
|
|
370
|
+
char: 'n',
|
|
371
|
+
description: 'Show what would be deleted without actually deleting',
|
|
372
|
+
default: false,
|
|
373
|
+
}),
|
|
374
|
+
force: Flags.boolean({
|
|
375
|
+
char: 'f',
|
|
376
|
+
description: 'Skip confirmation prompt',
|
|
377
|
+
default: false,
|
|
378
|
+
}),
|
|
379
|
+
output: Flags.boolean({
|
|
380
|
+
char: 'o',
|
|
381
|
+
description: 'Clean runtime output artifacts (temp files, caches, log rotation, archives)',
|
|
382
|
+
default: false,
|
|
383
|
+
exclusive: ['template'],
|
|
384
|
+
}),
|
|
385
|
+
template: Flags.string({
|
|
386
|
+
char: 't',
|
|
387
|
+
description: 'Clear only a specific template (e.g., cc-native)',
|
|
388
|
+
exclusive: ['output'],
|
|
389
|
+
}),
|
|
390
|
+
};
|
|
391
|
+
async run() {
|
|
392
|
+
const { flags } = await this.parse(ClearCommand);
|
|
393
|
+
const targetDir = process.cwd();
|
|
394
|
+
const isTemplateScoped = Boolean(flags.template);
|
|
395
|
+
// Handle --output flag separately (mutually exclusive with --template)
|
|
396
|
+
if (flags.output) {
|
|
397
|
+
await this.cleanRuntimeOutput(targetDir, flags);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
// Find all folders to clear
|
|
402
|
+
const methodRuntimeFolders = await this.findMethodRuntimeFolders(targetDir, flags.template);
|
|
403
|
+
const outputMethodFolders = await this.findOutputFolders(targetDir, flags.template);
|
|
404
|
+
const ideMethodFolders = await this.findIdeMethodFolders(targetDir, flags.template);
|
|
405
|
+
const coreRuntimeFolders = isTemplateScoped ? [] : await this.findCoreRuntimeFolders(targetDir);
|
|
406
|
+
const coreIdeFilesToRemove = isTemplateScoped ? 0 : await this.countCoreIdeManagedFiles(targetDir);
|
|
407
|
+
const methodsToRemove = await this.resolveMethodsToRemove(targetDir, flags.template, methodRuntimeFolders);
|
|
408
|
+
// Nothing to clear
|
|
409
|
+
if (methodRuntimeFolders.length === 0 &&
|
|
410
|
+
outputMethodFolders.length === 0 &&
|
|
411
|
+
ideMethodFolders.length === 0 &&
|
|
412
|
+
coreRuntimeFolders.length === 0 &&
|
|
413
|
+
coreIdeFilesToRemove === 0) {
|
|
414
|
+
const msg = flags.template
|
|
415
|
+
? `No folders found for template '${flags.template}'.`
|
|
416
|
+
: 'No AIW-managed folders or core template files found.';
|
|
417
|
+
this.logInfo(msg);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// Display pending changes
|
|
421
|
+
await this.displayPendingChanges(targetDir, {
|
|
422
|
+
methodRuntimeFolders, outputMethodFolders, ideMethodFolders, coreRuntimeFolders, methodsToRemove, coreIdeFilesToRemove,
|
|
423
|
+
});
|
|
424
|
+
// Dry run - just show what would happen
|
|
425
|
+
if (flags['dry-run']) {
|
|
426
|
+
this.logInfo('Dry run complete. No files or folders were deleted.');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Confirm deletion
|
|
430
|
+
const totalFolders = methodRuntimeFolders.length + outputMethodFolders.length + ideMethodFolders.length + coreRuntimeFolders.length;
|
|
431
|
+
const coreFilesSuffix = coreIdeFilesToRemove > 0 ? ` and clean ${coreIdeFilesToRemove} core IDE file(s)` : '';
|
|
432
|
+
if (!flags.force) {
|
|
433
|
+
const shouldDelete = await confirm({
|
|
434
|
+
message: `Delete ${totalFolders} folder(s)${coreFilesSuffix}?`,
|
|
435
|
+
default: false,
|
|
436
|
+
});
|
|
437
|
+
if (!shouldDelete) {
|
|
438
|
+
this.log('Operation cancelled.');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Execute deletion and cleanup
|
|
443
|
+
const deleteCounts = await this.executeFolderDeletion(methodRuntimeFolders, outputMethodFolders, ideMethodFolders, coreRuntimeFolders);
|
|
444
|
+
const cleanupResult = await this.performPostDeleteCleanup(targetDir, methodsToRemove, !isTemplateScoped);
|
|
445
|
+
this.reportClearResults(deleteCounts, cleanupResult);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
const err = error;
|
|
449
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
450
|
+
this.error(`Permission denied. ${err.message}`, {
|
|
451
|
+
exit: EXIT_CODES.ENVIRONMENT_ERROR,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
this.error(`Clear failed: ${err.message}`, {
|
|
455
|
+
exit: EXIT_CODES.GENERAL_ERROR,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Clean runtime output artifacts from _output/ at project root.
|
|
461
|
+
* Handles temp files, cache files, log rotation, and archive cleanup.
|
|
462
|
+
*
|
|
463
|
+
* @param targetDir - Project root directory
|
|
464
|
+
* @param flags - Command flags (dry-run, force)
|
|
465
|
+
* @param flags.force - Skip confirmation prompt
|
|
466
|
+
*/
|
|
467
|
+
// eslint-disable-next-line complexity
|
|
468
|
+
async cleanRuntimeOutput(targetDir, flags) {
|
|
469
|
+
const outputDir = join(targetDir, '_output');
|
|
470
|
+
if (!(await pathExists(outputDir))) {
|
|
471
|
+
this.logInfo('No _output/ directory found.');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const toDelete = [];
|
|
475
|
+
let logAction = null;
|
|
476
|
+
let archiveDir = null;
|
|
477
|
+
let archiveCount = 0;
|
|
478
|
+
try {
|
|
479
|
+
const entries = await fs.readdir(outputDir, { withFileTypes: true });
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
const entryPath = join(outputDir, entry.name);
|
|
482
|
+
// Temp files: .index_*.tmp (orphaned atomic write files)
|
|
483
|
+
if (entry.isFile() && entry.name.startsWith('.index_') && entry.name.endsWith('.tmp')) {
|
|
484
|
+
toDelete.push({ path: entryPath, reason: 'temp file' });
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// Cache files: .*-cache.json
|
|
488
|
+
if (entry.isFile() && entry.name.startsWith('.') && entry.name.endsWith('-cache.json')) {
|
|
489
|
+
toDelete.push({ path: entryPath, reason: 'cache file' });
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
// Log rotation: hook-log.jsonl > 1MB
|
|
493
|
+
if (entry.isFile() && entry.name === 'hook-log.jsonl') {
|
|
494
|
+
logAction = await checkLogRotation(entryPath); // eslint-disable-line no-await-in-loop
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
// Archive cleanup: contexts/_archive/
|
|
498
|
+
if (entry.isDirectory() && entry.name === 'contexts') {
|
|
499
|
+
const result = await checkArchiveDir(entryPath); // eslint-disable-line no-await-in-loop
|
|
500
|
+
if (result) {
|
|
501
|
+
archiveDir = result.path;
|
|
502
|
+
archiveCount = result.count;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
const err = error;
|
|
509
|
+
this.error(`Cannot read _output/: ${err.message}`, {
|
|
510
|
+
exit: EXIT_CODES.GENERAL_ERROR,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
// Nothing to clean
|
|
514
|
+
if (toDelete.length === 0 && !logAction && !archiveDir) {
|
|
515
|
+
this.logInfo('No runtime output artifacts to clean.');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Show what will be cleaned
|
|
519
|
+
this.log('');
|
|
520
|
+
this.logInfo('Runtime output cleanup:');
|
|
521
|
+
if (toDelete.length > 0) {
|
|
522
|
+
for (const item of toDelete) {
|
|
523
|
+
const relativePath = item.path.replace(targetDir + '\\', '').replace(targetDir + '/', '');
|
|
524
|
+
this.log(` ${relativePath} (${item.reason})`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (logAction) {
|
|
528
|
+
const sizeMB = (logAction.sizeBytes / 1_048_576).toFixed(1);
|
|
529
|
+
this.log(` _output/hook-log.jsonl (${sizeMB}MB → truncate to ~512KB)`);
|
|
530
|
+
}
|
|
531
|
+
if (archiveDir) {
|
|
532
|
+
this.log(` _output/contexts/_archive/ (${archiveCount} archived context(s))`);
|
|
533
|
+
}
|
|
534
|
+
this.log('');
|
|
535
|
+
// Dry run
|
|
536
|
+
if (flags['dry-run']) {
|
|
537
|
+
this.logInfo('Dry run complete. No files were modified.');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// Confirm archive deletion (unless --force)
|
|
541
|
+
if (archiveDir && !flags.force) {
|
|
542
|
+
const shouldDelete = await confirm({
|
|
543
|
+
message: `Delete ${archiveCount} archived context(s)?`,
|
|
544
|
+
default: false,
|
|
545
|
+
});
|
|
546
|
+
if (!shouldDelete) {
|
|
547
|
+
archiveDir = null;
|
|
548
|
+
archiveCount = 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Execute deletions
|
|
552
|
+
let deletedCount = 0;
|
|
553
|
+
for (const item of toDelete) {
|
|
554
|
+
try {
|
|
555
|
+
await fs.unlink(item.path); // eslint-disable-line no-await-in-loop
|
|
556
|
+
deletedCount++;
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
const err = error;
|
|
560
|
+
this.logWarning(`Failed to delete ${item.path}: ${err.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Log rotation
|
|
564
|
+
if (logAction) {
|
|
565
|
+
try {
|
|
566
|
+
const content = await fs.readFile(logAction.path, 'utf8');
|
|
567
|
+
// Keep the most recent 512KB
|
|
568
|
+
const truncated = content.slice(-524_288);
|
|
569
|
+
// Find the first complete line
|
|
570
|
+
const firstNewline = truncated.indexOf('\n');
|
|
571
|
+
const cleaned = firstNewline === -1 ? truncated : truncated.slice(firstNewline + 1);
|
|
572
|
+
await fs.writeFile(logAction.path, cleaned, 'utf8');
|
|
573
|
+
this.logDebug('Rotated hook-log.jsonl');
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
const err = error;
|
|
577
|
+
this.logWarning(`Failed to rotate log: ${err.message}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Archive cleanup
|
|
581
|
+
let archivedCleaned = 0;
|
|
582
|
+
if (archiveDir) {
|
|
583
|
+
try {
|
|
584
|
+
const archiveEntries = await fs.readdir(archiveDir);
|
|
585
|
+
await Promise.all(archiveEntries.map(async (entry) => {
|
|
586
|
+
try {
|
|
587
|
+
await fs.rm(join(archiveDir, entry), { force: true, recursive: true });
|
|
588
|
+
archivedCleaned++;
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Individual entry failed
|
|
592
|
+
}
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
const err = error;
|
|
597
|
+
this.logWarning(`Failed to clean archive: ${err.message}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Summary
|
|
601
|
+
this.log('');
|
|
602
|
+
const parts = [];
|
|
603
|
+
if (deletedCount > 0) {
|
|
604
|
+
parts.push(`${deletedCount} file(s) removed`);
|
|
605
|
+
}
|
|
606
|
+
if (logAction) {
|
|
607
|
+
parts.push('log rotated');
|
|
608
|
+
}
|
|
609
|
+
if (archivedCleaned > 0) {
|
|
610
|
+
parts.push(`${archivedCleaned} archived context(s) removed`);
|
|
611
|
+
}
|
|
612
|
+
if (parts.length > 0) {
|
|
613
|
+
this.logSuccess(`Output cleanup: ${parts.join(', ')}.`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
this.logInfo('No changes made.');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Clean up backup files created during settings reconstruction.
|
|
621
|
+
*
|
|
622
|
+
* @param targetDir - Project root directory
|
|
623
|
+
*/
|
|
624
|
+
async cleanupBackupFiles(targetDir) {
|
|
625
|
+
const cleanups = Object.values(IDE_FOLDERS).map(async (ide) => {
|
|
626
|
+
if (!ide.settingsFile)
|
|
627
|
+
return;
|
|
628
|
+
const backupPath = join(targetDir, ide.root, `${ide.settingsFile}.backup`);
|
|
629
|
+
try {
|
|
630
|
+
await fs.rm(backupPath, { force: true });
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// Backup doesn't exist or can't be removed
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
await Promise.all(cleanups);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Clean up git exclude entries and prune stale entries.
|
|
640
|
+
*
|
|
641
|
+
* @param targetDir - Project root directory
|
|
642
|
+
* @returns True if git exclude was updated
|
|
643
|
+
*/
|
|
644
|
+
async cleanupGitExclude(targetDir, isFullClear = false) {
|
|
645
|
+
const gitDir = await resolveGitDir(targetDir);
|
|
646
|
+
if (!gitDir)
|
|
647
|
+
return false;
|
|
648
|
+
const { toRemove, toKeep } = await computeExcludeRemovals(gitDir, targetDir, isFullClear ? [] : undefined);
|
|
649
|
+
for (const { entry, reason } of toKeep) {
|
|
650
|
+
this.logDebug(`Keeping ${entry}/ in git exclude (${reason})`);
|
|
651
|
+
}
|
|
652
|
+
if (toRemove.length > 0) {
|
|
653
|
+
await removeExcludeEntries(gitDir, toRemove);
|
|
654
|
+
this.logDebug(`Removed from git exclude: ${toRemove.join(', ')}`);
|
|
655
|
+
}
|
|
656
|
+
const pruned = await pruneExcludeStaleEntries(gitDir, targetDir);
|
|
657
|
+
if (pruned) {
|
|
658
|
+
this.logDebug('Pruned stale git exclude entries');
|
|
659
|
+
}
|
|
660
|
+
return toRemove.length > 0 || pruned;
|
|
661
|
+
}
|
|
662
|
+
async countCoreIdeManagedFiles(targetDir) {
|
|
663
|
+
const coreTemplatePath = await this.getCoreTemplatePathSafe();
|
|
664
|
+
if (!coreTemplatePath)
|
|
665
|
+
return 0;
|
|
666
|
+
let total = 0;
|
|
667
|
+
for (const ide of Object.values(IDE_FOLDERS)) {
|
|
668
|
+
const sourceIdeRoot = join(coreTemplatePath, ide.root);
|
|
669
|
+
const targetIdeRoot = join(targetDir, ide.root);
|
|
670
|
+
// eslint-disable-next-line no-await-in-loop
|
|
671
|
+
if (!(await pathExists(sourceIdeRoot)) || !(await pathExists(targetIdeRoot)))
|
|
672
|
+
continue;
|
|
673
|
+
// eslint-disable-next-line no-await-in-loop
|
|
674
|
+
total += await this.countMatchingManagedFiles(sourceIdeRoot, targetIdeRoot);
|
|
675
|
+
}
|
|
676
|
+
return total;
|
|
677
|
+
}
|
|
678
|
+
async countMatchingManagedFiles(sourceDir, targetDir) {
|
|
679
|
+
let entries;
|
|
680
|
+
try {
|
|
681
|
+
entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
let count = 0;
|
|
687
|
+
for (const entry of entries) {
|
|
688
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
689
|
+
const targetPath = join(targetDir, entry.name);
|
|
690
|
+
if (entry.isDirectory()) {
|
|
691
|
+
// eslint-disable-next-line no-await-in-loop
|
|
692
|
+
count += await this.countMatchingManagedFiles(sourcePath, targetPath);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (!entry.isFile())
|
|
696
|
+
continue;
|
|
697
|
+
if (SETTINGS_FILES_TO_SKIP.has(entry.name))
|
|
698
|
+
continue;
|
|
699
|
+
// eslint-disable-next-line no-await-in-loop
|
|
700
|
+
if (await pathExists(targetPath))
|
|
701
|
+
count++;
|
|
702
|
+
}
|
|
703
|
+
return count;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Display a list of folders to remove.
|
|
707
|
+
*
|
|
708
|
+
* @param targetDir - Base directory for relative path display
|
|
709
|
+
* @param folders - Array of folder paths
|
|
710
|
+
* @param label - Label for the folder type
|
|
711
|
+
*/
|
|
712
|
+
displayFolderList(targetDir, folders, label) {
|
|
713
|
+
if (folders.length === 0)
|
|
714
|
+
return;
|
|
715
|
+
this.logInfo(`${label} to remove (${folders.length}):`);
|
|
716
|
+
for (const folder of folders) {
|
|
717
|
+
const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
|
|
718
|
+
this.log(` ${folderName}/`);
|
|
719
|
+
}
|
|
720
|
+
this.log('');
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Display all pending changes before confirmation.
|
|
724
|
+
*
|
|
725
|
+
* @param targetDir - Project root directory
|
|
726
|
+
* @param folders - Discovered folders and methods to remove
|
|
727
|
+
* @param folders.methodRuntimeFolders - Method runtime folders to remove
|
|
728
|
+
* @param folders.outputMethodFolders - Output method folders to remove
|
|
729
|
+
* @param folders.ideMethodFolders - IDE method folders to remove
|
|
730
|
+
* @param folders.coreRuntimeFolders - Core runtime folders to remove
|
|
731
|
+
* @param folders.coreIdeFilesToRemove - Number of core IDE files to remove
|
|
732
|
+
* @param folders.methodsToRemove - Method names being removed
|
|
733
|
+
*/
|
|
734
|
+
async displayPendingChanges(targetDir, folders) {
|
|
735
|
+
const { methodRuntimeFolders, outputMethodFolders, ideMethodFolders, coreRuntimeFolders, methodsToRemove, coreIdeFilesToRemove, } = folders;
|
|
736
|
+
this.log('');
|
|
737
|
+
this.displayFolderList(targetDir, methodRuntimeFolders, 'Method runtime folders');
|
|
738
|
+
this.displayFolderList(targetDir, outputMethodFolders, 'Output folders');
|
|
739
|
+
this.displayFolderList(targetDir, ideMethodFolders, 'IDE method folders');
|
|
740
|
+
this.displayFolderList(targetDir, coreRuntimeFolders, 'Core runtime folders');
|
|
741
|
+
if (coreIdeFilesToRemove > 0) {
|
|
742
|
+
this.logInfo(`Core IDE template files to remove (${coreIdeFilesToRemove})`);
|
|
743
|
+
this.log('');
|
|
744
|
+
}
|
|
745
|
+
if (methodsToRemove.length > 0) {
|
|
746
|
+
this.logInfo(`Will reconstruct core IDE settings after removing: ${methodsToRemove.join(', ')}`);
|
|
747
|
+
this.log('');
|
|
748
|
+
}
|
|
749
|
+
// Check if _output will be empty after clearing
|
|
750
|
+
const allMethodFolders = await this.findOutputFolders(targetDir);
|
|
751
|
+
if (allMethodFolders.length > 0 && allMethodFolders.length === outputMethodFolders.length) {
|
|
752
|
+
this.logInfo(`${OUTPUT_FOLDER_NAME}/ folder will be removed (will be empty)`);
|
|
753
|
+
this.log('');
|
|
754
|
+
}
|
|
755
|
+
// Check if IDE folders might be removed after clearing
|
|
756
|
+
const [willClaudeFolderBeEmpty, willCodexFolderBeEmpty, willWindsurfFolderBeEmpty] = await Promise.all([
|
|
757
|
+
checkIdeRemovalEligibility(targetDir, IDE_FOLDERS.claude, ideMethodFolders),
|
|
758
|
+
checkIdeRemovalEligibility(targetDir, IDE_FOLDERS.codex, ideMethodFolders),
|
|
759
|
+
checkIdeRemovalEligibility(targetDir, IDE_FOLDERS.windsurf, ideMethodFolders),
|
|
760
|
+
]);
|
|
761
|
+
if (willClaudeFolderBeEmpty) {
|
|
762
|
+
this.logInfo(`${IDE_FOLDERS.claude.root}/ folder will be removed (will be empty)`);
|
|
763
|
+
this.log('');
|
|
764
|
+
}
|
|
765
|
+
if (willCodexFolderBeEmpty) {
|
|
766
|
+
this.logInfo(`${IDE_FOLDERS.codex.root}/ folder will be removed (will be empty)`);
|
|
767
|
+
this.log('');
|
|
768
|
+
}
|
|
769
|
+
if (willWindsurfFolderBeEmpty) {
|
|
770
|
+
this.logInfo(`${IDE_FOLDERS.windsurf.root}/ folder will be removed (will be empty)`);
|
|
771
|
+
this.log('');
|
|
772
|
+
}
|
|
773
|
+
// Compute git exclude changes for dry-run display
|
|
774
|
+
const gitDir = await resolveGitDir(targetDir);
|
|
775
|
+
const excludeSimulation = gitDir ? await computeExcludeRemovals(gitDir, targetDir) : { toRemove: [], toKeep: [] };
|
|
776
|
+
if (excludeSimulation.toRemove.length > 0 || excludeSimulation.toKeep.length > 0) {
|
|
777
|
+
this.logInfo('Git exclude changes:');
|
|
778
|
+
for (const { entry, reason } of excludeSimulation.toKeep) {
|
|
779
|
+
this.log(` keep ${entry}/ (${reason})`);
|
|
780
|
+
}
|
|
781
|
+
for (const entry of excludeSimulation.toRemove) {
|
|
782
|
+
this.log(` remove ${entry}/`);
|
|
783
|
+
}
|
|
784
|
+
this.log('');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Delete all discovered folders in parallel.
|
|
789
|
+
*
|
|
790
|
+
* @param methodRuntimeFolders - Method runtime folders to delete
|
|
791
|
+
* @param outputMethodFolders - Output method folders to delete
|
|
792
|
+
* @param ideMethodFolders - IDE method folders to delete
|
|
793
|
+
* @returns Count of successfully deleted folders by type
|
|
794
|
+
*/
|
|
795
|
+
async executeFolderDeletion(methodRuntimeFolders, outputMethodFolders, ideMethodFolders, coreRuntimeFolders) {
|
|
796
|
+
const deleteFolder = async (folder, type) => {
|
|
797
|
+
try {
|
|
798
|
+
await removeDirectory(folder);
|
|
799
|
+
this.logDebug(`Removed ${type} folder: ${folder}`);
|
|
800
|
+
return { success: true, type };
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
const err = error;
|
|
804
|
+
this.logWarning(`Failed to delete ${folder}: ${err.message}`);
|
|
805
|
+
return { success: false, type };
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const deleteResults = await Promise.all([
|
|
809
|
+
...methodRuntimeFolders.map((f) => deleteFolder(f, 'method runtime')),
|
|
810
|
+
...outputMethodFolders.map((f) => deleteFolder(f, 'output')),
|
|
811
|
+
...ideMethodFolders.map((f) => deleteFolder(f, 'IDE method')),
|
|
812
|
+
...coreRuntimeFolders.map((f) => deleteFolder(f, 'core runtime')),
|
|
813
|
+
]);
|
|
814
|
+
return {
|
|
815
|
+
deletedMethodRuntime: deleteResults.filter((r) => r.success && r.type === 'method runtime').length,
|
|
816
|
+
deletedOutput: deleteResults.filter((r) => r.success && r.type === 'output').length,
|
|
817
|
+
deletedIde: deleteResults.filter((r) => r.success && r.type === 'IDE method').length,
|
|
818
|
+
deletedCoreRuntime: deleteResults.filter((r) => r.success && r.type === 'core runtime').length,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Extract method names from runtime folder names (e.g., _gsd -> gsd).
|
|
823
|
+
*
|
|
824
|
+
* @param methodRuntimeFolders - Array of runtime folder paths
|
|
825
|
+
* @returns Array of method names
|
|
826
|
+
*/
|
|
827
|
+
extractMethodNames(methodRuntimeFolders) {
|
|
828
|
+
const methods = [];
|
|
829
|
+
for (const folder of methodRuntimeFolders) {
|
|
830
|
+
const folderName = folder.split(/[/\\]/).pop() || '';
|
|
831
|
+
if (folderName.startsWith('_')) {
|
|
832
|
+
methods.push(folderName.slice(1));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return methods;
|
|
836
|
+
}
|
|
837
|
+
async findCoreRuntimeFolders(targetDir) {
|
|
838
|
+
const containerDir = join(targetDir, AIWCLI_CONTAINER);
|
|
839
|
+
const paths = CORE_RUNTIME_FOLDERS.map((name) => join(containerDir, name));
|
|
840
|
+
const checks = await Promise.all(paths.map((p) => pathExists(p)));
|
|
841
|
+
return paths.filter((_, index) => checks[index]);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Find all IDE method folders by scanning subdirectories of each IDE root
|
|
845
|
+
* for children matching installed method names.
|
|
846
|
+
*
|
|
847
|
+
* For example, finds .claude/commands/cc-native/, .claude/skills/cc-native/,
|
|
848
|
+
* .windsurf/workflows/cc-native/ — without hardcoding which subdirectories exist.
|
|
849
|
+
*
|
|
850
|
+
* @param targetDir - Directory to search in
|
|
851
|
+
* @param template - Optional template/method name to filter by
|
|
852
|
+
* @returns Array of IDE method folder paths
|
|
853
|
+
*/
|
|
854
|
+
async findIdeMethodFolders(targetDir, template) {
|
|
855
|
+
// Build method set: from --template flag, or from installed methods
|
|
856
|
+
const methodNames = new Set(template ? [template] : await getInstalledMethods(targetDir));
|
|
857
|
+
if (methodNames.size === 0) {
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
// For each IDE root, scan all subdirectories for children matching method names
|
|
861
|
+
const ideRoots = Object.values(IDE_FOLDERS).map((ide) => join(targetDir, ide.root));
|
|
862
|
+
const ideResults = await Promise.all(ideRoots.map(async (ideRoot) => {
|
|
863
|
+
// Get all subdirectories of IDE root (e.g., .claude/commands/, .claude/skills/)
|
|
864
|
+
try {
|
|
865
|
+
const topEntries = await fs.readdir(ideRoot, { withFileTypes: true });
|
|
866
|
+
const subdirs = topEntries.filter((e) => e.isDirectory());
|
|
867
|
+
// For each subdirectory, check for method-named children
|
|
868
|
+
const subResults = await Promise.all(subdirs.map(async (subdir) => {
|
|
869
|
+
const subdirPath = join(ideRoot, subdir.name);
|
|
870
|
+
try {
|
|
871
|
+
const entries = await fs.readdir(subdirPath, { withFileTypes: true });
|
|
872
|
+
return entries
|
|
873
|
+
.filter((entry) => entry.isDirectory() && methodNames.has(entry.name))
|
|
874
|
+
.map((entry) => join(subdirPath, entry.name));
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
}));
|
|
880
|
+
return subResults.flat();
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return [];
|
|
884
|
+
}
|
|
885
|
+
}));
|
|
886
|
+
return ideResults.flat();
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Find all method runtime folders in the target directory.
|
|
890
|
+
* Looks for .aiwcli/_{method}/ structure (e.g., .aiwcli/_gsd/, .aiwcli/_bmad/).
|
|
891
|
+
*
|
|
892
|
+
* @param targetDir - Directory to search in
|
|
893
|
+
* @param template - Optional template/method name to filter by (e.g., 'bmad', 'gsd')
|
|
894
|
+
* @returns Array of method runtime folder paths
|
|
895
|
+
*/
|
|
896
|
+
async findMethodRuntimeFolders(targetDir, template) {
|
|
897
|
+
const foundFolders = [];
|
|
898
|
+
const containerDir = join(targetDir, AIWCLI_CONTAINER);
|
|
899
|
+
try {
|
|
900
|
+
const entries = await fs.readdir(containerDir, { withFileTypes: true });
|
|
901
|
+
for (const entry of entries) {
|
|
902
|
+
if (!entry.isDirectory() ||
|
|
903
|
+
!entry.name.startsWith('_') ||
|
|
904
|
+
entry.name === OUTPUT_FOLDER_NAME ||
|
|
905
|
+
entry.name === '_core' ||
|
|
906
|
+
false) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
// If template specified, only include matching folder
|
|
910
|
+
if (template && entry.name !== `_${template}`) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
foundFolders.push(join(containerDir, entry.name));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// Directory can't be read - return empty
|
|
918
|
+
}
|
|
919
|
+
return foundFolders;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Find all method output folders in the target directory.
|
|
923
|
+
* Looks for _output/{method}/ structure at project root.
|
|
924
|
+
*
|
|
925
|
+
* @param targetDir - Directory to search in
|
|
926
|
+
* @param template - Optional template/method name to filter by (e.g., 'bmad', 'gsd')
|
|
927
|
+
* @returns Array of output folder paths
|
|
928
|
+
*/
|
|
929
|
+
async findOutputFolders(targetDir, template) {
|
|
930
|
+
const outputDir = join(targetDir, OUTPUT_FOLDER_NAME);
|
|
931
|
+
// Check if _output folder exists
|
|
932
|
+
try {
|
|
933
|
+
const stat = await fs.stat(outputDir);
|
|
934
|
+
if (!stat.isDirectory()) {
|
|
935
|
+
return [];
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
// _output folder doesn't exist
|
|
940
|
+
return [];
|
|
941
|
+
}
|
|
942
|
+
// If template specified, only look for that specific method folder
|
|
943
|
+
if (template) {
|
|
944
|
+
const methodPath = join(outputDir, template);
|
|
945
|
+
try {
|
|
946
|
+
const stat = await fs.stat(methodPath);
|
|
947
|
+
if (stat.isDirectory()) {
|
|
948
|
+
return [methodPath];
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// Method folder doesn't exist
|
|
953
|
+
}
|
|
954
|
+
return [];
|
|
955
|
+
}
|
|
956
|
+
// No template filter - find all method folders within _output
|
|
957
|
+
const foundFolders = [];
|
|
958
|
+
try {
|
|
959
|
+
const entries = await fs.readdir(outputDir, { withFileTypes: true });
|
|
960
|
+
for (const entry of entries) {
|
|
961
|
+
if (entry.isDirectory()) {
|
|
962
|
+
foundFolders.push(join(outputDir, entry.name));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
// Directory can't be read - return empty
|
|
968
|
+
}
|
|
969
|
+
return foundFolders;
|
|
970
|
+
}
|
|
971
|
+
async getCoreTemplatePathSafe() {
|
|
972
|
+
try {
|
|
973
|
+
return await getTemplatePath(CORE_TEMPLATE_NAME);
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Perform all post-deletion cleanup: empty dir removal, git exclude, settings, IDE folders.
|
|
981
|
+
*
|
|
982
|
+
* @param targetDir - Project root directory
|
|
983
|
+
* @param methodsToRemove - Method names being removed
|
|
984
|
+
* @returns Cleanup result state
|
|
985
|
+
*/
|
|
986
|
+
async performPostDeleteCleanup(targetDir, methodsToRemove, isFullClear) {
|
|
987
|
+
const containerDir = join(targetDir, AIWCLI_CONTAINER);
|
|
988
|
+
const outputDir = join(targetDir, OUTPUT_FOLDER_NAME);
|
|
989
|
+
let removedCoreIdeFiles = 0;
|
|
990
|
+
let removedOutputDir = false;
|
|
991
|
+
let removedAiwcliContainer = false;
|
|
992
|
+
if (isFullClear) {
|
|
993
|
+
// Force-delete .aiwcli/ entirely on full clear.
|
|
994
|
+
try {
|
|
995
|
+
await fs.rm(containerDir, { recursive: true, force: true });
|
|
996
|
+
removedAiwcliContainer = true;
|
|
997
|
+
this.logDebug(`Force-deleted ${AIWCLI_CONTAINER}/ folder`);
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
// Directory may not exist
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
// Check if .aiwcli container is now empty and remove it.
|
|
1005
|
+
removedAiwcliContainer = await tryRemoveEmptyDir(containerDir);
|
|
1006
|
+
if (removedAiwcliContainer) {
|
|
1007
|
+
this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/ folder`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
// Check if the root _output folder is now empty and remove it.
|
|
1011
|
+
removedOutputDir = await tryRemoveEmptyDir(outputDir);
|
|
1012
|
+
if (removedOutputDir) {
|
|
1013
|
+
this.logDebug(`Removed empty ${OUTPUT_FOLDER_NAME}/ folder`);
|
|
1014
|
+
}
|
|
1015
|
+
if (isFullClear) {
|
|
1016
|
+
removedCoreIdeFiles = await this.removeCoreIdeContent(targetDir);
|
|
1017
|
+
}
|
|
1018
|
+
// Reconstruct IDE settings
|
|
1019
|
+
let { updatedClaudeSettings, updatedWindsurfSettings } = await this.reconstructSettingsAfterRemoval(targetDir, methodsToRemove, isFullClear);
|
|
1020
|
+
// Clean up backup files
|
|
1021
|
+
await this.cleanupBackupFiles(targetDir);
|
|
1022
|
+
// Check if IDE folders should be fully deleted
|
|
1023
|
+
const removedClaudeDir = await this.tryRemoveIdeFolder(targetDir, IDE_FOLDERS.claude);
|
|
1024
|
+
if (removedClaudeDir)
|
|
1025
|
+
updatedClaudeSettings = false;
|
|
1026
|
+
const removedCodexDir = await this.tryRemoveIdeFolder(targetDir, IDE_FOLDERS.codex);
|
|
1027
|
+
const removedWindsurfDir = await this.tryRemoveIdeFolder(targetDir, IDE_FOLDERS.windsurf);
|
|
1028
|
+
if (removedWindsurfDir)
|
|
1029
|
+
updatedWindsurfSettings = false;
|
|
1030
|
+
// Smart git exclude removal must happen after any now-empty IDE folders are deleted.
|
|
1031
|
+
const gitExcludeUpdated = await this.cleanupGitExclude(targetDir, isFullClear);
|
|
1032
|
+
return {
|
|
1033
|
+
removedCoreIdeFiles,
|
|
1034
|
+
removedOutputDir, removedAiwcliContainer, removedClaudeDir, removedCodexDir, removedWindsurfDir,
|
|
1035
|
+
updatedClaudeSettings, updatedWindsurfSettings, gitExcludeUpdated,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Reconstruct IDE settings after method removal.
|
|
1040
|
+
*
|
|
1041
|
+
* @param targetDir - Project root directory
|
|
1042
|
+
* @param methodsToRemove - Methods being removed
|
|
1043
|
+
* @returns Which IDE settings were updated
|
|
1044
|
+
*/
|
|
1045
|
+
async reconstructSettingsAfterRemoval(targetDir, methodsToRemove, isFullClear) {
|
|
1046
|
+
let updatedClaudeSettings = false;
|
|
1047
|
+
let updatedWindsurfSettings = false;
|
|
1048
|
+
if (methodsToRemove.length > 0) {
|
|
1049
|
+
for (const method of methodsToRemove) {
|
|
1050
|
+
await markMethodRemoved(targetDir, method); // eslint-disable-line no-await-in-loop
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (isFullClear) {
|
|
1054
|
+
await deleteInstallStateIfPresent(targetDir);
|
|
1055
|
+
await this.stripAiwSettingsForFullClear(targetDir);
|
|
1056
|
+
}
|
|
1057
|
+
else if (methodsToRemove.length > 0) {
|
|
1058
|
+
const remainingTemplates = (await getInstalledMethods(targetDir)).filter((method) => !methodsToRemove.includes(method));
|
|
1059
|
+
const ides = [];
|
|
1060
|
+
if (await pathExists(join(targetDir, IDE_FOLDERS.claude.root)))
|
|
1061
|
+
ides.push('claude');
|
|
1062
|
+
if (await pathExists(join(targetDir, IDE_FOLDERS.windsurf.root)))
|
|
1063
|
+
ides.push('windsurf');
|
|
1064
|
+
if (ides.length > 0) {
|
|
1065
|
+
await reconstructIdeSettings(targetDir, remainingTemplates, ides);
|
|
1066
|
+
if (ides.includes('claude')) {
|
|
1067
|
+
this.logDebug('Reconstructed .claude/settings.json (backup created)');
|
|
1068
|
+
updatedClaudeSettings = true;
|
|
1069
|
+
}
|
|
1070
|
+
if (ides.includes('windsurf')) {
|
|
1071
|
+
this.logDebug('Reconstructed .windsurf/hooks.json (backup created)');
|
|
1072
|
+
updatedWindsurfSettings = true;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const currentState = await readInstallState(targetDir);
|
|
1077
|
+
if (currentState && Object.keys(currentState.methods).length === 0 && !currentState.core.installed) {
|
|
1078
|
+
await deleteInstallStateIfPresent(targetDir);
|
|
1079
|
+
}
|
|
1080
|
+
return { updatedClaudeSettings, updatedWindsurfSettings };
|
|
1081
|
+
}
|
|
1082
|
+
async removeCoreIdeContent(targetDir) {
|
|
1083
|
+
const coreTemplatePath = await this.getCoreTemplatePathSafe();
|
|
1084
|
+
if (!coreTemplatePath)
|
|
1085
|
+
return 0;
|
|
1086
|
+
let removedFiles = 0;
|
|
1087
|
+
for (const ide of Object.values(IDE_FOLDERS)) {
|
|
1088
|
+
const sourceIdeRoot = join(coreTemplatePath, ide.root);
|
|
1089
|
+
const targetIdeRoot = join(targetDir, ide.root);
|
|
1090
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1091
|
+
if (!(await pathExists(sourceIdeRoot)) || !(await pathExists(targetIdeRoot)))
|
|
1092
|
+
continue;
|
|
1093
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1094
|
+
removedFiles += await removeMatchingFiles(sourceIdeRoot, targetIdeRoot);
|
|
1095
|
+
}
|
|
1096
|
+
return removedFiles;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Report the results of a clear operation.
|
|
1100
|
+
*
|
|
1101
|
+
* @param deleteCounts - Counts of deleted folders by type
|
|
1102
|
+
* @param deleteCounts.deletedMethodRuntime - Number of method runtime folders deleted
|
|
1103
|
+
* @param deleteCounts.deletedOutput - Number of output folders deleted
|
|
1104
|
+
* @param deleteCounts.deletedIde - Number of IDE method folders deleted
|
|
1105
|
+
* @param deleteCounts.deletedCoreRuntime - Number of core runtime folders deleted
|
|
1106
|
+
* @param cleanup - Cleanup operation results
|
|
1107
|
+
* @param cleanup.gitExcludeUpdated - Whether git exclude was updated
|
|
1108
|
+
* @param cleanup.removedOutputDir - Whether _output dir was removed
|
|
1109
|
+
* @param cleanup.removedAiwcliContainer - Whether .aiwcli dir was removed
|
|
1110
|
+
* @param cleanup.removedClaudeDir - Whether .claude dir was removed
|
|
1111
|
+
* @param cleanup.removedCodexDir - Whether .codex dir was removed
|
|
1112
|
+
* @param cleanup.removedWindsurfDir - Whether .windsurf dir was removed
|
|
1113
|
+
* @param cleanup.removedCoreIdeFiles - Number of core IDE files removed
|
|
1114
|
+
* @param cleanup.updatedClaudeSettings - Whether Claude settings were updated
|
|
1115
|
+
* @param cleanup.updatedWindsurfSettings - Whether Windsurf settings were updated
|
|
1116
|
+
*/
|
|
1117
|
+
reportClearResults(deleteCounts, cleanup) {
|
|
1118
|
+
this.log('');
|
|
1119
|
+
const parts = [];
|
|
1120
|
+
if (deleteCounts.deletedMethodRuntime > 0)
|
|
1121
|
+
parts.push(`${deleteCounts.deletedMethodRuntime} method runtime folder(s)`);
|
|
1122
|
+
if (deleteCounts.deletedOutput > 0)
|
|
1123
|
+
parts.push(`${deleteCounts.deletedOutput} output folder(s)`);
|
|
1124
|
+
if (deleteCounts.deletedIde > 0)
|
|
1125
|
+
parts.push(`${deleteCounts.deletedIde} IDE method folder(s)`);
|
|
1126
|
+
if (deleteCounts.deletedCoreRuntime > 0)
|
|
1127
|
+
parts.push(`${deleteCounts.deletedCoreRuntime} core runtime folder(s)`);
|
|
1128
|
+
if (cleanup.removedCoreIdeFiles > 0)
|
|
1129
|
+
parts.push(`${cleanup.removedCoreIdeFiles} core IDE file(s)`);
|
|
1130
|
+
if (cleanup.removedOutputDir)
|
|
1131
|
+
parts.push(`${OUTPUT_FOLDER_NAME}/ folder`);
|
|
1132
|
+
if (cleanup.removedAiwcliContainer)
|
|
1133
|
+
parts.push(`${AIWCLI_CONTAINER}/ folder`);
|
|
1134
|
+
if (cleanup.removedClaudeDir)
|
|
1135
|
+
parts.push(`${IDE_FOLDERS.claude.root}/ folder`);
|
|
1136
|
+
if (cleanup.removedCodexDir)
|
|
1137
|
+
parts.push(`${IDE_FOLDERS.codex.root}/ folder`);
|
|
1138
|
+
if (cleanup.removedWindsurfDir)
|
|
1139
|
+
parts.push(`${IDE_FOLDERS.windsurf.root}/ folder`);
|
|
1140
|
+
this.logSuccess(`Cleared: ${parts.join(', ')}.`);
|
|
1141
|
+
if (cleanup.gitExcludeUpdated) {
|
|
1142
|
+
this.logSuccess('Updated git exclude.');
|
|
1143
|
+
}
|
|
1144
|
+
if (cleanup.updatedClaudeSettings) {
|
|
1145
|
+
this.logSuccess('Updated .claude/settings.json (backup: settings.json.backup).');
|
|
1146
|
+
}
|
|
1147
|
+
if (cleanup.updatedWindsurfSettings) {
|
|
1148
|
+
this.logSuccess('Updated .windsurf/hooks.json (backup: hooks.json.backup).');
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async resolveMethodsToRemove(targetDir, template, methodRuntimeFolders) {
|
|
1152
|
+
if (template)
|
|
1153
|
+
return [template];
|
|
1154
|
+
const installedMethods = new Set(await getInstalledMethods(targetDir));
|
|
1155
|
+
const discoveredFromFolders = this.extractMethodNames(methodRuntimeFolders);
|
|
1156
|
+
for (const method of discoveredFromFolders) {
|
|
1157
|
+
installedMethods.add(method);
|
|
1158
|
+
}
|
|
1159
|
+
return [...installedMethods];
|
|
1160
|
+
}
|
|
1161
|
+
async stripAiwSettingsForFullClear(targetDir) {
|
|
1162
|
+
const ops = Object.values(IDE_FOLDERS).map(async (ide) => {
|
|
1163
|
+
if (!ide.settingsFile)
|
|
1164
|
+
return;
|
|
1165
|
+
const settingsPath = join(targetDir, ide.root, ide.settingsFile);
|
|
1166
|
+
try {
|
|
1167
|
+
const raw = await fs.readFile(settingsPath, 'utf8');
|
|
1168
|
+
const parsed = JSON.parse(raw);
|
|
1169
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1170
|
+
return;
|
|
1171
|
+
delete parsed.hooks;
|
|
1172
|
+
delete parsed.statusLine;
|
|
1173
|
+
delete parsed.fileSuggestion;
|
|
1174
|
+
delete parsed.methods;
|
|
1175
|
+
if (ide.root === IDE_FOLDERS.claude.root) {
|
|
1176
|
+
if (parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)) {
|
|
1177
|
+
delete parsed.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
1178
|
+
if (Object.keys(parsed.env).length === 0)
|
|
1179
|
+
delete parsed.env;
|
|
1180
|
+
}
|
|
1181
|
+
if (parsed.permissions && typeof parsed.permissions === 'object' && !Array.isArray(parsed.permissions)) {
|
|
1182
|
+
const permissionKeys = Object.keys(parsed.permissions);
|
|
1183
|
+
const hasOnlyAllowDeny = permissionKeys.every((key) => key === 'allow' || key === 'deny');
|
|
1184
|
+
const allow = Array.isArray(parsed.permissions.allow) ? parsed.permissions.allow : [];
|
|
1185
|
+
const deny = Array.isArray(parsed.permissions.deny) ? parsed.permissions.deny : [];
|
|
1186
|
+
if (hasOnlyAllowDeny && allow.length === 0 && deny.length === 0)
|
|
1187
|
+
delete parsed.permissions;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
await fs.writeFile(settingsPath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
// Ignore invalid/missing settings files
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
await Promise.all(ops);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Try to remove an IDE folder if it should be deleted (empty settings + empty subfolders).
|
|
1200
|
+
*
|
|
1201
|
+
* @param targetDir - Project root directory
|
|
1202
|
+
* @param ideFolder - IDE folder configuration
|
|
1203
|
+
* @param ideFolder.root - Root folder name (e.g., '.claude')
|
|
1204
|
+
* @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
|
|
1205
|
+
* @returns True if the folder was removed
|
|
1206
|
+
*/
|
|
1207
|
+
async tryRemoveIdeFolder(targetDir, ideFolder) {
|
|
1208
|
+
if (!(await shouldDeleteIdeFolder(targetDir, ideFolder)))
|
|
1209
|
+
return false;
|
|
1210
|
+
const dirPath = join(targetDir, ideFolder.root);
|
|
1211
|
+
try {
|
|
1212
|
+
await removeDirectory(dirPath);
|
|
1213
|
+
this.logDebug(`Removed empty ${ideFolder.root}/ folder`);
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
catch {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|