pi-background-tasks 0.6.0 → 0.7.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/PUBLISHING.md +15 -15
- package/README.md +78 -6
- package/TESTING.md +28 -12
- package/TEST_PLAN.md +21 -12
- package/extensions/background-tasks.ts +1 -1
- package/extensions/fusion-child.ts +1 -0
- package/package.json +16 -10
- package/src/core/attested-pi-run.ts +619 -0
- package/src/core/common.ts +599 -432
- package/src/core/extension-api.ts +548 -0
- package/src/core/fusion/artifacts.ts +453 -0
- package/src/core/fusion/config.ts +371 -0
- package/src/core/fusion/context.ts +179 -0
- package/src/core/fusion/evaluation.ts +362 -0
- package/src/core/fusion/orchestrator.ts +595 -0
- package/src/core/fusion/pi-child.ts +900 -0
- package/src/core/fusion/prompts.ts +155 -0
- package/src/core/fusion/types.ts +289 -0
- package/src/core/registry.ts +1352 -786
- package/src/core/update-check.ts +69 -63
- package/src/extension.ts +880 -524
- package/src/fusion-child-extension.ts +100 -0
- package/src/fusion-extension.ts +632 -0
- package/src/testing/normalize.ts +22 -3
- package/src/ui/background-tasks-manager.ts +703 -613
- package/src/ui/fusion-model-selector.ts +322 -0
package/src/core/common.ts
CHANGED
|
@@ -1,306 +1,432 @@
|
|
|
1
|
-
import { statSync } from
|
|
2
|
-
import { open } from
|
|
3
|
-
import { DEFAULT_MAX_BYTES } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
import { statSync, type WriteStream } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import type { BackgroundTaskChildProcess } from './registry.js';
|
|
5
|
+
|
|
6
|
+
export const TASK_STATUS_VALUES = ['running', 'completed', 'failed', 'killed'] as const;
|
|
7
|
+
export const TERMINAL_TASK_STATUS_VALUES = ['completed', 'failed', 'killed'] as const;
|
|
8
|
+
|
|
9
|
+
export type TaskStatus = (typeof TASK_STATUS_VALUES)[number];
|
|
10
|
+
export type TerminalTaskStatus = (typeof TERMINAL_TASK_STATUS_VALUES)[number];
|
|
11
|
+
export type KillKind = 'user' | 'timeout' | 'output_cap' | 'shutdown';
|
|
12
|
+
|
|
13
|
+
export type JsonObject = Readonly<Record<PropertyKey, unknown>>;
|
|
14
|
+
|
|
15
|
+
export interface TaskContextUsage {
|
|
16
|
+
tokens: number | null;
|
|
17
|
+
contextWindow: number;
|
|
18
|
+
percent: number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TaskTokenUsage {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
cacheRead: number;
|
|
25
|
+
cacheWrite: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
costTotal?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TaskToolUsage {
|
|
31
|
+
total: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
byName: Record<string, number>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BgTaskSnapshot {
|
|
37
|
+
id: string;
|
|
38
|
+
name?: string | undefined;
|
|
39
|
+
command: string;
|
|
40
|
+
description?: string | undefined;
|
|
41
|
+
status: TaskStatus;
|
|
42
|
+
outputPath: string;
|
|
43
|
+
cwd: string;
|
|
44
|
+
startTime: number;
|
|
45
|
+
endTime?: number | undefined;
|
|
46
|
+
exitCode?: number | null | undefined;
|
|
47
|
+
signal?: string | null | undefined;
|
|
48
|
+
pid?: number | undefined;
|
|
49
|
+
bytesWritten: number;
|
|
50
|
+
isAgent: boolean;
|
|
51
|
+
error?: string | undefined;
|
|
52
|
+
notified: boolean;
|
|
53
|
+
notifyOnCompletion: boolean;
|
|
54
|
+
triggerOnCompletion: boolean;
|
|
55
|
+
timeoutSeconds?: number | undefined;
|
|
56
|
+
contextUsage?: TaskContextUsage | undefined;
|
|
57
|
+
tokenUsage?: TaskTokenUsage | undefined;
|
|
58
|
+
toolUsage?: TaskToolUsage | undefined;
|
|
59
|
+
model?: string | undefined;
|
|
60
|
+
attestationPath?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AttestedPiTaskFiles {
|
|
64
|
+
eventsPath: string;
|
|
65
|
+
stderrPath: string;
|
|
66
|
+
wrapperPath: string;
|
|
67
|
+
attestationPath: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AttestedPiTaskSnapshot extends BgTaskSnapshot {
|
|
71
|
+
attestedPi?: AttestedPiTaskFiles | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
|
|
75
|
+
name: string;
|
|
76
|
+
outputAbsPath: string;
|
|
77
|
+
metadataAbsPath: string;
|
|
78
|
+
eventsAbsPath?: string | undefined;
|
|
79
|
+
stderrAbsPath?: string | undefined;
|
|
80
|
+
wrapperAbsPath?: string | undefined;
|
|
81
|
+
attestationAbsPath?: string | undefined;
|
|
82
|
+
child?: BackgroundTaskChildProcess | undefined;
|
|
83
|
+
stream?: WriteStream | undefined;
|
|
84
|
+
timeoutHandle?: NodeJS.Timeout | undefined;
|
|
85
|
+
killKind?: KillKind | undefined;
|
|
86
|
+
killSignalSent?: boolean | undefined;
|
|
87
|
+
capExceeded?: boolean | undefined;
|
|
88
|
+
finalized?: boolean | undefined;
|
|
89
|
+
terminalPublished?: boolean | undefined;
|
|
90
|
+
terminalPublishInFlight?: boolean | undefined;
|
|
91
|
+
terminalPublishRetryHandle?: NodeJS.Timeout | undefined;
|
|
92
|
+
/** Optional protocol barrier used by EventBus run requests so early child exits cannot publish before the run response is observable. */
|
|
93
|
+
terminalPublicationGate?: Promise<void> | undefined;
|
|
94
|
+
contextUsageBuffer?: string | undefined;
|
|
95
|
+
/** True when this task launched a telemetry-wrapped Pi agent; its stdout carries control lines, not raw output. */
|
|
96
|
+
telemetryWrapped?: boolean | undefined;
|
|
97
|
+
/** Partial trailing stdout line held between chunks while reconstructing wrapped-agent control lines. */
|
|
98
|
+
agentStdoutBuffer?: string | undefined;
|
|
99
|
+
attestationPath?: string | undefined;
|
|
100
|
+
attestedPi?: AttestedPiTaskFiles | undefined;
|
|
101
|
+
metadataWriteChain?: Promise<void> | undefined;
|
|
102
|
+
waiters: Array<() => void>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type CompletionDeliveryMode =
|
|
106
|
+
| 'notification-and-wake'
|
|
107
|
+
| 'notification-only'
|
|
108
|
+
| 'manual-monitoring';
|
|
109
|
+
|
|
110
|
+
export interface CompletionDeliveryGuidance {
|
|
111
|
+
readonly mode: CompletionDeliveryMode;
|
|
112
|
+
readonly notificationEnabled: boolean;
|
|
113
|
+
readonly automaticWakeEnabled: boolean;
|
|
114
|
+
readonly text: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Describe the actual parent-agent completion path for one bg_run launch.
|
|
119
|
+
* A wake request cannot take effect without the notification that carries it.
|
|
120
|
+
*/
|
|
121
|
+
export function deriveCompletionDeliveryGuidance(
|
|
122
|
+
notifyOnCompletion: boolean,
|
|
123
|
+
triggerOnCompletion: boolean,
|
|
124
|
+
): CompletionDeliveryGuidance {
|
|
125
|
+
if (notifyOnCompletion && triggerOnCompletion) {
|
|
126
|
+
return {
|
|
127
|
+
mode: 'notification-and-wake',
|
|
128
|
+
notificationEnabled: true,
|
|
129
|
+
automaticWakeEnabled: true,
|
|
130
|
+
text: [
|
|
131
|
+
'Terminal notification: enabled.',
|
|
132
|
+
'Automatic follow-up turn: enabled.',
|
|
133
|
+
'Next action: do not poll or sleep merely to wait; continue only independent useful work, otherwise end this turn and wait for <background-task-notification>.',
|
|
134
|
+
].join('\n'),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (notifyOnCompletion) {
|
|
139
|
+
return {
|
|
140
|
+
mode: 'notification-only',
|
|
141
|
+
notificationEnabled: true,
|
|
142
|
+
automaticWakeEnabled: false,
|
|
143
|
+
text: [
|
|
144
|
+
'Terminal notification: enabled.',
|
|
145
|
+
'Automatic follow-up turn: disabled. The terminal notification will be delivered, but it will not start an agent turn.',
|
|
146
|
+
'Next action: automatic wake-up was explicitly disabled; use bg_status/bg_logs only when deliberate monitoring is required, without tight polling.',
|
|
147
|
+
].join('\n'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
mode: 'manual-monitoring',
|
|
153
|
+
notificationEnabled: false,
|
|
154
|
+
automaticWakeEnabled: false,
|
|
155
|
+
text: [
|
|
156
|
+
'Terminal notification: disabled.',
|
|
157
|
+
triggerOnCompletion
|
|
158
|
+
? 'Automatic follow-up turn: disabled because terminal notifications are disabled. triggerOnCompletion has no effect while notifyOnCompletion is false.'
|
|
159
|
+
: 'Automatic follow-up turn: disabled.',
|
|
160
|
+
'Next action: completion delivery was explicitly disabled; use bg_status/bg_logs only for deliberate manual monitoring, without tight polling.',
|
|
161
|
+
].join('\n'),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface BgRunDetails {
|
|
166
|
+
task: BgTaskSnapshot;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface BgStatusDetails {
|
|
170
|
+
tasks: BgTaskSnapshot[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface BgLogsDetails {
|
|
174
|
+
task: BgTaskSnapshot;
|
|
175
|
+
path: string;
|
|
176
|
+
bytesRead: number;
|
|
177
|
+
truncated: boolean;
|
|
178
|
+
tail: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface BgKillDetails {
|
|
182
|
+
task: BgTaskSnapshot;
|
|
183
|
+
message: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface StartTaskOptions {
|
|
187
|
+
name?: string | undefined;
|
|
188
|
+
description?: string | undefined;
|
|
189
|
+
isAgent?: boolean | undefined;
|
|
190
|
+
timeoutSeconds?: number | undefined;
|
|
191
|
+
notifyOnCompletion?: boolean | undefined;
|
|
192
|
+
triggerOnCompletion?: boolean | undefined;
|
|
193
|
+
/** @internal EventBus protocol barrier; callers should not set this outside the extension service. */
|
|
194
|
+
terminalPublicationGate?: Promise<void> | undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface StartAttestedPiTaskOptions {
|
|
198
|
+
name: string;
|
|
199
|
+
provider: string;
|
|
200
|
+
model: string;
|
|
201
|
+
prompt: string;
|
|
202
|
+
reportPath: string;
|
|
203
|
+
extraPiArgs?: string[] | undefined;
|
|
204
|
+
thinking?: string | undefined;
|
|
205
|
+
timeoutSeconds?: number | undefined;
|
|
206
|
+
}
|
|
103
207
|
|
|
104
208
|
export const DEFAULT_LOG_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
|
|
105
209
|
export const MAX_LOG_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
|
|
106
210
|
export const COMMAND_PREVIEW_CHARS = 90;
|
|
211
|
+
const parseJsonValue: (text: string) => unknown = globalThis.JSON.parse;
|
|
212
|
+
|
|
213
|
+
export function isJsonObject(value: unknown): value is JsonObject {
|
|
214
|
+
return typeof value === 'object' && value !== null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function parseJsonText(text: string): unknown {
|
|
218
|
+
return parseJsonValue(text);
|
|
219
|
+
}
|
|
107
220
|
|
|
108
221
|
export function sanitizePathSegment(value: string): string {
|
|
109
|
-
|
|
110
|
-
|
|
222
|
+
const sanitized = value.replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
223
|
+
return sanitized || 'session';
|
|
111
224
|
}
|
|
112
225
|
|
|
113
226
|
export function stripMatchingQuotes(value: string): string {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
227
|
+
const trimmed = value.trim();
|
|
228
|
+
if (trimmed.length >= 2) {
|
|
229
|
+
const first = trimmed[0];
|
|
230
|
+
const last = trimmed[trimmed.length - 1];
|
|
231
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
232
|
+
return trimmed.slice(1, -1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return trimmed;
|
|
123
236
|
}
|
|
124
237
|
|
|
125
238
|
export function compactWhitespace(value: string): string {
|
|
126
|
-
|
|
239
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
127
240
|
}
|
|
128
241
|
|
|
129
242
|
export function truncateChars(value: string, maxChars: number): string {
|
|
130
|
-
|
|
131
|
-
|
|
243
|
+
if (value.length <= maxChars) return value;
|
|
244
|
+
return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
132
245
|
}
|
|
133
246
|
|
|
134
247
|
export function normalizeTaskName(value: unknown): string | undefined {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
248
|
+
if (typeof value !== 'string') return undefined;
|
|
249
|
+
const normalized = compactWhitespace(stripMatchingQuotes(value));
|
|
250
|
+
if (!normalized) return undefined;
|
|
251
|
+
return truncateChars(normalized, 80);
|
|
139
252
|
}
|
|
140
253
|
|
|
141
254
|
export function deriveTaskNameFromCommand(command: string): string {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
255
|
+
const normalized = compactWhitespace(stripMatchingQuotes(command));
|
|
256
|
+
if (!normalized) return 'Background task';
|
|
257
|
+
|
|
258
|
+
const packageScript = /^(npm|pnpm|yarn|bun)\s+(?:(run)\s+)?([^\s;&|]+)/.exec(normalized);
|
|
259
|
+
if (packageScript) {
|
|
260
|
+
const runner = packageScript[1] ?? 'npm';
|
|
261
|
+
const run = packageScript[2] !== undefined ? ' run' : '';
|
|
262
|
+
const script = packageScript[3] ?? '';
|
|
263
|
+
return truncateChars(`${runner}${run} ${script}`, 48);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const words = normalized.split(/\s+/).slice(0, 5).join(' ');
|
|
267
|
+
return truncateChars(words.length > 0 ? words : normalized, 48);
|
|
155
268
|
}
|
|
156
269
|
|
|
157
|
-
export function taskDisplayName(task: {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
270
|
+
export function taskDisplayName(task: {
|
|
271
|
+
name?: string | undefined;
|
|
272
|
+
description?: string | undefined;
|
|
273
|
+
command?: string | undefined;
|
|
274
|
+
id?: string | undefined;
|
|
275
|
+
}): string {
|
|
276
|
+
const commandName =
|
|
277
|
+
task.command && task.command.length > 0 ? deriveTaskNameFromCommand(task.command) : undefined;
|
|
278
|
+
return (
|
|
279
|
+
normalizeTaskName(task.name) ??
|
|
280
|
+
normalizeTaskName(task.description) ??
|
|
281
|
+
commandName ??
|
|
282
|
+
task.id ??
|
|
283
|
+
'Background task'
|
|
284
|
+
);
|
|
163
285
|
}
|
|
164
286
|
|
|
165
287
|
function parseNameValueAndRest(valueAndRest: string): { value: string; rest: string } | undefined {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function parseBgCommandArgs(args: string): {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
288
|
+
const input = valueAndRest.trimStart();
|
|
289
|
+
if (!input) return undefined;
|
|
290
|
+
const quote = input[0];
|
|
291
|
+
if (quote === '"' || quote === "'") {
|
|
292
|
+
let escaped = false;
|
|
293
|
+
let value = '';
|
|
294
|
+
for (let i = 1; i < input.length; i++) {
|
|
295
|
+
const char = input.charAt(i);
|
|
296
|
+
if (escaped) {
|
|
297
|
+
value += char;
|
|
298
|
+
escaped = false;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (char === '\\') {
|
|
302
|
+
escaped = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (char === quote) {
|
|
306
|
+
return { value, rest: input.slice(i + 1).trimStart() };
|
|
307
|
+
}
|
|
308
|
+
value += char;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(input);
|
|
313
|
+
if (!match) return undefined;
|
|
314
|
+
const parsedValue = match[1];
|
|
315
|
+
if (parsedValue === undefined) return undefined;
|
|
316
|
+
return { value: parsedValue, rest: match[2]?.trimStart() ?? '' };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function parseBgCommandArgs(args: string): {
|
|
320
|
+
name?: string;
|
|
321
|
+
command: string;
|
|
322
|
+
isAgent: boolean;
|
|
323
|
+
} {
|
|
324
|
+
let input = args.trim();
|
|
325
|
+
let name: string | undefined;
|
|
326
|
+
let isAgent = false;
|
|
327
|
+
|
|
328
|
+
while (input) {
|
|
329
|
+
let consumed = false;
|
|
330
|
+
for (const prefix of ['--name=', '-n=']) {
|
|
331
|
+
if (input.startsWith(prefix)) {
|
|
332
|
+
const parsed = parseNameValueAndRest(input.slice(prefix.length));
|
|
333
|
+
if (!parsed) throw new Error(`${prefix.slice(0, -1)} requires a task name`);
|
|
334
|
+
name = normalizeTaskName(parsed.value);
|
|
335
|
+
input = parsed.rest;
|
|
336
|
+
consumed = true;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (consumed) continue;
|
|
341
|
+
|
|
342
|
+
for (const prefix of ['--name', '-n']) {
|
|
343
|
+
if (input === prefix || input.startsWith(`${prefix} `) || input.startsWith(`${prefix}\t`)) {
|
|
344
|
+
const parsed = parseNameValueAndRest(input.slice(prefix.length));
|
|
345
|
+
if (!parsed) throw new Error(`${prefix} requires a task name`);
|
|
346
|
+
name = normalizeTaskName(parsed.value);
|
|
347
|
+
input = parsed.rest;
|
|
348
|
+
consumed = true;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (consumed) continue;
|
|
353
|
+
|
|
354
|
+
for (const flag of ['--agent', '--llm-agent']) {
|
|
355
|
+
if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
|
|
356
|
+
isAgent = true;
|
|
357
|
+
input = input.slice(flag.length).trimStart();
|
|
358
|
+
consumed = true;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (consumed) continue;
|
|
363
|
+
|
|
364
|
+
for (const flag of ['--script', '--no-agent']) {
|
|
365
|
+
if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
|
|
366
|
+
isAgent = false;
|
|
367
|
+
input = input.slice(flag.length).trimStart();
|
|
368
|
+
consumed = true;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (consumed) continue;
|
|
373
|
+
|
|
374
|
+
if (input === '--') {
|
|
375
|
+
input = '';
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (input.startsWith('-- ')) {
|
|
379
|
+
input = input.slice(3).trimStart();
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return name ? { name, command: input, isAgent } : { command: input, isAgent };
|
|
260
386
|
}
|
|
261
387
|
|
|
262
388
|
export function formatDuration(ms: number): string {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
389
|
+
if (ms < 1000) return `${String(ms)}ms`;
|
|
390
|
+
const seconds = Math.floor(ms / 1000);
|
|
391
|
+
if (seconds < 60) return `${String(seconds)}s`;
|
|
392
|
+
const minutes = Math.floor(seconds / 60);
|
|
393
|
+
const remSeconds = seconds % 60;
|
|
394
|
+
if (minutes < 60) return `${String(minutes)}m${remSeconds > 0 ? `${String(remSeconds)}s` : ''}`;
|
|
395
|
+
const hours = Math.floor(minutes / 60);
|
|
396
|
+
const remMinutes = minutes % 60;
|
|
397
|
+
return `${String(hours)}h${remMinutes > 0 ? `${String(remMinutes)}m` : ''}`;
|
|
272
398
|
}
|
|
273
399
|
|
|
274
400
|
export function formatCompactNumber(count: number): string {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
401
|
+
const normalized = Math.max(0, Math.floor(count));
|
|
402
|
+
if (normalized < 1000) return normalized.toString();
|
|
403
|
+
if (normalized < 10000) return `${(normalized / 1000).toFixed(1)}k`;
|
|
404
|
+
if (normalized < 1000000) return `${String(Math.round(normalized / 1000))}k`;
|
|
405
|
+
if (normalized < 10000000) return `${(normalized / 1000000).toFixed(1)}M`;
|
|
406
|
+
return `${String(Math.round(normalized / 1000000))}M`;
|
|
281
407
|
}
|
|
282
408
|
|
|
283
409
|
export function formatContextUsageSummary(usage?: TaskContextUsage): string | undefined {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
410
|
+
if (usage?.contextWindow === undefined || usage.contextWindow <= 0) return undefined;
|
|
411
|
+
const window = formatCompactNumber(usage.contextWindow);
|
|
412
|
+
if (usage.percent === null || usage.tokens === null) return `ctx=?/${window}`;
|
|
413
|
+
return `ctx=${usage.percent.toFixed(1)}%/${window}`;
|
|
288
414
|
}
|
|
289
415
|
|
|
290
416
|
export function formatTokenUsageSummary(usage?: TaskTokenUsage): string | undefined {
|
|
291
|
-
|
|
292
|
-
|
|
417
|
+
if (!usage || usage.totalTokens <= 0) return undefined;
|
|
418
|
+
return `tokens=${formatCompactNumber(usage.totalTokens)}`;
|
|
293
419
|
}
|
|
294
420
|
|
|
295
421
|
export function formatToolUsageSummary(usage?: TaskToolUsage): string | undefined {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
422
|
+
if (!usage || (usage.total <= 0 && usage.failed <= 0)) return undefined;
|
|
423
|
+
const failed = usage.failed > 0 ? ` failed=${String(usage.failed)}` : '';
|
|
424
|
+
return `tools=${String(usage.total)}${failed}`;
|
|
299
425
|
}
|
|
300
426
|
|
|
301
427
|
export function formatModelSummary(model?: string): string | undefined {
|
|
302
|
-
|
|
303
|
-
|
|
428
|
+
if (!model) return undefined;
|
|
429
|
+
return `model=${model}`;
|
|
304
430
|
}
|
|
305
431
|
|
|
306
432
|
/**
|
|
@@ -312,45 +438,58 @@ export function formatModelSummary(model?: string): string | undefined {
|
|
|
312
438
|
* file instead of leaking raw telemetry JSON. Both the parser and the formatter
|
|
313
439
|
* are pure so the visible transcript is fully unit-testable.
|
|
314
440
|
*/
|
|
315
|
-
export const AGENT_ACTIVITY_TYPE =
|
|
441
|
+
export const AGENT_ACTIVITY_TYPE = 'background-task-activity';
|
|
316
442
|
const AGENT_ACTIVITY_DETAIL_MAX = 80;
|
|
317
443
|
|
|
318
444
|
export type AgentActivity =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
445
|
+
| { kind: 'assistant_text'; text: string }
|
|
446
|
+
| { kind: 'reasoning'; text: string }
|
|
447
|
+
| { kind: 'tool_start'; tool: string; argsSummary: string }
|
|
448
|
+
| { kind: 'tool_end'; tool: string; isError: boolean; error?: string };
|
|
449
|
+
|
|
450
|
+
interface AgentActivityPayload extends JsonObject {
|
|
451
|
+
readonly type?: unknown;
|
|
452
|
+
readonly kind?: unknown;
|
|
453
|
+
readonly text?: unknown;
|
|
454
|
+
readonly tool?: unknown;
|
|
455
|
+
readonly argsSummary?: unknown;
|
|
456
|
+
readonly isError?: unknown;
|
|
457
|
+
readonly error?: unknown;
|
|
458
|
+
}
|
|
323
459
|
|
|
324
|
-
function readActivityString(
|
|
325
|
-
|
|
326
|
-
|
|
460
|
+
function readActivityString(
|
|
461
|
+
record: AgentActivityPayload,
|
|
462
|
+
key: 'text' | 'tool' | 'argsSummary' | 'error',
|
|
463
|
+
): string | undefined {
|
|
464
|
+
const value = record[key];
|
|
465
|
+
return typeof value === 'string' ? value : undefined;
|
|
327
466
|
}
|
|
328
467
|
|
|
329
468
|
/** Narrow a parsed `background-task-activity` control payload into a typed {@link AgentActivity}. */
|
|
330
469
|
export function parseAgentActivity(payload: unknown): AgentActivity | undefined {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
470
|
+
if (!isJsonObject(payload)) return undefined;
|
|
471
|
+
const record: AgentActivityPayload = payload;
|
|
472
|
+
if (record.type !== AGENT_ACTIVITY_TYPE) return undefined;
|
|
473
|
+
const kind = record.kind;
|
|
474
|
+
if (kind === 'assistant_text' || kind === 'reasoning') {
|
|
475
|
+
const text = readActivityString(record, 'text');
|
|
476
|
+
if (text === undefined) return undefined;
|
|
477
|
+
return { kind, text };
|
|
478
|
+
}
|
|
479
|
+
if (kind === 'tool_start') {
|
|
480
|
+
const tool = readActivityString(record, 'tool');
|
|
481
|
+
if (!tool) return undefined;
|
|
482
|
+
return { kind, tool, argsSummary: readActivityString(record, 'argsSummary') ?? '' };
|
|
483
|
+
}
|
|
484
|
+
if (kind === 'tool_end') {
|
|
485
|
+
const tool = readActivityString(record, 'tool');
|
|
486
|
+
if (!tool) return undefined;
|
|
487
|
+
const activity: AgentActivity = { kind, tool, isError: record.isError === true };
|
|
488
|
+
const error = readActivityString(record, 'error');
|
|
489
|
+
if (error !== undefined && error.trim().length > 0) activity.error = error;
|
|
490
|
+
return activity;
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
354
493
|
}
|
|
355
494
|
|
|
356
495
|
/**
|
|
@@ -360,189 +499,217 @@ export function parseAgentActivity(payload: unknown): AgentActivity | undefined
|
|
|
360
499
|
* line already announced the call, and the next line implies completion.
|
|
361
500
|
*/
|
|
362
501
|
export function formatAgentActivityLine(activity: AgentActivity): string | undefined {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
502
|
+
if (activity.kind === 'assistant_text') {
|
|
503
|
+
const text = activity.text.replace(/\s+$/u, '');
|
|
504
|
+
return text.trim().length > 0 ? text : undefined;
|
|
505
|
+
}
|
|
506
|
+
if (activity.kind === 'reasoning') {
|
|
507
|
+
const text = activity.text.replace(/\s+$/u, '');
|
|
508
|
+
return text.trim().length > 0 ? `\u2026 ${text}` : undefined;
|
|
509
|
+
}
|
|
510
|
+
if (activity.kind === 'tool_start') {
|
|
511
|
+
const summary = compactWhitespace(activity.argsSummary);
|
|
512
|
+
const suffix =
|
|
513
|
+
summary.length > 0 ? ` ${truncateChars(summary, AGENT_ACTIVITY_DETAIL_MAX)}` : '';
|
|
514
|
+
return `\u2192 ${activity.tool}${suffix}`;
|
|
515
|
+
}
|
|
516
|
+
if (!activity.isError) return undefined;
|
|
517
|
+
const detail = activity.error
|
|
518
|
+
? `: ${truncateChars(compactWhitespace(activity.error), AGENT_ACTIVITY_DETAIL_MAX)}`
|
|
519
|
+
: '';
|
|
520
|
+
return `\u2717 ${activity.tool} failed${detail}`;
|
|
379
521
|
}
|
|
380
522
|
|
|
381
523
|
export function shellQuote(value: string): string {
|
|
382
|
-
|
|
524
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
383
525
|
}
|
|
384
526
|
|
|
385
527
|
export function shellInvocation(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
528
|
+
command: string,
|
|
529
|
+
platform: NodeJS.Platform = process.platform,
|
|
530
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
389
531
|
): { shell: string; args: string[] } {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
532
|
+
if (platform === 'win32') {
|
|
533
|
+
const comSpec = env['ComSpec'];
|
|
534
|
+
return {
|
|
535
|
+
shell: comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe',
|
|
536
|
+
args: ['/d', '/s', '/c', command],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const shell = env['SHELL'];
|
|
540
|
+
return { shell: shell && shell.length > 0 ? shell : '/bin/sh', args: ['-c', command] };
|
|
394
541
|
}
|
|
395
542
|
|
|
396
543
|
export function normalizeMaxBytes(value: unknown, fallback = DEFAULT_LOG_BYTES): number {
|
|
397
|
-
|
|
398
|
-
|
|
544
|
+
const raw = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
|
|
545
|
+
return Math.max(1, Math.min(MAX_LOG_BYTES, raw));
|
|
399
546
|
}
|
|
400
547
|
|
|
401
548
|
export function snapshot(task: BgTask): BgTaskSnapshot {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
549
|
+
return {
|
|
550
|
+
id: task.id,
|
|
551
|
+
name: taskDisplayName(task),
|
|
552
|
+
command: task.command,
|
|
553
|
+
description: task.description,
|
|
554
|
+
status: task.status,
|
|
555
|
+
outputPath: task.outputPath,
|
|
556
|
+
cwd: task.cwd,
|
|
557
|
+
startTime: task.startTime,
|
|
558
|
+
endTime: task.endTime,
|
|
559
|
+
exitCode: task.exitCode,
|
|
560
|
+
signal: task.signal,
|
|
561
|
+
pid: task.pid,
|
|
562
|
+
bytesWritten: task.bytesWritten,
|
|
563
|
+
isAgent: task.isAgent,
|
|
564
|
+
error: task.error,
|
|
565
|
+
notified: task.notified,
|
|
566
|
+
notifyOnCompletion: task.notifyOnCompletion,
|
|
567
|
+
triggerOnCompletion: task.triggerOnCompletion,
|
|
568
|
+
timeoutSeconds: task.timeoutSeconds,
|
|
569
|
+
contextUsage: task.contextUsage,
|
|
570
|
+
tokenUsage: task.tokenUsage,
|
|
571
|
+
toolUsage: task.toolUsage,
|
|
572
|
+
model: task.model,
|
|
573
|
+
attestationPath: task.attestationPath,
|
|
574
|
+
};
|
|
427
575
|
}
|
|
428
576
|
|
|
429
577
|
export function formatSnapshotList(tasks: BgTaskSnapshot[], now = Date.now()): string {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
578
|
+
if (tasks.length === 0) return 'No background tasks in this Pi extension runtime.';
|
|
579
|
+
return tasks
|
|
580
|
+
.map((task) => {
|
|
581
|
+
const statusIcon =
|
|
582
|
+
task.status === 'running'
|
|
583
|
+
? '▶'
|
|
584
|
+
: task.status === 'completed'
|
|
585
|
+
? '✓'
|
|
586
|
+
: task.status === 'killed'
|
|
587
|
+
? '■'
|
|
588
|
+
: '✗';
|
|
589
|
+
const age = formatDuration((task.endTime ?? now) - task.startTime);
|
|
590
|
+
const code = task.exitCode !== undefined ? ` exit=${String(task.exitCode)}` : '';
|
|
591
|
+
const pid = task.pid !== undefined ? ` pid=${String(task.pid)}` : '';
|
|
592
|
+
const error = task.error ? ` error=${truncateChars(task.error, 80)}` : '';
|
|
593
|
+
const telemetry = [
|
|
594
|
+
formatContextUsageSummary(task.contextUsage),
|
|
595
|
+
formatModelSummary(task.model),
|
|
596
|
+
formatTokenUsageSummary(task.tokenUsage),
|
|
597
|
+
formatToolUsageSummary(task.toolUsage),
|
|
598
|
+
]
|
|
599
|
+
.filter(Boolean)
|
|
600
|
+
.join(' ');
|
|
601
|
+
const telemetryText = telemetry ? ` ${telemetry}` : '';
|
|
602
|
+
return `${statusIcon} ${task.id} ${task.status} ${age}${code}${pid}${telemetryText} — ${truncateChars(taskDisplayName(task), COMMAND_PREVIEW_CHARS)}${error}\n output: ${task.outputPath}`;
|
|
603
|
+
})
|
|
604
|
+
.join('\n');
|
|
446
605
|
}
|
|
447
606
|
|
|
448
607
|
export async function boundedRead(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
608
|
+
filePath: string,
|
|
609
|
+
maxBytes: number,
|
|
610
|
+
tail: boolean,
|
|
452
611
|
): Promise<{ content: string; truncated: boolean; bytesRead: number; totalBytes: number }> {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
612
|
+
const stats = statSync(filePath);
|
|
613
|
+
const totalBytes = stats.size;
|
|
614
|
+
const bytesToRead = Math.min(totalBytes, maxBytes);
|
|
615
|
+
if (bytesToRead === 0) return { content: '', truncated: false, bytesRead: 0, totalBytes };
|
|
616
|
+
|
|
617
|
+
const file = await open(filePath, 'r');
|
|
618
|
+
try {
|
|
619
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
620
|
+
const position = tail ? Math.max(0, totalBytes - bytesToRead) : 0;
|
|
621
|
+
const { bytesRead } = await file.read(buffer, 0, bytesToRead, position);
|
|
622
|
+
return {
|
|
623
|
+
content: buffer.subarray(0, bytesRead).toString('utf8'),
|
|
624
|
+
truncated: totalBytes > bytesRead,
|
|
625
|
+
bytesRead,
|
|
626
|
+
totalBytes,
|
|
627
|
+
};
|
|
628
|
+
} finally {
|
|
629
|
+
await file.close();
|
|
630
|
+
}
|
|
472
631
|
}
|
|
473
632
|
|
|
474
633
|
export function escapeXml(value: string): string {
|
|
475
|
-
|
|
634
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
476
635
|
}
|
|
477
636
|
|
|
478
|
-
export const UPDATE_COMMAND =
|
|
637
|
+
export const UPDATE_COMMAND = '/bg-update';
|
|
479
638
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
639
|
+
interface ParsedSemver {
|
|
640
|
+
major: number;
|
|
641
|
+
minor: number;
|
|
642
|
+
patch: number;
|
|
643
|
+
prerelease: string[];
|
|
644
|
+
}
|
|
486
645
|
|
|
487
646
|
const SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
488
647
|
|
|
489
648
|
export function parseSemver(value: string): ParsedSemver | undefined {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
649
|
+
if (typeof value !== 'string') return undefined;
|
|
650
|
+
const match = SEMVER_PATTERN.exec(value.trim());
|
|
651
|
+
if (!match) return undefined;
|
|
652
|
+
const majorRaw = match[1];
|
|
653
|
+
const minorRaw = match[2];
|
|
654
|
+
const patchRaw = match[3];
|
|
655
|
+
if (majorRaw === undefined || minorRaw === undefined || patchRaw === undefined) return undefined;
|
|
656
|
+
const major = Number(majorRaw);
|
|
657
|
+
const minor = Number(minorRaw);
|
|
658
|
+
const patch = Number(patchRaw);
|
|
659
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch))
|
|
660
|
+
return undefined;
|
|
661
|
+
const prerelease = match[4] !== undefined ? match[4].split('.') : [];
|
|
662
|
+
return { major, minor, patch, prerelease };
|
|
499
663
|
}
|
|
500
664
|
|
|
501
665
|
function comparePrerelease(a: string[], b: string[]): number {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
666
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
667
|
+
// A version without prerelease identifiers outranks the same core with prerelease identifiers.
|
|
668
|
+
if (a.length === 0) return 1;
|
|
669
|
+
if (b.length === 0) return -1;
|
|
670
|
+
const shared = Math.min(a.length, b.length);
|
|
671
|
+
for (let i = 0; i < shared; i++) {
|
|
672
|
+
const idA = a[i];
|
|
673
|
+
const idB = b[i];
|
|
674
|
+
if (idA === undefined || idB === undefined) break;
|
|
675
|
+
if (idA === idB) continue;
|
|
676
|
+
const numericA = /^\d+$/.test(idA);
|
|
677
|
+
const numericB = /^\d+$/.test(idB);
|
|
678
|
+
if (numericA && numericB) {
|
|
679
|
+
const diff = Number(idA) - Number(idB);
|
|
680
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
// Numeric identifiers always have lower precedence than non-numeric identifiers.
|
|
684
|
+
if (numericA) return -1;
|
|
685
|
+
if (numericB) return 1;
|
|
686
|
+
return idA < idB ? -1 : 1;
|
|
687
|
+
}
|
|
688
|
+
if (a.length === b.length) return 0;
|
|
689
|
+
return a.length < b.length ? -1 : 1;
|
|
526
690
|
}
|
|
527
691
|
|
|
528
692
|
/** Compare two semver strings. Returns -1/0/1, or undefined when either side is not valid semver. */
|
|
529
693
|
export function compareSemver(a: string, b: string): number | undefined {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
694
|
+
const left = parseSemver(a);
|
|
695
|
+
const right = parseSemver(b);
|
|
696
|
+
if (!left || !right) return undefined;
|
|
697
|
+
if (left.major !== right.major) return left.major < right.major ? -1 : 1;
|
|
698
|
+
if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
|
|
699
|
+
if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
|
|
700
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
537
701
|
}
|
|
538
702
|
|
|
539
703
|
export function isNewerVersion(latest: string, current: string): boolean {
|
|
540
|
-
|
|
704
|
+
return compareSemver(latest, current) === 1;
|
|
541
705
|
}
|
|
542
706
|
|
|
543
707
|
/** Footer segment shown only when a newer published version exists; undefined otherwise. */
|
|
544
|
-
export function formatUpdateSegment(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
708
|
+
export function formatUpdateSegment(
|
|
709
|
+
latest: string | undefined,
|
|
710
|
+
current: string,
|
|
711
|
+
): string | undefined {
|
|
712
|
+
if (!latest) return undefined;
|
|
713
|
+
if (!isNewerVersion(latest, current)) return undefined;
|
|
714
|
+
return `\u2b06 v${latest} ${UPDATE_COMMAND}`;
|
|
548
715
|
}
|