@tera-system/core 0.1.0

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 (161) hide show
  1. package/LICENSE.md +13 -0
  2. package/MANIFEST.json +24 -0
  3. package/README.md +67 -0
  4. package/RELEASES.md +28 -0
  5. package/WELCOME.md +35 -0
  6. package/agents/application-blueprint.md +407 -0
  7. package/agents/auditor.md +665 -0
  8. package/agents/design-reviewer.md +392 -0
  9. package/agents/domain-expert-agent.md +510 -0
  10. package/agents/domain-research-agent.md +360 -0
  11. package/agents/engineering-agent-dotnet.md +218 -0
  12. package/agents/engineering-agent-phaser.md +275 -0
  13. package/agents/engineering-agent-typescript.md +300 -0
  14. package/agents/engineering-agent.md +143 -0
  15. package/agents/monitor.md +395 -0
  16. package/agents/production-erp-expert.md +506 -0
  17. package/agents/project-knowledge-agent.md +271 -0
  18. package/agents/qa-agent.md +498 -0
  19. package/agents/tera-business-transformation-consultant.md +290 -0
  20. package/agents/tera-client-engagement.md +891 -0
  21. package/agents/tera-software-designer.md +237 -0
  22. package/agents/tera-strategic-advisor.md +325 -0
  23. package/agents/tera-system-evolution.md +759 -0
  24. package/agents/tera.md +520 -0
  25. package/agents/ui-designer.md +426 -0
  26. package/commands/tera-approve.md +34 -0
  27. package/commands/tera-diagnose.md +47 -0
  28. package/commands/tera-gate.md +40 -0
  29. package/commands/tera-help.md +41 -0
  30. package/commands/tera-new-project.md +35 -0
  31. package/commands/tera-plan.md +34 -0
  32. package/commands/tera-request-build.md +45 -0
  33. package/commands/tera-resume.md +34 -0
  34. package/commands/tera-review.md +52 -0
  35. package/commands/tera-status.md +38 -0
  36. package/commands/tera-update.md +48 -0
  37. package/core/project-control/templates/TASK_TEMPLATE.md +146 -0
  38. package/core/tera-system/AGENT_ACTIVATION_MATRIX.md +285 -0
  39. package/core/tera-system/AGENT_DEPENDENCY_MAP.md +117 -0
  40. package/core/tera-system/AGENT_GENERATION_TEMPLATE.md +312 -0
  41. package/core/tera-system/AGENT_PERMISSION_MODEL.md +343 -0
  42. package/core/tera-system/AIS_PROTOCOL.md +191 -0
  43. package/core/tera-system/TERA_AGENT_CONDUCT.md +102 -0
  44. package/core/tera-system/TERA_CONTINUOUS_IMPROVEMENT_POLICY.md +111 -0
  45. package/core/tera-system/TERA_DISTRIBUTION_POLICY.md +291 -0
  46. package/core/tera-system/TERA_PROJECT_DECISION.md +281 -0
  47. package/core/tera-system/TERA_USER_GUIDE.md +462 -0
  48. package/core/tera-system/TOOLING_AND_MCP_POLICY.md +285 -0
  49. package/core/tera-system/TeraApplicationQuestionBank.md +362 -0
  50. package/core/tera-system/TeraArchitectureMap.md +91 -0
  51. package/core/tera-system/TeraClientPolicy.md +366 -0
  52. package/core/tera-system/TeraHelperAgents.md +970 -0
  53. package/core/tera-system/TeraPolicyMap.md +131 -0
  54. package/core/tera-system/TeraPreExecutionGate.md +818 -0
  55. package/core/tera-system/TeraPreparationDocumentationGovernance.md +370 -0
  56. package/core/tera-system/TeraPricingPolicy.md +674 -0
  57. package/core/tera-system/TeraProjectIntakePolicy.md +164 -0
  58. package/core/tera-system/TeraScenarioStressTests.md +168 -0
  59. package/core/tera-system/TeraSubAgents.md +854 -0
  60. package/core/tera-system/TeraSystemMaintenanceChecklist.md +80 -0
  61. package/core/tera-system/TeraTokenPolicy.md +362 -0
  62. package/core/tera-system/Tera_Project_Preparation_Files.md +1045 -0
  63. package/core/tera-system/agent-helpers/application-blueprint-details.md +177 -0
  64. package/core/tera-system/client-helpers/tera-client-engagement-discovery-domains.md +99 -0
  65. package/core/tera-system/client-helpers/tera-client-engagement-gates.md +258 -0
  66. package/core/tera-system/client-helpers/tera-client-engagement-pricing.md +341 -0
  67. package/core/tera-system/client-helpers/tera-client-engagement-protocols.md +692 -0
  68. package/core/tera-system/consulting-helpers/BTCA_METHODOLOGY_FRAMEWORK.md +195 -0
  69. package/core/tera-system/consulting-helpers/BTCA_REPORT_TEMPLATES.md +266 -0
  70. package/core/tera-system/design-system/ACCESSIBILITY_RULES.md +31 -0
  71. package/core/tera-system/design-system/COMPONENT_LIBRARY_SCHEMA.md +46 -0
  72. package/core/tera-system/design-system/DESIGN_MD_INTEGRATION.md +59 -0
  73. package/core/tera-system/design-system/DESIGN_REVIEW_STANDARDS.md +241 -0
  74. package/core/tera-system/design-system/DESIGN_SOURCE_PROTOCOL.md +61 -0
  75. package/core/tera-system/design-system/DESIGN_SYSTEM_OVERVIEW.md +66 -0
  76. package/core/tera-system/design-system/DESIGN_TOKENS_SCHEMA.md +66 -0
  77. package/core/tera-system/design-system/EXTERNAL_REFERENCE_ANALYSIS.md +52 -0
  78. package/core/tera-system/design-system/FIGMA_INTEGRATION.md +138 -0
  79. package/core/tera-system/design-system/INTERNAL_KITS_INDEX.md +26 -0
  80. package/core/tera-system/design-system/LAYOUT_PATTERNS.md +52 -0
  81. package/core/tera-system/design-system/MOBILE_UI_UX_STANDARDS.md +342 -0
  82. package/core/tera-system/design-system/RTL_LTR_RULES.md +39 -0
  83. package/core/tera-system/design-system/UI_ACCEPTANCE_GATE.md +80 -0
  84. package/core/tera-system/design-system/kits/KIT_ADMIN_DASHBOARD.md +102 -0
  85. package/core/tera-system/engineering-governance/ENGINEERING_AGENT_RESPONSIBILITIES.md +210 -0
  86. package/core/tera-system/engineering-governance/ENGINEERING_BEST_PRACTICES.md +468 -0
  87. package/core/tera-system/engineering-governance/ENGINEERING_GOVERNANCE_GATE.md +131 -0
  88. package/core/tera-system/engineering-governance/ENGINEERING_REVIEW_CHECKLIST.md +129 -0
  89. package/core/tera-system/engineering-governance/QUALITY_GATE_THRESHOLDS.md +159 -0
  90. package/core/tera-system/engineering-helpers/engineering-agent-core.md +171 -0
  91. package/core/tera-system/knowledge-base/OPENHANDS_ARCHITECTURE_REFERENCE.md +243 -0
  92. package/core/tera-system/knowledge-base/manufacturing/00_INDEX.md +32 -0
  93. package/core/tera-system/knowledge-base/manufacturing/01_MANUFACTURING_ERP_CORE_CONCEPTS.md +7 -0
  94. package/core/tera-system/knowledge-base/manufacturing/02_SAP_MANUFACTURING_RESEARCH.md +7 -0
  95. package/core/tera-system/knowledge-base/manufacturing/03_DYNAMICS_365_MANUFACTURING_RESEARCH.md +7 -0
  96. package/core/tera-system/knowledge-base/manufacturing/04_ORACLE_MANUFACTURING_RESEARCH.md +7 -0
  97. package/core/tera-system/knowledge-base/manufacturing/05_ODOO_MANUFACTURING_RESEARCH.md +7 -0
  98. package/core/tera-system/knowledge-base/manufacturing/06_ERPNEXT_MANUFACTURING_RESEARCH.md +7 -0
  99. package/core/tera-system/knowledge-base/manufacturing/07_MANUFACTURING_COSTING_GUIDE.md +7 -0
  100. package/core/tera-system/knowledge-base/manufacturing/08_PRODUCTION_DISCOVERY_QUESTIONS.md +7 -0
  101. package/core/tera-system/knowledge-base/manufacturing/09_MANUFACTURING_BLUEPRINT_CHECKLIST.md +7 -0
  102. package/core/tera-system/knowledge-base/manufacturing/10_PRODUCTION_TEST_SCENARIOS.md +7 -0
  103. package/core/tera-system/knowledge-base/manufacturing/11_QUALITY_REWORK_AND_SCRAP_GUIDE.md +7 -0
  104. package/core/tera-system/knowledge-base/manufacturing/12_MRP_AND_PLANNING_GUIDE.md +7 -0
  105. package/core/tera-system/knowledge-base/manufacturing/13_LOCAL_AND_REGIONAL_MANUFACTURING_CONTEXT.md +7 -0
  106. package/core/tera-system/knowledge-base/manufacturing/14_VENDOR_COMPARISON_MATRIX.md +7 -0
  107. package/core/tera-system/product-standards/maintenance-apps/BEST_PRACTICES_DOMAIN.md +325 -0
  108. package/core/tera-system/product-standards/maintenance-apps/STANDARD_DEFINITION.md +142 -0
  109. package/core/tera-system/profiles/PROFILES_INDEX.md +57 -0
  110. package/core/tera-system/profiles/TEMPLATE.md +47 -0
  111. package/core/tera-system/profiles/dotnet-blazor-ef.md +76 -0
  112. package/core/tera-system/profiles/dotnet-razorpages-adonet.md +137 -0
  113. package/core/tera-system/profiles/dotnet-wpf-sqlite.md +159 -0
  114. package/core/tera-system/profiles/effect-bun-opencode.md +109 -0
  115. package/core/tera-system/profiles/flutter-mobile.md +369 -0
  116. package/core/tera-system/profiles/nextjs-prisma.md +110 -0
  117. package/core/tera-system/profiles/phaser-react-node.md +302 -0
  118. package/core/tera-system/profiles/react-pwa.md +97 -0
  119. package/core/tera-system/runtime/CLIENT_DISCOVERY_PROTOCOL.md +145 -0
  120. package/core/tera-system/runtime/DOMAIN_INTELLIGENCE_PROTOCOL.md +124 -0
  121. package/core/tera-system/runtime/MVP_DEFINITION_PROTOCOL.md +176 -0
  122. package/core/tera-system/runtime/TERA_RUNTIME_CHECKLISTS.md +646 -0
  123. package/core/tera-system/runtime/TERA_RUNTIME_PROTOCOLS.md +50 -0
  124. package/core/tera-system/runtime/TERA_RUNTIME_PROTOCOLS_CLIENT.md +355 -0
  125. package/core/tera-system/runtime/TERA_RUNTIME_PROTOCOLS_CORE.md +799 -0
  126. package/core/tera-system/runtime/TERA_RUNTIME_TEMPLATES.md +908 -0
  127. package/core/tera-system/runtime/TERA_RUNTIME_TEMPLATES_DELIVERY.md +584 -0
  128. package/core/tera-system/runtime/TERA_RUNTIME_TEMPLATES_PREPARATION.md +376 -0
  129. package/core/tera-system/runtime/TERA_SOLUTION_PREPARATION_PROTOCOL.md +335 -0
  130. package/core/tera-system/runtime/TERA_SOLUTION_PREPARATION_TEMPLATES.md +397 -0
  131. package/core/tera-system/runtime/VERSION_LIFECYCLE_PROTOCOL.md +296 -0
  132. package/core/tera-system/semgrep-rules/README.md +32 -0
  133. package/core/tera-system/semgrep-rules/tera-security.yml +66 -0
  134. package/core/tera-system/semgrep-rules/tera-standards.yml +49 -0
  135. package/core/tera-system/teranoo-ui/README.md +48 -0
  136. package/core/tera-system/teranoo-ui/components/button.tsx +51 -0
  137. package/core/tera-system/teranoo-ui/components/card.tsx +49 -0
  138. package/core/tera-system/teranoo-ui/components/dashboard-layout.tsx +36 -0
  139. package/core/tera-system/teranoo-ui/components/data-table.tsx +146 -0
  140. package/core/tera-system/teranoo-ui/components/empty-state.tsx +31 -0
  141. package/core/tera-system/teranoo-ui/components/kpi-card.tsx +42 -0
  142. package/core/tera-system/teranoo-ui/components/page-header.tsx +25 -0
  143. package/core/tera-system/teranoo-ui/components/search-input.tsx +40 -0
  144. package/core/tera-system/teranoo-ui/components/sidebar.tsx +66 -0
  145. package/core/tera-system/teranoo-ui/components/stats-card.tsx +37 -0
  146. package/core/tera-system/teranoo-ui/registry.json +77 -0
  147. package/core/tera-system/teranoo-ui/styles/teranoo-theme.css +61 -0
  148. package/opencode.tera.example.json +30 -0
  149. package/package.json +37 -0
  150. package/scripts/build.mjs +110 -0
  151. package/scripts/install.js +244 -0
  152. package/scripts/lib/license.mjs +90 -0
  153. package/scripts/lib/public-key.pem +3 -0
  154. package/scripts/tera-license.mjs +63 -0
  155. package/tools/tera-clean.ps1 +97 -0
  156. package/tools/tera-fetch.ps1 +165 -0
  157. package/tools/tera-release.ps1 +96 -0
  158. package/tools/tera-schedule.ps1 +59 -0
  159. package/tools/tera-update.ps1 +472 -0
  160. package/tools/tera-watch.ps1 +154 -0
  161. package/tools/update-client-repositories.ps1 +92 -0
@@ -0,0 +1,472 @@
1
+ <#
2
+ Tera Runtime Updater.
3
+ The release package must contain TERA_RELEASE_MANIFEST.json and SHA256SUMS.
4
+ Apply is fail-closed: it requires Git, a valid lockfile (or -Initialize),
5
+ verified hashes, and managed paths only.
6
+ #>
7
+ [CmdletBinding()]
8
+ param(
9
+ [Parameter(Mandatory = $true)]
10
+ [string]$Version,
11
+
12
+ [ValidateSet('Plan', 'Apply')]
13
+ [string]$Mode = 'Plan',
14
+
15
+ [string]$SourcePath,
16
+
17
+ [string]$RepositoryUrl,
18
+
19
+ [string]$RepoPath = (Get-Location).Path,
20
+
21
+ [ValidateSet('stable', 'beta')]
22
+ [string]$Channel = 'stable',
23
+
24
+ [switch]$Initialize,
25
+
26
+ [switch]$NoCommit
27
+ )
28
+
29
+ Set-StrictMode -Version 5
30
+ $ErrorActionPreference = 'Stop'
31
+
32
+ function Fail([string]$Message) {
33
+ throw $Message
34
+ }
35
+
36
+ # تنفيذ git بأمان في PS 5.1: تحذيرات stderr (LF->CRLF وغيرها) يجب ألا تُوقف التنفيذ.
37
+ # الحكم الحقيقي للنجاح/الفشل هو $LASTEXITCODE وليس نص stderr.
38
+ # ملاحظة: المصفوفات المتداخلة تُفلت، ونمرر -c core.autocrlf=false لتعطيل تحذيرات
39
+ # LF->CRLF التي كانت تقطع العملية في PS 5.1 مع ملفات LF داخل مستودع الزبون.
40
+ function Invoke-Git {
41
+ param(
42
+ [Parameter(Mandatory = $true)]
43
+ [object[]]$Arguments
44
+ )
45
+ $flat = @()
46
+ foreach ($arg in $Arguments) {
47
+ if ($arg -is [System.Array]) { $flat += @($arg) } else { $flat += $arg }
48
+ }
49
+ $previous = $ErrorActionPreference
50
+ $ErrorActionPreference = 'Continue'
51
+ try {
52
+ & git -c core.autocrlf=false @flat 2>$null
53
+ return $LASTEXITCODE
54
+ }
55
+ finally {
56
+ $ErrorActionPreference = $previous
57
+ }
58
+ }
59
+
60
+ function Invoke-GitCapture {
61
+ param(
62
+ [Parameter(Mandatory = $true)]
63
+ [object[]]$Arguments
64
+ )
65
+ $flat = @()
66
+ foreach ($arg in $Arguments) {
67
+ if ($arg -is [System.Array]) { $flat += @($arg) } else { $flat += $arg }
68
+ }
69
+ $previous = $ErrorActionPreference
70
+ $ErrorActionPreference = 'Continue'
71
+ try {
72
+ $output = @(& git -c core.autocrlf=false @flat 2>$null)
73
+ return [pscustomobject]@{ Output = $output; ExitCode = $LASTEXITCODE }
74
+ }
75
+ finally {
76
+ $ErrorActionPreference = $previous
77
+ }
78
+ }
79
+
80
+ function Normalize-RelativePath([string]$Path) {
81
+ $normalized = ($Path -replace '\\', '/')
82
+ while ($normalized.StartsWith('./')) {
83
+ $normalized = $normalized.Substring(2)
84
+ }
85
+ return $normalized
86
+ }
87
+
88
+ function Normalize-Version([string]$Value) {
89
+ $result = $Value -replace '^tera-', ''
90
+ return ($result -replace '^v', '')
91
+ }
92
+
93
+ function Get-RequiredProperty($Object, [string]$Name, [string]$Context) {
94
+ if ($null -eq $Object -or -not ($Object.PSObject.Properties.Name -contains $Name)) {
95
+ Fail "$Context is missing required property: $Name"
96
+ }
97
+ $value = $Object.$Name
98
+ if ($null -eq $value -or ([string]$value).Trim().Length -eq 0) {
99
+ Fail "$Context has an empty required property: $Name"
100
+ }
101
+ return $value
102
+ }
103
+
104
+ function Assert-ManagedRelativePath([string]$Path) {
105
+ $normalized = Normalize-RelativePath $Path
106
+ if ([System.IO.Path]::IsPathRooted($Path) -or $normalized -match '^[A-Za-z]:') {
107
+ Fail "Manifest contains an absolute path: $Path"
108
+ }
109
+ if ($normalized -match '(^|/)\.\.(/|$)') {
110
+ Fail "Manifest contains a traversal path: $Path"
111
+ }
112
+ $allowed = @('.opencode/', 'tera-system/', 'tera-workshop/')
113
+ $ok = $false
114
+ foreach ($root in $allowed) {
115
+ if ($normalized.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) {
116
+ $ok = $true
117
+ break
118
+ }
119
+ }
120
+ if (-not $ok) {
121
+ Fail "Manifest path is outside Tera-managed roots: $Path"
122
+ }
123
+ return $normalized
124
+ }
125
+
126
+ function Read-RequiredLock([string]$Path) {
127
+ if (-not (Test-Path -LiteralPath $Path)) {
128
+ return $null
129
+ }
130
+ try {
131
+ $lock = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
132
+ } catch {
133
+ Fail "lock.json is invalid JSON: $Path"
134
+ }
135
+ foreach ($name in @('version', 'tag', 'commit', 'channel', 'files')) {
136
+ [void](Get-RequiredProperty $lock $name 'lock.json')
137
+ }
138
+ if ($lock.channel -notin @('stable', 'beta')) {
139
+ Fail "lock.json has invalid channel: $($lock.channel)"
140
+ }
141
+ if ($lock.commit -notmatch '^[0-9a-fA-F]{40}$') {
142
+ Fail "lock.json commit must be a full 40-character SHA."
143
+ }
144
+ if ($lock.files -is [string]) {
145
+ Fail "lock.json files must be an array."
146
+ }
147
+ return $lock
148
+ }
149
+
150
+ function Read-Hashes([string]$Path) {
151
+ if (-not (Test-Path -LiteralPath $Path)) {
152
+ Fail "SHA256SUMS is required: $Path"
153
+ }
154
+ $hashes = @{}
155
+ foreach ($line in Get-Content -LiteralPath $Path) {
156
+ if ($line -match '^([0-9a-fA-F]{64})\s+\*?(.+)$') {
157
+ $hashes[(Normalize-RelativePath $matches[2])] = $matches[1].ToLowerInvariant()
158
+ }
159
+ }
160
+ if ($hashes.Count -eq 0) {
161
+ Fail "SHA256SUMS contains no valid entries."
162
+ }
163
+ return $hashes
164
+ }
165
+
166
+ function Get-GitStatusPaths([string]$Repo) {
167
+ $items = @()
168
+ $status = Invoke-GitCapture -Arguments @('-C', $Repo, 'status', '--porcelain')
169
+ foreach ($line in $status.Output) {
170
+ if ($line.Length -ge 4) {
171
+ $items += [pscustomobject]@{
172
+ Code = $line.Substring(0, 2)
173
+ Path = (Normalize-RelativePath $line.Substring(3).Trim('"'))
174
+ }
175
+ }
176
+ }
177
+ return $items
178
+ }
179
+
180
+ function Test-ReparsePoint([string]$Path) {
181
+ if (-not (Test-Path -LiteralPath $Path)) { return $false }
182
+ try {
183
+ $item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop
184
+ return ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
185
+ } catch {
186
+ return $false
187
+ }
188
+ }
189
+
190
+ function Assert-NoReparsePointInPath([string]$BaseRoot, [string]$RelativePath) {
191
+ $current = $BaseRoot
192
+ $parts = ($RelativePath -replace '\\', '/').Split('/')
193
+ for ($i = 0; $i -lt $parts.Count; $i++) {
194
+ if ([string]::IsNullOrWhiteSpace($parts[$i])) { continue }
195
+ $current = Join-Path $current $parts[$i]
196
+ if (Test-ReparsePoint $current) {
197
+ Fail "Refusing to write through reparse point (symlink/junction): $RelativePath (at $current)"
198
+ }
199
+ }
200
+ }
201
+
202
+ $RepoPath = [System.IO.Path]::GetFullPath($RepoPath)
203
+ if (-not (Test-Path -LiteralPath $RepoPath -PathType Container)) { Fail "RepoPath does not exist: $RepoPath" }
204
+
205
+ #---------------------------------------------------------------------
206
+ # 0. Resolve source: either a local release package or a remote fetch
207
+ #---------------------------------------------------------------------
208
+ $tempFetchedPath = $null
209
+ # تنظيف مضمون للحزمة المؤقتة عند أي خطأ غير مُلتقط (فشل تحقق، manifest، lock...)
210
+ trap {
211
+ if ($tempFetchedPath -and (Test-Path -LiteralPath $tempFetchedPath)) {
212
+ Remove-Item -LiteralPath $tempFetchedPath -Recurse -Force -ErrorAction SilentlyContinue
213
+ }
214
+ break
215
+ }
216
+ if (-not $SourcePath) {
217
+ if (-not $RepositoryUrl) {
218
+ Fail "Either -SourcePath or -RepositoryUrl is required."
219
+ }
220
+ $fetchTool = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'tera-fetch.ps1'
221
+ if (-not (Test-Path -LiteralPath $fetchTool -PathType Leaf)) { Fail "tera-fetch.ps1 not found: $fetchTool" }
222
+ $tempFetchedPath = Join-Path ([System.IO.Path]::GetTempPath()) ("tera-fetch-" + [guid]::NewGuid().ToString('N'))
223
+ & $fetchTool -RepositoryUrl $RepositoryUrl -Version $Version -Channel $Channel -OutPath $tempFetchedPath
224
+ if ($LASTEXITCODE -ne 0) { Fail "tera-fetch.ps1 failed for $Version from $RepositoryUrl" }
225
+ $SourcePath = $tempFetchedPath
226
+ }
227
+ $SourcePath = [System.IO.Path]::GetFullPath($SourcePath)
228
+ if (-not (Test-Path -LiteralPath $SourcePath -PathType Container)) { Fail "SourcePath does not exist: $SourcePath" }
229
+
230
+ $gitRootCheck = Invoke-GitCapture -Arguments @('-C', $RepoPath, 'rev-parse', '--show-toplevel')
231
+ $gitRoot = ($gitRootCheck.Output -join ' ').Trim()
232
+ if ($gitRootCheck.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($gitRoot)) {
233
+ Fail "RepoPath must be a Git repository."
234
+ }
235
+ $gitRoot = [System.IO.Path]::GetFullPath($gitRoot)
236
+
237
+ $manifestPath = Join-Path $SourcePath 'TERA_RELEASE_MANIFEST.json'
238
+ $sumsPath = Join-Path $SourcePath 'SHA256SUMS'
239
+ if (-not (Test-Path -LiteralPath $manifestPath)) { Fail "Release manifest is required: $manifestPath" }
240
+ try { $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json } catch { Fail "Release manifest is invalid JSON." }
241
+
242
+ $requestedVersion = Normalize-Version $Version
243
+ $manifestVersion = [string](Get-RequiredProperty $manifest 'version' 'release manifest')
244
+ $manifestTag = [string](Get-RequiredProperty $manifest 'tag' 'release manifest')
245
+ $manifestCommit = [string](Get-RequiredProperty $manifest 'commit' 'release manifest')
246
+ $manifestChannel = [string](Get-RequiredProperty $manifest 'channel' 'release manifest')
247
+ $manifestFiles = Get-RequiredProperty $manifest 'files' 'release manifest'
248
+
249
+ if ($manifestVersion -ne $requestedVersion) { Fail "Manifest version $manifestVersion does not match requested version $requestedVersion." }
250
+ if ($manifestTag -ne $Version) { Fail "Manifest tag $manifestTag does not match requested tag $Version." }
251
+ if ($manifestChannel -ne $Channel) { Fail "Manifest channel $manifestChannel does not match requested channel $Channel." }
252
+ if ($manifestCommit -notmatch '^[0-9a-fA-F]{40}$') { Fail 'Manifest commit must be a full 40-character SHA.' }
253
+
254
+ $hashes = Read-Hashes $sumsPath
255
+ $packageFiles = New-Object System.Collections.Generic.List[string]
256
+ foreach ($entry in @($manifestFiles)) {
257
+ if ($entry -isnot [string]) { Fail 'Manifest files must contain strings only.' }
258
+ $relative = Assert-ManagedRelativePath $entry
259
+ if ($packageFiles -contains $relative) { Fail "Manifest contains duplicate file: $relative" }
260
+ $packageFiles.Add($relative)
261
+ if (-not $hashes.ContainsKey($relative)) { Fail "SHA256SUMS has no hash for: $relative" }
262
+ $sourceFile = Join-Path $SourcePath $relative
263
+ if (-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) { Fail "Manifest file is missing from package: $relative" }
264
+ $actualHash = (Get-FileHash -LiteralPath $sourceFile -Algorithm SHA256).Hash.ToLowerInvariant()
265
+ if ($actualHash -ne $hashes[$relative]) { Fail "SHA256 mismatch: $relative" }
266
+ }
267
+ if ($packageFiles.Count -eq 0) { Fail 'Release manifest contains no files.' }
268
+
269
+ $lockPath = Join-Path $RepoPath '.tera\lock.json'
270
+ $currentLock = Read-RequiredLock $lockPath
271
+ if ($null -eq $currentLock -and -not $Initialize) {
272
+ Fail 'No lock.json found. Use -Initialize explicitly for first installation.'
273
+ }
274
+
275
+ $managedSet = @{}
276
+ foreach ($relative in $packageFiles) { $managedSet[$relative] = $true }
277
+ $oldFiles = @()
278
+ if ($currentLock) {
279
+ foreach ($entry in @($currentLock.files)) {
280
+ $oldFiles += Assert-ManagedRelativePath ([string]$entry)
281
+ $managedSet[$oldFiles[-1]] = $true
282
+ }
283
+ }
284
+ $allManaged = @($managedSet.Keys | Sort-Object)
285
+
286
+ $status = @(Get-GitStatusPaths $RepoPath)
287
+ $conflicts = @()
288
+ foreach ($item in $status) {
289
+ $itemPath = $item.Path.TrimEnd('/')
290
+ $isDir = $item.Path.EndsWith('/')
291
+ $isManaged = ($itemPath -eq '.tera/lock.json' -or $managedSet.ContainsKey($itemPath))
292
+ # مجلد غير متتبع بالكامل داخل جذور مُدارة = معالجة كمسار مُدار
293
+ if (-not $isManaged -and $isDir) {
294
+ foreach ($managedPath in $managedSet.Keys) {
295
+ if ($managedPath.StartsWith($itemPath + '/')) { $isManaged = $true; break }
296
+ }
297
+ }
298
+ if (-not $isManaged) { continue }
299
+ if ($Initialize -and $item.Code -eq '??') {
300
+ # مع -Initialize: السماح بملفات/مجلدات غير متتبعة فقط إذا لم تتعارض مع الحزمة
301
+ if ($packageFiles -contains $itemPath) { $conflicts += $itemPath; continue }
302
+ if ($isDir) {
303
+ $containsPkg = $false
304
+ foreach ($pkg in $packageFiles) {
305
+ if ($pkg.StartsWith($itemPath + '/')) { $containsPkg = $true; break }
306
+ }
307
+ if ($containsPkg) { $conflicts += $itemPath }
308
+ }
309
+ continue
310
+ }
311
+ $conflicts += $itemPath
312
+ }
313
+ if ($conflicts.Count -gt 0) {
314
+ Fail ("Local changes exist in Tera-managed paths: " + ($conflicts -join ', '))
315
+ }
316
+
317
+ $newFiles = @()
318
+ $changedFiles = @()
319
+ $sameFiles = @()
320
+ foreach ($relative in $packageFiles) {
321
+ $sourceFile = Join-Path $SourcePath $relative
322
+ $targetFile = Join-Path $RepoPath $relative
323
+ if (-not (Test-Path -LiteralPath $targetFile -PathType Leaf)) {
324
+ $newFiles += $relative
325
+ } else {
326
+ $targetHash = (Get-FileHash -LiteralPath $targetFile -Algorithm SHA256).Hash.ToLowerInvariant()
327
+ if ($targetHash -eq $hashes[$relative]) { $sameFiles += $relative } else { $changedFiles += $relative }
328
+ }
329
+ }
330
+ $deletedFiles = @()
331
+ foreach ($relative in $oldFiles) {
332
+ if ($packageFiles -notcontains $relative -and (Test-Path -LiteralPath (Join-Path $RepoPath $relative))) {
333
+ $deletedFiles += $relative
334
+ }
335
+ }
336
+
337
+ Write-Host "Tera update $Version [$Channel] ($Mode)"
338
+ Write-Host "New=$($newFiles.Count) Changed=$($changedFiles.Count) Same=$($sameFiles.Count) Deleted=$($deletedFiles.Count)"
339
+ if ($Mode -eq 'Plan') {
340
+ # تنظيف الحزمة المؤقتة قبل الخروج — لا نترك مجلدات في %TEMP%
341
+ if ($tempFetchedPath -and (Test-Path -LiteralPath $tempFetchedPath)) {
342
+ Remove-Item -LiteralPath $tempFetchedPath -Recurse -Force -ErrorAction SilentlyContinue
343
+ }
344
+ Write-Host 'PLAN complete: no files were written.'
345
+ return
346
+ }
347
+
348
+ $backupRoot = Join-Path $RepoPath ('.tera\backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss-fff'))
349
+ $backupCreated = $false
350
+ $touched = New-Object System.Collections.Generic.List[string]
351
+ try {
352
+ $excludePath = Join-Path $RepoPath '.git\info\exclude'
353
+ $excludeDir = Split-Path -Parent $excludePath
354
+ if (-not (Test-Path -LiteralPath $excludeDir)) { New-Item -ItemType Directory -Path $excludeDir -Force | Out-Null }
355
+ $excludeLines = @()
356
+ if (Test-Path -LiteralPath $excludePath) { $excludeLines = @(Get-Content -LiteralPath $excludePath) }
357
+ if ($excludeLines -notcontains '.tera/backups/') {
358
+ Add-Content -LiteralPath $excludePath -Value '.tera/backups/' -Encoding ASCII
359
+ }
360
+ foreach ($relative in $allManaged + @('.tera/lock.json')) {
361
+ $targetFile = Join-Path $RepoPath $relative
362
+ if (Test-Path -LiteralPath $targetFile -PathType Leaf) {
363
+ $backupFile = Join-Path $backupRoot $relative
364
+ $backupDir = Split-Path -Parent $backupFile
365
+ if (-not (Test-Path -LiteralPath $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null }
366
+ Copy-Item -LiteralPath $targetFile -Destination $backupFile -Force
367
+ $backupCreated = $true
368
+ }
369
+ }
370
+
371
+ foreach ($relative in $deletedFiles) {
372
+ Assert-NoReparsePointInPath $RepoPath $relative
373
+ Remove-Item -LiteralPath (Join-Path $RepoPath $relative) -Force
374
+ $touched.Add($relative)
375
+ }
376
+ foreach ($relative in $packageFiles) {
377
+ Assert-NoReparsePointInPath $RepoPath $relative
378
+ $sourceFile = Join-Path $SourcePath $relative
379
+ $targetFile = Join-Path $RepoPath $relative
380
+ $targetDir = Split-Path -Parent $targetFile
381
+ if (-not (Test-Path -LiteralPath $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
382
+ Copy-Item -LiteralPath $sourceFile -Destination $targetFile -Force
383
+ $touched.Add($relative)
384
+ }
385
+
386
+ $lockDir = Split-Path -Parent $lockPath
387
+ if (-not (Test-Path -LiteralPath $lockDir)) { New-Item -ItemType Directory -Path $lockDir -Force | Out-Null }
388
+ $lock = [ordered]@{
389
+ version = $requestedVersion
390
+ tag = $manifestTag
391
+ commit = $manifestCommit
392
+ channel = $manifestChannel
393
+ files = @($packageFiles)
394
+ }
395
+ $lock | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $lockPath -Encoding UTF8
396
+ $touched.Add('.tera/lock.json')
397
+
398
+ # ضمان توزيع .gitattributes في الزبون — بدونه يفسر git diff --cached --check
399
+ # كل سطر CRLF (بعد تحويل autocrlf) كـ trailing whitespace ويحطم بوابة الجودة.
400
+ $gitattributesPath = Join-Path $RepoPath '.gitattributes'
401
+ $gitattributesContent = @(
402
+ '# Disable automatic line ending conversion for all files',
403
+ '* -text',
404
+ '',
405
+ '# Treat CR at end-of-line as part of the line ending, not as trailing whitespace.',
406
+ '* whitespace=cr-at-eol'
407
+ ) -join "`n"
408
+ $existingAttributes = ''
409
+ if (Test-Path -LiteralPath $gitattributesPath) {
410
+ $existingAttributes = [System.IO.File]::ReadAllText($gitattributesPath)
411
+ }
412
+ if ($existingAttributes.Trim() -ne $gitattributesContent.Trim()) {
413
+ [System.IO.File]::WriteAllText($gitattributesPath, $gitattributesContent + "`n", (New-Object System.Text.UTF8Encoding $false))
414
+ Write-Host " [ATTR] Wrote .gitattributes to client repo (cr-at-eol)."
415
+ }
416
+ $gitPaths = @($packageFiles + $deletedFiles + @('.tera/lock.json', '.gitattributes'))
417
+ # git add على دفعات لتجنب حد طول سطر أوامر Windows (~32K)
418
+ $addOk = $true
419
+ $batchSize = 40
420
+ for ($i = 0; $i -lt $gitPaths.Count; $i += $batchSize) {
421
+ $end = [Math]::Min($i + $batchSize, $gitPaths.Count)
422
+ $batch = @($gitPaths[$i..($end - 1)])
423
+ $batchExit = Invoke-Git -Arguments @('-C', $RepoPath, 'add', '--', $batch)
424
+ if ($batchExit -ne 0) { $addOk = $false; break }
425
+ }
426
+ if (-not $addOk) { Fail "git add failed (exit $batchExit)." }
427
+ # diff --cached --check: النتيجة على stdout؛ الحكم عبر ExitCode
428
+ $check = Invoke-GitCapture -Arguments @('-C', $RepoPath, 'diff', '--cached', '--check')
429
+ if ($check.ExitCode -ne 0) { Fail ("git diff --cached --check failed: " + ($check.Output -join ' ')) }
430
+
431
+ if (-not $NoCommit) {
432
+ $headBefore = (Invoke-GitCapture -Arguments @('-C', $RepoPath, 'rev-parse', 'HEAD')).Output
433
+ $commitExit = Invoke-Git -Arguments @('-C', $RepoPath, 'commit', '-m', "TERA: update to $Version ($Channel)")
434
+ $headAfter = (Invoke-GitCapture -Arguments @('-C', $RepoPath, 'rev-parse', 'HEAD')).Output
435
+ # git commit قد يكتب قائمة الملفات الضخمة على stderr فتُقطع العملية قبل ضبط
436
+ # $LASTEXITCODE؛ الحكم الموثوق هو: هل تغيّر HEAD بعد الالتزام؟
437
+ $committed = ($headBefore -join '') -ne ($headAfter -join '')
438
+ if (-not $committed -and $commitExit -ne 0) {
439
+ Fail "git commit failed (exit $commitExit); changes remain staged for manual recovery."
440
+ }
441
+ if (-not $committed) {
442
+ Fail 'git commit did not change HEAD; changes remain staged for manual recovery.'
443
+ }
444
+ }
445
+ Write-Host "Apply complete. Backup: $backupRoot"
446
+ }
447
+ catch {
448
+ Write-Warning "Apply failed: $($_.Exception.Message)"
449
+ Write-Warning 'Restoring files from backup.'
450
+ foreach ($relative in $touched) {
451
+ $targetFile = Join-Path $RepoPath $relative
452
+ $backupFile = Join-Path $backupRoot $relative
453
+ if (Test-Path -LiteralPath $backupFile -PathType Leaf) {
454
+ Copy-Item -LiteralPath $backupFile -Destination $targetFile -Force
455
+ } elseif (Test-Path -LiteralPath $targetFile -PathType Leaf) {
456
+ Remove-Item -LiteralPath $targetFile -Force
457
+ }
458
+ }
459
+ [void](Invoke-Git -Arguments @('-C', $RepoPath, 'reset', '--', $allManaged, '.tera/lock.json'))
460
+ # إزالة أي index.lock عالق من git add/commit الفاشل — وإلا كل محاولة لاحقة تتعثر عليه
461
+ $staleLock = Join-Path $RepoPath '.git\index.lock'
462
+ if (Test-Path -LiteralPath $staleLock) {
463
+ Remove-Item -LiteralPath $staleLock -Force -ErrorAction SilentlyContinue
464
+ Write-Warning "Removed stale .git/index.lock left by a failed git operation."
465
+ }
466
+ throw
467
+ }
468
+ finally {
469
+ if ($tempFetchedPath -and (Test-Path -LiteralPath $tempFetchedPath)) {
470
+ Remove-Item -LiteralPath $tempFetchedPath -Recurse -Force -ErrorAction SilentlyContinue
471
+ }
472
+ }
@@ -0,0 +1,154 @@
1
+ <#
2
+ Tera Watch — automatic update notifier for client repositories.
3
+ Checks the origin repository for newer releases than the installed lock.json,
4
+ writes .tera/update-status.json, and shows a native Windows toast when an
5
+ update is available.
6
+
7
+ Modes:
8
+ -Once : check once and exit (used by the scheduled task)
9
+ (no -Once) : loop forever, checking every -IntervalHours
10
+
11
+ Exit codes:
12
+ 0 : up to date (or no prior installation)
13
+ 2 : update available
14
+ 1 : error
15
+
16
+ Source of truth: tera-system/TERA_DISTRIBUTION_POLICY.md (SCP-2026-08-15-013)
17
+ #>
18
+ [CmdletBinding()]
19
+ param(
20
+ [Parameter(Mandatory = $true)]
21
+ [string]$RepositoryUrl,
22
+
23
+ [string]$RepoPath = (Get-Location).Path,
24
+
25
+ [int]$IntervalHours = 6,
26
+
27
+ [switch]$Once
28
+ )
29
+
30
+ Set-StrictMode -Version 5
31
+ $ErrorActionPreference = 'Continue'
32
+
33
+ function Fail([string]$Message) {
34
+ throw $Message
35
+ }
36
+
37
+ function Compare-TeraVersion([string]$a, [string]$b) {
38
+ $aParts = ($a -replace '^tera-v', '').Split('.')
39
+ $bParts = ($b -replace '^tera-v', '').Split('.')
40
+ for ($i = 0; $i -lt [Math]::Max($aParts.Count, $bParts.Count); $i++) {
41
+ $pa = 0; $pb = 0
42
+ if ($i -lt $aParts.Count) { [void][int]::TryParse($aParts[$i], [ref]$pa) }
43
+ if ($i -lt $bParts.Count) { [void][int]::TryParse($bParts[$i], [ref]$pb) }
44
+ if ($pa -gt $pb) { return 1 }
45
+ if ($pa -lt $pb) { return -1 }
46
+ }
47
+ return 0
48
+ }
49
+
50
+ function Get-OriginVersions([string]$Url) {
51
+ $lines = @(& git ls-remote --tags $Url 2>$null)
52
+ if ($LASTEXITCODE -ne 0) { Fail "Could not reach origin repository: $Url" }
53
+ $versions = New-Object System.Collections.Generic.List[string]
54
+ foreach ($line in $lines) {
55
+ if ($line -match 'refs/tags/(tera-v[0-9]+\.[0-9]+\.[0-9]+)(\^\{\})?$') {
56
+ $versions.Add($matches[1])
57
+ }
58
+ }
59
+ $sorted = @($versions | Sort-Object -Unique)
60
+ return , $sorted
61
+ }
62
+
63
+ function Show-TeraToast([string]$Title, [string]$Message) {
64
+ # Native WinRT toast — no external module required (Windows 10/11)
65
+ try {
66
+ $null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
67
+ $null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
68
+ $null = [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime]
69
+
70
+ $appId = 'Tera Update Notifier'
71
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
72
+ $xml.LoadXml("<toast><visual><binding template='ToastText02'><text id='1'>$Title</text><text id='2'>$Message</text></binding></visual></toast>")
73
+ $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
74
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
75
+ return $true
76
+ } catch {
77
+ Write-Warning "Toast failed (non-critical): $($_.Exception.Message)"
78
+ return $false
79
+ }
80
+ }
81
+
82
+ function Invoke-CheckOnce {
83
+ $RepoPath = [System.IO.Path]::GetFullPath($RepoPath)
84
+ $versions = Get-OriginVersions $RepositoryUrl
85
+ if ($versions.Count -eq 0) {
86
+ Write-Host "No tera-vX.Y.Z tags found in origin."
87
+ return 1
88
+ }
89
+ $latest = $versions[-1]
90
+
91
+ $lockPath = Join-Path $RepoPath '.tera\lock.json'
92
+ $installed = $null
93
+ if (Test-Path -LiteralPath $lockPath) {
94
+ try {
95
+ $lock = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json
96
+ if ($lock.tag) { $installed = [string]$lock.tag }
97
+ } catch {
98
+ Write-Warning "lock.json unreadable at $lockPath"
99
+ }
100
+ }
101
+
102
+ $updateAvailable = $false
103
+ if ($installed) {
104
+ $updateAvailable = (Compare-TeraVersion $installed $latest) -lt 0
105
+ }
106
+
107
+ # Always write status file (readable by any session / OpenCode)
108
+ $status = [ordered]@{
109
+ checkedAt = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
110
+ installed = $installed
111
+ latest = $latest
112
+ updateAvailable = $updateAvailable
113
+ }
114
+ $teraDir = Join-Path $RepoPath '.tera'
115
+ if (-not (Test-Path -LiteralPath $teraDir)) { New-Item -ItemType Directory -Path $teraDir -Force | Out-Null }
116
+ $statusPath = Join-Path $teraDir 'update-status.json'
117
+ $status | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $statusPath -Encoding UTF8
118
+
119
+ if ($updateAvailable) {
120
+ Write-Host "UPDATE AVAILABLE: installed=$installed latest=$latest"
121
+ $null = Show-TeraToast 'Tera Update Available' "Installed: $installed | Latest: $latest"
122
+ return 2
123
+ }
124
+ if ($installed) {
125
+ Write-Host "Up to date: $installed (latest: $latest)"
126
+ return 0
127
+ }
128
+ Write-Host "No prior installation. Latest available: $latest"
129
+ return 0
130
+ }
131
+
132
+ # ---- main ----
133
+ if ($Once) {
134
+ try {
135
+ exit (Invoke-CheckOnce)
136
+ } catch {
137
+ Write-Warning "Check failed: $($_.Exception.Message)"
138
+ exit 1
139
+ }
140
+ }
141
+
142
+ # Loop mode
143
+ Write-Host "Tera Watch running. Checking every $IntervalHours hour(s). Ctrl+C to stop."
144
+ while ($true) {
145
+ try {
146
+ $code = Invoke-CheckOnce
147
+ if ($code -eq 2) {
148
+ Write-Host "Notification sent. Next check in $IntervalHours hour(s)."
149
+ }
150
+ } catch {
151
+ Write-Warning "Check failed (will retry): $($_.Exception.Message)"
152
+ }
153
+ Start-Sleep -Seconds ($IntervalHours * 3600)
154
+ }