@pi-in-go/pigpen-ahp 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 (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,64 @@
1
+ package svc
2
+
3
+ import (
4
+ "errors"
5
+ "unicode/utf8"
6
+ )
7
+
8
+ // PtyOptions describe the pseudoterminal to open.
9
+ type PtyOptions struct {
10
+ Name string // $TERM
11
+ Cols, Rows int
12
+ Cwd string
13
+ Env []string // extra environment entries; nil inherits the host process environment
14
+ }
15
+
16
+ // PtyHandlers receive the process's output and exit. OnData gets whole UTF-8 text (a multi-byte
17
+ // character is never split across two calls); OnExit is called once, after the last OnData.
18
+ type PtyHandlers struct {
19
+ OnData func(data string)
20
+ OnExit func(exitCode int)
21
+ }
22
+
23
+ // PtyProcess is a running child on a pseudoterminal.
24
+ type PtyProcess interface {
25
+ Write(data string) error
26
+ Resize(cols, rows int) error
27
+ Kill() error
28
+ }
29
+
30
+ // PtySpawner starts file on a new pseudoterminal.
31
+ type PtySpawner func(file string, args []string, opts PtyOptions, h PtyHandlers) (PtyProcess, error)
32
+
33
+ // ErrPtyUnsupported is returned by the default spawner on platforms without pseudoterminals.
34
+ var ErrPtyUnsupported = errors.New("pseudoterminals are not supported on this platform")
35
+
36
+ // utf8Decoder turns a byte stream into strings without splitting a character: an incomplete
37
+ // trailing sequence is held back until the next chunk (or flushed at the end).
38
+ type utf8Decoder struct{ held []byte }
39
+
40
+ func (d *utf8Decoder) Write(chunk []byte) string {
41
+ buf := append(d.held, chunk...)
42
+ d.held = nil
43
+ // hold back an incomplete trailing sequence (at most 3 bytes)
44
+ for back := 1; back <= 3 && back <= len(buf); back++ {
45
+ b := buf[len(buf)-back]
46
+ if b < 0x80 {
47
+ break
48
+ }
49
+ if utf8.RuneStart(b) {
50
+ if !utf8.FullRune(buf[len(buf)-back:]) {
51
+ d.held = append([]byte(nil), buf[len(buf)-back:]...)
52
+ buf = buf[:len(buf)-back]
53
+ }
54
+ break
55
+ }
56
+ }
57
+ return string(buf)
58
+ }
59
+
60
+ func (d *utf8Decoder) Flush() string {
61
+ out := string(d.held)
62
+ d.held = nil
63
+ return out
64
+ }
@@ -0,0 +1,42 @@
1
+ //go:build darwin
2
+
3
+ package svc
4
+
5
+ import (
6
+ "os"
7
+ "syscall"
8
+ "unsafe"
9
+ )
10
+
11
+ const (
12
+ tiocPTYGRANT = 0x20007454
13
+ tiocPTYUNLK = 0x20007452
14
+ tiocPTYGNAME = 0x40807453
15
+ )
16
+
17
+ func openPty() (master, slave *os.File, err error) {
18
+ master, err = os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
19
+ if err != nil {
20
+ return nil, nil, err
21
+ }
22
+ fail := func(e error) (*os.File, *os.File, error) { master.Close(); return nil, nil, e }
23
+ if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), tiocPTYGRANT, 0); e != 0 {
24
+ return fail(e)
25
+ }
26
+ if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), tiocPTYUNLK, 0); e != 0 {
27
+ return fail(e)
28
+ }
29
+ var name [128]byte
30
+ if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), tiocPTYGNAME, uintptr(unsafe.Pointer(&name[0]))); e != 0 {
31
+ return fail(e)
32
+ }
33
+ n := 0
34
+ for n < len(name) && name[n] != 0 {
35
+ n++
36
+ }
37
+ slave, err = os.OpenFile(string(name[:n]), os.O_RDWR|syscall.O_NOCTTY, 0)
38
+ if err != nil {
39
+ return fail(err)
40
+ }
41
+ return master, slave, nil
42
+ }
@@ -0,0 +1,36 @@
1
+ //go:build linux
2
+
3
+ package svc
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "syscall"
9
+ "unsafe"
10
+ )
11
+
12
+ const (
13
+ tiocGPTN = 0x80045430
14
+ tiocSPTLCK = 0x40045431
15
+ )
16
+
17
+ func openPty() (master, slave *os.File, err error) {
18
+ master, err = os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
19
+ if err != nil {
20
+ return nil, nil, err
21
+ }
22
+ fail := func(e error) (*os.File, *os.File, error) { master.Close(); return nil, nil, e }
23
+ var unlock int32
24
+ if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), tiocSPTLCK, uintptr(unsafe.Pointer(&unlock))); e != 0 {
25
+ return fail(e)
26
+ }
27
+ var n uint32
28
+ if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, master.Fd(), tiocGPTN, uintptr(unsafe.Pointer(&n))); e != 0 {
29
+ return fail(e)
30
+ }
31
+ slave, err = os.OpenFile(fmt.Sprintf("/dev/pts/%d", n), os.O_RDWR|syscall.O_NOCTTY, 0)
32
+ if err != nil {
33
+ return fail(err)
34
+ }
35
+ return master, slave, nil
36
+ }
@@ -0,0 +1,9 @@
1
+ //go:build !linux && !darwin
2
+
3
+ package svc
4
+
5
+ // SpawnPty is unavailable here: the standard library has no ConPTY or BSD pty support, so the
6
+ // terminal service refuses to start (a documented gap, see PORT.md).
7
+ func SpawnPty(string, []string, PtyOptions, PtyHandlers) (PtyProcess, error) {
8
+ return nil, ErrPtyUnsupported
9
+ }
@@ -0,0 +1,114 @@
1
+ //go:build linux || darwin
2
+
3
+ package svc
4
+
5
+ import (
6
+ "strings"
7
+ "sync"
8
+ "testing"
9
+ "time"
10
+
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
12
+ )
13
+
14
+ // Twins of upstream test/pty.test.ts: a real shell on a real pseudoterminal.
15
+
16
+ type ptyRun struct {
17
+ mu sync.Mutex
18
+ output strings.Builder
19
+ exited chan int
20
+ onData func(all string)
21
+ }
22
+
23
+ func spawnRun(t *testing.T, file string, args []string, onData func(*ptyRun, string)) (*ptyRun, PtyProcess) {
24
+ t.Helper()
25
+ run := &ptyRun{exited: make(chan int, 1)}
26
+ pty, err := SpawnPty(file, args, PtyOptions{Name: "xterm-256color", Cols: 80, Rows: 24, Cwd: t.TempDir()}, PtyHandlers{
27
+ OnData: func(data string) {
28
+ run.mu.Lock()
29
+ run.output.WriteString(data)
30
+ all := run.output.String()
31
+ run.mu.Unlock()
32
+ if onData != nil {
33
+ onData(run, all)
34
+ }
35
+ },
36
+ OnExit: func(code int) { run.exited <- code },
37
+ })
38
+ if err != nil {
39
+ t.Fatalf("spawn: %v", err)
40
+ }
41
+ t.Cleanup(func() { _ = pty.Kill() })
42
+ return run, pty
43
+ }
44
+
45
+ func (r *ptyRun) Output() string { r.mu.Lock(); defer r.mu.Unlock(); return r.output.String() }
46
+
47
+ func (r *ptyRun) wait(t *testing.T) int {
48
+ t.Helper()
49
+ select {
50
+ case code := <-r.exited:
51
+ return code
52
+ case <-time.After(5 * time.Second):
53
+ t.Fatalf("PTY did not exit; output: %q", r.Output())
54
+ return -1
55
+ }
56
+ }
57
+
58
+ func TestPty(t *testing.T) {
59
+ twin.Run(t, "pty", "round-trips input through a resized TTY", func(t *testing.T) {
60
+ marker := "pi-ahp-" + newUUID()
61
+ script := `test -t 0 && test -t 1 && test -t 2 || exit 70; test "$TERM" = xterm-256color || exit 71; IFS= read -r line; set -- $(stty size); printf "PTY:%s:%sx%s\n" "$line" "$1" "$2"`
62
+ run, pty := spawnRun(t, "sh", []string{"-c", script}, nil)
63
+ if err := pty.Resize(97, 31); err != nil {
64
+ t.Fatal(err)
65
+ }
66
+ if err := pty.Write(marker + "\r"); err != nil {
67
+ t.Fatal(err)
68
+ }
69
+ if code := run.wait(t); code != 0 {
70
+ t.Fatalf("PTY setup failed (exit %d); output: %q", code, run.Output())
71
+ }
72
+ expected := "PTY:" + marker + ":31x97"
73
+ if !strings.Contains(run.Output(), expected) {
74
+ t.Fatalf("missing %s in %q", expected, run.Output())
75
+ }
76
+ })
77
+
78
+ twin.Run(t, "pty", "delivers Ctrl-C to the foreground job without killing the shell", func(t *testing.T) {
79
+ marker := strings.ReplaceAll(newUUID(), "-", "")
80
+ childScript := `trap 'printf "INT:%s\n" "$MARKER"; exit 42' INT; printf "READY:%s\n" "$MARKER"; while :; do sleep 1; done`
81
+ stage := 0
82
+ var pty PtyProcess
83
+ var mu sync.Mutex
84
+ run, p := spawnRun(t, "sh", nil, func(_ *ptyRun, all string) {
85
+ mu.Lock()
86
+ defer mu.Unlock()
87
+ if stage == 0 && strings.Contains(all, "READY:"+marker) {
88
+ stage = 1
89
+ _ = pty.Write("\x03")
90
+ }
91
+ if stage == 1 && strings.Contains(all, "INT:"+marker) {
92
+ stage = 2
93
+ _ = pty.Write("printf 'AFTER:%s\\n' '" + marker + "'; exit\r")
94
+ }
95
+ })
96
+ mu.Lock()
97
+ pty = p
98
+ mu.Unlock()
99
+ // Expected markers are assembled by the child, so terminal echo cannot satisfy them.
100
+ quoted := "'" + strings.ReplaceAll(childScript, "'", `'\''`) + "'"
101
+ if err := p.Write("MARKER='" + marker + "' sh -c " + quoted + "\r"); err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ code := run.wait(t)
105
+ mu.Lock()
106
+ defer mu.Unlock()
107
+ if code != 0 {
108
+ t.Fatalf("interactive shell died (exit %d): %q", code, run.Output())
109
+ }
110
+ if stage != 2 || !strings.Contains(run.Output(), "AFTER:"+marker) {
111
+ t.Fatalf("Ctrl-C flow stopped at stage %d: %q", stage, run.Output())
112
+ }
113
+ })
114
+ }
@@ -0,0 +1,147 @@
1
+ //go:build linux || darwin
2
+
3
+ package svc
4
+
5
+ import (
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "os"
10
+ "os/exec"
11
+ "sync"
12
+ "syscall"
13
+ "time"
14
+ "unsafe"
15
+ )
16
+
17
+ // SpawnPty starts file on a new pseudoterminal using only the standard library: it opens the
18
+ // master, unlocks and opens the slave, makes it the child's controlling terminal (new session) and
19
+ // reads the master until the child side closes.
20
+ func SpawnPty(file string, args []string, opts PtyOptions, h PtyHandlers) (PtyProcess, error) {
21
+ master, slave, err := openPty()
22
+ if err != nil {
23
+ return nil, fmt.Errorf("open pty: %w", err)
24
+ }
25
+ p := &unixPty{master: master}
26
+ if err := p.Resize(opts.Cols, opts.Rows); err != nil {
27
+ master.Close()
28
+ slave.Close()
29
+ return nil, err
30
+ }
31
+ cmd := exec.Command(file, args...)
32
+ cmd.Dir = opts.Cwd
33
+ env := opts.Env
34
+ if env == nil {
35
+ env = os.Environ()
36
+ }
37
+ name := opts.Name
38
+ if name == "" {
39
+ name = "xterm-256color"
40
+ }
41
+ cmd.Env = append(append([]string(nil), env...), "TERM="+name)
42
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = slave, slave, slave
43
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0}
44
+ if err := cmd.Start(); err != nil {
45
+ master.Close()
46
+ slave.Close()
47
+ return nil, err
48
+ }
49
+ slave.Close() // the child owns the only slave end now
50
+ p.cmd = cmd
51
+ p.exited = make(chan struct{})
52
+
53
+ readDone := make(chan struct{})
54
+ go func() {
55
+ defer close(readDone)
56
+ var dec utf8Decoder
57
+ buf := make([]byte, 32*1024)
58
+ for {
59
+ n, err := master.Read(buf)
60
+ if n > 0 && h.OnData != nil {
61
+ if text := dec.Write(buf[:n]); text != "" {
62
+ h.OnData(text)
63
+ }
64
+ }
65
+ if err != nil {
66
+ // EIO is how a Linux master reports that every slave end closed.
67
+ if !errors.Is(err, io.EOF) && !errors.Is(err, syscall.EIO) && !errors.Is(err, os.ErrClosed) {
68
+ _ = err
69
+ }
70
+ if tail := dec.Flush(); tail != "" && h.OnData != nil {
71
+ h.OnData(tail)
72
+ }
73
+ return
74
+ }
75
+ }
76
+ }()
77
+ go func() {
78
+ waitErr := cmd.Wait()
79
+ code := 0
80
+ if state := cmd.ProcessState; state != nil {
81
+ code = state.ExitCode()
82
+ if ws, ok := state.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
83
+ code = 128 + int(ws.Signal())
84
+ }
85
+ } else if waitErr != nil {
86
+ code = 1
87
+ }
88
+ // Let the reader drain what the child wrote before it died; a grandchild that kept the slave
89
+ // open must not hold the exit back forever.
90
+ select {
91
+ case <-readDone:
92
+ case <-time.After(250 * time.Millisecond):
93
+ }
94
+ master.Close()
95
+ <-readDone
96
+ close(p.exited)
97
+ if h.OnExit != nil {
98
+ h.OnExit(code)
99
+ }
100
+ }()
101
+ return p, nil
102
+ }
103
+
104
+ type unixPty struct {
105
+ master *os.File
106
+ cmd *exec.Cmd
107
+ exited chan struct{}
108
+
109
+ killOnce sync.Once
110
+ }
111
+
112
+ func (p *unixPty) Write(data string) error {
113
+ _, err := p.master.WriteString(data)
114
+ return err
115
+ }
116
+
117
+ type winsize struct{ Row, Col, X, Y uint16 }
118
+
119
+ func (p *unixPty) Resize(cols, rows int) error {
120
+ ws := winsize{Row: uint16(rows), Col: uint16(cols)}
121
+ if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, p.master.Fd(), uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(&ws))); e != 0 {
122
+ return e
123
+ }
124
+ return nil
125
+ }
126
+
127
+ // Kill hangs up the session's process group and escalates to SIGKILL if it lingers.
128
+ func (p *unixPty) Kill() error {
129
+ var err error
130
+ p.killOnce.Do(func() {
131
+ if p.cmd == nil || p.cmd.Process == nil {
132
+ return
133
+ }
134
+ pid := p.cmd.Process.Pid
135
+ if e := syscall.Kill(-pid, syscall.SIGHUP); e != nil && !errors.Is(e, syscall.ESRCH) {
136
+ err = e
137
+ }
138
+ go func() {
139
+ select {
140
+ case <-p.exited:
141
+ case <-time.After(2 * time.Second):
142
+ _ = syscall.Kill(-pid, syscall.SIGKILL)
143
+ }
144
+ }()
145
+ })
146
+ return err
147
+ }