@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.
Files changed (86) hide show
  1. package/.github/workflows/publish-npm.yml +40 -0
  2. package/.github/workflows/validate.yml +93 -0
  3. package/AUDIT_POST_SETUP_PROMPT_v1.md +280 -0
  4. package/BOOTSTRAP_PROJECT_DOCS_PROMPT_v1.md +3 -0
  5. package/CHANGELOG.md +313 -0
  6. package/CLAUDE_universal_v1.md +1021 -0
  7. package/CONTRIBUTING.md +101 -0
  8. package/FIRST_SESSION_PROMPT_v1.md +7 -0
  9. package/JALANKAN_KIT.md +188 -0
  10. package/LICENSE +21 -0
  11. package/MULAI_DI_SINI.md +145 -0
  12. package/PROJECT_KICKOFF_PROMPT_v1.md +3 -0
  13. package/PROJECT_LIFECYCLE_PROMPT_v1.md +536 -0
  14. package/PROJECT_MIGRATION_PROMPT_v1.md +3 -0
  15. package/README.md +505 -0
  16. package/SETUP_POLA_B_PROMPT_v1.md +5 -0
  17. package/SPLIT_REPO_MIGRATION_PROMPT_v1.md +485 -0
  18. package/TEAM_ROLLOUT_GUIDE_v1.md +172 -0
  19. package/UPDATE_DOCS_PROMPT_v1.md +3 -0
  20. package/UPDATE_KIT_PROMPT_v1.md +213 -0
  21. package/bin/lintasai.js +81 -0
  22. package/docs/SIGNED_RELEASE.md +162 -0
  23. package/install-windows.ps1 +225 -0
  24. package/kit.ps1 +508 -0
  25. package/lib/agents-md.ps1 +174 -0
  26. package/lib/git-helpers.ps1 +104 -0
  27. package/lib/kit-files.psd1 +133 -0
  28. package/lib/manifest-signing.ps1 +65 -0
  29. package/lib/manifest.ps1 +267 -0
  30. package/lib/rollback.ps1 +241 -0
  31. package/lib/safety.ps1 +193 -0
  32. package/lib/template-deploy.ps1 +242 -0
  33. package/lib/version-detect.ps1 +161 -0
  34. package/package.json +36 -0
  35. package/setup-pola-b.ps1 +687 -0
  36. package/templates/ANALOGI_LIBRARY.md +7 -0
  37. package/templates/CLAUDE_TEAM_GUIDE.md +505 -0
  38. package/templates/CROSS_REPO_TYPES_PIPELINE.md +473 -0
  39. package/templates/DB_SCHEMA_SCAN_PROMPT.md +194 -0
  40. package/templates/DISCORD_BOT_INTEGRATION.md +187 -0
  41. package/templates/GLOSSARY_NON_PROGRAMMER.md +361 -0
  42. package/templates/INDEX.md +157 -0
  43. package/templates/MCP_SETUP.md +1145 -0
  44. package/templates/MIGRATE_TO_SUBFOLDER_PROMPT_v1.md +220 -0
  45. package/templates/ONBOARDING.md +172 -0
  46. package/templates/PROJECT_STARTER_TEMPLATES.md +264 -0
  47. package/templates/PROMPT_LIBRARY.md +790 -0
  48. package/templates/RLS_SETUP_PROMPT.md +167 -0
  49. package/templates/SECURITY_INCIDENT_PLAYBOOK.md +191 -0
  50. package/templates/SPLIT_REPO_AGENTS_TEMPLATES.md +32 -0
  51. package/templates/SPLIT_REPO_NON_PROGRAMMER_PROMPTS.md +604 -0
  52. package/templates/SPLIT_REPO_TOOLS_SETUP.md +388 -0
  53. package/templates/STACK_DETECTION_PATTERN.md +261 -0
  54. package/templates/STACK_GUIDE.md +564 -0
  55. package/templates/STACK_MIGRATION_GUIDE.md +154 -0
  56. package/templates/STACK_VERSIONS.md +31 -0
  57. package/templates/UPDATE_GUIDE.md +246 -0
  58. package/templates/_EXAMPLE.md +110 -0
  59. package/templates/_PATTERNS.md +173 -0
  60. package/templates/architecture.md +180 -0
  61. package/templates/architecture_auto.md +61 -0
  62. package/templates/decisions/README.md +108 -0
  63. package/templates/decisions/_TEMPLATE.md +84 -0
  64. package/templates/feature-flags-advanced.md +171 -0
  65. package/templates/github/CODEOWNERS.template +61 -0
  66. package/templates/github/GENERATE_TYPES_SCRIPT.md +77 -0
  67. package/templates/github/PUBLISH_SHARED_WORKFLOW.yml +52 -0
  68. package/templates/github/RECEIVE_BACKEND_UPDATE.yml +106 -0
  69. package/templates/github/RENOVATE_FRONTEND.json +28 -0
  70. package/templates/github/TRIGGER_FRONTEND_UPDATE.yml +29 -0
  71. package/templates/github/pull_request_template.md +44 -0
  72. package/templates/github/scripts/ai-review.js +153 -0
  73. package/templates/github/workflows/ai-review.yml +61 -0
  74. package/templates/github/workflows/backup-schemas.yml +169 -0
  75. package/templates/glossary.md +110 -0
  76. package/templates/split-agents/BACKEND.md +149 -0
  77. package/templates/split-agents/FRONTEND.md +141 -0
  78. package/templates/split-agents/SHARED.md +82 -0
  79. package/templates/split-agents/TOOLS.md +77 -0
  80. package/tests/Run-Tests.ps1 +19 -0
  81. package/tests/lib-safety.Tests.ps1 +66 -0
  82. package/tests/rollback.Tests.ps1 +66 -0
  83. package/tests/uninstall.Tests.ps1 +265 -0
  84. package/tests/update-kit.Tests.ps1 +78 -0
  85. package/uninstall.ps1 +794 -0
  86. package/update-kit.ps1 +907 -0
@@ -0,0 +1,104 @@
1
+ <#
2
+ .SYNOPSIS
3
+ lib/git-helpers.ps1 - Git metadata cleanup + Mark-of-the-Web unblock helpers
4
+ untuk lintasAI kit scripts.
5
+
6
+ .DESCRIPTION
7
+ Modul ini berisi dua helper yang sebelumnya tertanam inline di setup-pola-b.ps1
8
+ (dan kemungkinan di-share dengan update-kit.ps1 / install-windows.ps1):
9
+
10
+ - Remove-GitMetadata : Hapus folder .git/ di dalam kit directory.
11
+ Penting karena kalau kit di-CLONE (bukan zip-extract),
12
+ folder .git/ ikut ke project user. Itu menyebabkan:
13
+ * Bloat (history kit-dev, potensial ratusan MB)
14
+ * Expose history internal kit-dev ke user repo
15
+ * User accidentally bikin `git submodule` /
16
+ confuse `git log`
17
+ Idempotent: skip kalau folder .git/ tidak ada.
18
+
19
+ - Remove-MotwBlock : Unblock-File recursive untuk semua file di kit.
20
+ Mark-of-the-Web (MOTW) = NTFS Alternate Data Stream
21
+ Zone.Identifier yang di-set Windows kalau file
22
+ di-download dari internet (zip dari GitHub, dst.).
23
+ File MOTW-blocked = script .ps1 di-reject oleh
24
+ ExecutionPolicy walaupun signed. Unblock = strip
25
+ Zone.Identifier ADS supaya kit bisa di-eksekusi.
26
+
27
+ Behavior umum:
28
+ - Best-effort: gagal hapus / gagal unblock TIDAK throw — return $false.
29
+ Caller bisa pilih lanjut atau abort. Konsisten dengan
30
+ pattern inline di setup-pola-b.ps1 (try/catch + warn).
31
+ - PowerShell 5.1 compatible: tidak pakai operator $?:, ??, ?., -AsHashtable,
32
+ atau syntax PS 7+ lainnya.
33
+ - Verbose logging via Write-Verbose (silent secara default). Caller bisa
34
+ enable dengan -Verbose untuk debugging.
35
+
36
+ .NOTES
37
+ Versi : 1.0.0
38
+ Tanggal: 2026-06-06
39
+ Extracted from: setup-pola-b.ps1 v1.9 (lines 173-200)
40
+ Compat: PowerShell 5.1+
41
+ #>
42
+
43
+ # ---- Remove-GitMetadata ----
44
+ # Hapus folder .git/ di -Path. Idempotent (skip kalau tidak ada).
45
+ # Return: $true kalau sukses ATAU folder .git/ tidak ada (no-op success).
46
+ # $false kalau gagal hapus (folder ada tapi Remove-Item throw).
47
+ function Remove-GitMetadata {
48
+ [CmdletBinding()]
49
+ param(
50
+ [Parameter(Mandatory = $true)]
51
+ [string]$Path
52
+ )
53
+
54
+ if (-not (Test-Path -LiteralPath $Path)) {
55
+ Write-Verbose ("Remove-GitMetadata: Path tidak ada, skip: {0}" -f $Path)
56
+ return $true
57
+ }
58
+
59
+ $gitDir = Join-Path $Path '.git'
60
+ if (-not (Test-Path -LiteralPath $gitDir)) {
61
+ Write-Verbose ("Remove-GitMetadata: .git/ tidak ada di {0}, no-op." -f $Path)
62
+ return $true
63
+ }
64
+
65
+ try {
66
+ Remove-Item -Recurse -Force -LiteralPath $gitDir -ErrorAction Stop
67
+ Write-Verbose ("Remove-GitMetadata: .git/ removed dari {0}" -f $Path)
68
+ return $true
69
+ } catch {
70
+ Write-Warning ("Remove-GitMetadata: gagal hapus {0}: {1}" -f $gitDir, $_)
71
+ Write-Warning (" Fix manual: Remove-Item -Recurse -Force '{0}'" -f $gitDir)
72
+ return $false
73
+ }
74
+ }
75
+
76
+ # ---- Remove-MotwBlock ----
77
+ # Recursive Unblock-File untuk semua file di -Path. Best-effort.
78
+ # Return: $true kalau semua file ter-unblock (atau tidak ada file sama sekali).
79
+ # $false kalau Unblock-File throw error fatal di top-level enumeration.
80
+ # Individual file fail = silent (Unblock-File -ErrorAction SilentlyContinue),
81
+ # konsisten dengan pattern inline di setup-pola-b.ps1.
82
+ function Remove-MotwBlock {
83
+ [CmdletBinding()]
84
+ param(
85
+ [Parameter(Mandatory = $true)]
86
+ [string]$Path
87
+ )
88
+
89
+ if (-not (Test-Path -LiteralPath $Path)) {
90
+ Write-Verbose ("Remove-MotwBlock: Path tidak ada, skip: {0}" -f $Path)
91
+ return $true
92
+ }
93
+
94
+ try {
95
+ Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
96
+ Unblock-File -ErrorAction SilentlyContinue
97
+ Write-Verbose ("Remove-MotwBlock: MOTW di-unblock untuk semua file di {0}" -f $Path)
98
+ return $true
99
+ } catch {
100
+ Write-Warning ("Remove-MotwBlock: Unblock-File gagal di {0}: {1}" -f $Path, $_)
101
+ Write-Warning " Script tetap bisa lanjut, tapi mungkin perlu Unblock-File manual."
102
+ return $false
103
+ }
104
+ }
@@ -0,0 +1,133 @@
1
+ # kit-files.psd1 - SINGLE SOURCE OF TRUTH untuk file kit
2
+ # Update file ini WHEN add/rename/remove file kit
3
+ # Konsumen: setup-pola-b.ps1, kit.ps1 doctor, install-windows.ps1, uninstall.ps1
4
+
5
+ @{
6
+ schema_version = 1
7
+
8
+ # Tier 1: CORE prompts (selalu deploy, wajib ada)
9
+ core_prompts = @(
10
+ 'MULAI_DI_SINI.md',
11
+ 'JALANKAN_KIT.md',
12
+ 'UPDATE_KIT_PROMPT_v1.md',
13
+ 'AUDIT_POST_SETUP_PROMPT_v1.md',
14
+ 'PROJECT_LIFECYCLE_PROMPT_v1.md',
15
+ 'SPLIT_REPO_MIGRATION_PROMPT_v1.md'
16
+ )
17
+
18
+ # Tier 2: Universal rules (auto-loaded AI tiap sesi)
19
+ universal_rules = @(
20
+ 'CLAUDE_universal_v1.md',
21
+ 'AGENTS.md.template'
22
+ )
23
+
24
+ # Tier 3: PowerShell scripts
25
+ scripts = @(
26
+ 'kit.ps1',
27
+ 'setup-pola-b.ps1',
28
+ 'update-kit.ps1',
29
+ 'uninstall.ps1',
30
+ 'install-windows.ps1'
31
+ )
32
+
33
+ # Tier 4: Lib modules
34
+ lib_files = @(
35
+ 'lib/rollback.ps1',
36
+ 'lib/safety.ps1',
37
+ 'lib/manifest-signing.ps1'
38
+ )
39
+
40
+ # Tier 5: Templates (deployed to project saat setup)
41
+ templates = @(
42
+ 'templates/PROMPT_LIBRARY.md',
43
+ 'templates/ANALOGI_LIBRARY.md',
44
+ 'templates/UPDATE_GUIDE.md',
45
+ 'templates/glossary.md',
46
+ 'templates/INDEX.md',
47
+ 'templates/SPLIT_REPO_AGENTS_TEMPLATES.md',
48
+ 'templates/SPLIT_REPO_NON_PROGRAMMER_PROMPTS.md',
49
+ 'templates/SPLIT_REPO_TOOLS_SETUP.md',
50
+ 'templates/DISCORD_BOT_INTEGRATION.md',
51
+ 'templates/STACK_DETECTION_PATTERN.md',
52
+ 'templates/PROJECT_STARTER_TEMPLATES.md',
53
+ 'templates/split-agents/FRONTEND.md',
54
+ 'templates/split-agents/BACKEND.md',
55
+ 'templates/split-agents/SHARED.md',
56
+ 'templates/split-agents/TOOLS.md',
57
+ 'templates/CROSS_REPO_TYPES_PIPELINE.md',
58
+ 'templates/github/RENOVATE_FRONTEND.json',
59
+ 'templates/github/PUBLISH_SHARED_WORKFLOW.yml',
60
+ 'templates/github/GENERATE_TYPES_SCRIPT.md',
61
+ 'templates/github/TRIGGER_FRONTEND_UPDATE.yml',
62
+ 'templates/github/RECEIVE_BACKEND_UPDATE.yml',
63
+ 'templates/architecture.md',
64
+ 'templates/architecture_auto.md',
65
+ 'templates/CLAUDE_TEAM_GUIDE.md',
66
+ 'templates/feature-flags-advanced.md',
67
+ 'templates/MCP_SETUP.md',
68
+ 'templates/ONBOARDING.md',
69
+ 'templates/RLS_SETUP_PROMPT.md',
70
+ 'templates/STACK_GUIDE.md',
71
+ 'templates/STACK_VERSIONS.md',
72
+ 'templates/MIGRATE_TO_SUBFOLDER_PROMPT_v1.md',
73
+ 'templates/_EXAMPLE.md',
74
+ 'templates/_PATTERNS.md',
75
+ 'templates/STACK_MIGRATION_GUIDE.md',
76
+ 'templates/GLOSSARY_NON_PROGRAMMER.md',
77
+ 'templates/SECURITY_INCIDENT_PLAYBOOK.md',
78
+ 'templates/DB_SCHEMA_SCAN_PROMPT.md'
79
+ )
80
+
81
+ # Tier 5b: Deprecated stubs (legacy prompts, still on disk, akan dihapus)
82
+ deprecated_stubs = @(
83
+ 'BOOTSTRAP_PROJECT_DOCS_PROMPT_v1.md',
84
+ 'FIRST_SESSION_PROMPT_v1.md',
85
+ 'PROJECT_KICKOFF_PROMPT_v1.md',
86
+ 'PROJECT_MIGRATION_PROMPT_v1.md',
87
+ 'SETUP_POLA_B_PROMPT_v1.md',
88
+ 'TEAM_ROLLOUT_GUIDE_v1.md',
89
+ 'UPDATE_DOCS_PROMPT_v1.md'
90
+ )
91
+
92
+ # Tier 5c: Decisions / ADR templates
93
+ decisions = @(
94
+ 'templates/decisions/_TEMPLATE.md',
95
+ 'templates/decisions/README.md'
96
+ )
97
+
98
+ # Tier 5d: GitHub assets (CODEOWNERS, PR template, workflows, scripts)
99
+ github_assets = @(
100
+ 'templates/github/CODEOWNERS.template',
101
+ 'templates/github/pull_request_template.md',
102
+ 'templates/github/scripts/ai-review.js',
103
+ 'templates/github/workflows/ai-review.yml',
104
+ 'templates/github/workflows/backup-schemas.yml'
105
+ )
106
+
107
+ # Tier 6: Docs
108
+ docs = @(
109
+ 'docs/SIGNED_RELEASE.md'
110
+ )
111
+
112
+ # Tier 7: Tests
113
+ tests = @(
114
+ 'tests/Run-Tests.ps1',
115
+ 'tests/lib-safety.Tests.ps1',
116
+ 'tests/rollback.Tests.ps1',
117
+ 'tests/update-kit.Tests.ps1',
118
+ 'tests/uninstall.Tests.ps1'
119
+ )
120
+
121
+ # Tier 8: CI
122
+ ci = @(
123
+ '.github/workflows/validate.yml'
124
+ )
125
+
126
+ # Tier 9: Project meta
127
+ meta = @(
128
+ 'CHANGELOG.md',
129
+ 'README.md',
130
+ 'LICENSE',
131
+ 'CONTRIBUTING.md'
132
+ )
133
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ Manifest HMAC signing untuk prevent tampering attack.
5
+ .DESCRIPTION
6
+ Sign .install-manifest.json dengan HMAC-SHA256 pakai salt derived dari
7
+ kit version constant. Verify signature sebelum trust manifest entries.
8
+
9
+ HMAC = anti-EDIT detection (manifest tidak diam-diam dimodifikasi),
10
+ BUKAN forge-proof secret. Pakai detached signature kalau perlu real secret.
11
+ #>
12
+
13
+ function Get-ManifestSecret {
14
+ param([string]$KitVersion)
15
+
16
+ if ($env:LINTASAI_MANIFEST_SECRET) {
17
+ return $env:LINTASAI_MANIFEST_SECRET
18
+ }
19
+
20
+ # Constant kit-version-based key (anti-EDIT, BUKAN forge-proof secret)
21
+ # Same kit version = same key = manifest portable across machines
22
+ $material = "lintasAI-manifest-v1|$KitVersion|signed-2026"
23
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($material)
24
+ $hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
25
+ return [Convert]::ToBase64String($hash)
26
+ }
27
+
28
+ function New-ManifestSignature {
29
+ param(
30
+ [Parameter(Mandatory)][hashtable]$Manifest,
31
+ [Parameter(Mandatory)][string]$KitVersion
32
+ )
33
+
34
+ $secret = Get-ManifestSecret -KitVersion $KitVersion
35
+ $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($secret)
36
+
37
+ # Sign canonical JSON (sort keys recursively)
38
+ $canonical = $Manifest | ConvertTo-Json -Depth 10 -Compress
39
+ $canonicalBytes = [System.Text.Encoding]::UTF8.GetBytes($canonical)
40
+
41
+ $hmac = New-Object System.Security.Cryptography.HMACSHA256
42
+ $hmac.Key = $secretBytes
43
+ $signature = $hmac.ComputeHash($canonicalBytes)
44
+ return [Convert]::ToBase64String($signature)
45
+ }
46
+
47
+ function Test-ManifestSignature {
48
+ param(
49
+ [Parameter(Mandatory)][hashtable]$Manifest,
50
+ [Parameter(Mandatory)][string]$KitVersion,
51
+ [Parameter(Mandatory)][string]$ExpectedSignature
52
+ )
53
+
54
+ $actualSignature = New-ManifestSignature -Manifest $Manifest -KitVersion $KitVersion
55
+
56
+ # Constant-time compare untuk prevent timing attack
57
+ if ($actualSignature.Length -ne $ExpectedSignature.Length) { return $false }
58
+ $equal = $true
59
+ for ($i = 0; $i -lt $actualSignature.Length; $i++) {
60
+ if ($actualSignature[$i] -ne $ExpectedSignature[$i]) { $equal = $false }
61
+ }
62
+ return $equal
63
+ }
64
+
65
+ # Functions auto-exposed via dot-source (no Export-ModuleMember karena .ps1)
@@ -0,0 +1,267 @@
1
+ # =============================================================================
2
+ # Manifest helpers - extracted from setup-pola-b.ps1 v1.1.0
3
+ # =============================================================================
4
+ # Tujuan:
5
+ # Helper functions untuk build + sign install manifest (.install-manifest.json)
6
+ # yang dipakai uninstall.ps1 untuk safe-delete (hash-based pristine detection).
7
+ #
8
+ # Beda dengan versi inline di setup-pola-b.ps1:
9
+ # - TIDAK pakai $script: scope variables. Setiap function terima parameter
10
+ # eksplisit -> reusable dari script manapun (setup, update-kit, kit.ps1).
11
+ # - State (installedItems + createdDirs) diwakili object [pscustomobject]
12
+ # yang di-init via Initialize-Manifest, lalu di-mutate via Add-ToManifest /
13
+ # Add-DirToManifest, lalu di-flush via Save-Manifest.
14
+ #
15
+ # PowerShell 5.1 compatible (no ternary, no null-coalescing, no class syntax).
16
+ #
17
+ # Public API:
18
+ # - Initialize-Manifest -ProjectRoot <path>
19
+ # -> returns [pscustomobject] state container
20
+ # - Get-FileSha256 -FilePath <path>
21
+ # -> returns hex SHA-256 string (uppercase, no separator)
22
+ # - Add-ToManifest -State <obj> -FilePath <path> -Kind <string> [-From <string>]
23
+ # -> append file entry (path/kind/sha256[/from]) ke State.Files
24
+ # - Add-DirToManifest -State <obj> -DirPath <path>
25
+ # -> append dir relative path ke State.Directories (dedup)
26
+ # - Save-Manifest -State <obj> -KitDir <path> -KitVersion <string> -ProjectName <string>
27
+ # [-PreserveExisting] [-SignManifest]
28
+ # -> merge dengan manifest sebelumnya (kalau ada), HMAC sign, write JSON
29
+ # -> returns [pscustomobject] dengan FilesCount, DirsCount, Merged, Signed, ManifestPath
30
+ #
31
+ # Catatan path:
32
+ # - State.ProjectRoot disimpan supaya Add-ToManifest bisa compute relative path
33
+ # tanpa minta caller pass ulang setiap call.
34
+ # - Relative path pakai forward slash (portable di JSON, sama dengan setup-pola-b.ps1).
35
+ # =============================================================================
36
+
37
+ Set-StrictMode -Version Latest
38
+
39
+ function Initialize-Manifest {
40
+ <#
41
+ .SYNOPSIS
42
+ Buat state container baru untuk tracking install manifest.
43
+ .PARAMETER ProjectRoot
44
+ Absolute path ke root proyek (dipakai untuk compute relative paths).
45
+ #>
46
+ param(
47
+ [Parameter(Mandatory)][string]$ProjectRoot
48
+ )
49
+ return [pscustomobject]@{
50
+ ProjectRoot = $ProjectRoot
51
+ Files = @()
52
+ Directories = @()
53
+ }
54
+ }
55
+
56
+ function Get-FileSha256 {
57
+ <#
58
+ .SYNOPSIS
59
+ Compute SHA-256 hash file (hex uppercase, no separator).
60
+ .PARAMETER FilePath
61
+ Path ke file yang mau di-hash. Pakai -LiteralPath (no glob expansion).
62
+ .OUTPUTS
63
+ [string] hex SHA-256, atau $null kalau file tidak ada.
64
+ #>
65
+ param(
66
+ [Parameter(Mandatory)][string]$FilePath
67
+ )
68
+ if (-not (Test-Path -LiteralPath $FilePath)) { return $null }
69
+ return (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash
70
+ }
71
+
72
+ function ConvertTo-ManifestRelativePath {
73
+ <#
74
+ .SYNOPSIS
75
+ Convert absolute path -> relative path (forward slash, no leading slash).
76
+ .NOTES
77
+ Helper internal. Sengaja TIDAK di-export dari API publik karena
78
+ tightly coupled dengan State.ProjectRoot.
79
+ #>
80
+ param(
81
+ [Parameter(Mandatory)][string]$AbsolutePath,
82
+ [Parameter(Mandatory)][string]$ProjectRoot
83
+ )
84
+ $rel = $AbsolutePath.Replace($ProjectRoot, '')
85
+ $rel = $rel.TrimStart('\','/')
86
+ return $rel.Replace('\','/')
87
+ }
88
+
89
+ function Add-ToManifest {
90
+ <#
91
+ .SYNOPSIS
92
+ Track file ke manifest state dengan sha256 hash.
93
+ .PARAMETER State
94
+ Object dari Initialize-Manifest.
95
+ .PARAMETER FilePath
96
+ Absolute path file (yang sudah di-copy ke project).
97
+ .PARAMETER Kind
98
+ Kategori file (mis. 'lib', 'backup', 'filled_template', 'skeleton', 'team_file').
99
+ Dipakai uninstall.ps1 untuk decide treatment per kategori.
100
+ .PARAMETER From
101
+ Opsional source label (mis. 'AGENTS.md.template'). Disimpan ke field 'from'
102
+ di manifest entry untuk audit trail.
103
+ #>
104
+ param(
105
+ [Parameter(Mandatory)][pscustomobject]$State,
106
+ [Parameter(Mandatory)][string]$FilePath,
107
+ [Parameter(Mandatory)][string]$Kind,
108
+ [string]$From
109
+ )
110
+ if (-not (Test-Path -LiteralPath $FilePath)) { return }
111
+
112
+ $sha = Get-FileSha256 -FilePath $FilePath
113
+ $relPath = ConvertTo-ManifestRelativePath -AbsolutePath $FilePath -ProjectRoot $State.ProjectRoot
114
+
115
+ $entry = [ordered]@{
116
+ path = $relPath
117
+ kind = $Kind
118
+ sha256 = $sha
119
+ }
120
+ if ($From) { $entry.from = $From }
121
+
122
+ $State.Files += $entry
123
+ }
124
+
125
+ function Add-DirToManifest {
126
+ <#
127
+ .SYNOPSIS
128
+ Track directory ke manifest state (dedup).
129
+ .PARAMETER State
130
+ Object dari Initialize-Manifest.
131
+ .PARAMETER DirPath
132
+ Absolute path directory yang baru dibuat kit.
133
+ #>
134
+ param(
135
+ [Parameter(Mandatory)][pscustomobject]$State,
136
+ [Parameter(Mandatory)][string]$DirPath
137
+ )
138
+ if (-not (Test-Path -LiteralPath $DirPath)) { return }
139
+
140
+ $relPath = ConvertTo-ManifestRelativePath -AbsolutePath $DirPath -ProjectRoot $State.ProjectRoot
141
+ if ($State.Directories -notcontains $relPath) {
142
+ $State.Directories += $relPath
143
+ }
144
+ }
145
+
146
+ function Save-Manifest {
147
+ <#
148
+ .SYNOPSIS
149
+ Merge dengan manifest sebelumnya (kalau ada), HMAC sign, write JSON ke disk.
150
+ .PARAMETER State
151
+ Object dari Initialize-Manifest (sudah di-populate via Add-ToManifest / Add-DirToManifest).
152
+ .PARAMETER KitDir
153
+ Absolute path ke folder kit (mis. <project>/.claude-kit). Manifest ditulis ke
154
+ $KitDir/.install-manifest.json.
155
+ .PARAMETER KitVersion
156
+ Versi kit (mis. 'v1.1.0' atau 'pre-launch (testing)'). Disimpan di metadata
157
+ + dipakai untuk HMAC signing key derivation.
158
+ .PARAMETER ProjectName
159
+ Nama proyek (leaf dari ProjectRoot). Disimpan ke manifest untuk audit.
160
+ .PARAMETER PreserveExisting
161
+ Kalau di-set: merge dengan manifest sebelumnya — preserve entries yang file-nya
162
+ masih ada di disk + path-nya tidak konflik dengan entry baru. Default: ON.
163
+ .PARAMETER SignManifest
164
+ Kalau di-set: HMAC sign manifest via lib/manifest-signing.ps1. Default: ON.
165
+ Set -SignManifest:$false untuk skip (mis. test mode).
166
+ .OUTPUTS
167
+ [pscustomobject] dengan: ManifestPath, FilesCount, DirsCount, Merged, Signed.
168
+ #>
169
+ param(
170
+ [Parameter(Mandatory)][pscustomobject]$State,
171
+ [Parameter(Mandatory)][string]$KitDir,
172
+ [Parameter(Mandatory)][string]$KitVersion,
173
+ [Parameter(Mandatory)][string]$ProjectName,
174
+ [switch]$PreserveExisting = $true,
175
+ [switch]$SignManifest = $true
176
+ )
177
+
178
+ $manifestPath = Join-Path $KitDir '.install-manifest.json'
179
+ $merged = $false
180
+
181
+ # ---- Baca manifest sebelumnya (kalau ada) untuk merge ----
182
+ $previous = $null
183
+ if ($PreserveExisting -and (Test-Path $manifestPath)) {
184
+ try {
185
+ $previous = Get-Content $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
186
+ } catch {
187
+ Write-Host "WARN Manifest sebelumnya corrupt, akan di-overwrite: $_" -ForegroundColor Yellow
188
+ }
189
+ }
190
+
191
+ # ---- Merge files: preserve entries dari manifest sebelumnya yang masih ada di disk ----
192
+ if ($previous -and $previous.PSObject.Properties.Name -contains 'files' -and $previous.files) {
193
+ $newPaths = @($State.Files | ForEach-Object { $_.path })
194
+ foreach ($prevEntry in $previous.files) {
195
+ if ($newPaths -contains $prevEntry.path) { continue }
196
+ $fullPath = Join-Path $State.ProjectRoot (([string]$prevEntry.path) -replace '/', '\')
197
+ if (Test-Path -LiteralPath $fullPath) {
198
+ $entry = [ordered]@{
199
+ path = [string]$prevEntry.path
200
+ kind = [string]$prevEntry.kind
201
+ sha256 = [string]$prevEntry.sha256
202
+ }
203
+ if ($prevEntry.PSObject.Properties.Name -contains 'from' -and $prevEntry.from) {
204
+ $entry.from = [string]$prevEntry.from
205
+ }
206
+ $State.Files += $entry
207
+ $merged = $true
208
+ }
209
+ }
210
+ }
211
+
212
+ # ---- Merge dirs: preserve yang masih ada ----
213
+ if ($previous -and $previous.PSObject.Properties.Name -contains 'directories_created' -and $previous.directories_created) {
214
+ foreach ($prevDir in $previous.directories_created) {
215
+ $prevDirStr = [string]$prevDir
216
+ if ($State.Directories -contains $prevDirStr) { continue }
217
+ $fullDir = Join-Path $State.ProjectRoot ($prevDirStr -replace '/', '\')
218
+ if (Test-Path -LiteralPath $fullDir) {
219
+ $State.Directories += $prevDirStr
220
+ $merged = $true
221
+ }
222
+ }
223
+ }
224
+
225
+ # ---- Build manifest object (anonymized project_root + installed_by) ----
226
+ $manifestMetadata = [ordered]@{
227
+ kit_version = $KitVersion
228
+ installed_at = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')
229
+ installer = 'setup-pola-b.ps1'
230
+ }
231
+ $manifest = [ordered]@{
232
+ schema_version = 1
233
+ kit_version = $KitVersion
234
+ installed_at = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss')
235
+ installed_by = '<USER>'
236
+ project_name = $ProjectName
237
+ project_root = '<PROJECT_ROOT>'
238
+ metadata = $manifestMetadata
239
+ files = $State.Files
240
+ directories_created = $State.Directories
241
+ }
242
+
243
+ # ---- HMAC sign (optional) ----
244
+ $signed = $false
245
+ if ($SignManifest) {
246
+ try {
247
+ . (Join-Path $KitDir 'lib\manifest-signing.ps1')
248
+ $manifest.metadata.signature = New-ManifestSignature -Manifest $manifest -KitVersion $KitVersion
249
+ $signed = $true
250
+ } catch {
251
+ Write-Host "WARN Gagal sign manifest (manifest-signing.ps1 issue): $_" -ForegroundColor Yellow
252
+ Write-Host " Manifest tetap ditulis tanpa signature. Uninstall akan prompt karena unsigned." -ForegroundColor Yellow
253
+ }
254
+ }
255
+
256
+ # ---- Write JSON (UTF-8 NO BOM, supaya hash deterministic + git-friendly) ----
257
+ $json = $manifest | ConvertTo-Json -Depth 10
258
+ [System.IO.File]::WriteAllText($manifestPath, $json, (New-Object System.Text.UTF8Encoding $false))
259
+
260
+ return [pscustomobject]@{
261
+ ManifestPath = $manifestPath
262
+ FilesCount = $State.Files.Count
263
+ DirsCount = $State.Directories.Count
264
+ Merged = $merged
265
+ Signed = $signed
266
+ }
267
+ }