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