@ojokesusu/lintasai 1.1.2
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/.github/workflows/publish-npm.yml +40 -0
- package/.github/workflows/validate.yml +93 -0
- package/AUDIT_POST_SETUP_PROMPT_v1.md +280 -0
- package/BOOTSTRAP_PROJECT_DOCS_PROMPT_v1.md +3 -0
- package/CHANGELOG.md +313 -0
- package/CLAUDE_universal_v1.md +1021 -0
- package/CONTRIBUTING.md +101 -0
- package/FIRST_SESSION_PROMPT_v1.md +7 -0
- package/JALANKAN_KIT.md +188 -0
- package/LICENSE +21 -0
- package/MULAI_DI_SINI.md +145 -0
- package/PROJECT_KICKOFF_PROMPT_v1.md +3 -0
- package/PROJECT_LIFECYCLE_PROMPT_v1.md +536 -0
- package/PROJECT_MIGRATION_PROMPT_v1.md +3 -0
- package/README.md +505 -0
- package/SETUP_POLA_B_PROMPT_v1.md +5 -0
- package/SPLIT_REPO_MIGRATION_PROMPT_v1.md +485 -0
- package/TEAM_ROLLOUT_GUIDE_v1.md +172 -0
- package/UPDATE_DOCS_PROMPT_v1.md +3 -0
- package/UPDATE_KIT_PROMPT_v1.md +213 -0
- package/bin/lintasai.js +81 -0
- package/docs/SIGNED_RELEASE.md +162 -0
- package/install-windows.ps1 +225 -0
- package/kit.ps1 +508 -0
- package/lib/agents-md.ps1 +174 -0
- package/lib/git-helpers.ps1 +104 -0
- package/lib/kit-files.psd1 +133 -0
- package/lib/manifest-signing.ps1 +65 -0
- package/lib/manifest.ps1 +267 -0
- package/lib/rollback.ps1 +241 -0
- package/lib/safety.ps1 +193 -0
- package/lib/template-deploy.ps1 +242 -0
- package/lib/version-detect.ps1 +161 -0
- package/package.json +36 -0
- package/setup-pola-b.ps1 +687 -0
- package/templates/ANALOGI_LIBRARY.md +7 -0
- package/templates/CLAUDE_TEAM_GUIDE.md +505 -0
- package/templates/CROSS_REPO_TYPES_PIPELINE.md +473 -0
- package/templates/DB_SCHEMA_SCAN_PROMPT.md +194 -0
- package/templates/DISCORD_BOT_INTEGRATION.md +187 -0
- package/templates/GLOSSARY_NON_PROGRAMMER.md +361 -0
- package/templates/INDEX.md +157 -0
- package/templates/MCP_SETUP.md +1145 -0
- package/templates/MIGRATE_TO_SUBFOLDER_PROMPT_v1.md +220 -0
- package/templates/ONBOARDING.md +172 -0
- package/templates/PROJECT_STARTER_TEMPLATES.md +264 -0
- package/templates/PROMPT_LIBRARY.md +790 -0
- package/templates/RLS_SETUP_PROMPT.md +167 -0
- package/templates/SECURITY_INCIDENT_PLAYBOOK.md +191 -0
- package/templates/SPLIT_REPO_AGENTS_TEMPLATES.md +32 -0
- package/templates/SPLIT_REPO_NON_PROGRAMMER_PROMPTS.md +604 -0
- package/templates/SPLIT_REPO_TOOLS_SETUP.md +388 -0
- package/templates/STACK_DETECTION_PATTERN.md +261 -0
- package/templates/STACK_GUIDE.md +564 -0
- package/templates/STACK_MIGRATION_GUIDE.md +154 -0
- package/templates/STACK_VERSIONS.md +31 -0
- package/templates/UPDATE_GUIDE.md +246 -0
- package/templates/_EXAMPLE.md +110 -0
- package/templates/_PATTERNS.md +173 -0
- package/templates/architecture.md +180 -0
- package/templates/architecture_auto.md +61 -0
- package/templates/decisions/README.md +108 -0
- package/templates/decisions/_TEMPLATE.md +84 -0
- package/templates/feature-flags-advanced.md +171 -0
- package/templates/github/CODEOWNERS.template +61 -0
- package/templates/github/GENERATE_TYPES_SCRIPT.md +77 -0
- package/templates/github/PUBLISH_SHARED_WORKFLOW.yml +52 -0
- package/templates/github/RECEIVE_BACKEND_UPDATE.yml +106 -0
- package/templates/github/RENOVATE_FRONTEND.json +28 -0
- package/templates/github/TRIGGER_FRONTEND_UPDATE.yml +29 -0
- package/templates/github/pull_request_template.md +44 -0
- package/templates/github/scripts/ai-review.js +153 -0
- package/templates/github/workflows/ai-review.yml +61 -0
- package/templates/github/workflows/backup-schemas.yml +169 -0
- package/templates/glossary.md +110 -0
- package/templates/split-agents/BACKEND.md +149 -0
- package/templates/split-agents/FRONTEND.md +141 -0
- package/templates/split-agents/SHARED.md +82 -0
- package/templates/split-agents/TOOLS.md +77 -0
- package/tests/Run-Tests.ps1 +19 -0
- package/tests/lib-safety.Tests.ps1 +66 -0
- package/tests/rollback.Tests.ps1 +66 -0
- package/tests/uninstall.Tests.ps1 +265 -0
- package/tests/update-kit.Tests.ps1 +78 -0
- package/uninstall.ps1 +794 -0
- package/update-kit.ps1 +907 -0
package/uninstall.ps1
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
uninstall.ps1 - Hapus lintasAI dari proyek dengan AMAN (manifest-based diff)
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Script ini baca .claude-kit/.install-manifest.json (manifest yang dibuat setup-pola-b.ps1)
|
|
7
|
+
lalu compare hash sha256 tiap file kit-template vs file di disk:
|
|
8
|
+
|
|
9
|
+
PRISTINE (hash match) -> auto-delete, file persis sama dengan yang kit copy waktu install.
|
|
10
|
+
MODIFIED (hash beda) -> SKIP by default (user sudah edit). -AllowModified -> backup ke
|
|
11
|
+
.pre-uninstall-<timestamp>.bak lalu hapus. (-Force = alias deprecated)
|
|
12
|
+
SYMLINK (reparse pt) -> SKIP selalu. Symlink / junction tidak pernah di-auto-delete
|
|
13
|
+
supaya tidak bocorin isi file di luar project ke .bak.
|
|
14
|
+
BLOCKED (out of root)-> SKIP. Manifest entry yang resolve ke luar project ditolak
|
|
15
|
+
(proteksi path traversal). Lihat juga -AllowProjectRootMismatch.
|
|
16
|
+
LOCKED (hash gagal) -> SKIP. File ke-lock editor/AV; user diminta tutup editor.
|
|
17
|
+
MISSING (file gone) -> skip silent (user mungkin sudah hapus manual).
|
|
18
|
+
|
|
19
|
+
Direktori yang dibuat saat install (docs/, .github/, dst.) hanya di-delete kalau EMPTY
|
|
20
|
+
setelah file dihapus. Project file kamu di sana TETAP aman.
|
|
21
|
+
|
|
22
|
+
AGENTS.md default SKIP delete (heavy customization expected). Pakai -DeleteAgents untuk hapus.
|
|
23
|
+
|
|
24
|
+
Default uninstall TIDAK self-delete folder .claude-kit\ -- script tidak bisa hapus folder
|
|
25
|
+
yang sedang dia jalankan dari sana. Instruksi 1-langkah ditampilkan di akhir.
|
|
26
|
+
|
|
27
|
+
.PARAMETER DryRun
|
|
28
|
+
Tampilkan rencana hapus tanpa eksekusi (RECOMMENDED first run).
|
|
29
|
+
.PARAMETER AllowModified
|
|
30
|
+
Hapus juga file yang MODIFIED (dengan backup .bak). Default = skip modified.
|
|
31
|
+
Flag ini menggantikan -Force (deprecated) untuk bypass modified-file check.
|
|
32
|
+
.PARAMETER Force
|
|
33
|
+
[DEPRECATED] Backward-compat alias untuk -AllowModified. Akan menampilkan
|
|
34
|
+
deprecation warning. Pakai -AllowModified untuk explicit intent.
|
|
35
|
+
.PARAMETER DeleteAgents
|
|
36
|
+
Ikut hapus AGENTS.md di project root. Default = skip (user heavy-edit).
|
|
37
|
+
.PARAMETER KeepKit
|
|
38
|
+
Default-nya script memang TIDAK self-delete folder .claude-kit\ (tidak bisa saat running),
|
|
39
|
+
tapi instruksi manual ditampilkan. Pakai -KeepKit untuk SUPPRESS instruksi tersebut kalau
|
|
40
|
+
kamu memang mau retain folder kit (mis. lanjut pakai kit, cuma cleanup file deploy).
|
|
41
|
+
.PARAMETER Yes
|
|
42
|
+
Auto-confirm prompt Y/N (skip Read-Host). Untuk CI/automation. Manual user: tidak perlu.
|
|
43
|
+
PAKAI cuma kalau sudah lihat dry-run.
|
|
44
|
+
.PARAMETER AllowProjectRootMismatch
|
|
45
|
+
Override hard-fail kalau manifest project_root TIDAK match lokasi sekarang (folder di-rename
|
|
46
|
+
/ di-move setelah install). Tanpa flag ini, script abort untuk mencegah delete ke salah lokasi.
|
|
47
|
+
|
|
48
|
+
.NOTES
|
|
49
|
+
Versi : 1.0.1
|
|
50
|
+
Tanggal: 2026-06-03
|
|
51
|
+
Run : .\.claude-kit\uninstall.ps1 -DryRun (lihat rencana dulu)
|
|
52
|
+
Atau : .\.claude-kit\uninstall.ps1 (eksekusi konservatif)
|
|
53
|
+
Atau : .\.claude-kit\uninstall.ps1 -AllowModified (hapus juga modified, dengan backup)
|
|
54
|
+
Legacy : .\.claude-kit\uninstall.ps1 -Force (DEPRECATED alias untuk -AllowModified)
|
|
55
|
+
Lebih ringkas: .\.claude-kit\kit.ps1 uninstall
|
|
56
|
+
#>
|
|
57
|
+
|
|
58
|
+
[CmdletBinding()]
|
|
59
|
+
param(
|
|
60
|
+
[switch]$DryRun,
|
|
61
|
+
[switch]$AllowModified,
|
|
62
|
+
[switch]$Force,
|
|
63
|
+
[switch]$DeleteAgents,
|
|
64
|
+
[switch]$KeepKit,
|
|
65
|
+
[switch]$Yes,
|
|
66
|
+
[switch]$AllowProjectRootMismatch
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# ---- Deprecation handling: -Force jadi alias backward-compat untuk -AllowModified ----
|
|
70
|
+
# Sebelumnya -Force overloaded (modified bypass + signature bypass). Pisahkan supaya
|
|
71
|
+
# intent jelas. -Force masih bekerja untuk backward-compat tapi warn user.
|
|
72
|
+
if ($Force) {
|
|
73
|
+
Write-Host ''
|
|
74
|
+
Write-Host '[DEPRECATED] -Force flag. Pakai -AllowModified.' -ForegroundColor Yellow
|
|
75
|
+
Write-Host ' -Force masih bekerja sebagai alias untuk backward-compat,' -ForegroundColor Yellow
|
|
76
|
+
Write-Host ' tapi akan dihapus di versi mendatang. Ganti pemanggilan script kamu.' -ForegroundColor Yellow
|
|
77
|
+
Write-Host ''
|
|
78
|
+
if (-not $AllowModified) { $AllowModified = $true }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$ErrorActionPreference = 'Stop'
|
|
82
|
+
|
|
83
|
+
# ---- Resolve folder kit (tempat script ini berada) ----
|
|
84
|
+
$KitDir = if ($PSScriptRoot) { $PSScriptRoot } elseif ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { (Get-Location).Path }
|
|
85
|
+
$ProjectRoot = Split-Path -Parent $KitDir
|
|
86
|
+
$ProjectName = Split-Path -Leaf $ProjectRoot
|
|
87
|
+
$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
88
|
+
|
|
89
|
+
# ---- Load shared safety helpers (Resolve-SafeProjectPath, Test-PathHasReparsePoint) ----
|
|
90
|
+
# Dot-source supaya helper jalan di scope script ini (perlu akses $ProjectRoot &
|
|
91
|
+
# $script:ProjectRootCanonical). Import-Module .ps1 tidak supported di PS 5.1 dan
|
|
92
|
+
# function di module punya scope sendiri (helper tidak akan lihat $ProjectRoot caller).
|
|
93
|
+
. (Join-Path $PSScriptRoot 'lib\safety.ps1')
|
|
94
|
+
|
|
95
|
+
# ---- Validasi posisi script: script HARUS di dalam folder bernama .claude-kit ----
|
|
96
|
+
# Kalau bukan, user mungkin salah jalankan dari folder lain. Fail-aman dengan pesan jelas.
|
|
97
|
+
$kitFolderName = Split-Path -Leaf $KitDir
|
|
98
|
+
if ($kitFolderName -ne '.claude-kit') {
|
|
99
|
+
Write-Host ''
|
|
100
|
+
Write-Host 'STOP: Script ini tidak berada di dalam folder .claude-kit\.' -ForegroundColor Red
|
|
101
|
+
Write-Host " Lokasi script sekarang: $KitDir" -ForegroundColor Red
|
|
102
|
+
Write-Host ''
|
|
103
|
+
Write-Host 'Kemungkinan:' -ForegroundColor Yellow
|
|
104
|
+
Write-Host ' (A) Kamu jalankan dari folder SALAH (proyek ini belum pernah install lintasAI).' -ForegroundColor Yellow
|
|
105
|
+
Write-Host ' -> Cek di File Explorer: buka root proyek, lihat ada folder .claude-kit\ atau tidak.' -ForegroundColor Yellow
|
|
106
|
+
Write-Host ' (B) Folder kit di-rename dari .claude-kit\ ke nama lain.' -ForegroundColor Yellow
|
|
107
|
+
Write-Host ' -> Rename balik jadi .claude-kit, lalu ulangi.' -ForegroundColor Yellow
|
|
108
|
+
Write-Host ''
|
|
109
|
+
Write-Host 'TIDAK ADA satu pun file proyek kamu yang disentuh script ini. Aman.' -ForegroundColor Green
|
|
110
|
+
exit 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ---- Cek manifest ----
|
|
114
|
+
$manifestPath = Join-Path $KitDir '.install-manifest.json'
|
|
115
|
+
if (-not (Test-Path $manifestPath)) {
|
|
116
|
+
Write-Host ''
|
|
117
|
+
Write-Host 'STOP: Tidak bisa lanjut karena file pencatat install hilang.' -ForegroundColor Red
|
|
118
|
+
Write-Host ''
|
|
119
|
+
Write-Host 'Apa yang terjadi:' -ForegroundColor White
|
|
120
|
+
Write-Host ' Setiap kali kit ter-install, dia bikin catatan kecil:' -ForegroundColor White
|
|
121
|
+
Write-Host " $manifestPath" -ForegroundColor DarkGray
|
|
122
|
+
Write-Host ' Catatan ini berisi DAFTAR file kit + sidik-jari (hash) tiap file.' -ForegroundColor White
|
|
123
|
+
Write-Host ' Tanpa catatan ini, script TIDAK BERANI hapus apapun karena' -ForegroundColor White
|
|
124
|
+
Write-Host ' takut salah hapus file proyek kamu (BUKAN file kit).' -ForegroundColor White
|
|
125
|
+
Write-Host ''
|
|
126
|
+
Write-Host 'Kenapa hilang? Tiga kemungkinan:' -ForegroundColor Yellow
|
|
127
|
+
Write-Host ' 1. Kit kamu versi LAMA (sebelum fitur catatan ini ada).' -ForegroundColor Yellow
|
|
128
|
+
Write-Host ' 2. Install dulu cuma preview (pakai -DryRun) = catatan tidak ditulis.' -ForegroundColor Yellow
|
|
129
|
+
Write-Host ' 3. File catatan ter-hapus / kena git revert / setup gagal di tengah.' -ForegroundColor Yellow
|
|
130
|
+
Write-Host ''
|
|
131
|
+
Write-Host 'Recovery (pilih salah satu):' -ForegroundColor Cyan
|
|
132
|
+
Write-Host ' A. Re-run setup: .\.claude-kit\setup-pola-b.ps1 -Force' -ForegroundColor Cyan
|
|
133
|
+
Write-Host ' (akan re-generate manifest dari file kit yang ada di project).' -ForegroundColor Cyan
|
|
134
|
+
Write-Host ' B. Hapus manual sambil pelan-pelan:' -ForegroundColor Cyan
|
|
135
|
+
Write-Host ' 1. Buka README.md, cari section "Kalau manifest TIDAK ADA".' -ForegroundColor Cyan
|
|
136
|
+
Write-Host ' 2. Di sana ada DAFTAR FILE yang kit install. Cek satu-satu.' -ForegroundColor Cyan
|
|
137
|
+
Write-Host ' 3. Hapus file dari list itu (yang BELUM kamu edit).' -ForegroundColor Cyan
|
|
138
|
+
Write-Host ' 4. Terakhir hapus folder .claude-kit\ sendiri.' -ForegroundColor Cyan
|
|
139
|
+
exit 1
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ---- Load manifest signing helpers (HMAC verify) ----
|
|
143
|
+
# Dot-source supaya New-ManifestSignature + Test-ManifestSignature available di scope ini.
|
|
144
|
+
$signingLib = Join-Path $KitDir 'lib\manifest-signing.ps1'
|
|
145
|
+
$signingLibOk = $false
|
|
146
|
+
if (Test-Path -LiteralPath $signingLib) {
|
|
147
|
+
try {
|
|
148
|
+
. $signingLib
|
|
149
|
+
$signingLibOk = $true
|
|
150
|
+
} catch {
|
|
151
|
+
Write-Host "WARN Gagal load lib\manifest-signing.ps1: $_" -ForegroundColor Yellow
|
|
152
|
+
Write-Host " Manifest signature verification akan di-skip (legacy fallback)." -ForegroundColor Yellow
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ---- Helper: deep-convert PSCustomObject (from ConvertFrom-Json) ke nested hashtable ----
|
|
157
|
+
# Diperlukan supaya hashtable-based signing function (Clone + Remove + ConvertTo-Json) bisa
|
|
158
|
+
# jalan ulang dengan output identik dengan saat manifest di-write (waktu setup-pola-b.ps1).
|
|
159
|
+
function ConvertTo-HashtableDeep {
|
|
160
|
+
param($Object)
|
|
161
|
+
if ($null -eq $Object) { return $null }
|
|
162
|
+
if ($Object -is [System.Collections.IDictionary]) {
|
|
163
|
+
$ht = [ordered]@{}
|
|
164
|
+
foreach ($k in $Object.Keys) { $ht[$k] = ConvertTo-HashtableDeep -Object $Object[$k] }
|
|
165
|
+
return $ht
|
|
166
|
+
}
|
|
167
|
+
if ($Object -is [System.Management.Automation.PSCustomObject]) {
|
|
168
|
+
$ht = [ordered]@{}
|
|
169
|
+
foreach ($prop in $Object.PSObject.Properties) {
|
|
170
|
+
$ht[$prop.Name] = ConvertTo-HashtableDeep -Object $prop.Value
|
|
171
|
+
}
|
|
172
|
+
return $ht
|
|
173
|
+
}
|
|
174
|
+
if ($Object -is [System.Collections.IEnumerable] -and -not ($Object -is [string])) {
|
|
175
|
+
$arr = @()
|
|
176
|
+
foreach ($item in $Object) { $arr += ,(ConvertTo-HashtableDeep -Object $item) }
|
|
177
|
+
return ,$arr
|
|
178
|
+
}
|
|
179
|
+
return $Object
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ---- Parse manifest ----
|
|
183
|
+
try {
|
|
184
|
+
$manifest = Get-Content $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
185
|
+
} catch {
|
|
186
|
+
Write-Host ''
|
|
187
|
+
Write-Host 'ERROR: Manifest tidak bisa di-parse (corrupt JSON):' -ForegroundColor Red
|
|
188
|
+
Write-Host " Path : $manifestPath" -ForegroundColor Red
|
|
189
|
+
Write-Host " Error: $_" -ForegroundColor Red
|
|
190
|
+
Write-Host ''
|
|
191
|
+
Write-Host 'Saran:' -ForegroundColor Yellow
|
|
192
|
+
Write-Host " 1. Rename file ke .install-manifest.json.corrupt-$Timestamp.bak supaya bisa di-debug." -ForegroundColor Yellow
|
|
193
|
+
Write-Host ' 2. Re-run setup-pola-b.ps1 -Force untuk regenerate manifest dari file yang ada.' -ForegroundColor Yellow
|
|
194
|
+
Write-Host ' 3. Atau lihat README section "Kalau manifest TIDAK ADA" untuk fallback manual.' -ForegroundColor Yellow
|
|
195
|
+
exit 1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# ---- Verify manifest HMAC signature (prevent tampering) ----
|
|
199
|
+
# Manifest unsigned (legacy / older kit) -> warn + prompt continue.
|
|
200
|
+
# Manifest signature INVALID -> warn + prompt continue (default ABORT kalau interactive).
|
|
201
|
+
# Manifest signature VALID -> silent, continue normally.
|
|
202
|
+
if ($signingLibOk) {
|
|
203
|
+
$expectedSig = $null
|
|
204
|
+
if ($manifest.metadata -and $manifest.metadata.signature) {
|
|
205
|
+
$expectedSig = [string]$manifest.metadata.signature
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (-not $expectedSig) {
|
|
209
|
+
Write-Host ''
|
|
210
|
+
Write-Host '[!] Manifest tidak ber-signature (kit versi lama / install pre-HMAC).' -ForegroundColor Yellow
|
|
211
|
+
if (-not $Force -and -not $Yes) {
|
|
212
|
+
$legacyChoice = 'n'
|
|
213
|
+
try {
|
|
214
|
+
$legacyChoice = Read-Host 'Lanjut uninstall dengan manifest unsigned? (y/N)'
|
|
215
|
+
} catch {
|
|
216
|
+
Write-Host 'INFO: NonInteractive shell. Pakai -Yes atau -Force untuk override.' -ForegroundColor Yellow
|
|
217
|
+
$legacyChoice = 'n'
|
|
218
|
+
}
|
|
219
|
+
if ($legacyChoice -notmatch '^[Yy]') {
|
|
220
|
+
throw 'Uninstall aborted: unsigned manifest (user declined to proceed)'
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
try {
|
|
225
|
+
$manifestHt = ConvertTo-HashtableDeep -Object $manifest
|
|
226
|
+
# Clone + drop signature field sebelum re-sign (signature dihitung pada manifest WITHOUT signature)
|
|
227
|
+
$manifestCopy = ConvertTo-HashtableDeep -Object $manifest
|
|
228
|
+
if ($manifestCopy.metadata -is [System.Collections.IDictionary]) {
|
|
229
|
+
$null = $manifestCopy.metadata.Remove('signature')
|
|
230
|
+
}
|
|
231
|
+
$kitVerForVerify = ''
|
|
232
|
+
if ($manifest.metadata -and $manifest.metadata.kit_version) {
|
|
233
|
+
$kitVerForVerify = [string]$manifest.metadata.kit_version
|
|
234
|
+
} elseif ($manifest.kit_version) {
|
|
235
|
+
$kitVerForVerify = [string]$manifest.kit_version
|
|
236
|
+
}
|
|
237
|
+
$valid = Test-ManifestSignature -Manifest $manifestCopy -KitVersion $kitVerForVerify -ExpectedSignature $expectedSig
|
|
238
|
+
if (-not $valid) {
|
|
239
|
+
Write-Warning '[!] Manifest signature INVALID. Manifest mungkin di-tamper.'
|
|
240
|
+
if (-not $Force) {
|
|
241
|
+
$choice = 'n'
|
|
242
|
+
try {
|
|
243
|
+
$choice = Read-Host 'Lanjut uninstall dengan manifest unsigned/tampered? (y/N)'
|
|
244
|
+
} catch {
|
|
245
|
+
Write-Host 'INFO: NonInteractive shell. Pakai -Force untuk override.' -ForegroundColor Yellow
|
|
246
|
+
$choice = 'n'
|
|
247
|
+
}
|
|
248
|
+
if ($choice -ne 'y') { throw 'Uninstall aborted: untrusted manifest' }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
$null = $manifestHt # reserved for downstream tooling kalau perlu hashtable view
|
|
252
|
+
} catch {
|
|
253
|
+
if ($_.Exception.Message -match 'Uninstall aborted') { throw }
|
|
254
|
+
Write-Host "WARN Manifest signature verification gagal jalan: $_" -ForegroundColor Yellow
|
|
255
|
+
Write-Host ' Lanjut dengan asumsi manifest trusted (legacy fallback).' -ForegroundColor Yellow
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# ---- Validate schema_version (forward-compat guard) ----
|
|
261
|
+
$schemaVersion = [int]([string]$manifest.schema_version)
|
|
262
|
+
if ($schemaVersion -ne 1) {
|
|
263
|
+
Write-Host ''
|
|
264
|
+
Write-Host "ERROR: Schema version manifest = $schemaVersion (script ini cuma kenal schema_version=1)." -ForegroundColor Red
|
|
265
|
+
Write-Host ' Manifest mungkin dari kit versi lebih baru dari script uninstall ini.' -ForegroundColor Red
|
|
266
|
+
Write-Host ' Update kit ke versi terbaru (.\.claude-kit\kit.ps1 update) atau gunakan uninstall.ps1 versi sama.' -ForegroundColor Red
|
|
267
|
+
exit 1
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# ---- Sanity check: project_root manifest match folder kit saat ini? ----
|
|
271
|
+
# Default HARD-FAIL untuk mencegah manifest dari project lain delete file di project ini.
|
|
272
|
+
# Override pakai -AllowProjectRootMismatch (mis. legitimate rename folder).
|
|
273
|
+
$manifestProjectRoot = [string]$manifest.project_root
|
|
274
|
+
if ($manifestProjectRoot -and $manifestProjectRoot -ne $ProjectRoot -and $manifestProjectRoot -ne '<PROJECT_ROOT>') {
|
|
275
|
+
Write-Host ''
|
|
276
|
+
if (-not $AllowProjectRootMismatch) {
|
|
277
|
+
Write-Host 'ABORT: Project root di manifest TIDAK match lokasi sekarang.' -ForegroundColor Red
|
|
278
|
+
Write-Host " Manifest installed di : $manifestProjectRoot" -ForegroundColor Red
|
|
279
|
+
Write-Host " Lokasi sekarang : $ProjectRoot" -ForegroundColor Red
|
|
280
|
+
Write-Host ''
|
|
281
|
+
Write-Host 'Kemungkinan:' -ForegroundColor Yellow
|
|
282
|
+
Write-Host ' (A) Folder proyek di-rename / di-move setelah install.' -ForegroundColor Yellow
|
|
283
|
+
Write-Host ' (B) Folder .claude-kit\ di-copy dari proyek lain (manifest milik proyek lain).' -ForegroundColor Yellow
|
|
284
|
+
Write-Host ''
|
|
285
|
+
Write-Host 'Untuk proceed:' -ForegroundColor Cyan
|
|
286
|
+
Write-Host ' - Kalau (A): jalankan dengan -AllowProjectRootMismatch supaya pakai lokasi sekarang.' -ForegroundColor Cyan
|
|
287
|
+
Write-Host ' - Kalau (B): JANGAN proceed. Manifest entries kemungkinan punya path proyek lain.' -ForegroundColor Cyan
|
|
288
|
+
Write-Host ' Hapus manual sesuai daftar file di README section "Kalau manifest TIDAK ADA".' -ForegroundColor Cyan
|
|
289
|
+
exit 1
|
|
290
|
+
} else {
|
|
291
|
+
Write-Host 'PERINGATAN: project_root mismatch di-override via -AllowProjectRootMismatch.' -ForegroundColor Yellow
|
|
292
|
+
Write-Host " Manifest installed di : $manifestProjectRoot" -ForegroundColor Yellow
|
|
293
|
+
Write-Host " Lokasi sekarang : $ProjectRoot" -ForegroundColor Yellow
|
|
294
|
+
Write-Host ' Script pakai lokasi sekarang ($ProjectRoot) sebagai base.' -ForegroundColor Yellow
|
|
295
|
+
Write-Host ''
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# ---- Setup canonical project root (consumed by lib/safety.ps1 helpers) ----
|
|
300
|
+
# Helpers Resolve-SafeProjectPath & Test-PathHasReparsePoint baca $script:ProjectRootCanonical
|
|
301
|
+
# sebagai base containment check. WAJIB di-set sebelum panggil helper apapun.
|
|
302
|
+
$script:ProjectRootCanonical = [System.IO.Path]::GetFullPath($ProjectRoot)
|
|
303
|
+
if (-not $script:ProjectRootCanonical.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
|
|
304
|
+
$script:ProjectRootCanonical += [System.IO.Path]::DirectorySeparatorChar
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# ---- Header ----
|
|
308
|
+
Write-Host ''
|
|
309
|
+
Write-Host '================================================================' -ForegroundColor Cyan
|
|
310
|
+
Write-Host ' lintasAI uninstall - safe diff-based delete' -ForegroundColor Cyan
|
|
311
|
+
Write-Host '================================================================' -ForegroundColor Cyan
|
|
312
|
+
Write-Host "Proyek : $ProjectName ($ProjectRoot)"
|
|
313
|
+
Write-Host "Kit version : $($manifest.kit_version)"
|
|
314
|
+
Write-Host "Installed at : $($manifest.installed_at) by $($manifest.installed_by)"
|
|
315
|
+
Write-Host "Files tracked : $((@($manifest.files)).Count)"
|
|
316
|
+
Write-Host "Dirs tracked : $((@($manifest.directories_created)).Count)"
|
|
317
|
+
|
|
318
|
+
# Flag aktif (collected + bulleted, bukan repeated "Mode" lines)
|
|
319
|
+
$flags = @()
|
|
320
|
+
if ($DryRun) { $flags += 'DRY-RUN (preview saja, tidak ada yang dihapus)' }
|
|
321
|
+
if ($AllowModified) { $flags += '-AllowModified (modified files akan di-backup + hapus)' }
|
|
322
|
+
if ($Force -and -not $AllowModified) { $flags += '-Force (DEPRECATED, alias untuk -AllowModified)' }
|
|
323
|
+
if ($DeleteAgents) { $flags += '-DeleteAgents (AGENTS.md ikut dievaluasi)' }
|
|
324
|
+
if ($KeepKit) { $flags += '-KeepKit (instruksi self-delete .claude-kit\ disuppress)' }
|
|
325
|
+
if ($Yes) { $flags += '-Yes (auto-confirm Y, skip prompt)' }
|
|
326
|
+
if ($AllowProjectRootMismatch) { $flags += '-AllowProjectRootMismatch (project_root manifest override)' }
|
|
327
|
+
if ($flags.Count -gt 0) {
|
|
328
|
+
Write-Host 'Flag aktif :'
|
|
329
|
+
foreach ($f in $flags) { Write-Host " - $f" -ForegroundColor Yellow }
|
|
330
|
+
} else {
|
|
331
|
+
Write-Host 'Flag aktif : (default - konservatif, skip modified)' -ForegroundColor DarkGray
|
|
332
|
+
}
|
|
333
|
+
Write-Host ''
|
|
334
|
+
|
|
335
|
+
# ---- Classify each file: pristine / modified / symlink / blocked / locked / missing ----
|
|
336
|
+
$pristine = @()
|
|
337
|
+
$modified = @()
|
|
338
|
+
$symlinked = @()
|
|
339
|
+
$blocked = @()
|
|
340
|
+
$locked = @()
|
|
341
|
+
$missing = @()
|
|
342
|
+
$skipped = @()
|
|
343
|
+
$backups = @() # kind='backup' entries — preserved + reported, never auto-deleted
|
|
344
|
+
|
|
345
|
+
foreach ($item in @($manifest.files)) {
|
|
346
|
+
$relPath = [string]$item.path
|
|
347
|
+
$itemKind = [string]$item.kind
|
|
348
|
+
|
|
349
|
+
# Skip AGENTS.md by default (heavy customization expected)
|
|
350
|
+
if ($relPath -eq 'AGENTS.md' -and -not $DeleteAgents) {
|
|
351
|
+
$skipped += @{ item = $item; reason = 'AGENTS.md skipped (pakai -DeleteAgents untuk override)' }
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Backup files (kind='backup') — preserved + reported, never auto-deleted
|
|
356
|
+
if ($itemKind -eq 'backup') {
|
|
357
|
+
$backups += @{ item = $item }
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Path containment check (block traversal + absolute paths).
|
|
362
|
+
# Resolve-SafeProjectPath throws on reject (security boundary). Catch supaya satu entry
|
|
363
|
+
# bermasalah tidak abort seluruh uninstall - tetap append ke $blocked + lanjut entry berikutnya.
|
|
364
|
+
$fullPath = $null
|
|
365
|
+
try {
|
|
366
|
+
$fullPath = Resolve-SafeProjectPath -RelPath $relPath -Label "file '$relPath'"
|
|
367
|
+
} catch {
|
|
368
|
+
$blocked += @{ item = $item; reason = "path escapes project root (rejected: $($_.Exception.Message))" }
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
if ($null -eq $fullPath) {
|
|
372
|
+
$blocked += @{ item = $item; reason = 'path escapes project root (rejected, lihat REJECT log di atas)' }
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (-not (Test-Path -LiteralPath $fullPath)) {
|
|
377
|
+
$missing += @{ item = $item; path = $fullPath }
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Reparse point check (junction / symlink)
|
|
382
|
+
if (Test-PathHasReparsePoint -FullPath $fullPath) {
|
|
383
|
+
$symlinked += @{ item = $item; path = $fullPath }
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Hash compute — guarded with try/catch supaya 1 file locked tidak abort seluruh script
|
|
388
|
+
try {
|
|
389
|
+
$currentHash = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256 -ErrorAction Stop).Hash
|
|
390
|
+
} catch {
|
|
391
|
+
$locked += @{ item = $item; path = $fullPath; reason = $_.Exception.Message }
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if ($currentHash -eq $item.sha256) {
|
|
396
|
+
$pristine += @{ item = $item; path = $fullPath }
|
|
397
|
+
} else {
|
|
398
|
+
$modified += @{ item = $item; path = $fullPath; current_sha = $currentHash }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# ---- Print plan ----
|
|
403
|
+
Write-Host '--- RENCANA HAPUS ---' -ForegroundColor Cyan
|
|
404
|
+
|
|
405
|
+
if ($pristine.Count -gt 0) {
|
|
406
|
+
Write-Host ''
|
|
407
|
+
Write-Host ("[PRISTINE] {0} file (hash match, AUTO-DELETE):" -f $pristine.Count) -ForegroundColor Green
|
|
408
|
+
foreach ($p in $pristine) {
|
|
409
|
+
Write-Host (" - {0}" -f $p.item.path)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if ($modified.Count -gt 0) {
|
|
414
|
+
Write-Host ''
|
|
415
|
+
if ($AllowModified) {
|
|
416
|
+
Write-Host ("[MODIFIED] {0} file (user-edited, BACKUP + DELETE karena -AllowModified):" -f $modified.Count) -ForegroundColor Yellow
|
|
417
|
+
} else {
|
|
418
|
+
Write-Host ("[MODIFIED] {0} file (user-edited, SKIP - pakai -AllowModified untuk hapus dengan backup):" -f $modified.Count) -ForegroundColor Yellow
|
|
419
|
+
}
|
|
420
|
+
foreach ($m in $modified) {
|
|
421
|
+
Write-Host (" [edit] {0}" -f $m.item.path) -ForegroundColor Yellow
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if ($symlinked.Count -gt 0) {
|
|
426
|
+
Write-Host ''
|
|
427
|
+
Write-Host ("[SYMLINK] {0} file (junction/symlink terdeteksi, SKIP selalu - cek manual):" -f $symlinked.Count) -ForegroundColor Magenta
|
|
428
|
+
foreach ($s in $symlinked) {
|
|
429
|
+
Write-Host (" [link] {0}" -f $s.item.path) -ForegroundColor Magenta
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if ($locked.Count -gt 0) {
|
|
434
|
+
Write-Host ''
|
|
435
|
+
Write-Host ("[LOCKED] {0} file (hash gagal - mungkin di-buka editor/AV):" -f $locked.Count) -ForegroundColor DarkYellow
|
|
436
|
+
foreach ($l in $locked) {
|
|
437
|
+
Write-Host (" [lock] {0}" -f $l.item.path) -ForegroundColor DarkYellow
|
|
438
|
+
Write-Host (" reason: {0}" -f $l.reason) -ForegroundColor DarkGray
|
|
439
|
+
}
|
|
440
|
+
Write-Host ' HINT: Tutup file di editor (VS Code/Notepad++/Notepad), lalu re-run.' -ForegroundColor DarkYellow
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if ($blocked.Count -gt 0) {
|
|
444
|
+
Write-Host ''
|
|
445
|
+
Write-Host ("[BLOCKED] {0} file (path escape ke luar project - DITOLAK, lihat REJECT log di atas):" -f $blocked.Count) -ForegroundColor Red
|
|
446
|
+
foreach ($b in $blocked) {
|
|
447
|
+
Write-Host (" [reject] {0}" -f $b.item.path) -ForegroundColor Red
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if ($missing.Count -gt 0) {
|
|
452
|
+
Write-Host ''
|
|
453
|
+
Write-Host ("[MISSING] {0} file (sudah tidak ada, skip):" -f $missing.Count) -ForegroundColor DarkGray
|
|
454
|
+
foreach ($m in $missing) {
|
|
455
|
+
Write-Host (" - {0}" -f $m.item.path) -ForegroundColor DarkGray
|
|
456
|
+
}
|
|
457
|
+
Write-Host (' INFO: kalau curiga file di-rename, cek manual di project tree.') -ForegroundColor DarkGray
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if ($skipped.Count -gt 0) {
|
|
461
|
+
Write-Host ''
|
|
462
|
+
Write-Host ("[SKIPPED] {0} file:" -f $skipped.Count) -ForegroundColor Cyan
|
|
463
|
+
foreach ($s in $skipped) {
|
|
464
|
+
Write-Host (" - {0} ({1})" -f $s.item.path, $s.reason) -ForegroundColor Cyan
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if ($backups.Count -gt 0) {
|
|
469
|
+
Write-Host ''
|
|
470
|
+
Write-Host ("[BACKUPS] {0} file backup pre-setup ditemukan (PRESERVED, hapus manual kalau mau):" -f $backups.Count) -ForegroundColor Cyan
|
|
471
|
+
foreach ($b in $backups) {
|
|
472
|
+
Write-Host (" - {0} (dari setup -Force re-run sebelumnya)" -f $b.item.path) -ForegroundColor Cyan
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# Dirs plan
|
|
477
|
+
Write-Host ''
|
|
478
|
+
Write-Host '[DIRS] Direktori dari manifest (cek empty sebelum hapus):' -ForegroundColor DarkCyan
|
|
479
|
+
foreach ($d in @($manifest.directories_created)) {
|
|
480
|
+
Write-Host (" - {0}" -f $d) -ForegroundColor DarkCyan
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# Self-delete plan
|
|
484
|
+
Write-Host ''
|
|
485
|
+
if ($KeepKit) {
|
|
486
|
+
Write-Host '[KIT] Folder .claude-kit\ DIPERTAHANKAN (-KeepKit aktif - instruksi self-delete tidak akan ditampilkan)' -ForegroundColor Cyan
|
|
487
|
+
} else {
|
|
488
|
+
Write-Host '[KIT] Folder .claude-kit\ harus kamu hapus MANUAL (script tidak bisa self-delete saat running, instruksi di akhir).' -ForegroundColor Cyan
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# ---- DRY-RUN summary + exit ----
|
|
492
|
+
if ($DryRun) {
|
|
493
|
+
Write-Host ''
|
|
494
|
+
Write-Host '--- RINGKASAN DRY-RUN ---' -ForegroundColor Cyan
|
|
495
|
+
Write-Host (" Aman dihapus (pristine) : $($pristine.Count) file") -ForegroundColor Green
|
|
496
|
+
if ($modified.Count -gt 0) {
|
|
497
|
+
if ($AllowModified) {
|
|
498
|
+
Write-Host (" User-edit + backup : $($modified.Count) file (karena -AllowModified)") -ForegroundColor Yellow
|
|
499
|
+
} else {
|
|
500
|
+
Write-Host (" User-edit (DILEWATI) : $($modified.Count) file - pakai -AllowModified kalau mau ikut hapus") -ForegroundColor Yellow
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if ($symlinked.Count -gt 0) {
|
|
504
|
+
Write-Host (" Symlink/junction : $($symlinked.Count) file (SKIP selalu)") -ForegroundColor Magenta
|
|
505
|
+
}
|
|
506
|
+
if ($locked.Count -gt 0) {
|
|
507
|
+
Write-Host (" Locked (cek editor) : $($locked.Count) file") -ForegroundColor DarkYellow
|
|
508
|
+
}
|
|
509
|
+
if ($blocked.Count -gt 0) {
|
|
510
|
+
Write-Host (" BLOCKED (path traversal) : $($blocked.Count) file - manifest mungkin di-tamper") -ForegroundColor Red
|
|
511
|
+
}
|
|
512
|
+
Write-Host (" Sudah hilang : $($missing.Count) file") -ForegroundColor DarkGray
|
|
513
|
+
Write-Host (" Sengaja dilewati : $($skipped.Count) file (mis. AGENTS.md)") -ForegroundColor Cyan
|
|
514
|
+
if ($backups.Count -gt 0) {
|
|
515
|
+
Write-Host (" Backup pre-setup : $($backups.Count) file (preserved)") -ForegroundColor Cyan
|
|
516
|
+
}
|
|
517
|
+
Write-Host ''
|
|
518
|
+
Write-Host 'Yakin dengan rencana di atas?' -ForegroundColor White
|
|
519
|
+
Write-Host ' - Lanjut hapus beneran : jalankan ulang TANPA -DryRun' -ForegroundColor White
|
|
520
|
+
Write-Host ' - Batal : tutup window ini, tidak ada yang berubah' -ForegroundColor White
|
|
521
|
+
if ($modified.Count -gt 0 -and -not $AllowModified) {
|
|
522
|
+
Write-Host ' - Modified ikut : jalankan dengan -AllowModified (akan dibackup .bak dulu)' -ForegroundColor White
|
|
523
|
+
}
|
|
524
|
+
Write-Host ''
|
|
525
|
+
Write-Host 'Catatan: plan ini adalah SNAPSHOT saat dry-run. Kalau kamu edit file setelah ini' -ForegroundColor DarkGray
|
|
526
|
+
Write-Host ' dan sebelum run eksekusi, ulangi -DryRun untuk lihat plan terbaru.' -ForegroundColor DarkGray
|
|
527
|
+
exit 0
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# ---- Confirm sebelum execute ----
|
|
531
|
+
Write-Host ''
|
|
532
|
+
$forceSuffix = ''
|
|
533
|
+
if ($AllowModified) { $forceSuffix = " + $($modified.Count) modified (with backup)" }
|
|
534
|
+
Write-Host ("Total file akan dihapus: {0} pristine{1}" -f $pristine.Count, $forceSuffix) -ForegroundColor Cyan
|
|
535
|
+
if ($Yes) {
|
|
536
|
+
Write-Host 'Auto-confirm via -Yes (skip Read-Host).' -ForegroundColor Cyan
|
|
537
|
+
$confirm = 'Y'
|
|
538
|
+
} else {
|
|
539
|
+
try {
|
|
540
|
+
$confirm = Read-Host 'Lanjut hapus? (Y/N)'
|
|
541
|
+
} catch {
|
|
542
|
+
Write-Host ''
|
|
543
|
+
Write-Host 'INFO: Terminal sekarang tidak bisa nerima jawaban Y/N interaktif.' -ForegroundColor Yellow
|
|
544
|
+
Write-Host ' (umumnya terjadi kalau dijalankan dari VSCode tab Output, CI, atau script wrapper).' -ForegroundColor Yellow
|
|
545
|
+
Write-Host ''
|
|
546
|
+
Write-Host 'Solusi:' -ForegroundColor Cyan
|
|
547
|
+
Write-Host ' A. Buka PowerShell terpisah (Start Menu -> ketik PowerShell -> Enter), cd ke project, lalu ulangi.' -ForegroundColor Cyan
|
|
548
|
+
Write-Host ' B. Sudah yakin dengan rencana? Jalankan ulang dengan tambah -Yes di akhir.' -ForegroundColor Cyan
|
|
549
|
+
Write-Host ' CONTOH: .\.claude-kit\uninstall.ps1 -Yes' -ForegroundColor Cyan
|
|
550
|
+
Write-Host ' (sama dengan jawab YA tanpa ditanya. PAKAI cuma kalau sudah lihat dry-run.)' -ForegroundColor Cyan
|
|
551
|
+
exit 0
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if ($confirm -notmatch '^[Yy]') {
|
|
555
|
+
Write-Host 'Dibatalkan oleh user. Tidak ada file yang dihapus.' -ForegroundColor Yellow
|
|
556
|
+
exit 0
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
# ---- Execute: delete pristine files (with re-hash before delete = TOCTOU close) ----
|
|
560
|
+
Write-Host ''
|
|
561
|
+
Write-Host '--- EKSEKUSI ---' -ForegroundColor Cyan
|
|
562
|
+
$deletedCount = 0
|
|
563
|
+
$errorCount = 0
|
|
564
|
+
$rehashSkipped = 0
|
|
565
|
+
foreach ($p in $pristine) {
|
|
566
|
+
# Re-hash right before delete: kalau user edit file antara plan & confirm, skip.
|
|
567
|
+
try {
|
|
568
|
+
$rehash = (Get-FileHash -LiteralPath $p.path -Algorithm SHA256 -ErrorAction Stop).Hash
|
|
569
|
+
} catch {
|
|
570
|
+
Write-Host ("LOCK {0}: {1}" -f $p.item.path, $_.Exception.Message) -ForegroundColor DarkYellow
|
|
571
|
+
Write-Host ' HINT: Tutup file di editor (VS Code/Notepad++/Notepad), lalu re-run.' -ForegroundColor DarkYellow
|
|
572
|
+
$errorCount++
|
|
573
|
+
continue
|
|
574
|
+
}
|
|
575
|
+
if ($rehash -ne $p.item.sha256) {
|
|
576
|
+
Write-Host ("SKIP {0}: file berubah sejak plan dibuat (kemungkinan user edit setelah dry-run)" -f $p.item.path) -ForegroundColor Yellow
|
|
577
|
+
$rehashSkipped++
|
|
578
|
+
continue
|
|
579
|
+
}
|
|
580
|
+
# TOCTOU close: re-check reparse point IMMEDIATELY before Remove-Item.
|
|
581
|
+
# Attacker bisa race-swap file ke junction antara plan phase dan execute phase.
|
|
582
|
+
# Re-check di sini menutup window itu (best-effort, tidak atomic 100% tapi sangat menyempit window).
|
|
583
|
+
if (Test-PathHasReparsePoint -FullPath $p.path) {
|
|
584
|
+
Write-Host ("SKIP {0}: TOCTOU - jadi reparse point setelah plan, tidak dihapus" -f $p.item.path) -ForegroundColor Magenta
|
|
585
|
+
$rehashSkipped++
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
$itemInfo = Get-Item -Force -LiteralPath $p.path -ErrorAction SilentlyContinue
|
|
589
|
+
if ($itemInfo -and $itemInfo.LinkType) {
|
|
590
|
+
Write-Host ("SKIP {0}: TOCTOU - sekarang symlink/junction (LinkType={1}), tidak dihapus" -f $p.item.path, $itemInfo.LinkType) -ForegroundColor Magenta
|
|
591
|
+
$rehashSkipped++
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
Remove-Item -LiteralPath $p.path -Force -ErrorAction Stop
|
|
596
|
+
Write-Host ("DEL {0}" -f $p.item.path) -ForegroundColor Green
|
|
597
|
+
$deletedCount++
|
|
598
|
+
} catch {
|
|
599
|
+
Write-Host ("FAIL {0}: {1}" -f $p.item.path, $_.Exception.Message) -ForegroundColor Red
|
|
600
|
+
Write-Host ' HINT: Tutup file di editor (VS Code/Notepad++/Notepad), lalu re-run.' -ForegroundColor DarkYellow
|
|
601
|
+
$errorCount++
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
# ---- Execute: backup + delete modified files (kalau -AllowModified) ----
|
|
606
|
+
if ($AllowModified -and $modified.Count -gt 0) {
|
|
607
|
+
foreach ($m in $modified) {
|
|
608
|
+
# Defense: derive bakPath dari validated $m.path (bukan $m.item.path raw)
|
|
609
|
+
$bakPath = "$($m.path).pre-uninstall-$Timestamp.bak"
|
|
610
|
+
# Defense: validate bakPath also stays inside project root (defense-in-depth)
|
|
611
|
+
try {
|
|
612
|
+
$bakResolved = [System.IO.Path]::GetFullPath($bakPath)
|
|
613
|
+
} catch {
|
|
614
|
+
Write-Host ("FAIL {0}: backup path tidak valid" -f $m.item.path) -ForegroundColor Red
|
|
615
|
+
$errorCount++
|
|
616
|
+
continue
|
|
617
|
+
}
|
|
618
|
+
if (-not $bakResolved.StartsWith($script:ProjectRootCanonical, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
619
|
+
Write-Host ("FAIL {0}: backup path escape project root (REJECT)" -f $m.item.path) -ForegroundColor Red
|
|
620
|
+
$errorCount++
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
# Re-check: file still exists + still MODIFIED candidate (TOCTOU close)
|
|
624
|
+
if (-not (Test-Path -LiteralPath $m.path)) {
|
|
625
|
+
Write-Host ("SKIP {0}: file hilang sejak plan dibuat" -f $m.item.path) -ForegroundColor Yellow
|
|
626
|
+
continue
|
|
627
|
+
}
|
|
628
|
+
# TOCTOU close: re-check reparse point IMMEDIATELY before Copy-Item + Remove-Item.
|
|
629
|
+
# Attacker bisa race-swap file ke junction antara plan phase dan execute phase, yang
|
|
630
|
+
# bisa bocorin isi file di luar project ke .bak ATAU bikin delete redirect ke luar.
|
|
631
|
+
if (Test-PathHasReparsePoint -FullPath $m.path) {
|
|
632
|
+
Write-Host ("SKIP {0}: TOCTOU - jadi reparse point setelah plan, tidak di-backup/hapus" -f $m.item.path) -ForegroundColor Magenta
|
|
633
|
+
continue
|
|
634
|
+
}
|
|
635
|
+
$itemInfoM = Get-Item -Force -LiteralPath $m.path -ErrorAction SilentlyContinue
|
|
636
|
+
if ($itemInfoM -and $itemInfoM.LinkType) {
|
|
637
|
+
Write-Host ("SKIP {0}: TOCTOU - sekarang symlink/junction (LinkType={1}), tidak di-backup/hapus" -f $m.item.path, $itemInfoM.LinkType) -ForegroundColor Magenta
|
|
638
|
+
continue
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
Copy-Item -LiteralPath $m.path -Destination $bakPath -Force -ErrorAction Stop
|
|
642
|
+
Remove-Item -LiteralPath $m.path -Force -ErrorAction Stop
|
|
643
|
+
Write-Host ("BAK {0} -> {1}" -f $m.item.path, (Split-Path -Leaf $bakPath)) -ForegroundColor Yellow
|
|
644
|
+
$deletedCount++
|
|
645
|
+
} catch {
|
|
646
|
+
Write-Host ("FAIL {0}: {1}" -f $m.item.path, $_.Exception.Message) -ForegroundColor Red
|
|
647
|
+
Write-Host ' HINT: Tutup file di editor (VS Code/Notepad++/Notepad), lalu re-run.' -ForegroundColor DarkYellow
|
|
648
|
+
# Kalau .bak terbuat tapi Remove gagal, kasih info supaya user tahu state filesystem
|
|
649
|
+
if (Test-Path -LiteralPath $bakPath) {
|
|
650
|
+
Write-Host (" NOTE: backup ada di {0} (file original tetap di tempat, hapus manual kalau perlu)" -f (Split-Path -Leaf $bakPath)) -ForegroundColor DarkGray
|
|
651
|
+
}
|
|
652
|
+
$errorCount++
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
# ---- Try remove empty directories (deepest first supaya nested directories aman) ----
|
|
658
|
+
# Skip reparse-point dirs (junction redirect) untuk defense-in-depth.
|
|
659
|
+
$dirsSorted = @($manifest.directories_created) | Sort-Object -Descending @{
|
|
660
|
+
Expression = { ($_.ToString() -split '[\\/]').Count }
|
|
661
|
+
}
|
|
662
|
+
$dirDeleted = 0
|
|
663
|
+
$dirsMissing = @()
|
|
664
|
+
foreach ($d in $dirsSorted) {
|
|
665
|
+
$dStr = [string]$d
|
|
666
|
+
# Resolve-SafeProjectPath throws on reject; catch supaya satu dir bermasalah tidak abort loop.
|
|
667
|
+
$fullDir = $null
|
|
668
|
+
try {
|
|
669
|
+
$fullDir = Resolve-SafeProjectPath -RelPath $dStr -Label "dir '$dStr'"
|
|
670
|
+
} catch {
|
|
671
|
+
# Path traversal in dir entry — already logged by Resolve-SafeProjectPath
|
|
672
|
+
$errorCount++
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
if ($null -eq $fullDir) {
|
|
676
|
+
# Path traversal in dir entry — already logged by Resolve-SafeProjectPath
|
|
677
|
+
$errorCount++
|
|
678
|
+
continue
|
|
679
|
+
}
|
|
680
|
+
if (-not (Test-Path -LiteralPath $fullDir)) {
|
|
681
|
+
$dirsMissing += $dStr
|
|
682
|
+
continue
|
|
683
|
+
}
|
|
684
|
+
# Reparse-point guard: jangan ikuti junction yang mungkin redirect ke luar.
|
|
685
|
+
if (Test-PathHasReparsePoint -FullPath $fullDir) {
|
|
686
|
+
Write-Host ("KEEP {0}\ (reparse point terdeteksi, tidak diikuti)" -f $dStr) -ForegroundColor Magenta
|
|
687
|
+
continue
|
|
688
|
+
}
|
|
689
|
+
$entries = @(Get-ChildItem -LiteralPath $fullDir -Force -ErrorAction SilentlyContinue)
|
|
690
|
+
# Whitelist: file system noise (Windows/mac) yang clearly bukan user-intent
|
|
691
|
+
$systemNoise = @('desktop.ini', 'Thumbs.db', '.DS_Store')
|
|
692
|
+
$realEntries = @($entries | Where-Object { $_.Name -notin $systemNoise })
|
|
693
|
+
if ($realEntries.Count -eq 0) {
|
|
694
|
+
# Cleanup noise sebelum RMDIR
|
|
695
|
+
foreach ($noise in $entries) {
|
|
696
|
+
if ($noise.Name -in $systemNoise) {
|
|
697
|
+
try { Remove-Item -LiteralPath $noise.FullName -Force -ErrorAction Stop } catch { }
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
Remove-Item -LiteralPath $fullDir -Force -ErrorAction Stop
|
|
702
|
+
Write-Host ("RMDIR {0}\" -f $dStr) -ForegroundColor Green
|
|
703
|
+
$dirDeleted++
|
|
704
|
+
} catch {
|
|
705
|
+
Write-Host ("FAIL rmdir {0}: {1}" -f $dStr, $_.Exception.Message) -ForegroundColor Red
|
|
706
|
+
$errorCount++
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
# Print breakdown isi yang tersisa (max 5) supaya user audit
|
|
710
|
+
$sample = @($realEntries | Select-Object -First 5 -ExpandProperty Name)
|
|
711
|
+
$sampleStr = $sample -join ', '
|
|
712
|
+
if ($realEntries.Count -gt 5) { $sampleStr += ', ...' }
|
|
713
|
+
Write-Host ("KEEP {0}\ ({1} file/dir user tersisa: {2})" -f $dStr, $realEntries.Count, $sampleStr) -ForegroundColor Cyan
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
# ---- REASSURANCE FIRST (sebelum self-delete instruction supaya user tenang dulu) ----
|
|
718
|
+
Write-Host ''
|
|
719
|
+
Write-Host '=== FILE PROYEK KAMU - STATUS ===' -ForegroundColor Green
|
|
720
|
+
Write-Host 'Yang AMAN (tidak disentuh sama sekali oleh script ini):' -ForegroundColor Green
|
|
721
|
+
foreach ($d in @($manifest.directories_created)) {
|
|
722
|
+
$dStr = [string]$d
|
|
723
|
+
$fullDir = Join-Path $ProjectRoot ($dStr -replace '/', '\')
|
|
724
|
+
if (Test-Path -LiteralPath $fullDir) {
|
|
725
|
+
$userFiles = @(Get-ChildItem -LiteralPath $fullDir -Force -ErrorAction SilentlyContinue)
|
|
726
|
+
if ($userFiles.Count -gt 0) {
|
|
727
|
+
Write-Host (" - {0}\ : {1} file/folder milikmu, AMAN" -f $dStr, $userFiles.Count) -ForegroundColor Green
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if ($modified.Count -gt 0 -and -not $AllowModified) {
|
|
732
|
+
Write-Host (" - {0} file kit yang sudah kamu edit (modified) TETAP ADA di tempatnya." -f $modified.Count) -ForegroundColor Green
|
|
733
|
+
}
|
|
734
|
+
if ($skipped.Count -gt 0) {
|
|
735
|
+
Write-Host (' - AGENTS.md (kalau ada) TETAP ADA - default skip karena user edit.') -ForegroundColor Green
|
|
736
|
+
}
|
|
737
|
+
Write-Host 'Verifikasi: jalankan `git status` di project root - file project tidak boleh muncul sebagai deleted.' -ForegroundColor DarkGray
|
|
738
|
+
|
|
739
|
+
# ---- Self-delete instructions (kecuali -KeepKit) ----
|
|
740
|
+
if (-not $KeepKit) {
|
|
741
|
+
Write-Host ''
|
|
742
|
+
Write-Host '=== LANGKAH TERAKHIR (manual) ===' -ForegroundColor Cyan
|
|
743
|
+
Write-Host 'Folder .claude-kit\ TIDAK BISA dihapus oleh script ini (karena script ada di dalamnya).' -ForegroundColor Cyan
|
|
744
|
+
Write-Host ''
|
|
745
|
+
Write-Host 'Cara hapus:' -ForegroundColor White
|
|
746
|
+
Write-Host ' 1. TUTUP semua VSCode / editor yang sedang buka file di .claude-kit\ (cegah Access Denied).' -ForegroundColor White
|
|
747
|
+
Write-Host ' 2. Buka PowerShell baru di root proyek (folder INDUK dari .claude-kit\).' -ForegroundColor White
|
|
748
|
+
Write-Host ' 3. Copy-paste perintah berikut PERSIS:' -ForegroundColor White
|
|
749
|
+
Write-Host ''
|
|
750
|
+
Write-Host " Remove-Item -Recurse -Force '$KitDir'" -ForegroundColor Yellow
|
|
751
|
+
Write-Host ''
|
|
752
|
+
Write-Host ' 4. Cek folder .claude-kit\ sudah hilang dari File Explorer.' -ForegroundColor White
|
|
753
|
+
Write-Host ''
|
|
754
|
+
Write-Host 'Kalau muncul error "Access Denied": tutup VSCode, restart PowerShell, ulangi langkah 3.' -ForegroundColor DarkGray
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
# ---- Summary ----
|
|
758
|
+
Write-Host ''
|
|
759
|
+
Write-Host '================================================================' -ForegroundColor Green
|
|
760
|
+
Write-Host ' lintasAI uninstall - SELESAI' -ForegroundColor Green
|
|
761
|
+
Write-Host '================================================================' -ForegroundColor Green
|
|
762
|
+
Write-Host ("File dihapus : {0}" -f $deletedCount)
|
|
763
|
+
Write-Host ("Direktori dihapus : {0}" -f $dirDeleted)
|
|
764
|
+
if ($rehashSkipped -gt 0) {
|
|
765
|
+
Write-Host ("Re-hash mismatch : {0} (file berubah sejak plan, tidak dihapus)" -f $rehashSkipped) -ForegroundColor Yellow
|
|
766
|
+
}
|
|
767
|
+
if ($modified.Count -gt 0 -and -not $AllowModified) {
|
|
768
|
+
Write-Host ("Modified disimpan : {0} (run dengan -AllowModified kalau mau ikut hapus)" -f $modified.Count) -ForegroundColor Yellow
|
|
769
|
+
}
|
|
770
|
+
if ($symlinked.Count -gt 0) {
|
|
771
|
+
Write-Host ("Symlink dilewati : {0} (cek manual)" -f $symlinked.Count) -ForegroundColor Magenta
|
|
772
|
+
}
|
|
773
|
+
if ($locked.Count -gt 0) {
|
|
774
|
+
Write-Host ("Locked dilewati : {0} (tutup editor, re-run)" -f $locked.Count) -ForegroundColor DarkYellow
|
|
775
|
+
}
|
|
776
|
+
if ($blocked.Count -gt 0) {
|
|
777
|
+
Write-Host ("BLOCKED (traversal): {0} - manifest mungkin di-tamper, audit manual" -f $blocked.Count) -ForegroundColor Red
|
|
778
|
+
}
|
|
779
|
+
if ($dirsMissing.Count -gt 0) {
|
|
780
|
+
Write-Host ("Dir tracked hilang: {0} (mungkin di-rename: {1})" -f $dirsMissing.Count, ($dirsMissing -join ', ')) -ForegroundColor Yellow
|
|
781
|
+
}
|
|
782
|
+
if ($backups.Count -gt 0) {
|
|
783
|
+
Write-Host ("Backup pre-setup : {0} (preserved, hapus manual kalau mau)" -f $backups.Count) -ForegroundColor Cyan
|
|
784
|
+
}
|
|
785
|
+
if ($skipped.Count -gt 0) {
|
|
786
|
+
Write-Host ("Skipped : {0} (AGENTS.md / lain-lain)" -f $skipped.Count) -ForegroundColor Cyan
|
|
787
|
+
}
|
|
788
|
+
if ($errorCount -gt 0) {
|
|
789
|
+
Write-Host ("ERROR : {0} (cek log di atas)" -f $errorCount) -ForegroundColor Red
|
|
790
|
+
}
|
|
791
|
+
Write-Host ''
|
|
792
|
+
|
|
793
|
+
if ($errorCount -gt 0) { exit 1 }
|
|
794
|
+
exit 0
|