@jals1212/ci-local-pi 0.3.1 → 0.4.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/CHANGELOG.md +10 -0
- package/README.md +20 -0
- package/extensions/ci-local.ts +22 -0
- package/extensions/core.js +151 -0
- package/package.json +1 -1
- package/skills/ci-local/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ add or change behaviour and a patch version fixes.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Herdr sidebar: in a Herdr pane, the status line is also the pane's `$ci`
|
|
14
|
+
metadata token while a run is in flight and for 5 minutes after it ends, so
|
|
15
|
+
a `["$ci"]` row in Herdr's Agents sidebar shows ci-local next to the
|
|
16
|
+
agent. Display only, refreshed with a 30-second TTL, and cleared when the
|
|
17
|
+
session ends. (#54)
|
|
18
|
+
|
|
9
19
|
## 0.3.1
|
|
10
20
|
|
|
11
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -35,6 +35,26 @@ runs ci-local or Docker itself:
|
|
|
35
35
|
shows it where it lists extension statuses: Gentle Shell, for one, under
|
|
36
36
|
**Integrations** in the sidebar's Status card (or in its bottom bar on a
|
|
37
37
|
narrow terminal).
|
|
38
|
+
- **Herdr sidebar**: in a [Herdr](https://herdr.dev) pane (`HERDR_ENV=1`,
|
|
39
|
+
`HERDR_PANE_ID`, `HERDR_SOCKET_PATH`), the status line is also the pane's
|
|
40
|
+
`ci` metadata token while a run is queued or running, and its result for 5
|
|
41
|
+
minutes after it ends. The token is display only: the agent's state in
|
|
42
|
+
Herdr does not change. It is reported again every 10 seconds with a
|
|
43
|
+
30-second TTL, so it goes away when Pi does. To show it, add a `$ci` row to
|
|
44
|
+
the Agents sidebar in Herdr's `config.toml`, then run `herdr server reload-config`:
|
|
45
|
+
|
|
46
|
+
```toml
|
|
47
|
+
[ui.sidebar.agents]
|
|
48
|
+
rows = [
|
|
49
|
+
["state_icon", "machine", "workspace", "tab"],
|
|
50
|
+
["agent"],
|
|
51
|
+
[{ token = "$ci", rules = [
|
|
52
|
+
{ contains = "✗", fg = "#f38ba8" },
|
|
53
|
+
{ contains = "✓", fg = "#a6e3a1" },
|
|
54
|
+
{ contains = "⧗", fg = "#f9e2af" },
|
|
55
|
+
] }],
|
|
56
|
+
]
|
|
57
|
+
```
|
|
38
58
|
- **Only the session's own runs** get the cards and the notification, and
|
|
39
59
|
reach the agent: runs this session started. A run started from Pi's bash
|
|
40
60
|
tool names its session (ci-local records Pi's `PI_SESSION_ID` as `session`
|
package/extensions/ci-local.ts
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
// this session's process (a background job, even orphaned). Another
|
|
14
14
|
// session's run in the same repository, or one started in a terminal, shows
|
|
15
15
|
// in the status line alone.
|
|
16
|
+
// - Herdr: in a pane Herdr runs, the status line is also the pane's `$ci`
|
|
17
|
+
// metadata token while a run is in flight and for 5 minutes after it ends,
|
|
18
|
+
// so a `["$ci"]` row in Herdr's Agents sidebar shows it (display only).
|
|
16
19
|
// - When a run starts: a card in the transcript when it is starting (or
|
|
17
20
|
// waiting for its turn), another when its steps start; for you only (session
|
|
18
21
|
// entries, never in the model's context, never repeated after a reload).
|
|
@@ -42,6 +45,8 @@ import {
|
|
|
42
45
|
LAUNCH_WAIT_MS,
|
|
43
46
|
WAKE_TEXT,
|
|
44
47
|
cardContent,
|
|
48
|
+
createHerdrLine,
|
|
49
|
+
createHerdrPublisher,
|
|
45
50
|
createDelivery,
|
|
46
51
|
createStatusLine,
|
|
47
52
|
createTracker,
|
|
@@ -49,6 +54,8 @@ import {
|
|
|
49
54
|
eventsFileFor,
|
|
50
55
|
exitCard,
|
|
51
56
|
findProject,
|
|
57
|
+
herdrPane,
|
|
58
|
+
herdrSend,
|
|
52
59
|
launchReport,
|
|
53
60
|
launchRun,
|
|
54
61
|
ownsRun,
|
|
@@ -98,6 +105,10 @@ function ciLocal(pi: ExtensionAPI, { launchWaitMs }: { launchWaitMs: number }) {
|
|
|
98
105
|
// belongs to the session that started it; another session drops it.
|
|
99
106
|
type Launch = ReturnType<typeof launchRun> & { seen: boolean; waiting: boolean; sessionId: string }
|
|
100
107
|
const launches = new Map<number, Launch>()
|
|
108
|
+
// In a Herdr pane, the footer line is also the pane's `$ci` sidebar token
|
|
109
|
+
// (see createHerdrLine in core.js); null elsewhere. Read when a session
|
|
110
|
+
// starts following, so a test or a moved pane gets its own.
|
|
111
|
+
let herdr: { pane: string; socket: string; publisher: ReturnType<typeof createHerdrPublisher> } | undefined
|
|
101
112
|
|
|
102
113
|
pi.on('agent_start', async () => {
|
|
103
114
|
runActive = true
|
|
@@ -249,6 +260,11 @@ function ciLocal(pi: ExtensionAPI, { launchWaitMs }: { launchWaitMs: number }) {
|
|
|
249
260
|
// already showed, and records each finished run it shows (see
|
|
250
261
|
// createStatusLine in core.js).
|
|
251
262
|
const statusLine = createStatusLine({ file, sessionId, reason })
|
|
263
|
+
const pane = herdrPane(process.env)
|
|
264
|
+
if (pane && (herdr?.pane !== pane.pane || herdr.socket !== pane.socket)) {
|
|
265
|
+
herdr = { ...pane, publisher: createHerdrPublisher({ send: herdrSend(pane) }) }
|
|
266
|
+
}
|
|
267
|
+
const herdrLine = pane ? createHerdrLine() : undefined
|
|
252
268
|
|
|
253
269
|
const tick = () => {
|
|
254
270
|
let run
|
|
@@ -259,6 +275,7 @@ function ciLocal(pi: ExtensionAPI, { launchWaitMs }: { launchWaitMs: number }) {
|
|
|
259
275
|
}
|
|
260
276
|
const { footer, changed } = statusLine.update(run)
|
|
261
277
|
if (changed) ctx.ui.setStatus(KEY, footer)
|
|
278
|
+
if (herdrLine) herdr?.publisher.update(herdrLine.text(run))
|
|
262
279
|
settleLaunches(run)
|
|
263
280
|
// Only this session's own runs get cards and reach the agent, as Gentle
|
|
264
281
|
// AI's job notices go only to the session that started the job. Another
|
|
@@ -336,6 +353,11 @@ function ciLocal(pi: ExtensionAPI, { launchWaitMs }: { launchWaitMs: number }) {
|
|
|
336
353
|
|
|
337
354
|
pi.on('session_shutdown', async () => {
|
|
338
355
|
stop()
|
|
356
|
+
// The sidebar token goes with the session (its TTL would end it anyway).
|
|
357
|
+
if (herdr) {
|
|
358
|
+
herdr.publisher.update(null)
|
|
359
|
+
await herdr.publisher.idle()
|
|
360
|
+
}
|
|
339
361
|
startReason = undefined
|
|
340
362
|
delete (globalThis as Record<symbol, unknown>)[FLAG]
|
|
341
363
|
})
|
package/extensions/core.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
statSync,
|
|
20
20
|
writeFileSync,
|
|
21
21
|
} from 'node:fs'
|
|
22
|
+
import { createConnection } from 'node:net'
|
|
22
23
|
import { tmpdir } from 'node:os'
|
|
23
24
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
24
25
|
|
|
@@ -663,3 +664,153 @@ export function exitCard({ pid, code, signal = null, output, tail = outputTail(o
|
|
|
663
664
|
},
|
|
664
665
|
}
|
|
665
666
|
}
|
|
667
|
+
|
|
668
|
+
// Herdr (https://herdr.dev): in a pane Herdr runs, the footer line is also the
|
|
669
|
+
// pane's `$ci` metadata token, so a row `["$ci"]` in Herdr's Agents sidebar
|
|
670
|
+
// shows the run. A token is display only: it never touches the agent's state.
|
|
671
|
+
|
|
672
|
+
/** The token's name, as a sidebar row names it: `$ci`. */
|
|
673
|
+
export const HERDR_TOKEN = 'ci'
|
|
674
|
+
/** Who reports the token. No sequence numbers: one sender, one report at a time. */
|
|
675
|
+
export const HERDR_SOURCE = 'ci-local'
|
|
676
|
+
/** Herdr drops the token this long after the last report: a Pi gone leaves nothing behind. */
|
|
677
|
+
export const HERDR_TTL_MS = 30_000
|
|
678
|
+
/** A token unchanged is reported again this often, well inside its TTL. */
|
|
679
|
+
export const HERDR_REFRESH_MS = 10_000
|
|
680
|
+
/** How long a run's result stays in the sidebar after the run ends. */
|
|
681
|
+
export const HERDR_RESULT_MS = 5 * 60_000
|
|
682
|
+
/** Herdr keeps at most this many characters of a token. */
|
|
683
|
+
export const HERDR_TOKEN_MAX = 80
|
|
684
|
+
|
|
685
|
+
/** The pane and socket of the Herdr pane this process runs in, or null outside one. */
|
|
686
|
+
export function herdrPane(env = process.env) {
|
|
687
|
+
const pane = env.HERDR_PANE_ID?.trim()
|
|
688
|
+
const socket = env.HERDR_SOCKET_PATH?.trim()
|
|
689
|
+
return env.HERDR_ENV === '1' && pane && socket ? { pane, socket } : null
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function clip(text) {
|
|
693
|
+
const chars = [...text]
|
|
694
|
+
return chars.length <= HERDR_TOKEN_MAX ? text : chars.slice(0, HERDR_TOKEN_MAX - 1).join('') + '…'
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* What the token says for a run: the footer line while it is in flight, and
|
|
699
|
+
* its result for HERDR_RESULT_MS after it ended (by its end event; a run that
|
|
700
|
+
* stopped without one, from when this session saw it stop). Null otherwise:
|
|
701
|
+
* no run, or a result older than that, or a run found already stopped.
|
|
702
|
+
*/
|
|
703
|
+
export function createHerdrLine({ now = Date.now } = {}) {
|
|
704
|
+
let flying // the key of the last run seen in flight
|
|
705
|
+
const endedAt = new Map()
|
|
706
|
+
return {
|
|
707
|
+
text(run) {
|
|
708
|
+
if (!run) return null
|
|
709
|
+
if (!FINISHED.has(run.state.state)) {
|
|
710
|
+
flying = run.key
|
|
711
|
+
return clip(footerText(run.state))
|
|
712
|
+
}
|
|
713
|
+
if (!endedAt.has(run.key)) {
|
|
714
|
+
const ended = Date.parse(run.state.end?.ts ?? '')
|
|
715
|
+
endedAt.clear()
|
|
716
|
+
endedAt.set(run.key, Number.isFinite(ended) ? ended : run.key === flying ? now() : null)
|
|
717
|
+
}
|
|
718
|
+
const ended = endedAt.get(run.key)
|
|
719
|
+
return ended !== null && now() - ended < HERDR_RESULT_MS ? clip(footerText(run.state)) : null
|
|
720
|
+
},
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** The socket request that sets the token (with its TTL), or clears it (null). */
|
|
725
|
+
export function herdrReport(pane, value) {
|
|
726
|
+
const params = { pane_id: pane, source: HERDR_SOURCE, tokens: { [HERDR_TOKEN]: value } }
|
|
727
|
+
if (value !== null) params.ttl_ms = HERDR_TTL_MS
|
|
728
|
+
return { method: 'pane.report_metadata', params }
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let herdrRequests = 0
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* A sender of the token to Herdr's socket (one JSON line, one reply): resolves
|
|
735
|
+
* true when Herdr accepted it, false on any error or after `timeoutMs`.
|
|
736
|
+
*/
|
|
737
|
+
export function herdrSend({ pane, socket }, { timeoutMs = 2000 } = {}) {
|
|
738
|
+
return (value) =>
|
|
739
|
+
new Promise((resolve) => {
|
|
740
|
+
const id = `ci-local-${process.pid}-${++herdrRequests}`
|
|
741
|
+
let reply = ''
|
|
742
|
+
let done = false
|
|
743
|
+
const finish = (ok) => {
|
|
744
|
+
if (done) return
|
|
745
|
+
done = true
|
|
746
|
+
clearTimeout(timer)
|
|
747
|
+
conn.destroy()
|
|
748
|
+
resolve(ok)
|
|
749
|
+
}
|
|
750
|
+
const conn = createConnection(socket)
|
|
751
|
+
const timer = setTimeout(() => finish(false), timeoutMs)
|
|
752
|
+
timer.unref?.()
|
|
753
|
+
conn.on('connect', () => conn.write(JSON.stringify({ id, ...herdrReport(pane, value) }) + '\n'))
|
|
754
|
+
conn.on('data', (data) => {
|
|
755
|
+
reply += data
|
|
756
|
+
const end = reply.indexOf('\n')
|
|
757
|
+
if (end < 0) return
|
|
758
|
+
try {
|
|
759
|
+
const answer = JSON.parse(reply.slice(0, end))
|
|
760
|
+
finish(answer.id === id && answer.result?.type === 'ok')
|
|
761
|
+
} catch {
|
|
762
|
+
finish(false)
|
|
763
|
+
}
|
|
764
|
+
})
|
|
765
|
+
conn.on('error', () => finish(false))
|
|
766
|
+
conn.on('close', () => finish(false))
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Keeps Herdr's token in step with `update(value)` (null clears it): a report
|
|
772
|
+
* when the value changes, again every HERDR_REFRESH_MS while it stays, one
|
|
773
|
+
* report at a time (the latest value waits for the one on its way). A failed
|
|
774
|
+
* report is tried again at the next update. `idle()` resolves once nothing is
|
|
775
|
+
* on its way.
|
|
776
|
+
*/
|
|
777
|
+
export function createHerdrPublisher({ send, now = Date.now, refreshMs = HERDR_REFRESH_MS }) {
|
|
778
|
+
let shown = null // what Herdr has
|
|
779
|
+
let sentAt = 0
|
|
780
|
+
let inFlight = null
|
|
781
|
+
let pending // the value to report once inFlight is done
|
|
782
|
+
const update = (value) => {
|
|
783
|
+
if (inFlight) {
|
|
784
|
+
pending = value
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
const due = value !== shown || (value !== null && now() - sentAt >= refreshMs)
|
|
788
|
+
if (!due) return
|
|
789
|
+
let sending
|
|
790
|
+
try {
|
|
791
|
+
sending = Promise.resolve(send(value))
|
|
792
|
+
} catch {
|
|
793
|
+
sending = Promise.resolve(false)
|
|
794
|
+
}
|
|
795
|
+
inFlight = sending
|
|
796
|
+
.catch(() => false)
|
|
797
|
+
.then((ok) => {
|
|
798
|
+
if (ok) {
|
|
799
|
+
shown = value
|
|
800
|
+
sentAt = now()
|
|
801
|
+
}
|
|
802
|
+
inFlight = null
|
|
803
|
+
if (pending !== undefined) {
|
|
804
|
+
const next = pending
|
|
805
|
+
pending = undefined
|
|
806
|
+
update(next)
|
|
807
|
+
}
|
|
808
|
+
})
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
update,
|
|
812
|
+
async idle() {
|
|
813
|
+
while (inFlight) await inFlight
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jals1212/ci-local-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi extension and skill for @jals1212/ci-local: the state of this repository's ci-local run in Pi's status line, cards and a read-only tool for the agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "jals1212",
|