team 0.1.0 → 0.1.2
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/README.md +20 -5
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +75 -8
- package/dist/approve/fingerprint.d.ts +15 -4
- package/dist/approve/fingerprint.js +37 -12
- package/dist/budgets/gate.d.ts +12 -7
- package/dist/budgets/gate.js +52 -17
- package/dist/budgets/readings.d.ts +71 -5
- package/dist/budgets/readings.js +136 -9
- package/dist/budgets/table.d.ts +2 -2
- package/dist/budgets/table.js +24 -11
- package/dist/commands/add.js +12 -5
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/down.d.ts +2 -2
- package/dist/commands/down.js +8 -1
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +21 -2
- package/dist/commands/status.js +3 -2
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -7
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +94 -16
- package/dist/file/types.d.ts +5 -0
- package/dist/file/validate.js +31 -11
- package/dist/herdr.d.ts +4 -0
- package/dist/herdr.js +40 -11
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +12 -0
- package/dist/launch/deliver.d.ts +3 -1
- package/dist/launch/deliver.js +10 -1
- package/dist/launch/execute.d.ts +2 -2
- package/dist/launch/execute.js +15 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +13 -0
- package/dist/profiles/claude-code.yaml +30 -0
- package/dist/profiles/cursor.yaml +4 -1
- package/dist/profiles/quota.d.ts +7 -2
- package/dist/profiles/quota.js +15 -14
- package/dist/state.d.ts +8 -3
- package/dist/state.js +37 -0
- package/dist/status/statusline.js +3 -1
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +4 -1
- package/dist/watch/pass.d.ts +14 -1
- package/dist/watch/pass.js +64 -28
- package/dist/watch/screen-core.d.ts +7 -1
- package/dist/watch/screen-core.js +146 -54
- package/dist/watch/screen-data.d.ts +9 -0
- package/dist/watch/screen-file.js +25 -7
- package/dist/watch/screen.d.ts +5 -0
- package/dist/watch/screen.js +16 -2
- package/examples/team.yaml +8 -0
- package/package.json +2 -2
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { QuotaFigure, WindowName } from '../profiles/quota.ts';
|
|
2
|
+
import type { CheckWindow } from './run.ts';
|
|
3
|
+
/** Where a reading came from: a pane's own status line, or an approved check command (§ 3). */
|
|
4
|
+
export type ReadingSource = 'status_line' | 'check';
|
|
2
5
|
export type Seen = {
|
|
3
6
|
account: string;
|
|
4
7
|
window: WindowName;
|
|
@@ -6,7 +9,9 @@ export type Seen = {
|
|
|
6
9
|
used: number;
|
|
7
10
|
changedAt: number;
|
|
8
11
|
resetsAt: number | null;
|
|
9
|
-
seat
|
|
12
|
+
/** The seat whose screen showed it; null for a check reading, which belongs to no seat. */
|
|
13
|
+
seat: string | null;
|
|
14
|
+
source: ReadingSource;
|
|
10
15
|
confirmed: boolean;
|
|
11
16
|
};
|
|
12
17
|
export type StoredReading = {
|
|
@@ -16,7 +21,9 @@ export type StoredReading = {
|
|
|
16
21
|
used: number;
|
|
17
22
|
changedAt: string;
|
|
18
23
|
resetsAt: string | null;
|
|
19
|
-
seat: string;
|
|
24
|
+
seat: string | null;
|
|
25
|
+
/** Absent in state files written before sources were recorded: a screen reading then. */
|
|
26
|
+
source?: ReadingSource;
|
|
20
27
|
confirmed: boolean;
|
|
21
28
|
};
|
|
22
29
|
export type Verdict = {
|
|
@@ -25,8 +32,31 @@ export type Verdict = {
|
|
|
25
32
|
} | {
|
|
26
33
|
kind: 'unknown';
|
|
27
34
|
};
|
|
35
|
+
/** A spend check reading (RFC 0003 § 5): the money the account still holds. */
|
|
36
|
+
export type SpendReading = {
|
|
37
|
+
account: string;
|
|
38
|
+
amount: number;
|
|
39
|
+
currency: string;
|
|
40
|
+
/** When the check ran: a spend line carries no time of its own (§ 5). */
|
|
41
|
+
at: number;
|
|
42
|
+
};
|
|
43
|
+
export type StoredSpend = {
|
|
44
|
+
account: string;
|
|
45
|
+
amount: number;
|
|
46
|
+
currency: string;
|
|
47
|
+
at: string;
|
|
48
|
+
};
|
|
28
49
|
/** Fold one seat's figure into the readings kept for this pass. */
|
|
29
50
|
export declare function observe(list: readonly Seen[], figure: QuotaFigure, seat: string, now: number): Seen[];
|
|
51
|
+
/**
|
|
52
|
+
* Fold a check command's windows into the readings kept for this pass (§ 5). A check reading is
|
|
53
|
+
* confirmed at first sight and belongs to no seat; one is kept per account and window, so a new
|
|
54
|
+
* check replaces the last, and the screen readings of the same window are left alone.
|
|
55
|
+
*/
|
|
56
|
+
export declare function observeCheck(list: readonly Seen[], account: string, windows: readonly CheckWindow[]): Seen[];
|
|
57
|
+
/** The screen readings in a list, and the check readings: the two slots of the same account. */
|
|
58
|
+
export declare function screenOf(list: readonly Seen[]): Seen[];
|
|
59
|
+
export declare function checkOf(list: readonly Seen[]): Seen[];
|
|
30
60
|
/**
|
|
31
61
|
* The reading that counts. A figure from before a known reset is dropped.
|
|
32
62
|
* Among confirmed live readings, the newest change wins. A first sight counts
|
|
@@ -35,12 +65,48 @@ export declare function observe(list: readonly Seen[], figure: QuotaFigure, seat
|
|
|
35
65
|
* with no reset time it is unknown.
|
|
36
66
|
*/
|
|
37
67
|
export declare function verdict(list: readonly Seen[], now: number, staleAfterMs: number, reserve: number | null): Verdict;
|
|
68
|
+
export type CountedReading = {
|
|
69
|
+
kind: 'counted';
|
|
70
|
+
reading: Seen;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'unconfirmed';
|
|
73
|
+
reading: Seen;
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'unknown';
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The reading that counts for one window, from the sources the account names in order (§ 3, § 5):
|
|
79
|
+
* a lower source is used only when every higher one is failed, unknown or stale. A check reading
|
|
80
|
+
* counts while it is fresh by § 5's own time and its reset has not passed; a screen reading falls
|
|
81
|
+
* through when it is unknown, and a bare first sight falls through too, so a fresh check below it
|
|
82
|
+
* still counts (§ 4.3 rule 5). A stale reading inside its reserve with its reset ahead is kept as
|
|
83
|
+
* the fallback, whichever source it came from, so rule 4 still refuses when nothing below counts.
|
|
84
|
+
*/
|
|
85
|
+
export declare function countedFor(sources: readonly ReadingSource[], screen: readonly Seen[], checks: readonly Seen[], now: number, staleAfterMs: number, reserve: number | null): CountedReading;
|
|
38
86
|
/** Readings whose reset has passed are left out. One with no reset time is kept. */
|
|
39
87
|
export declare function remember(list: readonly Seen[], now: number): Record<string, StoredReading>;
|
|
40
88
|
export declare function recall(stored: Record<string, StoredReading> | undefined): Seen[];
|
|
41
|
-
/** Write the readings that still count. The state file is the per-project cache. */
|
|
42
|
-
export declare function saveReadings(dir: string,
|
|
43
|
-
|
|
89
|
+
/** Write the readings that still count. The state file is the per-project cache (§ 4.4). */
|
|
90
|
+
export declare function saveReadings(dir: string, list: readonly Seen[], now?: number): void;
|
|
91
|
+
/**
|
|
92
|
+
* One pass's fold, read and written under the state lock (§ 4.4): `fold` is handed the readings
|
|
93
|
+
* the state holds and returns the list to keep, so a watch folding while another watch writes
|
|
94
|
+
* folds onto what was written, never over a list it read before it. Whatever the fold returns as
|
|
95
|
+
* its `value` is handed back.
|
|
96
|
+
*/
|
|
97
|
+
export declare function updateReadings<T>(dir: string, now: number, fold: (stored: Seen[]) => {
|
|
98
|
+
readings: Seen[];
|
|
99
|
+
value: T;
|
|
100
|
+
}): T;
|
|
101
|
+
export declare function loadReadings(dir: string): Seen[];
|
|
102
|
+
/**
|
|
103
|
+
* Write the spend readings a pass's checks read, merging by account: an account whose check did
|
|
104
|
+
* not run this pass keeps the reading the state already holds. Nothing read, nothing written.
|
|
105
|
+
*/
|
|
106
|
+
export declare function saveSpendReadings(dir: string, list: readonly SpendReading[]): void;
|
|
107
|
+
export declare function loadSpendReadings(dir: string): SpendReading[];
|
|
108
|
+
export declare function storeSpend(reading: SpendReading): StoredSpend;
|
|
109
|
+
export declare function recallSpend(stored: Record<string, StoredSpend> | undefined): SpendReading[];
|
|
44
110
|
export declare function store(reading: Seen): StoredReading;
|
|
45
111
|
export declare function revive(stored: StoredReading): Seen;
|
|
46
112
|
/** When a duration such as `114h4m` ends, counted from a time. Null for no or an odd duration. */
|
package/dist/budgets/readings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readState, updateState } from "../state.js";
|
|
2
2
|
/** Fold one seat's figure into the readings kept for this pass. */
|
|
3
3
|
export function observe(list, figure, seat, now) {
|
|
4
4
|
const next = list.slice();
|
|
@@ -19,6 +19,7 @@ export function observe(list, figure, seat, now) {
|
|
|
19
19
|
changedAt: same && previous ? previous.changedAt : now,
|
|
20
20
|
resetsAt: same && previous && previous.resetsAt !== null ? previous.resetsAt : resetsFrom(figure.resets, now),
|
|
21
21
|
seat,
|
|
22
|
+
source: 'status_line',
|
|
22
23
|
confirmed: previous === null ? agreed : same ? previous.confirmed || agreed : true,
|
|
23
24
|
};
|
|
24
25
|
if (at < 0)
|
|
@@ -27,6 +28,40 @@ export function observe(list, figure, seat, now) {
|
|
|
27
28
|
next[at] = reading;
|
|
28
29
|
return next;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Fold a check command's windows into the readings kept for this pass (§ 5). A check reading is
|
|
33
|
+
* confirmed at first sight and belongs to no seat; one is kept per account and window, so a new
|
|
34
|
+
* check replaces the last, and the screen readings of the same window are left alone.
|
|
35
|
+
*/
|
|
36
|
+
export function observeCheck(list, account, windows) {
|
|
37
|
+
const next = list.slice();
|
|
38
|
+
for (const window of windows) {
|
|
39
|
+
const reading = {
|
|
40
|
+
account,
|
|
41
|
+
window: window.window,
|
|
42
|
+
left: window.left,
|
|
43
|
+
used: window.used,
|
|
44
|
+
changedAt: window.at,
|
|
45
|
+
resetsAt: window.resetsAt,
|
|
46
|
+
seat: null,
|
|
47
|
+
source: 'check',
|
|
48
|
+
confirmed: true,
|
|
49
|
+
};
|
|
50
|
+
const at = next.findIndex((item) => item.account === account && item.window === window.window && item.source === 'check');
|
|
51
|
+
if (at < 0)
|
|
52
|
+
next.push(reading);
|
|
53
|
+
else
|
|
54
|
+
next[at] = reading;
|
|
55
|
+
}
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
/** The screen readings in a list, and the check readings: the two slots of the same account. */
|
|
59
|
+
export function screenOf(list) {
|
|
60
|
+
return list.filter((item) => item.source === 'status_line');
|
|
61
|
+
}
|
|
62
|
+
export function checkOf(list) {
|
|
63
|
+
return list.filter((item) => item.source === 'check');
|
|
64
|
+
}
|
|
30
65
|
/**
|
|
31
66
|
* The reading that counts. A figure from before a known reset is dropped.
|
|
32
67
|
* Among confirmed live readings, the newest change wins. A first sight counts
|
|
@@ -56,13 +91,57 @@ export function verdict(list, now, staleAfterMs, reserve) {
|
|
|
56
91
|
return { kind: 'unknown' };
|
|
57
92
|
return { kind: 'stale', reading };
|
|
58
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The reading that counts for one window, from the sources the account names in order (§ 3, § 5):
|
|
96
|
+
* a lower source is used only when every higher one is failed, unknown or stale. A check reading
|
|
97
|
+
* counts while it is fresh by § 5's own time and its reset has not passed; a screen reading falls
|
|
98
|
+
* through when it is unknown, and a bare first sight falls through too, so a fresh check below it
|
|
99
|
+
* still counts (§ 4.3 rule 5). A stale reading inside its reserve with its reset ahead is kept as
|
|
100
|
+
* the fallback, whichever source it came from, so rule 4 still refuses when nothing below counts.
|
|
101
|
+
*/
|
|
102
|
+
export function countedFor(sources, screen, checks, now, staleAfterMs, reserve) {
|
|
103
|
+
let fallback = null;
|
|
104
|
+
let unconfirmed = null;
|
|
105
|
+
for (const source of sources) {
|
|
106
|
+
if (source === 'check') {
|
|
107
|
+
const reading = checks.find((item) => (item.resetsAt === null || item.resetsAt > now) && now - item.changedAt < staleAfterMs);
|
|
108
|
+
if (reading)
|
|
109
|
+
return { kind: 'counted', reading };
|
|
110
|
+
// Rule 4 is about the reserve, not the source: a stale check figure inside it keeps
|
|
111
|
+
// refusing until its known reset, and a fresh source below still wins over this fallback.
|
|
112
|
+
const inside = checks.find((item) => item.resetsAt !== null && item.resetsAt > now && reserve !== null && item.left <= reserve);
|
|
113
|
+
if (inside)
|
|
114
|
+
fallback ??= inside;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const result = verdict(screen, now, staleAfterMs, reserve);
|
|
118
|
+
if (result.kind === 'unknown')
|
|
119
|
+
continue;
|
|
120
|
+
if (result.kind === 'unconfirmed') {
|
|
121
|
+
unconfirmed ??= result.reading;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (result.kind === 'fresh')
|
|
125
|
+
return { kind: 'counted', reading: result.reading };
|
|
126
|
+
fallback ??= result.reading;
|
|
127
|
+
}
|
|
128
|
+
if (fallback)
|
|
129
|
+
return { kind: 'counted', reading: fallback };
|
|
130
|
+
if (unconfirmed)
|
|
131
|
+
return { kind: 'unconfirmed', reading: unconfirmed };
|
|
132
|
+
return { kind: 'unknown' };
|
|
133
|
+
}
|
|
59
134
|
/** Readings whose reset has passed are left out. One with no reset time is kept. */
|
|
60
135
|
export function remember(list, now) {
|
|
61
136
|
const out = {};
|
|
62
137
|
for (const reading of list) {
|
|
63
138
|
if (reading.resetsAt !== null && reading.resetsAt <= now)
|
|
64
139
|
continue;
|
|
65
|
-
|
|
140
|
+
// A screen reading is kept per seat; a check reading belongs to no seat and is kept alone.
|
|
141
|
+
const key = reading.source === 'check'
|
|
142
|
+
? `${reading.account}/${reading.window}`
|
|
143
|
+
: `${reading.account}/${reading.window}/${reading.seat}`;
|
|
144
|
+
out[key] = store(reading);
|
|
66
145
|
}
|
|
67
146
|
return out;
|
|
68
147
|
}
|
|
@@ -71,16 +150,62 @@ export function recall(stored) {
|
|
|
71
150
|
return [];
|
|
72
151
|
return Object.values(stored).map(revive);
|
|
73
152
|
}
|
|
74
|
-
/** Write the readings that still count. The state file is the per-project cache. */
|
|
75
|
-
export function saveReadings(dir,
|
|
153
|
+
/** Write the readings that still count. The state file is the per-project cache (§ 4.4). */
|
|
154
|
+
export function saveReadings(dir, list, now = Date.now()) {
|
|
155
|
+
updateState(dir, (state) => {
|
|
156
|
+
state.budgets = remember(list, now);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* One pass's fold, read and written under the state lock (§ 4.4): `fold` is handed the readings
|
|
161
|
+
* the state holds and returns the list to keep, so a watch folding while another watch writes
|
|
162
|
+
* folds onto what was written, never over a list it read before it. Whatever the fold returns as
|
|
163
|
+
* its `value` is handed back.
|
|
164
|
+
*/
|
|
165
|
+
export function updateReadings(dir, now, fold) {
|
|
166
|
+
return updateState(dir, (state) => {
|
|
167
|
+
const folded = fold(recall(state.budgets));
|
|
168
|
+
state.budgets = remember(folded.readings, now);
|
|
169
|
+
return folded.value;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export function loadReadings(dir) {
|
|
173
|
+
return recall(readState(dir).budgets);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Write the spend readings a pass's checks read, merging by account: an account whose check did
|
|
177
|
+
* not run this pass keeps the reading the state already holds. Nothing read, nothing written.
|
|
178
|
+
*/
|
|
179
|
+
export function saveSpendReadings(dir, list) {
|
|
180
|
+
if (!list.length)
|
|
181
|
+
return;
|
|
76
182
|
updateState(dir, (state) => {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
183
|
+
const spend = state.spend ?? {};
|
|
184
|
+
for (const reading of list)
|
|
185
|
+
spend[reading.account] = storeSpend(reading);
|
|
186
|
+
state.spend = spend;
|
|
80
187
|
});
|
|
81
188
|
}
|
|
82
|
-
export function
|
|
83
|
-
return
|
|
189
|
+
export function loadSpendReadings(dir) {
|
|
190
|
+
return recallSpend(readState(dir).spend);
|
|
191
|
+
}
|
|
192
|
+
export function storeSpend(reading) {
|
|
193
|
+
return {
|
|
194
|
+
account: reading.account,
|
|
195
|
+
amount: reading.amount,
|
|
196
|
+
currency: reading.currency,
|
|
197
|
+
at: new Date(reading.at).toISOString(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function recallSpend(stored) {
|
|
201
|
+
if (!stored)
|
|
202
|
+
return [];
|
|
203
|
+
return Object.values(stored).map((one) => ({
|
|
204
|
+
account: one.account,
|
|
205
|
+
amount: one.amount,
|
|
206
|
+
currency: one.currency,
|
|
207
|
+
at: Date.parse(one.at),
|
|
208
|
+
}));
|
|
84
209
|
}
|
|
85
210
|
export function store(reading) {
|
|
86
211
|
return {
|
|
@@ -91,6 +216,7 @@ export function store(reading) {
|
|
|
91
216
|
changedAt: new Date(reading.changedAt).toISOString(),
|
|
92
217
|
resetsAt: reading.resetsAt === null ? null : new Date(reading.resetsAt).toISOString(),
|
|
93
218
|
seat: reading.seat,
|
|
219
|
+
source: reading.source,
|
|
94
220
|
confirmed: reading.confirmed,
|
|
95
221
|
};
|
|
96
222
|
}
|
|
@@ -103,6 +229,7 @@ export function revive(stored) {
|
|
|
103
229
|
changedAt: Date.parse(stored.changedAt),
|
|
104
230
|
resetsAt: stored.resetsAt === null ? null : Date.parse(stored.resetsAt),
|
|
105
231
|
seat: stored.seat,
|
|
232
|
+
source: stored.source ?? 'status_line',
|
|
106
233
|
confirmed: stored.confirmed,
|
|
107
234
|
};
|
|
108
235
|
}
|
package/dist/budgets/table.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TeamFile } from '../file/types.ts';
|
|
2
2
|
import type { WindowName } from '../profiles/quota.ts';
|
|
3
|
-
import { type Seen } from './readings.ts';
|
|
3
|
+
import { type ReadingSource, type Seen } from './readings.ts';
|
|
4
4
|
export type BudgetState = 'fresh' | 'unconfirmed' | 'stale' | 'refusing' | 'unknown';
|
|
5
5
|
export type BudgetRow = {
|
|
6
6
|
account: string;
|
|
@@ -10,7 +10,7 @@ export type BudgetRow = {
|
|
|
10
10
|
resetsIn: string | null;
|
|
11
11
|
seat: string | null;
|
|
12
12
|
age: string | null;
|
|
13
|
-
source:
|
|
13
|
+
source: ReadingSource | null;
|
|
14
14
|
state: BudgetState;
|
|
15
15
|
/** True when a subscription's left figure is at or inside its reserve. */
|
|
16
16
|
inside: boolean;
|
package/dist/budgets/table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkOf, countedFor, screenOf } from "./readings.js";
|
|
2
2
|
const WINDOWS = ['session', 'daily', 'weekly'];
|
|
3
3
|
/** Rows for the accounts the budgets in force name, and for any reading still in the state. */
|
|
4
4
|
export function budgetTable(budgets, list, now) {
|
|
@@ -17,7 +17,11 @@ export function budgetTable(budgets, list, now) {
|
|
|
17
17
|
const window = group[0]?.window ?? null;
|
|
18
18
|
named.add(account);
|
|
19
19
|
const reserve = reserveOf(budgets, account);
|
|
20
|
-
|
|
20
|
+
// The window's reading from the sources the account names, in order (§ 3, § 5).
|
|
21
|
+
const result = countedFor(sourcesOf(budgets, account), screenOf(group), checkOf(group), now, staleAfterMs, reserve);
|
|
22
|
+
rows.push(result.kind === 'unknown'
|
|
23
|
+
? { ...blank(account, reserve), window }
|
|
24
|
+
: rowOf(result, now, staleAfterMs, reserve));
|
|
21
25
|
}
|
|
22
26
|
for (const account of Object.keys(budgets.accounts)) {
|
|
23
27
|
if (!named.has(account))
|
|
@@ -35,25 +39,34 @@ export function budgetLine(row) {
|
|
|
35
39
|
return [row.account, row.window, 'unknown'].filter((part) => part).join(' ');
|
|
36
40
|
}
|
|
37
41
|
const reset = row.resetsIn === null ? 'resets unknown' : `resets in ${row.resetsIn}`;
|
|
38
|
-
const from = row.source === 'status_line' ? 'status line' : 'unknown source';
|
|
42
|
+
const from = row.source === 'status_line' ? 'status line' : row.source === 'check' ? 'check' : 'unknown source';
|
|
43
|
+
const when = row.source === 'check' ? 'read' : 'changed';
|
|
39
44
|
const state = row.inside && row.reserve !== null ? `${row.state}, inside reserve ${row.reserve}%` : row.state;
|
|
40
|
-
return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat}
|
|
45
|
+
return `${row.account} ${row.window} left ${row.left}% used ${row.used}% ${reset} ${row.seat ?? '-'} ${when} ${row.age} ago ${from} ${state}`;
|
|
41
46
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
/** The sources the account's figures are read from, in order: a status line when the file is silent. */
|
|
48
|
+
function sourcesOf(budgets, account) {
|
|
49
|
+
return budgets.accounts[account]?.sources ?? ['status_line'];
|
|
50
|
+
}
|
|
51
|
+
function rowOf(result, now, staleAfterMs, reserve) {
|
|
45
52
|
const reading = result.reading;
|
|
53
|
+
const inside = reserve !== null && reading.left <= reserve;
|
|
54
|
+
const state = result.kind === 'unconfirmed'
|
|
55
|
+
? 'unconfirmed'
|
|
56
|
+
: now - reading.changedAt < staleAfterMs
|
|
57
|
+
? 'fresh'
|
|
58
|
+
: inside && reading.resetsAt !== null ? 'refusing' : 'stale';
|
|
46
59
|
return {
|
|
47
|
-
account,
|
|
60
|
+
account: reading.account,
|
|
48
61
|
window: reading.window,
|
|
49
62
|
left: reading.left,
|
|
50
63
|
used: reading.used,
|
|
51
64
|
resetsIn: reading.resetsAt === null ? null : span(reading.resetsAt - now),
|
|
52
65
|
seat: reading.seat,
|
|
53
66
|
age: span(now - reading.changedAt),
|
|
54
|
-
source:
|
|
55
|
-
state
|
|
56
|
-
inside
|
|
67
|
+
source: reading.source,
|
|
68
|
+
state,
|
|
69
|
+
inside,
|
|
57
70
|
reserve,
|
|
58
71
|
};
|
|
59
72
|
}
|
package/dist/commands/add.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import { approvalDifferences, budgetsInForce } from "../approve/approval.js";
|
|
4
|
+
import { approvalDifferences, budgetsInForce, recordSeatDigest } from "../approve/approval.js";
|
|
5
5
|
import { readArgs } from "../args.js";
|
|
6
6
|
import { callerOf, describeCaller, isOwner, mayChangeTeam } from "../caller.js";
|
|
7
7
|
import { blocksLaunch, doctorFindings, realSources as doctorSources } from "../commands/doctor.js";
|
|
8
8
|
import { seatBudget } from "../budgets/gate.js";
|
|
9
|
-
import { loadReadings } from "../budgets/readings.js";
|
|
9
|
+
import { loadReadings, loadSpendReadings } from "../budgets/readings.js";
|
|
10
10
|
import { rulesOf } from "../commands/up.js";
|
|
11
11
|
import { branchPresent, readMerge } from "../end/condition.js";
|
|
12
12
|
import { clearStopped, hasSeat, restoreSeat, seatIsStopped } from "../file/lines.js";
|
|
13
13
|
import { loadTeamFile, placedProblems } from "../file/load.js";
|
|
14
14
|
import { validateTeamFile } from "../file/validate.js";
|
|
15
15
|
import { writeTeamFile } from "../file/write.js";
|
|
16
|
-
import { agentList, agentRename, paneRead, paneRun, sessionRunning, sessionState, startServer, workspaceClose, workspaceCreate, workspaceList, } from "../herdr.js";
|
|
16
|
+
import { agentList, agentRename, paneForeground, paneRead, paneRun, sessionRunning, sessionState, startServer, workspaceClose, workspaceCreate, workspaceList, } from "../herdr.js";
|
|
17
17
|
import { executePlan } from "../launch/execute.js";
|
|
18
18
|
import { formatPlan, upPlan } from "../launch/plan.js";
|
|
19
19
|
import { logLine } from "../log.js";
|
|
@@ -36,6 +36,7 @@ const realLaunch = {
|
|
|
36
36
|
return agents === null ? null : agents.map((agent) => agent.pane);
|
|
37
37
|
},
|
|
38
38
|
paneText: (session, pane) => paneRead(pane, 200, aim(session)),
|
|
39
|
+
foreground: (session, pane) => paneForeground(pane, aim(session)),
|
|
39
40
|
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
40
41
|
now: () => new Date(),
|
|
41
42
|
};
|
|
@@ -168,7 +169,10 @@ export async function runAdd(argv, io, sources = realSources) {
|
|
|
168
169
|
const doctorTeam = built.temporary
|
|
169
170
|
? { ...prepared.team, seats: prepared.team.seats.map((item) => item.name === built.temporary?.like ? { ...item, stopped: false } : item) }
|
|
170
171
|
: prepared.team;
|
|
171
|
-
|
|
172
|
+
// `team` is the file on disk, already the approved one. `doctorTeam` is the
|
|
173
|
+
// seat about to run, so a stopped seat's CLI is still checked. The digest is
|
|
174
|
+
// recorded with the write, after a refusal has left the file alone.
|
|
175
|
+
for (const finding of doctorFindings(doctorTeam, root, dir, session, sources.doctor, prepared.warnings, team)) {
|
|
172
176
|
if (blocksLaunch(finding)) {
|
|
173
177
|
io.stderr(`team add: ${finding.text}\n`);
|
|
174
178
|
return 1;
|
|
@@ -198,7 +202,7 @@ export async function runAdd(argv, io, sources = realSources) {
|
|
|
198
202
|
// The gate reads the approved budgets, never the edited file's: the seat this `add` inserts
|
|
199
203
|
// changes no section of its own, and no unapproved reserve may unblock a launch (#50).
|
|
200
204
|
const budgets = budgetsInForce(prepared.team, root, sources.home);
|
|
201
|
-
const decision = seatBudget(budgets, loadReadings(dir
|
|
205
|
+
const decision = seatBudget(budgets, loadReadings(dir), built.seat, sources.now().getTime(), loadSpendReadings(dir));
|
|
202
206
|
const stray = unnamedIn(built.seat.label, agents, workspaces);
|
|
203
207
|
const starting = seatPlan(prepared.team, built.seat, start);
|
|
204
208
|
const planned = stray
|
|
@@ -246,6 +250,9 @@ export async function runAdd(argv, io, sources = realSources) {
|
|
|
246
250
|
io.stderr(`team add: ${where(problem)}${problem.message}\n`);
|
|
247
251
|
return 2;
|
|
248
252
|
}
|
|
253
|
+
const parsed = validateTeamFile(built.edited);
|
|
254
|
+
if (parsed.ok)
|
|
255
|
+
recordSeatDigest(parsed.team, root, built.name, sources.home);
|
|
249
256
|
}
|
|
250
257
|
const plan = upPlan({ root, session, sessionRunning: live === 'running', seats: [seatForPlan], watchAlive: true });
|
|
251
258
|
const who = describeCaller(caller);
|
|
@@ -33,6 +33,8 @@ export default doctor;
|
|
|
33
33
|
export declare function doctorFindings(team: TeamFile, root: string, dir: string, session: string, sources: DoctorSources, warnings: {
|
|
34
34
|
line: number;
|
|
35
35
|
message: string;
|
|
36
|
-
}[]
|
|
36
|
+
}[],
|
|
37
|
+
/** The file as approved. `add` hands the team about to run, whose `stopped` mark is already cleared. */
|
|
38
|
+
approved?: TeamFile): Finding[];
|
|
37
39
|
export declare function doctorLoginFindings(team: TeamFile, sources: DoctorSources): Finding[];
|
|
38
40
|
export declare function runDoctor(argv: string[], io: Io, sources: DoctorSources): Promise<number>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -150,12 +150,14 @@ function watchFindings(watch, dir, session, running, now) {
|
|
|
150
150
|
}
|
|
151
151
|
return [{ level: 'ok', text: 'the watch is running' }];
|
|
152
152
|
}
|
|
153
|
-
export function doctorFindings(team, root, dir, session, sources, warnings
|
|
153
|
+
export function doctorFindings(team, root, dir, session, sources, warnings,
|
|
154
|
+
/** The file as approved. `add` hands the team about to run, whose `stopped` mark is already cleared. */
|
|
155
|
+
approved = team) {
|
|
154
156
|
const findings = warnings.map((warning) => ({
|
|
155
157
|
level: 'warn',
|
|
156
158
|
text: `the file, line ${warning.line}: ${warning.message}`,
|
|
157
159
|
}));
|
|
158
|
-
findings.push(...approvalFindings(
|
|
160
|
+
findings.push(...approvalFindings(approved, root, sources.home), ...checkFindings(team, root, sources.home));
|
|
159
161
|
const herdr = sources.herdrVersion();
|
|
160
162
|
const running = herdr === null ? null : sources.sessionRunning(session);
|
|
161
163
|
if (herdr === null)
|
package/dist/commands/down.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type DownSources = {
|
|
|
10
10
|
screen(session: string, pane: string, cli: string): Screen;
|
|
11
11
|
/** Herdr's own status for the pane. The Enter waits for idle or done. */
|
|
12
12
|
status(session: string, pane: string): string | null;
|
|
13
|
-
/** Foreground
|
|
14
|
-
foreground
|
|
13
|
+
/** Foreground argv0 names, or null when the pane can't be read. */
|
|
14
|
+
foreground(session: string, pane: string): string[] | null;
|
|
15
15
|
now(): Date;
|
|
16
16
|
sleep?(ms: number): Promise<void>;
|
|
17
17
|
launch?: DownLaunch;
|
package/dist/commands/down.js
CHANGED
|
@@ -3,6 +3,7 @@ import { callerOf, describeCaller, mayChangeTeam } from "../caller.js";
|
|
|
3
3
|
import { currentTeam } from "../file/current.js";
|
|
4
4
|
import { agentList, agentStatus, paneForeground, paneRead, sessionRunning, pressEnter, sessionStop, typeText, workspaceClose, } from "../herdr.js";
|
|
5
5
|
import { executePlan } from "../launch/execute.js";
|
|
6
|
+
import { reportedLiveAgent } from "../launch/agent.js";
|
|
6
7
|
import { downPlan, formatPlan } from "../launch/plan.js";
|
|
7
8
|
import { profileFor } from "../profiles/index.js";
|
|
8
9
|
import { logLine } from "../log.js";
|
|
@@ -204,6 +205,10 @@ export async function runDown(argv, io, sources) {
|
|
|
204
205
|
// status is asked both times; Enter waits until it is idle or done and the screen is idle
|
|
205
206
|
// or holding unsent text.
|
|
206
207
|
const cli = seats.find((seat) => seat.pane === pane)?.cli ?? '';
|
|
208
|
+
const names = profileFor(cli)?.processNames ?? [];
|
|
209
|
+
const live = () => reportedLiveAgent(sources.foreground(sessionName, pane), names);
|
|
210
|
+
if (!live())
|
|
211
|
+
return 'no-agent';
|
|
207
212
|
const look = () => sources.screen(sessionName, pane, cli).kind;
|
|
208
213
|
const resting = () => {
|
|
209
214
|
const status = sources.status(sessionName, pane);
|
|
@@ -213,6 +218,8 @@ export async function runDown(argv, io, sources) {
|
|
|
213
218
|
return false;
|
|
214
219
|
if (!launch.typeText(sessionName, pane, text))
|
|
215
220
|
return false;
|
|
221
|
+
if (!live())
|
|
222
|
+
return 'no-agent';
|
|
216
223
|
const after = look();
|
|
217
224
|
if (!resting() || (after !== 'idle' && after !== 'unsent'))
|
|
218
225
|
return false;
|
|
@@ -232,7 +239,7 @@ export async function runDown(argv, io, sources) {
|
|
|
232
239
|
const names = cli ? profileFor(cli)?.processNames : undefined;
|
|
233
240
|
if (!names)
|
|
234
241
|
return true;
|
|
235
|
-
return paneStillRunning(sources.foreground
|
|
242
|
+
return paneStillRunning(sources.foreground(sessionName, pane), names);
|
|
236
243
|
});
|
|
237
244
|
},
|
|
238
245
|
classify: () => 'unknown',
|
|
@@ -4,6 +4,8 @@ import { type DownSources } from './down.ts';
|
|
|
4
4
|
export type RemoveSources = DownSources & {
|
|
5
5
|
/** Foreground process names in the pane, or null when the pane can't be read. */
|
|
6
6
|
foreground(session: string, pane: string): string[] | null;
|
|
7
|
+
/** Approval store home. The real command uses the owner's home. */
|
|
8
|
+
home?: string;
|
|
7
9
|
};
|
|
8
10
|
export declare const realSources: RemoveSources;
|
|
9
11
|
export declare const USAGE = "Usage: team remove <name> [--keep] [--abandon] [--session <name>] [--file <path>]\n";
|
package/dist/commands/remove.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
4
|
+
import { recordSeatDigest } from "../approve/approval.js";
|
|
3
5
|
import { readArgs } from "../args.js";
|
|
4
6
|
import { callerOf, describeCaller, isOwner, mayChangeTeam } from "../caller.js";
|
|
5
7
|
import { loadTeamFile } from "../file/load.js";
|
|
@@ -7,6 +9,7 @@ import { markStopped, takeOut } from "../file/lines.js";
|
|
|
7
9
|
import { validateTeamFile } from "../file/validate.js";
|
|
8
10
|
import { writeTeamFile } from "../file/write.js";
|
|
9
11
|
import { paneForeground } from "../herdr.js";
|
|
12
|
+
import { reportedLiveAgent } from "../launch/agent.js";
|
|
10
13
|
import { executePlan } from "../launch/execute.js";
|
|
11
14
|
import { downPlan } from "../launch/plan.js";
|
|
12
15
|
import { logLine } from "../log.js";
|
|
@@ -60,7 +63,7 @@ export async function runRemove(argv, io, sources = realSources) {
|
|
|
60
63
|
io.stderr(`team remove: only the owner, the coordinator or the operator runs it; this call is ${describeCaller(caller)}\n`);
|
|
61
64
|
return 1;
|
|
62
65
|
}
|
|
63
|
-
const { team, path } = loaded;
|
|
66
|
+
const { team, path, root } = loaded;
|
|
64
67
|
const session = args.values.session ?? team.session;
|
|
65
68
|
if (session === 'default') {
|
|
66
69
|
io.stderr('team remove: session can\'t be "default", herdr\'s own session\n');
|
|
@@ -133,6 +136,7 @@ export async function runRemove(argv, io, sources = realSources) {
|
|
|
133
136
|
if (!stopped)
|
|
134
137
|
return 1;
|
|
135
138
|
}
|
|
139
|
+
let kept = null;
|
|
136
140
|
if (!temporary) {
|
|
137
141
|
const refused = withLock(dir, () => {
|
|
138
142
|
const text = readFileSync(path, 'utf8');
|
|
@@ -140,7 +144,11 @@ export async function runRemove(argv, io, sources = realSources) {
|
|
|
140
144
|
if (next === text)
|
|
141
145
|
return null;
|
|
142
146
|
const wrote = writeTeamFile(path, next);
|
|
143
|
-
|
|
147
|
+
if (!wrote.ok)
|
|
148
|
+
return wrote.errors;
|
|
149
|
+
if (args.flags.has('keep'))
|
|
150
|
+
kept = next;
|
|
151
|
+
return null;
|
|
144
152
|
});
|
|
145
153
|
if (refused) {
|
|
146
154
|
for (const problem of refused) {
|
|
@@ -149,6 +157,11 @@ export async function runRemove(argv, io, sources = realSources) {
|
|
|
149
157
|
return 2;
|
|
150
158
|
}
|
|
151
159
|
}
|
|
160
|
+
if (kept !== null) {
|
|
161
|
+
const parsed = validateTeamFile(kept);
|
|
162
|
+
if (parsed.ok)
|
|
163
|
+
recordSeatDigest(parsed.team, root, name, sources.home ?? homedir());
|
|
164
|
+
}
|
|
152
165
|
if (!agent && recorded) {
|
|
153
166
|
updateState(dir, (file) => {
|
|
154
167
|
const seats = file.sessions[session]?.seats;
|
|
@@ -185,6 +198,10 @@ export async function stopRunning(input) {
|
|
|
185
198
|
createWorkspace: () => null,
|
|
186
199
|
paneRun: () => false,
|
|
187
200
|
typeLine(sessionName, pane, text) {
|
|
201
|
+
const names = profileFor(seat.cli)?.processNames ?? [];
|
|
202
|
+
const live = () => reportedLiveAgent(sources.foreground(sessionName, pane), names);
|
|
203
|
+
if (!live())
|
|
204
|
+
return 'no-agent';
|
|
188
205
|
const look = () => sources.screen(sessionName, pane, seat.cli).kind;
|
|
189
206
|
const resting = () => {
|
|
190
207
|
const status = sources.status(sessionName, pane);
|
|
@@ -194,6 +211,8 @@ export async function stopRunning(input) {
|
|
|
194
211
|
return false;
|
|
195
212
|
if (!launch.typeText(sessionName, pane, text))
|
|
196
213
|
return false;
|
|
214
|
+
if (!live())
|
|
215
|
+
return 'no-agent';
|
|
197
216
|
const after = look();
|
|
198
217
|
if (!resting() || (after !== 'idle' && after !== 'unsent'))
|
|
199
218
|
return false;
|
package/dist/commands/status.js
CHANGED
|
@@ -67,8 +67,9 @@ export async function runStatus(argv, io, sources) {
|
|
|
67
67
|
io.stderr('team status: herdr doesn\'t answer; is it installed and running?\n');
|
|
68
68
|
return 2;
|
|
69
69
|
}
|
|
70
|
-
const
|
|
71
|
-
const
|
|
70
|
+
const whole = readState(dir);
|
|
71
|
+
const state = whole.sessions[session] ?? emptySession();
|
|
72
|
+
const budgets = budgetTable(sources.budgetsInForce(team, root), recall(whole.budgets), sources.now().getTime());
|
|
72
73
|
const comparison = compare(team, session, state, live, sources.now(), sources.watchInForce(team, root));
|
|
73
74
|
if (!live.running)
|
|
74
75
|
comparison.notes.unshift(`the herdr session "${session}" is not running`);
|
package/dist/commands/up.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type Launch = {
|
|
|
35
35
|
typeText?(session: string, pane: string, text: string): boolean;
|
|
36
36
|
pressEnter?(session: string, pane: string): boolean;
|
|
37
37
|
agentStatus?(session: string, pane: string): string | null;
|
|
38
|
+
/** Foreground argv0 names, or null when the pane can't be read. */
|
|
39
|
+
foreground(session: string, pane: string): string[] | null;
|
|
38
40
|
sleep(ms: number): Promise<void>;
|
|
39
41
|
now(): Date;
|
|
40
42
|
};
|
|
@@ -44,6 +46,6 @@ export declare const realSources: UpSources;
|
|
|
44
46
|
export declare const USAGE = "Usage: team up [--dry-run] [--session <name>] [--file <path>]\n";
|
|
45
47
|
export declare const up: Command;
|
|
46
48
|
export default up;
|
|
47
|
-
/** The rules one seat gets at launch, with its own signature lines. */
|
|
49
|
+
/** The rules one seat gets at launch, with its own signature lines, as its delivery carries them. */
|
|
48
50
|
export declare function rulesOf(team: TeamFile, seat: Seat): string;
|
|
49
51
|
export declare function runUp(argv: string[], io: Io, sources: UpSources): Promise<number>;
|