@userreason/cli 0.1.1 → 0.2.1
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 +217 -0
- package/dist/bin.js +93 -8
- package/dist/catalog.js +29 -3
- package/dist/client.d.ts +8 -2
- package/dist/client.js +26 -11
- package/dist/first-survey-preparation.d.ts +8 -0
- package/dist/first-survey-preparation.js +29 -0
- package/dist/first-survey-source.d.ts +61 -0
- package/dist/first-survey-source.js +298 -0
- package/dist/first-survey-terminal.d.ts +5 -0
- package/dist/first-survey-terminal.js +16 -0
- package/dist/first-survey.d.ts +88 -0
- package/dist/first-survey.js +227 -0
- package/dist/help.js +6 -6
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/profile.js +1 -1
- package/dist/recommendation-source.d.ts +13 -0
- package/dist/recommendation-source.js +211 -0
- package/dist/recommended-surveys.d.ts +30 -0
- package/dist/recommended-surveys.js +201 -0
- package/dist/setup-diagnostics.d.ts +2 -0
- package/dist/setup-diagnostics.js +26 -0
- package/dist/setup-executor.d.ts +42 -0
- package/dist/setup-executor.js +350 -0
- package/dist/setup-files.d.ts +11 -0
- package/dist/setup-files.js +96 -0
- package/dist/setup-heartbeat.d.ts +268 -0
- package/dist/setup-heartbeat.js +120 -0
- package/dist/setup-runner.d.ts +16 -0
- package/dist/setup-runner.js +191 -0
- package/dist/setup-source.d.ts +32 -0
- package/dist/setup-source.js +48 -0
- package/dist/setup-store.d.ts +37 -0
- package/dist/setup-store.js +266 -0
- package/dist/setup-terminal.d.ts +3 -0
- package/dist/setup-terminal.js +6 -0
- package/dist/setup-wire.d.ts +1367 -0
- package/dist/setup-wire.js +190 -0
- package/dist/setup.d.ts +1423 -0
- package/dist/setup.js +264 -0
- package/dist/starter-survey.d.ts +22 -0
- package/dist/starter-survey.js +457 -0
- package/dist/telemetry.d.ts +67 -0
- package/dist/telemetry.js +304 -0
- package/dist/workos.d.ts +1 -1
- package/dist/workos.js +4 -2
- package/package.json +3 -2
- package/skills/userreason/SKILL.md +43 -0
- package/skills/userreason/references/management.md +6 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, realpath } from 'node:fs/promises';
|
|
4
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { SetupError } from './setup-store.js';
|
|
6
|
+
export const setupHash = (value) => createHash('sha256').update(value).digest('hex');
|
|
7
|
+
export function setupCanonical(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return `[${value.map(setupCanonical).join(',')}]`;
|
|
10
|
+
if (value && typeof value === 'object')
|
|
11
|
+
return `{${Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${setupCanonical(v)}`).join(',')}}`;
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
export const setupDigest = (value) => setupHash(setupCanonical(value));
|
|
15
|
+
/** All paths are project relative. Refuse links, hard links and escaping ancestors. */
|
|
16
|
+
async function checkedPath(directory, name, createParents = false) {
|
|
17
|
+
const root = await realpath(directory);
|
|
18
|
+
if (!name || name.length > 240 || /[\\:\x00-\x1f\x7f]/.test(name) || name.split('/').some(p => !p || p === '.' || p === '..'))
|
|
19
|
+
throw new SetupError('setup_path_unsafe', 'The reviewed path must be inside the original real project directory.');
|
|
20
|
+
const target = resolve(root, name);
|
|
21
|
+
if (relative(root, target).startsWith(`..${sep}`))
|
|
22
|
+
throw new SetupError('setup_path_unsafe', 'The reviewed path escapes the project.');
|
|
23
|
+
let parent = root;
|
|
24
|
+
for (const part of name.split('/').slice(0, -1)) {
|
|
25
|
+
parent = join(parent, part);
|
|
26
|
+
let info = await lstat(parent).catch(error => { if (error.code === 'ENOENT')
|
|
27
|
+
return null; throw error; });
|
|
28
|
+
if (!info && createParents) {
|
|
29
|
+
await mkdir(parent);
|
|
30
|
+
info = await lstat(parent);
|
|
31
|
+
}
|
|
32
|
+
if (!info)
|
|
33
|
+
return target;
|
|
34
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
35
|
+
throw new SetupError('setup_path_unsafe', 'A reviewed directory was replaced or is a link.');
|
|
36
|
+
}
|
|
37
|
+
return target;
|
|
38
|
+
}
|
|
39
|
+
export async function readSetupFile(directory, path) {
|
|
40
|
+
const target = await checkedPath(directory, path);
|
|
41
|
+
let before;
|
|
42
|
+
try {
|
|
43
|
+
before = await lstat(target);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === 'ENOENT')
|
|
47
|
+
return null;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > 2_000_000)
|
|
51
|
+
throw new SetupError('setup_path_unsafe', 'A reviewed file is not a bounded ordinary file.');
|
|
52
|
+
const handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
53
|
+
try {
|
|
54
|
+
const current = await handle.stat();
|
|
55
|
+
if (before.dev !== current.dev || before.ino !== current.ino || current.nlink !== 1 || current.size > 2_000_000)
|
|
56
|
+
throw new SetupError('setup_path_unsafe', 'A reviewed file changed while reading.');
|
|
57
|
+
const bytes = await handle.readFile();
|
|
58
|
+
if (bytes.length > 2_000_000)
|
|
59
|
+
throw new SetupError('setup_path_unsafe', 'A reviewed file exceeds the size limit.');
|
|
60
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function writeSetupFile(directory, file) {
|
|
67
|
+
// Verify drift before creating even a parent directory for a planned file.
|
|
68
|
+
if (await readSetupFile(directory, file.path) !== file.before)
|
|
69
|
+
throw new SetupError('setup_files_changed', 'The reviewed file changed. Review a new plan before applying it.');
|
|
70
|
+
const target = await checkedPath(directory, file.path, true);
|
|
71
|
+
if (await readSetupFile(directory, file.path) !== file.before)
|
|
72
|
+
throw new SetupError('setup_files_changed', 'The reviewed file changed. Review a new plan before applying it.');
|
|
73
|
+
if (file.before === file.after)
|
|
74
|
+
return;
|
|
75
|
+
const info = file.before === null ? null : await lstat(target);
|
|
76
|
+
const handle = await open(target, info ? constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) : constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o644);
|
|
77
|
+
try {
|
|
78
|
+
const now = await handle.stat();
|
|
79
|
+
if (!now.isFile() || now.nlink !== 1 || info && (now.dev !== info.dev || now.ino !== info.ino))
|
|
80
|
+
throw new SetupError('setup_files_changed', 'The reviewed file changed while opening.');
|
|
81
|
+
if (info && await readSetupFile(directory, file.path) !== file.before)
|
|
82
|
+
throw new SetupError('setup_files_changed', 'The reviewed file changed while opening.');
|
|
83
|
+
await handle.writeFile(file.after);
|
|
84
|
+
await handle.truncate(Buffer.byteLength(file.after));
|
|
85
|
+
await handle.sync();
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await handle.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function verifySetupFiles(directory, files, state) {
|
|
92
|
+
for (const file of files)
|
|
93
|
+
if (await readSetupFile(directory, file.path) !== file[state])
|
|
94
|
+
return false;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { BrowserSetupClient } from './setup.js';
|
|
2
|
+
import type { SetupSessionView } from './setup-wire.js';
|
|
3
|
+
/** Stop waiting when this run ends, including user-supplied preparations. */
|
|
4
|
+
export declare function setupAbortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
5
|
+
/** One lease-aware heartbeat for the entire run, independent of its current stage. */
|
|
6
|
+
export declare class SetupHeartbeat {
|
|
7
|
+
private client;
|
|
8
|
+
private options;
|
|
9
|
+
private session?;
|
|
10
|
+
private timer?;
|
|
11
|
+
private pending?;
|
|
12
|
+
private stopped;
|
|
13
|
+
private paused;
|
|
14
|
+
private failure?;
|
|
15
|
+
private lease?;
|
|
16
|
+
private readonly lifetime;
|
|
17
|
+
readonly signal: AbortSignal;
|
|
18
|
+
constructor(client: Pick<BrowserSetupClient, 'identity' | 'heartbeat'>, options?: {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
heartbeatMs?: number;
|
|
21
|
+
});
|
|
22
|
+
observe(next: SetupSessionView): SetupSessionView;
|
|
23
|
+
get current(): ({
|
|
24
|
+
phase: "expired" | "cancelled" | "analyzing" | "configuring" | "awaiting_local_approval" | "applying" | "ready_for_recommendations" | "selecting_drafts" | "creating_drafts" | "agent_test" | "complete";
|
|
25
|
+
revision: string;
|
|
26
|
+
workspace_id: string | null;
|
|
27
|
+
site_id: string | null;
|
|
28
|
+
project_fingerprint: string;
|
|
29
|
+
choices: {
|
|
30
|
+
install_sdk: boolean;
|
|
31
|
+
install_skill: boolean;
|
|
32
|
+
footer_badge: boolean;
|
|
33
|
+
} & {};
|
|
34
|
+
capabilities: {
|
|
35
|
+
framework: string;
|
|
36
|
+
package_manager: string;
|
|
37
|
+
install_packages: boolean;
|
|
38
|
+
initialize_sdk: boolean;
|
|
39
|
+
install_skill: boolean;
|
|
40
|
+
insert_footer: boolean;
|
|
41
|
+
source_review: boolean;
|
|
42
|
+
sample_read: boolean;
|
|
43
|
+
} & {
|
|
44
|
+
onboarding_v2?: boolean | undefined;
|
|
45
|
+
project_name?: string | undefined;
|
|
46
|
+
launcher?: ({
|
|
47
|
+
wizard_version: string;
|
|
48
|
+
api_origin: string;
|
|
49
|
+
} & {
|
|
50
|
+
registry?: string | undefined;
|
|
51
|
+
}) | undefined;
|
|
52
|
+
terminal?: ({
|
|
53
|
+
platform: string;
|
|
54
|
+
app: string;
|
|
55
|
+
} & {}) | undefined;
|
|
56
|
+
};
|
|
57
|
+
local_plan: ({
|
|
58
|
+
project_fingerprint: string;
|
|
59
|
+
choices_digest: string;
|
|
60
|
+
effects: import("./setup-wire.js").SetupEffect[];
|
|
61
|
+
} & {
|
|
62
|
+
manual_steps?: ({
|
|
63
|
+
kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
|
|
64
|
+
code: string;
|
|
65
|
+
} & {})[] | undefined;
|
|
66
|
+
}) | null;
|
|
67
|
+
plan_digest: string | null;
|
|
68
|
+
commands: ({
|
|
69
|
+
operation_id: string;
|
|
70
|
+
sequence: number;
|
|
71
|
+
kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
|
|
72
|
+
expected_revision: string;
|
|
73
|
+
input: import("./setup-wire.js").SetupEffect;
|
|
74
|
+
input_digest: string;
|
|
75
|
+
approval_digest: string;
|
|
76
|
+
status: "approved" | "manual" | "cancelled" | "failed" | "outcome_unknown" | "prepared" | "leased" | "running" | "succeeded";
|
|
77
|
+
executor_generation: number | null;
|
|
78
|
+
lease_generation: number;
|
|
79
|
+
lease_expires_at: string | null;
|
|
80
|
+
result: ({
|
|
81
|
+
code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown";
|
|
82
|
+
} & {
|
|
83
|
+
verified?: boolean | undefined;
|
|
84
|
+
}) | null;
|
|
85
|
+
result_digest: string | null;
|
|
86
|
+
created_at: string;
|
|
87
|
+
started_at: string | null;
|
|
88
|
+
finished_at: string | null;
|
|
89
|
+
updated_at: string;
|
|
90
|
+
} & {})[];
|
|
91
|
+
executor_generation: number;
|
|
92
|
+
connection: string;
|
|
93
|
+
heartbeat_at: string | null;
|
|
94
|
+
sample: {
|
|
95
|
+
status: string;
|
|
96
|
+
first_received_at: string | null;
|
|
97
|
+
latest_received_at: string | null;
|
|
98
|
+
latest_request_id: string | null;
|
|
99
|
+
} & {};
|
|
100
|
+
draft_outcomes: ({
|
|
101
|
+
candidate_id: string;
|
|
102
|
+
status: string;
|
|
103
|
+
prompt_id: string;
|
|
104
|
+
request_id: string;
|
|
105
|
+
revision: string | null;
|
|
106
|
+
code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown" | null;
|
|
107
|
+
} & {})[];
|
|
108
|
+
bound_at: string;
|
|
109
|
+
last_activity_at: string;
|
|
110
|
+
idle_expires_at: string;
|
|
111
|
+
absolute_expires_at: string;
|
|
112
|
+
terminal_at: string | null;
|
|
113
|
+
schema_version: 1;
|
|
114
|
+
setup_id: string;
|
|
115
|
+
} & {
|
|
116
|
+
source?: ({
|
|
117
|
+
preparation_id: string;
|
|
118
|
+
status: string;
|
|
119
|
+
packet_digest: string;
|
|
120
|
+
provider_fingerprint: string;
|
|
121
|
+
authorized_at: string | null;
|
|
122
|
+
expires_at: string;
|
|
123
|
+
error_code: "operation_failed" | "planning_unavailable" | "planning_paused" | "planning_daily_budget" | "planning_allowance_exhausted" | "planning_provider_unavailable" | "planning_policy_changed" | "planning_consent_expired" | "planning_input_budget" | "planning_invalid_output" | "planning_timeout" | "source_changed" | "source_unavailable" | "recommendations_no_source" | null;
|
|
124
|
+
} & {
|
|
125
|
+
analysis_id?: string | undefined;
|
|
126
|
+
analysis_started_at?: string | undefined;
|
|
127
|
+
analysis_completed_at?: string | undefined;
|
|
128
|
+
deadline_at?: string | undefined;
|
|
129
|
+
timings?: ({} & {
|
|
130
|
+
extraction_ms?: number | undefined;
|
|
131
|
+
consolidation_ms?: number | undefined;
|
|
132
|
+
authoring_ms?: number | undefined;
|
|
133
|
+
total_ms?: number | undefined;
|
|
134
|
+
}) | undefined;
|
|
135
|
+
}) | null | undefined;
|
|
136
|
+
recommendation_digest?: string | null | undefined;
|
|
137
|
+
selected_candidates?: string[] | undefined;
|
|
138
|
+
}) | undefined;
|
|
139
|
+
setLease(lease: {
|
|
140
|
+
operation_id: string;
|
|
141
|
+
executor_generation: number;
|
|
142
|
+
lease_generation: number;
|
|
143
|
+
}): void;
|
|
144
|
+
latest(fallback: SetupSessionView): {
|
|
145
|
+
phase: "expired" | "cancelled" | "analyzing" | "configuring" | "awaiting_local_approval" | "applying" | "ready_for_recommendations" | "selecting_drafts" | "creating_drafts" | "agent_test" | "complete";
|
|
146
|
+
revision: string;
|
|
147
|
+
workspace_id: string | null;
|
|
148
|
+
site_id: string | null;
|
|
149
|
+
project_fingerprint: string;
|
|
150
|
+
choices: {
|
|
151
|
+
install_sdk: boolean;
|
|
152
|
+
install_skill: boolean;
|
|
153
|
+
footer_badge: boolean;
|
|
154
|
+
} & {};
|
|
155
|
+
capabilities: {
|
|
156
|
+
framework: string;
|
|
157
|
+
package_manager: string;
|
|
158
|
+
install_packages: boolean;
|
|
159
|
+
initialize_sdk: boolean;
|
|
160
|
+
install_skill: boolean;
|
|
161
|
+
insert_footer: boolean;
|
|
162
|
+
source_review: boolean;
|
|
163
|
+
sample_read: boolean;
|
|
164
|
+
} & {
|
|
165
|
+
onboarding_v2?: boolean | undefined;
|
|
166
|
+
project_name?: string | undefined;
|
|
167
|
+
launcher?: ({
|
|
168
|
+
wizard_version: string;
|
|
169
|
+
api_origin: string;
|
|
170
|
+
} & {
|
|
171
|
+
registry?: string | undefined;
|
|
172
|
+
}) | undefined;
|
|
173
|
+
terminal?: ({
|
|
174
|
+
platform: string;
|
|
175
|
+
app: string;
|
|
176
|
+
} & {}) | undefined;
|
|
177
|
+
};
|
|
178
|
+
local_plan: ({
|
|
179
|
+
project_fingerprint: string;
|
|
180
|
+
choices_digest: string;
|
|
181
|
+
effects: import("./setup-wire.js").SetupEffect[];
|
|
182
|
+
} & {
|
|
183
|
+
manual_steps?: ({
|
|
184
|
+
kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
|
|
185
|
+
code: string;
|
|
186
|
+
} & {})[] | undefined;
|
|
187
|
+
}) | null;
|
|
188
|
+
plan_digest: string | null;
|
|
189
|
+
commands: ({
|
|
190
|
+
operation_id: string;
|
|
191
|
+
sequence: number;
|
|
192
|
+
kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
|
|
193
|
+
expected_revision: string;
|
|
194
|
+
input: import("./setup-wire.js").SetupEffect;
|
|
195
|
+
input_digest: string;
|
|
196
|
+
approval_digest: string;
|
|
197
|
+
status: "approved" | "manual" | "cancelled" | "failed" | "outcome_unknown" | "prepared" | "leased" | "running" | "succeeded";
|
|
198
|
+
executor_generation: number | null;
|
|
199
|
+
lease_generation: number;
|
|
200
|
+
lease_expires_at: string | null;
|
|
201
|
+
result: ({
|
|
202
|
+
code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown";
|
|
203
|
+
} & {
|
|
204
|
+
verified?: boolean | undefined;
|
|
205
|
+
}) | null;
|
|
206
|
+
result_digest: string | null;
|
|
207
|
+
created_at: string;
|
|
208
|
+
started_at: string | null;
|
|
209
|
+
finished_at: string | null;
|
|
210
|
+
updated_at: string;
|
|
211
|
+
} & {})[];
|
|
212
|
+
executor_generation: number;
|
|
213
|
+
connection: string;
|
|
214
|
+
heartbeat_at: string | null;
|
|
215
|
+
sample: {
|
|
216
|
+
status: string;
|
|
217
|
+
first_received_at: string | null;
|
|
218
|
+
latest_received_at: string | null;
|
|
219
|
+
latest_request_id: string | null;
|
|
220
|
+
} & {};
|
|
221
|
+
draft_outcomes: ({
|
|
222
|
+
candidate_id: string;
|
|
223
|
+
status: string;
|
|
224
|
+
prompt_id: string;
|
|
225
|
+
request_id: string;
|
|
226
|
+
revision: string | null;
|
|
227
|
+
code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown" | null;
|
|
228
|
+
} & {})[];
|
|
229
|
+
bound_at: string;
|
|
230
|
+
last_activity_at: string;
|
|
231
|
+
idle_expires_at: string;
|
|
232
|
+
absolute_expires_at: string;
|
|
233
|
+
terminal_at: string | null;
|
|
234
|
+
schema_version: 1;
|
|
235
|
+
setup_id: string;
|
|
236
|
+
} & {
|
|
237
|
+
source?: ({
|
|
238
|
+
preparation_id: string;
|
|
239
|
+
status: string;
|
|
240
|
+
packet_digest: string;
|
|
241
|
+
provider_fingerprint: string;
|
|
242
|
+
authorized_at: string | null;
|
|
243
|
+
expires_at: string;
|
|
244
|
+
error_code: "operation_failed" | "planning_unavailable" | "planning_paused" | "planning_daily_budget" | "planning_allowance_exhausted" | "planning_provider_unavailable" | "planning_policy_changed" | "planning_consent_expired" | "planning_input_budget" | "planning_invalid_output" | "planning_timeout" | "source_changed" | "source_unavailable" | "recommendations_no_source" | null;
|
|
245
|
+
} & {
|
|
246
|
+
analysis_id?: string | undefined;
|
|
247
|
+
analysis_started_at?: string | undefined;
|
|
248
|
+
analysis_completed_at?: string | undefined;
|
|
249
|
+
deadline_at?: string | undefined;
|
|
250
|
+
timings?: ({} & {
|
|
251
|
+
extraction_ms?: number | undefined;
|
|
252
|
+
consolidation_ms?: number | undefined;
|
|
253
|
+
authoring_ms?: number | undefined;
|
|
254
|
+
total_ms?: number | undefined;
|
|
255
|
+
}) | undefined;
|
|
256
|
+
}) | null | undefined;
|
|
257
|
+
recommendation_digest?: string | null | undefined;
|
|
258
|
+
selected_candidates?: string[] | undefined;
|
|
259
|
+
};
|
|
260
|
+
check(): void;
|
|
261
|
+
private schedule;
|
|
262
|
+
pulse(): Promise<void>;
|
|
263
|
+
/** Finish the old lease's in-flight heartbeat before closing it in a report. */
|
|
264
|
+
duringReport<T>(operation: () => Promise<T>): Promise<T>;
|
|
265
|
+
stop(): Promise<void>;
|
|
266
|
+
}
|
|
267
|
+
/** Observe all responses; cancel outstanding reads/source work when the run ends. */
|
|
268
|
+
export declare function observeSetupClient<T extends object>(client: T, heartbeat: SetupHeartbeat, signal?: AbortSignal): T;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const terminal = new Set(['complete', 'cancelled', 'expired']);
|
|
2
|
+
/** Stop waiting when this run ends, including user-supplied preparations. */
|
|
3
|
+
export function setupAbortable(work, signal) {
|
|
4
|
+
if (!signal)
|
|
5
|
+
return work;
|
|
6
|
+
signal.throwIfAborted();
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const abort = () => reject(signal.reason);
|
|
9
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
10
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** One lease-aware heartbeat for the entire run, independent of its current stage. */
|
|
14
|
+
export class SetupHeartbeat {
|
|
15
|
+
client;
|
|
16
|
+
options;
|
|
17
|
+
session;
|
|
18
|
+
timer;
|
|
19
|
+
pending;
|
|
20
|
+
stopped = false;
|
|
21
|
+
paused = 0;
|
|
22
|
+
failure;
|
|
23
|
+
lease;
|
|
24
|
+
lifetime = new AbortController();
|
|
25
|
+
signal;
|
|
26
|
+
constructor(client, options = {}) {
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.signal = AbortSignal.any([this.lifetime.signal, ...(options.signal ? [options.signal] : [])]);
|
|
30
|
+
}
|
|
31
|
+
observe(next) {
|
|
32
|
+
const current = this.session;
|
|
33
|
+
if (!current || BigInt(next.revision) >= BigInt(current.revision)) {
|
|
34
|
+
const heartbeat = current?.heartbeat_at && (!next.heartbeat_at || Date.parse(current.heartbeat_at) > Date.parse(next.heartbeat_at)) ? current.heartbeat_at : next.heartbeat_at;
|
|
35
|
+
this.session = { ...next, heartbeat_at: heartbeat };
|
|
36
|
+
}
|
|
37
|
+
if (terminal.has(this.session.phase) && !this.signal.aborted)
|
|
38
|
+
this.lifetime.abort(new Error('Setup ended'));
|
|
39
|
+
if (!this.stopped && !this.timer && !this.pending && !this.paused && !terminal.has(this.session.phase))
|
|
40
|
+
this.schedule(0);
|
|
41
|
+
return this.session;
|
|
42
|
+
}
|
|
43
|
+
get current() { return this.session; }
|
|
44
|
+
setLease(lease) { this.lease = lease; }
|
|
45
|
+
latest(fallback) { return this.observe(fallback); }
|
|
46
|
+
check() { this.options.signal?.throwIfAborted(); if (this.failure)
|
|
47
|
+
throw this.failure; }
|
|
48
|
+
schedule(delay = this.options.heartbeatMs ?? 10000) {
|
|
49
|
+
if (this.stopped || this.paused || !this.session || terminal.has(this.session.phase) || this.signal.aborted)
|
|
50
|
+
return;
|
|
51
|
+
this.timer = setTimeout(() => { this.timer = undefined; void this.pulse(); }, delay);
|
|
52
|
+
}
|
|
53
|
+
async pulse() {
|
|
54
|
+
if (this.pending)
|
|
55
|
+
return this.pending;
|
|
56
|
+
if (this.stopped || this.paused || this.signal.aborted || !this.session || terminal.has(this.session.phase))
|
|
57
|
+
return;
|
|
58
|
+
if (this.timer) {
|
|
59
|
+
clearTimeout(this.timer);
|
|
60
|
+
this.timer = undefined;
|
|
61
|
+
}
|
|
62
|
+
const session = this.session;
|
|
63
|
+
this.pending = setupAbortable(this.client.heartbeat({ schema_version: 1, setup_id: this.client.identity.setup_id, executor_generation: session.executor_generation,
|
|
64
|
+
operation_id: this.lease?.operation_id ?? null, lease_generation: this.lease?.lease_generation ?? null }), this.signal)
|
|
65
|
+
.then(value => { this.observe(value); })
|
|
66
|
+
.catch(error => { if (!this.stopped && !this.options.signal?.aborted) {
|
|
67
|
+
this.failure = error;
|
|
68
|
+
this.lifetime.abort(error);
|
|
69
|
+
} })
|
|
70
|
+
.finally(() => { this.pending = undefined; this.schedule(); });
|
|
71
|
+
return this.pending;
|
|
72
|
+
}
|
|
73
|
+
/** Finish the old lease's in-flight heartbeat before closing it in a report. */
|
|
74
|
+
async duringReport(operation) {
|
|
75
|
+
this.paused++;
|
|
76
|
+
if (this.timer) {
|
|
77
|
+
clearTimeout(this.timer);
|
|
78
|
+
this.timer = undefined;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await this.pending;
|
|
82
|
+
this.lease = undefined;
|
|
83
|
+
return await operation();
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
this.paused--;
|
|
87
|
+
this.schedule();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async stop() {
|
|
91
|
+
this.stopped = true;
|
|
92
|
+
if (this.timer)
|
|
93
|
+
clearTimeout(this.timer);
|
|
94
|
+
this.lifetime.abort(new Error('Setup run ended'));
|
|
95
|
+
await this.pending;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Observe all responses; cancel outstanding reads/source work when the run ends. */
|
|
99
|
+
export function observeSetupClient(client, heartbeat, signal) {
|
|
100
|
+
return new Proxy(client, { get(target, key, receiver) {
|
|
101
|
+
const value = Reflect.get(target, key, target);
|
|
102
|
+
if (typeof value !== 'function')
|
|
103
|
+
return value;
|
|
104
|
+
if (key === 'withSignal')
|
|
105
|
+
return value.bind(target);
|
|
106
|
+
return async (...args) => {
|
|
107
|
+
// Reports settle already-recorded effects, including a cancelled operation.
|
|
108
|
+
if (key !== 'report')
|
|
109
|
+
signal?.throwIfAborted();
|
|
110
|
+
// Acknowledged mutations remain acknowledged if cancellation races their
|
|
111
|
+
// response. Reads and the long source upload can stop waiting immediately.
|
|
112
|
+
const requestSignal = ['poll', 'discovery', 'sourceInfo', 'sourceUpload'].includes(String(key)) ? signal : undefined;
|
|
113
|
+
const result = await setupAbortable(Reflect.apply(value, target, args), requestSignal);
|
|
114
|
+
const view = result && typeof result === 'object' && 'session' in result ? result.session : result;
|
|
115
|
+
if (view && typeof view === 'object' && 'phase' in view && 'revision' in view && typeof view.phase === 'string' && typeof view.revision === 'string')
|
|
116
|
+
heartbeat.observe(view);
|
|
117
|
+
return result;
|
|
118
|
+
};
|
|
119
|
+
} });
|
|
120
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BrowserSetupClient } from './setup.js';
|
|
2
|
+
import { type SetupExecutorOptions, type SetupExecutorRuntime } from './setup-executor.js';
|
|
3
|
+
import { type PreparedSetupSource } from './setup-source.js';
|
|
4
|
+
import { type SetupSessionView } from './setup-wire.js';
|
|
5
|
+
export interface BrowserSetupRunnerOptions extends SetupExecutorOptions {
|
|
6
|
+
automaticSetup?: boolean;
|
|
7
|
+
preparedSource?: Promise<PreparedSetupSource | null>;
|
|
8
|
+
onSourceIssue?: (code: string) => void;
|
|
9
|
+
/** Local display only. Never forward this callback's source content to telemetry. */
|
|
10
|
+
onSourcePrepared?: (packet: PreparedSetupSource) => void;
|
|
11
|
+
onSample?: (session: SetupSessionView) => void;
|
|
12
|
+
}
|
|
13
|
+
export type BrowserSetupRunnerClient = Pick<BrowserSetupClient, 'identity' | 'poll' | 'plan' | 'claim' | 'startCommand' | 'report' | 'heartbeat' | 'reconcile' | 'discovery' | 'sourceInfo' | 'sourcePlan' | 'sourceUpload'> & Partial<Pick<BrowserSetupClient, 'withSignal'>>;
|
|
14
|
+
/** Browser controls consent and drafts. This runner holds only the setup capability,
|
|
15
|
+
* executes reviewed local effects, and uploads the separately approved source packet. */
|
|
16
|
+
export declare function runBrowserSetup(client: BrowserSetupRunnerClient, directory: string, runtime: SetupExecutorRuntime, options?: BrowserSetupRunnerOptions): Promise<SetupSessionView>;
|