acstack 0.0.0-stage → 0.47.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/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +389 -2
- package/THIRD_PARTY_NOTICES +14 -0
- package/TROUBLESHOOTING.md +225 -0
- package/agents/codex/frontend-verifier.toml +352 -0
- package/agents/codex/task-checker.toml +345 -0
- package/agents/codex/task-executor.toml +157 -0
- package/agents/codex/test-writer.toml +149 -0
- package/agents/frontend-verifier.md +351 -0
- package/agents/task-checker.md +344 -0
- package/agents/task-executor.md +156 -0
- package/agents/test-writer.md +148 -0
- package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
- package/assets/playwriter-bridge/doctor.ps1 +450 -0
- package/assets/playwriter-bridge/launcher-src.cs +131 -0
- package/assets/playwriter-bridge/opener.js +330 -0
- package/assets/playwriter-bridge/opener.ps1 +63 -0
- package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
- package/assets/playwriter-bridge/relay.ps1 +50 -0
- package/assets/playwriter-bridge/stop.ps1 +125 -0
- package/assets/playwriter-bridge/sync.ps1 +271 -0
- package/assets/playwriter-bridge/tunnel.ps1 +325 -0
- package/assets/playwriter-bridge/vm-free-port.sh +166 -0
- package/assets/terminal-workspace/client-keybindings.md +87 -0
- package/assets/terminal-workspace/config.example.yaml +329 -0
- package/assets/terminal-workspace/setup.sh +4317 -0
- package/cli/README.md +47 -0
- package/cli/bin/acstack.js +3 -0
- package/cli/dist/index.js +25984 -0
- package/cli/dist/index.js.map +7 -0
- package/cli/package.json +59 -0
- package/docs/adr/0001-ste-writing-standard.md +16 -0
- package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
- package/docs/adr/0003-handoff-ack-command.md +19 -0
- package/docs/adr/0004-dev-setup-per-host.md +13 -0
- package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
- package/docs/adr/0006-document-opener-service.md +89 -0
- package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
- package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
- package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
- package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
- package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
- package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
- package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
- package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
- package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
- package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
- package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
- package/docs/adr/index.md +17 -0
- package/docs/config-reference.md +572 -0
- package/docs/dev-setups.md +164 -0
- package/docs/migrate-0.8.md +28 -0
- package/docs/playwriter-bridge-windows.md +15 -0
- package/docs/terminal-workspace-keys.md +137 -0
- package/docs/terminal-workspace.md +1774 -0
- package/docs/workflows.md +309 -0
- package/hooks/ac-writing-style-reminder.sh +86 -0
- package/hooks/context-pressure.sh +583 -0
- package/hooks/hooks.json +50 -0
- package/hooks/session-start-handoff.sh +165 -0
- package/hooks/session-start.sh +214 -0
- package/orchestrator/AGENTS.md +532 -0
- package/output-styles/ac-writing-style.md +286 -0
- package/package.json +50 -4
- package/pi/agents/frontend-verifier.md +357 -0
- package/pi/agents/task-checker.md +348 -0
- package/pi/agents/task-executor.md +160 -0
- package/pi/agents/test-writer.md +154 -0
- package/pi/extension.ts +723 -0
- package/profiles/fullstack-python.yaml +29 -0
- package/profiles/node-typescript.yaml +23 -0
- package/profiles/python.yaml +24 -0
- package/skills/archive/SKILL.md +160 -0
- package/skills/bootstrap/SKILL.md +109 -0
- package/skills/bridge/HANDOFF.md +93 -0
- package/skills/bridge/SKILL-VM.md +292 -0
- package/skills/bridge/SKILL-WINDOWS.md +526 -0
- package/skills/bridge/SKILL.md +121 -0
- package/skills/browser-verify/SKILL.md +229 -0
- package/skills/code-review/SKILL.md +44 -0
- package/skills/commit/SKILL.md +106 -0
- package/skills/compact-and-continue/SKILL.md +241 -0
- package/skills/explore/SKILL.md +149 -0
- package/skills/grilling/SKILL.md +134 -0
- package/skills/handoff/SKILL.md +250 -0
- package/skills/handoff/launch-herdr.md +197 -0
- package/skills/handoff/launch-without-herdr.md +30 -0
- package/skills/handoff/lead-check.md +59 -0
- package/skills/handoff/outcomes.md +162 -0
- package/skills/handoff/running-agent.md +50 -0
- package/skills/handoff/watch.md +115 -0
- package/skills/host/SKILL.md +247 -0
- package/skills/host/detect-host.sh +297 -0
- package/skills/host/herdr.md +79 -0
- package/skills/host/install-herdr-block.sh +467 -0
- package/skills/host/install-orca.sh +512 -0
- package/skills/host/notes.md +97 -0
- package/skills/host/orca.md +160 -0
- package/skills/host/questions.md +163 -0
- package/skills/host/report.md +42 -0
- package/skills/implement/SKILL.md +230 -0
- package/skills/implement/decision-log.md +62 -0
- package/skills/implement/design-pass.md +32 -0
- package/skills/implement/relaxed-tdd.md +28 -0
- package/skills/map/SKILL.md +250 -0
- package/skills/map/frontend.md +227 -0
- package/skills/map/trackers/files.md +74 -0
- package/skills/map/trackers/github.md +180 -0
- package/skills/map/visuals.md +387 -0
- package/skills/new-worktree/SKILL.md +155 -0
- package/skills/new-worktree/detect-orchestrator.sh +328 -0
- package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
- package/skills/new-worktree/herdr.md +84 -0
- package/skills/new-worktree/orca.md +101 -0
- package/skills/pre-commit-review/SKILL.md +242 -0
- package/skills/pre-commit-review/auto-fix.md +19 -0
- package/skills/pre-commit-review/interactive.md +35 -0
- package/skills/quick-fix/SKILL.md +125 -0
- package/skills/record-context/SKILL.md +106 -0
- package/skills/setup/SKILL.md +250 -0
- package/skills/setup/browser.md +123 -0
- package/skills/setup/design-review.md +97 -0
- package/skills/setup/first-run.md +114 -0
- package/skills/setup/report.md +68 -0
- package/skills/setup/row-fixes.md +196 -0
- package/skills/setup/terminal-workspace.md +153 -0
- package/skills/sync/SKILL.md +171 -0
- package/skills/teardown-worktree/SKILL.md +245 -0
- package/skills/teardown-worktree/detector.md +39 -0
- package/skills/teardown-worktree/herdr.md +113 -0
- package/skills/teardown-worktree/orca.md +61 -0
- package/skills/terminal-workspace/SKILL.md +249 -0
- package/skills/terminal-workspace/components.md +36 -0
- package/skills/terminal-workspace/config-choices.md +69 -0
- package/skills/terminal-workspace/emulator-keys.md +48 -0
- package/skills/terminal-workspace/headless-clipboard.md +175 -0
- package/skills/terminal-workspace/turn-off-and-update.md +104 -0
- package/skills/test-conventions/SKILL.md +59 -0
- package/skills/visual-docs/SKILL.md +246 -0
- package/skills/writing/SKILL.md +231 -0
- package/skills/writing/examples.md +153 -0
- package/skills/writing/vocabulary.md +96 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# register-tasks.ps1 – Register Playwriter bridge scheduled tasks.
|
|
2
|
+
#
|
|
3
|
+
# Creates three kinds of scheduled tasks, all at user logon, LogonType=Interactive:
|
|
4
|
+
#
|
|
5
|
+
# Playwriter-Relay – runs relay.ps1 (always one)
|
|
6
|
+
# Playwriter-Opener – runs opener.ps1 (always one)
|
|
7
|
+
# Playwriter-Tunnel-VM1 – runs tunnel.ps1 -TargetHost <vm1>
|
|
8
|
+
# Playwriter-Tunnel-VM2 – runs tunnel.ps1 -TargetHost <vm2>
|
|
9
|
+
# ... (one per VM in -VMs, auto-numbered)
|
|
10
|
+
# Playwriter-OpenerTunnel-VM1 – runs tunnel.ps1 -TargetHost <vm1> -Port 19989
|
|
11
|
+
# ... (one per VM, carrying the opener's port)
|
|
12
|
+
#
|
|
13
|
+
# The opener gets its own tunnel task rather than a second -R on the bridge's
|
|
14
|
+
# connection. ssh runs with ExitOnForwardFailure=yes, so one wedged port fails
|
|
15
|
+
# the whole connection – sharing one would mean a stuck opener port takes the
|
|
16
|
+
# bridge down too. See docs/adr/0006-document-opener-service.md.
|
|
17
|
+
#
|
|
18
|
+
# The tasks run as the current user, so the ssh-agent named pipe (SID-ACL'd
|
|
19
|
+
# to the user) stays reachable and no password is ever stored. Each task
|
|
20
|
+
# action invokes launcher.exe, which spawns PowerShell with
|
|
21
|
+
# CREATE_NO_WINDOW – no console is ever allocated, so there is no flash.
|
|
22
|
+
#
|
|
23
|
+
# Run build-launcher.ps1 first to produce launcher.exe; this script refuses
|
|
24
|
+
# to continue without it.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
#
|
|
28
|
+
# .\register-tasks.ps1 -VMs @("vm1.example.com", "vm2.internal")
|
|
29
|
+
# .\register-tasks.ps1 -VMs @("vm1.example.com") -TunnelUser dev
|
|
30
|
+
#
|
|
31
|
+
# A fresh install does not need admin. Re-registering tasks that were
|
|
32
|
+
# previously created from an elevated shell will fail with "Access is
|
|
33
|
+
# denied" – re-run this script from an elevated PowerShell in that case.
|
|
34
|
+
#
|
|
35
|
+
# Scripts are expected to live in %USERPROFILE%\playwriter-bridge\ by default.
|
|
36
|
+
|
|
37
|
+
param(
|
|
38
|
+
[Parameter(Mandatory = $true)]
|
|
39
|
+
[string[]]$VMs,
|
|
40
|
+
|
|
41
|
+
[Parameter(Mandatory = $false)]
|
|
42
|
+
[string]$TunnelUser = $env:USERNAME,
|
|
43
|
+
|
|
44
|
+
[Parameter(Mandatory = $false)]
|
|
45
|
+
[string]$ScriptsDir = (Join-Path $env:USERPROFILE "playwriter-bridge")
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
$ErrorActionPreference = "Stop"
|
|
49
|
+
|
|
50
|
+
# Soft elevation check. Fresh installs register fine without admin. Replacing
|
|
51
|
+
# tasks that were previously registered from an elevated shell does require
|
|
52
|
+
# admin – warn now so the user knows what to do if Register-ScheduledTask
|
|
53
|
+
# later throws "Access is denied".
|
|
54
|
+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
55
|
+
$principalCheck = New-Object Security.Principal.WindowsPrincipal($identity)
|
|
56
|
+
if (-not $principalCheck.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
57
|
+
Write-Warning "Not running elevated. Fresh installs work fine. If the tasks already exist and were registered elevated, Register-ScheduledTask will fail with 'Access is denied' – re-run this from an elevated PowerShell in that case."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Validate that the script files are actually where we expect them.
|
|
61
|
+
foreach ($file in @("relay.ps1", "tunnel.ps1", "opener.ps1", "opener.js")) {
|
|
62
|
+
$full = Join-Path $ScriptsDir $file
|
|
63
|
+
if (-not (Test-Path $full)) {
|
|
64
|
+
Write-Error "Missing required script: $full. Copy relay.ps1, tunnel.ps1, opener.ps1 and opener.js into $ScriptsDir before running this."
|
|
65
|
+
exit 1
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$launcherExe = Join-Path $ScriptsDir "launcher.exe"
|
|
70
|
+
if (-not (Test-Path $launcherExe)) {
|
|
71
|
+
Write-Error "Missing launcher: $launcherExe. Run build-launcher.ps1 from $ScriptsDir first to compile it from launcher-src.cs."
|
|
72
|
+
exit 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$psExe = Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
76
|
+
|
|
77
|
+
# The scripts themselves own resilience (tunnel.ps1 has exponential backoff
|
|
78
|
+
# and fatal-error classification; relay.ps1 loops internally). The scheduler
|
|
79
|
+
# restart is a soft backstop for rare PowerShell-host crashes, not a retry
|
|
80
|
+
# engine. Keep it small so it does not fight manual stops.
|
|
81
|
+
$settings = New-ScheduledTaskSettingsSet `
|
|
82
|
+
-AllowStartIfOnBatteries `
|
|
83
|
+
-DontStopIfGoingOnBatteries `
|
|
84
|
+
-DontStopOnIdleEnd `
|
|
85
|
+
-ExecutionTimeLimit ([TimeSpan]::Zero) `
|
|
86
|
+
-RestartCount 2 `
|
|
87
|
+
-RestartInterval (New-TimeSpan -Minutes 5)
|
|
88
|
+
|
|
89
|
+
$userId = "$env:USERDOMAIN\$env:USERNAME"
|
|
90
|
+
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
|
91
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $userId
|
|
92
|
+
|
|
93
|
+
# Unregister-ScheduledTask removes the task definition but does not take down a
|
|
94
|
+
# running instance, and the scheduler force-terminates launcher.exe without
|
|
95
|
+
# letting its ProcessExit handler run - so the launcher's WMI descendant kill
|
|
96
|
+
# never fires and the child PowerShell, plus the ssh.exe or node.exe it was
|
|
97
|
+
# holding, survives as an orphan still bound to 19988/19989. The task registered
|
|
98
|
+
# next then never binds. Observed on Windows 11 26200 re-running this script
|
|
99
|
+
# over a live bridge: two orphaned tunnel trees competing on 19988, each with
|
|
100
|
+
# its own retry counter, writing interleaved lines into one log.
|
|
101
|
+
#
|
|
102
|
+
# Scoped by the task's own registered argument string so a tunnel task never
|
|
103
|
+
# reaps its sibling on the other port or the other VM. launcher.exe drops the
|
|
104
|
+
# quotes around -File when it builds the child command line, hence the de-quoted
|
|
105
|
+
# comparison on both sides.
|
|
106
|
+
function Stop-BridgeTaskTree {
|
|
107
|
+
param([string]$Name)
|
|
108
|
+
|
|
109
|
+
$task = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue
|
|
110
|
+
$want = if ($task) { ($task.Actions.Arguments -replace '"', '') } else { $null }
|
|
111
|
+
|
|
112
|
+
Stop-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue
|
|
113
|
+
|
|
114
|
+
for ($i = 0; $i -lt 20; $i++) {
|
|
115
|
+
$state = (Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue).State
|
|
116
|
+
if ($state -ne 'Running') { break }
|
|
117
|
+
Start-Sleep -Milliseconds 250
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (-not $want) { return }
|
|
121
|
+
|
|
122
|
+
$survivors = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
|
|
123
|
+
$_.ProcessId -ne $PID -and $_.CommandLine -and ($_.CommandLine -replace '"', '') -eq $want
|
|
124
|
+
})
|
|
125
|
+
foreach ($p in $survivors) {
|
|
126
|
+
# Children first. Killing the supervisor alone re-orphans the ssh.exe or
|
|
127
|
+
# node.exe it was holding, which is the whole bug this guards against.
|
|
128
|
+
Get-CimInstance Win32_Process -Filter "ParentProcessId=$($p.ProcessId)" -ErrorAction SilentlyContinue |
|
|
129
|
+
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
130
|
+
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
131
|
+
Write-Host " reaped surviving $Name process (PID $($p.ProcessId))"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Register-BridgeTask {
|
|
136
|
+
param(
|
|
137
|
+
[string]$Name,
|
|
138
|
+
[string]$ScriptFile,
|
|
139
|
+
[string[]]$ExtraArgs = @()
|
|
140
|
+
)
|
|
141
|
+
$scriptPath = Join-Path $ScriptsDir $ScriptFile
|
|
142
|
+
# launcher.exe spawns args[0] (PowerShell) with CREATE_NO_WINDOW. Its
|
|
143
|
+
# remaining args get joined and passed as PowerShell's Arguments. Any
|
|
144
|
+
# path with spaces must be pre-quoted here.
|
|
145
|
+
$launcherArgs = @(
|
|
146
|
+
"`"$psExe`"",
|
|
147
|
+
"-NoProfile",
|
|
148
|
+
"-ExecutionPolicy", "Bypass",
|
|
149
|
+
"-File", "`"$scriptPath`""
|
|
150
|
+
) + $ExtraArgs
|
|
151
|
+
$argString = $launcherArgs -join ' '
|
|
152
|
+
|
|
153
|
+
$action = New-ScheduledTaskAction -Execute $launcherExe -Argument $argString
|
|
154
|
+
|
|
155
|
+
# Explicit Unregister: do NOT silently swallow "Access is denied".
|
|
156
|
+
# Previous versions hid that error, then Register-ScheduledTask threw
|
|
157
|
+
# a non-terminating CIM "Cannot create a file when that file already
|
|
158
|
+
# exists" and the old task (possibly with stale settings) stayed put
|
|
159
|
+
# while the script falsely reported success.
|
|
160
|
+
$existing = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue
|
|
161
|
+
if ($existing) {
|
|
162
|
+
# Take the running instance down first, children included. Unregistering
|
|
163
|
+
# a running task leaves its process tree alive and holding the port.
|
|
164
|
+
Stop-BridgeTaskTree -Name $Name
|
|
165
|
+
try {
|
|
166
|
+
Unregister-ScheduledTask -TaskName $Name -Confirm:$false -ErrorAction Stop
|
|
167
|
+
} catch {
|
|
168
|
+
Write-Error ("Cannot replace task '{0}': {1}`n" -f $Name, $_.Exception.Message) -ErrorAction Continue
|
|
169
|
+
Write-Error "This task was probably registered from an elevated PowerShell. Re-run this script from an elevated PowerShell (Run as Administrator)." -ErrorAction Continue
|
|
170
|
+
throw "Unregister-ScheduledTask failed for $Name"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
Register-ScheduledTask `
|
|
176
|
+
-TaskName $Name `
|
|
177
|
+
-Action $action `
|
|
178
|
+
-Trigger $trigger `
|
|
179
|
+
-Principal $principal `
|
|
180
|
+
-Settings $settings `
|
|
181
|
+
-ErrorAction Stop | Out-Null
|
|
182
|
+
} catch {
|
|
183
|
+
Write-Error ("Register-ScheduledTask failed for '{0}': {1}" -f $Name, $_.Exception.Message) -ErrorAction Continue
|
|
184
|
+
throw
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Verify the new settings actually landed. Guards against a silent
|
|
188
|
+
# partial failure where the task exists but with old settings. We check
|
|
189
|
+
# every field whose property name is identical on the input
|
|
190
|
+
# ScheduledTaskSettings object and on the fetched task's Settings – the
|
|
191
|
+
# battery/idle switch-params invert to 'Disallow*'/'StopIf*' in the
|
|
192
|
+
# stored representation and aren't safely comparable by name, so they
|
|
193
|
+
# are left out. RestartCount, RestartInterval and ExecutionTimeLimit
|
|
194
|
+
# map 1:1 on both sides and are the fields most likely to silently
|
|
195
|
+
# reject.
|
|
196
|
+
$check = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue
|
|
197
|
+
if (-not $check) {
|
|
198
|
+
throw "Post-register check: task '$Name' is missing."
|
|
199
|
+
}
|
|
200
|
+
$fieldsToCheck = @('RestartCount', 'RestartInterval', 'ExecutionTimeLimit')
|
|
201
|
+
$mismatches = @()
|
|
202
|
+
foreach ($key in $fieldsToCheck) {
|
|
203
|
+
$want = $settings.$key
|
|
204
|
+
$got = $check.Settings.$key
|
|
205
|
+
if ($got -ne $want) {
|
|
206
|
+
$mismatches += ("{0}: got '{1}', expected '{2}'" -f $key, $got, $want)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if ($mismatches.Count -gt 0) {
|
|
210
|
+
throw ("Post-register check: task '{0}' has stale settings ({1}). The register silently applied stale settings – re-run from an elevated PowerShell." -f $Name, ($mismatches -join '; '))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Write-Host ("Registered: {0} ({1} settings fields verified: {2})" -f $Name, $fieldsToCheck.Count, ($fieldsToCheck -join ', '))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Register-BridgeTask -Name "Playwriter-Relay" -ScriptFile "relay.ps1"
|
|
217
|
+
Register-BridgeTask -Name "Playwriter-Opener" -ScriptFile "opener.ps1"
|
|
218
|
+
|
|
219
|
+
for ($i = 0; $i -lt $VMs.Count; $i++) {
|
|
220
|
+
$vm = $VMs[$i]
|
|
221
|
+
Register-BridgeTask `
|
|
222
|
+
-Name "Playwriter-Tunnel-VM$($i + 1)" `
|
|
223
|
+
-ScriptFile "tunnel.ps1" `
|
|
224
|
+
-ExtraArgs @("-TargetHost", $vm, "-User", $TunnelUser)
|
|
225
|
+
Register-BridgeTask `
|
|
226
|
+
-Name "Playwriter-OpenerTunnel-VM$($i + 1)" `
|
|
227
|
+
-ScriptFile "tunnel.ps1" `
|
|
228
|
+
-ExtraArgs @("-TargetHost", $vm, "-User", $TunnelUser, "-Port", "19989")
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# The opener refuses every request that does not carry this, so each VM needs
|
|
232
|
+
# a copy. Generated here if the opener has not already made one; pushed over
|
|
233
|
+
# the same ssh the tunnel uses, so it needs no new access. A VM that cannot be
|
|
234
|
+
# reached is a warning, not a failure: the tunnel tasks are still worth
|
|
235
|
+
# registering, and the token can be copied by hand afterwards.
|
|
236
|
+
$tokenFile = Join-Path $env:LOCALAPPDATA "playwriter-bridge\opener.token"
|
|
237
|
+
if (-not (Test-Path $tokenFile)) {
|
|
238
|
+
New-Item -Path (Split-Path $tokenFile) -ItemType Directory -Force | Out-Null
|
|
239
|
+
$bytes = New-Object byte[] 32
|
|
240
|
+
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
|
|
241
|
+
Set-Content -Path $tokenFile -Value ([System.BitConverter]::ToString($bytes).Replace('-', '').ToLower()) -NoNewline -Encoding ascii
|
|
242
|
+
Write-Host "Generated opener token: $tokenFile"
|
|
243
|
+
}
|
|
244
|
+
$token = (Get-Content $tokenFile -Raw).Trim()
|
|
245
|
+
|
|
246
|
+
Write-Host ""
|
|
247
|
+
Write-Host "Pushing opener token to VMs..."
|
|
248
|
+
# Resolve ssh the same way tunnel.ps1 does. A bare 'ssh' can resolve to the Git
|
|
249
|
+
# for Windows build, which cannot reach the Windows ssh-agent named pipe and so
|
|
250
|
+
# fails with "Permission denied (publickey)" - the push would then be skipped
|
|
251
|
+
# with only a warning and the VM would 401 on every opener request.
|
|
252
|
+
$sshExe = Join-Path $env:WINDIR "System32\OpenSSH\ssh.exe"
|
|
253
|
+
if (-not (Test-Path $sshExe)) {
|
|
254
|
+
$onPath = Get-Command ssh -ErrorAction SilentlyContinue
|
|
255
|
+
if ($onPath) {
|
|
256
|
+
$sshExe = $onPath.Source
|
|
257
|
+
} else {
|
|
258
|
+
# A warning, never a throw. The tasks are already registered by this
|
|
259
|
+
# point and the start loop is still ahead, so throwing here left every
|
|
260
|
+
# task registered and none of them running.
|
|
261
|
+
Write-Warning "ssh.exe not found, so the opener token was not pushed. Enable the Windows OpenSSH Client optional feature, then copy $tokenFile to ~/.config/acstack/opener.token on each VM by hand."
|
|
262
|
+
$sshExe = $null
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
foreach ($vm in $VMs) {
|
|
266
|
+
if (-not $sshExe) { break }
|
|
267
|
+
$remote = "$TunnelUser@$vm"
|
|
268
|
+
# Single-quoted on the remote side so the shell there cannot interpret it,
|
|
269
|
+
# and 0600 because it is a bearer credential for opening pages on this
|
|
270
|
+
# screen.
|
|
271
|
+
$cmd = "mkdir -p ~/.config/acstack && umask 077 && printf '%s' '$token' > ~/.config/acstack/opener.token"
|
|
272
|
+
# $ErrorActionPreference is 'Stop' for this script, and merging a native
|
|
273
|
+
# command's stderr into the pipeline under it turns any stderr line into a
|
|
274
|
+
# terminating NativeCommandError. ssh writes one on a first connection:
|
|
275
|
+
# "Warning: Permanently added ... to the list of known hosts". That aborted
|
|
276
|
+
# the run before the start loop, on nothing but a routine notice. Discard
|
|
277
|
+
# stderr and read the real result from the exit code.
|
|
278
|
+
$prevEap = $ErrorActionPreference
|
|
279
|
+
$ErrorActionPreference = 'Continue'
|
|
280
|
+
& $sshExe -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 $remote $cmd 2>$null | Out-Null
|
|
281
|
+
$sshCode = $LASTEXITCODE
|
|
282
|
+
$ErrorActionPreference = $prevEap
|
|
283
|
+
if ($sshCode -eq 0) {
|
|
284
|
+
Write-Host " $vm : token installed at ~/.config/acstack/opener.token"
|
|
285
|
+
} else {
|
|
286
|
+
Write-Warning " $vm : could not push the token (ssh exit $sshCode). Copy $tokenFile to ~/.config/acstack/opener.token on that VM by hand."
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Tasks whose VM is not in -VMs. A re-run with a shorter list leaves the earlier
|
|
291
|
+
# tunnels registered and untouched: this script removes nothing. The case that
|
|
292
|
+
# matters is a VM behind a VPN that was down at registration time. Its
|
|
293
|
+
# Playwriter-Tunnel-VM2 must survive a re-run that could not reach it, because
|
|
294
|
+
# dropping the task would silently drop that VM from the bridge - and the user
|
|
295
|
+
# would find out the next time they needed a browser on it.
|
|
296
|
+
#
|
|
297
|
+
# Reported here rather than after the start loop, because a warning printed
|
|
298
|
+
# under "Starting tasks..." reads as a task that failed to start.
|
|
299
|
+
$leftover = @()
|
|
300
|
+
foreach ($t in @(Get-ScheduledTask -TaskName 'Playwriter-Tunnel-*','Playwriter-OpenerTunnel-*' -ErrorAction SilentlyContinue)) {
|
|
301
|
+
# The VM is in the action arguments, not the task name: an index-based guess
|
|
302
|
+
# would mislabel every task whenever -VMs is reordered.
|
|
303
|
+
$argText = ($t.Actions | ForEach-Object { $_.Arguments }) -join ' '
|
|
304
|
+
if ($argText -match '-TargetHost\s+(\S+)') {
|
|
305
|
+
$taskTarget = $Matches[1].Trim('"')
|
|
306
|
+
if ($VMs -notcontains $taskTarget) {
|
|
307
|
+
$leftover += [pscustomobject]@{ Name = $t.TaskName; Target = $taskTarget }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if ($leftover.Count -gt 0) {
|
|
312
|
+
Write-Host ""
|
|
313
|
+
foreach ($l in $leftover) {
|
|
314
|
+
Write-Warning ("Left alone: task '{0}' forwards to {1}, which is not in -VMs. It stays registered and keeps running. This script removes nothing. Re-run with {1} in -VMs to update it, or delete the task yourself in Task Scheduler." -f $l.Name, $l.Target)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
Write-Host ""
|
|
319
|
+
Write-Host "Starting tasks..."
|
|
320
|
+
Start-ScheduledTask -TaskName "Playwriter-Relay"
|
|
321
|
+
Start-ScheduledTask -TaskName "Playwriter-Opener"
|
|
322
|
+
Start-Sleep -Seconds 2
|
|
323
|
+
for ($i = 0; $i -lt $VMs.Count; $i++) {
|
|
324
|
+
Start-ScheduledTask -TaskName "Playwriter-Tunnel-VM$($i + 1)"
|
|
325
|
+
Start-ScheduledTask -TaskName "Playwriter-OpenerTunnel-VM$($i + 1)"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
Write-Host "Done. Logs: $env:LOCALAPPDATA\playwriter-bridge\"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# relay.ps1 – Playwriter relay with auto-reconnect.
|
|
2
|
+
#
|
|
3
|
+
# Runs `playwriter serve --host 127.0.0.1 --replace` in a loop. Intended to
|
|
4
|
+
# be invoked through launcher.exe by a scheduled task registered by
|
|
5
|
+
# register-tasks.ps1. Logs to %LOCALAPPDATA%\playwriter-bridge\relay.log.
|
|
6
|
+
#
|
|
7
|
+
# Console hiding is handled by launcher.exe (spawns PowerShell with
|
|
8
|
+
# CREATE_NO_WINDOW). Nothing to do here.
|
|
9
|
+
|
|
10
|
+
$ErrorActionPreference = "Continue"
|
|
11
|
+
|
|
12
|
+
$logDir = Join-Path $env:LOCALAPPDATA "playwriter-bridge"
|
|
13
|
+
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
|
14
|
+
$logFile = Join-Path $logDir "relay.log"
|
|
15
|
+
|
|
16
|
+
function Write-Log {
|
|
17
|
+
param([string]$Message)
|
|
18
|
+
"[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message" | Out-File -Append -FilePath $logFile
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Resolve-PlaywriterCommand {
|
|
22
|
+
# Prefer the npm shim on PATH – it routes through Node correctly without
|
|
23
|
+
# us needing to know where either lives.
|
|
24
|
+
$cmd = Get-Command playwriter -ErrorAction SilentlyContinue
|
|
25
|
+
if ($cmd) { return @{ Exe = $cmd.Source; Args = @() } }
|
|
26
|
+
|
|
27
|
+
# Fallback: invoke Node directly against the installed CLI JS.
|
|
28
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
29
|
+
if (-not $node) {
|
|
30
|
+
throw "Neither 'playwriter' nor 'node' is on PATH. Install Node LTS and 'npm install -g playwriter' first."
|
|
31
|
+
}
|
|
32
|
+
$cliJs = Join-Path $env:APPDATA "npm\node_modules\playwriter\dist\cli.js"
|
|
33
|
+
if (-not (Test-Path $cliJs)) {
|
|
34
|
+
throw "Found Node but not Playwriter CLI at $cliJs. Run 'npm install -g playwriter'."
|
|
35
|
+
}
|
|
36
|
+
return @{ Exe = $node.Source; Args = @($cliJs) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
while ($true) {
|
|
40
|
+
try {
|
|
41
|
+
$resolved = Resolve-PlaywriterCommand
|
|
42
|
+
Write-Log "Starting Playwriter relay ($($resolved.Exe))"
|
|
43
|
+
$allArgs = $resolved.Args + @("serve", "--host", "127.0.0.1", "--replace")
|
|
44
|
+
& $resolved.Exe @allArgs 2>&1 | Out-File -Append -FilePath $logFile
|
|
45
|
+
Write-Log "Relay exited (code $LASTEXITCODE), sleeping 5s"
|
|
46
|
+
} catch {
|
|
47
|
+
Write-Log "Error: $($_.Exception.Message). Sleeping 5s"
|
|
48
|
+
}
|
|
49
|
+
Start-Sleep -Seconds 5
|
|
50
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# stop.ps1 – Canonical "stop the bridge" entry point.
|
|
2
|
+
#
|
|
3
|
+
# Stops every Playwriter-* scheduled task - which covers the document opener
|
|
4
|
+
# and its tunnel too, both registered under the same prefix - kills any
|
|
5
|
+
# lingering bridge
|
|
6
|
+
# processes (relay.ps1, tunnel.ps1), reaps orphan ssh.exe children that
|
|
7
|
+
# were carrying the reverse forward, and confirms 127.0.0.1:19988 is no
|
|
8
|
+
# longer bound. Prints a one-line verdict.
|
|
9
|
+
#
|
|
10
|
+
# Safe to run at any time. No-ops if the bridge is already down.
|
|
11
|
+
#
|
|
12
|
+
# Implementation notes:
|
|
13
|
+
# - The sweep matches on command line. Since this script itself may run
|
|
14
|
+
# from a shell whose command line contains 'playwriter-bridge', we
|
|
15
|
+
# exclude $PID (and its parent) to avoid self-kill.
|
|
16
|
+
# - When the launcher is force-killed, its ProcessExit handler does NOT
|
|
17
|
+
# run, so the ssh.exe child it spawned is orphaned. A dedicated pass
|
|
18
|
+
# looks for ssh.exe processes carrying the 19988 reverse forward and
|
|
19
|
+
# kills them explicitly.
|
|
20
|
+
|
|
21
|
+
$ErrorActionPreference = 'Continue'
|
|
22
|
+
|
|
23
|
+
$selfPid = $PID
|
|
24
|
+
try { $selfParent = (Get-CimInstance Win32_Process -Filter "ProcessId=$selfPid").ParentProcessId } catch { $selfParent = 0 }
|
|
25
|
+
|
|
26
|
+
function Should-Skip {
|
|
27
|
+
param([int]$TargetPid)
|
|
28
|
+
return ($TargetPid -eq $selfPid) -or ($TargetPid -eq $selfParent)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Write-Host "Stopping Playwriter-* scheduled tasks..."
|
|
32
|
+
$tasks = Get-ScheduledTask -TaskName 'Playwriter-*' -ErrorAction SilentlyContinue
|
|
33
|
+
foreach ($t in $tasks) {
|
|
34
|
+
try {
|
|
35
|
+
Stop-ScheduledTask -TaskName $t.TaskName -ErrorAction SilentlyContinue
|
|
36
|
+
Write-Host " stopped: $($t.TaskName)"
|
|
37
|
+
} catch {
|
|
38
|
+
Write-Host " (could not stop $($t.TaskName): $_)"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Pass 1: kill launcher.exe / PowerShell children that were driving the
|
|
43
|
+
# bridge. Excludes this script and its parent so we do not kill ourselves.
|
|
44
|
+
$bridgePattern = 'playwriter-bridge'
|
|
45
|
+
$pass1 = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
46
|
+
Where-Object {
|
|
47
|
+
$_.CommandLine -and
|
|
48
|
+
$_.CommandLine -match $bridgePattern -and
|
|
49
|
+
-not (Should-Skip $_.ProcessId)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
foreach ($p in $pass1) {
|
|
53
|
+
try {
|
|
54
|
+
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
55
|
+
Write-Host " killed: PID $($p.ProcessId) ($($p.Name))"
|
|
56
|
+
} catch { }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Start-Sleep -Milliseconds 500
|
|
60
|
+
|
|
61
|
+
# Pass 2: reap orphan ssh.exe reverse-forward children. Required because
|
|
62
|
+
# Stop-Process -Force on the launcher does not run the launcher's
|
|
63
|
+
# ProcessExit handler, so the ssh.exe it parented survives. Match on the
|
|
64
|
+
# reverse-forward argument pattern (narrow, scoped to the two ports this setup
|
|
65
|
+
# owns - 19988 for the bridge, 19989 for the document opener) so this never
|
|
66
|
+
# touches an unrelated ssh session.
|
|
67
|
+
$sshRemnants = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
68
|
+
Where-Object {
|
|
69
|
+
$_.Name -eq 'ssh.exe' -and
|
|
70
|
+
$_.CommandLine -and
|
|
71
|
+
($_.CommandLine -match '1998[89]:127\.0\.0\.1:1998[89]' -or $_.CommandLine -match '-R\s+"?1998[89]:') -and
|
|
72
|
+
-not (Should-Skip $_.ProcessId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
foreach ($p in $sshRemnants) {
|
|
76
|
+
try {
|
|
77
|
+
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
78
|
+
Write-Host " killed: PID $($p.ProcessId) (ssh.exe, reverse forward)"
|
|
79
|
+
} catch { }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Start-Sleep -Milliseconds 500
|
|
83
|
+
|
|
84
|
+
# Pass 3: reap the orphan node.exe that relay.ps1 spawned (playwriter serve)
|
|
85
|
+
# and still binds 19988, and the one opener.ps1 spawned and still binds 19989.
|
|
86
|
+
# Identify each by whatever owns the port, then kill only if its command line
|
|
87
|
+
# names the thing we expect - so we never touch an unrelated listener that
|
|
88
|
+
# happens to be on either port.
|
|
89
|
+
$owned = @{ 19988 = 'playwriter'; 19989 = 'opener\.js' }
|
|
90
|
+
foreach ($port in $owned.Keys) {
|
|
91
|
+
$listener = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
|
92
|
+
foreach ($conn in $listener) {
|
|
93
|
+
$ownerPid = [int]$conn.OwningProcess
|
|
94
|
+
if ($ownerPid -le 0) { continue }
|
|
95
|
+
if (Should-Skip $ownerPid) { continue }
|
|
96
|
+
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$ownerPid" -ErrorAction SilentlyContinue
|
|
97
|
+
if (-not $proc) { continue }
|
|
98
|
+
if ($proc.CommandLine -and $proc.CommandLine -match $owned[$port]) {
|
|
99
|
+
try {
|
|
100
|
+
Stop-Process -Id $ownerPid -Force -ErrorAction SilentlyContinue
|
|
101
|
+
Write-Host " killed: PID $ownerPid ($($proc.Name), orphan listener on $port)"
|
|
102
|
+
} catch { }
|
|
103
|
+
} else {
|
|
104
|
+
Write-Host " NOT killing PID $ownerPid ($($proc.Name)): command line does not match '$($owned[$port])'" -ForegroundColor Yellow
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Start-Sleep -Milliseconds 500
|
|
110
|
+
|
|
111
|
+
$stillUp = @()
|
|
112
|
+
foreach ($port in $owned.Keys) {
|
|
113
|
+
if (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) {
|
|
114
|
+
$stillUp += $port
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if ($stillUp) {
|
|
118
|
+
Write-Host ""
|
|
119
|
+
Write-Host "VERDICT: PARTIALLY down – something still listens on $($stillUp -join ', ')" -ForegroundColor Yellow
|
|
120
|
+
exit 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Write-Host ""
|
|
124
|
+
Write-Host "VERDICT: bridge and opener are down" -ForegroundColor Green
|
|
125
|
+
exit 0
|