@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.
- package/LICENSE.md +13 -0
- package/MANIFEST.json +24 -0
- package/README.md +67 -0
- package/RELEASES.md +28 -0
- package/WELCOME.md +35 -0
- package/agents/application-blueprint.md +407 -0
- package/agents/auditor.md +665 -0
- package/agents/design-reviewer.md +392 -0
- package/agents/domain-expert-agent.md +510 -0
- package/agents/domain-research-agent.md +360 -0
- package/agents/engineering-agent-dotnet.md +218 -0
- package/agents/engineering-agent-phaser.md +275 -0
- package/agents/engineering-agent-typescript.md +300 -0
- package/agents/engineering-agent.md +143 -0
- package/agents/monitor.md +395 -0
- package/agents/production-erp-expert.md +506 -0
- package/agents/project-knowledge-agent.md +271 -0
- package/agents/qa-agent.md +498 -0
- package/agents/tera-business-transformation-consultant.md +290 -0
- package/agents/tera-client-engagement.md +891 -0
- package/agents/tera-software-designer.md +237 -0
- package/agents/tera-strategic-advisor.md +325 -0
- package/agents/tera-system-evolution.md +759 -0
- package/agents/tera.md +520 -0
- package/agents/ui-designer.md +426 -0
- package/commands/tera-approve.md +34 -0
- package/commands/tera-diagnose.md +47 -0
- package/commands/tera-gate.md +40 -0
- package/commands/tera-help.md +41 -0
- package/commands/tera-new-project.md +35 -0
- package/commands/tera-plan.md +34 -0
- package/commands/tera-request-build.md +45 -0
- package/commands/tera-resume.md +34 -0
- package/commands/tera-review.md +52 -0
- package/commands/tera-status.md +38 -0
- package/commands/tera-update.md +48 -0
- package/core/project-control/templates/TASK_TEMPLATE.md +146 -0
- package/core/tera-system/AGENT_ACTIVATION_MATRIX.md +285 -0
- package/core/tera-system/AGENT_DEPENDENCY_MAP.md +117 -0
- package/core/tera-system/AGENT_GENERATION_TEMPLATE.md +312 -0
- package/core/tera-system/AGENT_PERMISSION_MODEL.md +343 -0
- package/core/tera-system/AIS_PROTOCOL.md +191 -0
- package/core/tera-system/TERA_AGENT_CONDUCT.md +102 -0
- package/core/tera-system/TERA_CONTINUOUS_IMPROVEMENT_POLICY.md +111 -0
- package/core/tera-system/TERA_DISTRIBUTION_POLICY.md +291 -0
- package/core/tera-system/TERA_PROJECT_DECISION.md +281 -0
- package/core/tera-system/TERA_USER_GUIDE.md +462 -0
- package/core/tera-system/TOOLING_AND_MCP_POLICY.md +285 -0
- package/core/tera-system/TeraApplicationQuestionBank.md +362 -0
- package/core/tera-system/TeraArchitectureMap.md +91 -0
- package/core/tera-system/TeraClientPolicy.md +366 -0
- package/core/tera-system/TeraHelperAgents.md +970 -0
- package/core/tera-system/TeraPolicyMap.md +131 -0
- package/core/tera-system/TeraPreExecutionGate.md +818 -0
- package/core/tera-system/TeraPreparationDocumentationGovernance.md +370 -0
- package/core/tera-system/TeraPricingPolicy.md +674 -0
- package/core/tera-system/TeraProjectIntakePolicy.md +164 -0
- package/core/tera-system/TeraScenarioStressTests.md +168 -0
- package/core/tera-system/TeraSubAgents.md +854 -0
- package/core/tera-system/TeraSystemMaintenanceChecklist.md +80 -0
- package/core/tera-system/TeraTokenPolicy.md +362 -0
- package/core/tera-system/Tera_Project_Preparation_Files.md +1045 -0
- package/core/tera-system/agent-helpers/application-blueprint-details.md +177 -0
- package/core/tera-system/client-helpers/tera-client-engagement-discovery-domains.md +99 -0
- package/core/tera-system/client-helpers/tera-client-engagement-gates.md +258 -0
- package/core/tera-system/client-helpers/tera-client-engagement-pricing.md +341 -0
- package/core/tera-system/client-helpers/tera-client-engagement-protocols.md +692 -0
- package/core/tera-system/consulting-helpers/BTCA_METHODOLOGY_FRAMEWORK.md +195 -0
- package/core/tera-system/consulting-helpers/BTCA_REPORT_TEMPLATES.md +266 -0
- package/core/tera-system/design-system/ACCESSIBILITY_RULES.md +31 -0
- package/core/tera-system/design-system/COMPONENT_LIBRARY_SCHEMA.md +46 -0
- package/core/tera-system/design-system/DESIGN_MD_INTEGRATION.md +59 -0
- package/core/tera-system/design-system/DESIGN_REVIEW_STANDARDS.md +241 -0
- package/core/tera-system/design-system/DESIGN_SOURCE_PROTOCOL.md +61 -0
- package/core/tera-system/design-system/DESIGN_SYSTEM_OVERVIEW.md +66 -0
- package/core/tera-system/design-system/DESIGN_TOKENS_SCHEMA.md +66 -0
- package/core/tera-system/design-system/EXTERNAL_REFERENCE_ANALYSIS.md +52 -0
- package/core/tera-system/design-system/FIGMA_INTEGRATION.md +138 -0
- package/core/tera-system/design-system/INTERNAL_KITS_INDEX.md +26 -0
- package/core/tera-system/design-system/LAYOUT_PATTERNS.md +52 -0
- package/core/tera-system/design-system/MOBILE_UI_UX_STANDARDS.md +342 -0
- package/core/tera-system/design-system/RTL_LTR_RULES.md +39 -0
- package/core/tera-system/design-system/UI_ACCEPTANCE_GATE.md +80 -0
- package/core/tera-system/design-system/kits/KIT_ADMIN_DASHBOARD.md +102 -0
- package/core/tera-system/engineering-governance/ENGINEERING_AGENT_RESPONSIBILITIES.md +210 -0
- package/core/tera-system/engineering-governance/ENGINEERING_BEST_PRACTICES.md +468 -0
- package/core/tera-system/engineering-governance/ENGINEERING_GOVERNANCE_GATE.md +131 -0
- package/core/tera-system/engineering-governance/ENGINEERING_REVIEW_CHECKLIST.md +129 -0
- package/core/tera-system/engineering-governance/QUALITY_GATE_THRESHOLDS.md +159 -0
- package/core/tera-system/engineering-helpers/engineering-agent-core.md +171 -0
- package/core/tera-system/knowledge-base/OPENHANDS_ARCHITECTURE_REFERENCE.md +243 -0
- package/core/tera-system/knowledge-base/manufacturing/00_INDEX.md +32 -0
- package/core/tera-system/knowledge-base/manufacturing/01_MANUFACTURING_ERP_CORE_CONCEPTS.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/02_SAP_MANUFACTURING_RESEARCH.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/03_DYNAMICS_365_MANUFACTURING_RESEARCH.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/04_ORACLE_MANUFACTURING_RESEARCH.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/05_ODOO_MANUFACTURING_RESEARCH.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/06_ERPNEXT_MANUFACTURING_RESEARCH.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/07_MANUFACTURING_COSTING_GUIDE.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/08_PRODUCTION_DISCOVERY_QUESTIONS.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/09_MANUFACTURING_BLUEPRINT_CHECKLIST.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/10_PRODUCTION_TEST_SCENARIOS.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/11_QUALITY_REWORK_AND_SCRAP_GUIDE.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/12_MRP_AND_PLANNING_GUIDE.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/13_LOCAL_AND_REGIONAL_MANUFACTURING_CONTEXT.md +7 -0
- package/core/tera-system/knowledge-base/manufacturing/14_VENDOR_COMPARISON_MATRIX.md +7 -0
- package/core/tera-system/product-standards/maintenance-apps/BEST_PRACTICES_DOMAIN.md +325 -0
- package/core/tera-system/product-standards/maintenance-apps/STANDARD_DEFINITION.md +142 -0
- package/core/tera-system/profiles/PROFILES_INDEX.md +57 -0
- package/core/tera-system/profiles/TEMPLATE.md +47 -0
- package/core/tera-system/profiles/dotnet-blazor-ef.md +76 -0
- package/core/tera-system/profiles/dotnet-razorpages-adonet.md +137 -0
- package/core/tera-system/profiles/dotnet-wpf-sqlite.md +159 -0
- package/core/tera-system/profiles/effect-bun-opencode.md +109 -0
- package/core/tera-system/profiles/flutter-mobile.md +369 -0
- package/core/tera-system/profiles/nextjs-prisma.md +110 -0
- package/core/tera-system/profiles/phaser-react-node.md +302 -0
- package/core/tera-system/profiles/react-pwa.md +97 -0
- package/core/tera-system/runtime/CLIENT_DISCOVERY_PROTOCOL.md +145 -0
- package/core/tera-system/runtime/DOMAIN_INTELLIGENCE_PROTOCOL.md +124 -0
- package/core/tera-system/runtime/MVP_DEFINITION_PROTOCOL.md +176 -0
- package/core/tera-system/runtime/TERA_RUNTIME_CHECKLISTS.md +646 -0
- package/core/tera-system/runtime/TERA_RUNTIME_PROTOCOLS.md +50 -0
- package/core/tera-system/runtime/TERA_RUNTIME_PROTOCOLS_CLIENT.md +355 -0
- package/core/tera-system/runtime/TERA_RUNTIME_PROTOCOLS_CORE.md +799 -0
- package/core/tera-system/runtime/TERA_RUNTIME_TEMPLATES.md +908 -0
- package/core/tera-system/runtime/TERA_RUNTIME_TEMPLATES_DELIVERY.md +584 -0
- package/core/tera-system/runtime/TERA_RUNTIME_TEMPLATES_PREPARATION.md +376 -0
- package/core/tera-system/runtime/TERA_SOLUTION_PREPARATION_PROTOCOL.md +335 -0
- package/core/tera-system/runtime/TERA_SOLUTION_PREPARATION_TEMPLATES.md +397 -0
- package/core/tera-system/runtime/VERSION_LIFECYCLE_PROTOCOL.md +296 -0
- package/core/tera-system/semgrep-rules/README.md +32 -0
- package/core/tera-system/semgrep-rules/tera-security.yml +66 -0
- package/core/tera-system/semgrep-rules/tera-standards.yml +49 -0
- package/core/tera-system/teranoo-ui/README.md +48 -0
- package/core/tera-system/teranoo-ui/components/button.tsx +51 -0
- package/core/tera-system/teranoo-ui/components/card.tsx +49 -0
- package/core/tera-system/teranoo-ui/components/dashboard-layout.tsx +36 -0
- package/core/tera-system/teranoo-ui/components/data-table.tsx +146 -0
- package/core/tera-system/teranoo-ui/components/empty-state.tsx +31 -0
- package/core/tera-system/teranoo-ui/components/kpi-card.tsx +42 -0
- package/core/tera-system/teranoo-ui/components/page-header.tsx +25 -0
- package/core/tera-system/teranoo-ui/components/search-input.tsx +40 -0
- package/core/tera-system/teranoo-ui/components/sidebar.tsx +66 -0
- package/core/tera-system/teranoo-ui/components/stats-card.tsx +37 -0
- package/core/tera-system/teranoo-ui/registry.json +77 -0
- package/core/tera-system/teranoo-ui/styles/teranoo-theme.css +61 -0
- package/opencode.tera.example.json +30 -0
- package/package.json +37 -0
- package/scripts/build.mjs +110 -0
- package/scripts/install.js +244 -0
- package/scripts/lib/license.mjs +90 -0
- package/scripts/lib/public-key.pem +3 -0
- package/scripts/tera-license.mjs +63 -0
- package/tools/tera-clean.ps1 +97 -0
- package/tools/tera-fetch.ps1 +165 -0
- package/tools/tera-release.ps1 +96 -0
- package/tools/tera-schedule.ps1 +59 -0
- package/tools/tera-update.ps1 +472 -0
- package/tools/tera-watch.ps1 +154 -0
- 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
|
+
}
|