presubmit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +51 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
- package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
- package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
- package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
- package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
- package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
- package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
- package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
- package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
- package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/apps/web/dist/index.html +6 -0
- package/apps/web/dist/service-worker.js +11 -0
- package/dist/apps/server/src/agent-builder.js +204 -0
- package/dist/apps/server/src/attach.js +163 -0
- package/dist/apps/server/src/attachments.js +86 -0
- package/dist/apps/server/src/browser-access.js +131 -0
- package/dist/apps/server/src/check-provenance.js +95 -0
- package/dist/apps/server/src/check-supervisor.js +112 -0
- package/dist/apps/server/src/checks.js +402 -0
- package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
- package/dist/apps/server/src/child-sessions.js +191 -0
- package/dist/apps/server/src/cli.js +162 -0
- package/dist/apps/server/src/conversation-search.js +12 -0
- package/dist/apps/server/src/coordinator-messages.js +72 -0
- package/dist/apps/server/src/dev-auth.js +54 -0
- package/dist/apps/server/src/dev-server.js +201 -0
- package/dist/apps/server/src/dev-worker.js +48 -0
- package/dist/apps/server/src/diagnostics.js +105 -0
- package/dist/apps/server/src/fast-mode.js +8 -0
- package/dist/apps/server/src/folder-picker.js +54 -0
- package/dist/apps/server/src/git-blobs.js +62 -0
- package/dist/apps/server/src/git-handoff.js +127 -0
- package/dist/apps/server/src/history-sources.js +59 -0
- package/dist/apps/server/src/hub.js +159 -0
- package/dist/apps/server/src/internal-transport.js +38 -0
- package/dist/apps/server/src/local-embeddings.js +70 -0
- package/dist/apps/server/src/ollama-setup.js +56 -0
- package/dist/apps/server/src/planning.js +30 -0
- package/dist/apps/server/src/project-agents.js +276 -0
- package/dist/apps/server/src/project-memory.js +94 -0
- package/dist/apps/server/src/projects.js +106 -0
- package/dist/apps/server/src/queued-launch.js +13 -0
- package/dist/apps/server/src/reply-search.js +172 -0
- package/dist/apps/server/src/review-context.js +60 -0
- package/dist/apps/server/src/review-refresh.js +14 -0
- package/dist/apps/server/src/reviewers.js +75 -0
- package/dist/apps/server/src/search-settings.js +22 -0
- package/dist/apps/server/src/semantic-history.js +186 -0
- package/dist/apps/server/src/server.js +4338 -0
- package/dist/apps/server/src/session-changes.js +181 -0
- package/dist/apps/server/src/session-router-settings.js +198 -0
- package/dist/apps/server/src/session-workspaces.js +135 -0
- package/dist/apps/server/src/sessions.js +579 -0
- package/dist/apps/server/src/shared-auth.js +70 -0
- package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
- package/dist/apps/server/src/store.js +203 -0
- package/dist/apps/server/src/tailscale-process.js +115 -0
- package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
- package/dist/apps/server/src/tailscale-serve.js +276 -0
- package/dist/apps/server/src/task-capacity.js +92 -0
- package/dist/apps/server/src/task-graph.js +190 -0
- package/dist/apps/server/src/task-logs.js +74 -0
- package/dist/apps/server/src/task-numbers.js +25 -0
- package/dist/apps/server/src/task-orchestrator.js +1088 -0
- package/dist/apps/server/src/terminal.js +182 -0
- package/dist/apps/server/src/terminals.js +80 -0
- package/dist/apps/server/src/tool-safety.js +2 -0
- package/dist/apps/server/src/workspace-lock.js +140 -0
- package/dist/apps/server/src/workspace-watcher.js +65 -0
- package/dist/apps/server/src/workspace.js +517 -0
- package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
- package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
- package/dist/packages/agent-pi/src/ask-user.js +86 -0
- package/dist/packages/agent-pi/src/auth.js +510 -0
- package/dist/packages/agent-pi/src/check-tools.js +72 -0
- package/dist/packages/agent-pi/src/command-policy.js +246 -0
- package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
- package/dist/packages/agent-pi/src/fast-mode.js +67 -0
- package/dist/packages/agent-pi/src/file-tools.js +163 -0
- package/dist/packages/agent-pi/src/history-tool.js +40 -0
- package/dist/packages/agent-pi/src/history.js +34 -0
- package/dist/packages/agent-pi/src/index.js +177 -0
- package/dist/packages/agent-pi/src/ipc.js +314 -0
- package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
- package/dist/packages/agent-pi/src/jev-tools.js +71 -0
- package/dist/packages/agent-pi/src/local-model.js +277 -0
- package/dist/packages/agent-pi/src/memory-tools.js +119 -0
- package/dist/packages/agent-pi/src/model.js +25 -0
- package/dist/packages/agent-pi/src/planning.js +206 -0
- package/dist/packages/agent-pi/src/resource-loader.js +23 -0
- package/dist/packages/agent-pi/src/review-tools.js +158 -0
- package/dist/packages/agent-pi/src/review-transition.js +73 -0
- package/dist/packages/agent-pi/src/review.js +40 -0
- package/dist/packages/agent-pi/src/routing.js +27 -0
- package/dist/packages/agent-pi/src/session-setup.js +118 -0
- package/dist/packages/agent-pi/src/shell-context.js +51 -0
- package/dist/packages/agent-pi/src/shell-policy.js +281 -0
- package/dist/packages/agent-pi/src/shell-safety.js +205 -0
- package/dist/packages/agent-pi/src/shell.js +121 -0
- package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
- package/dist/packages/agent-pi/src/spike.js +41 -0
- package/dist/packages/agent-pi/src/task-tools.js +53 -0
- package/dist/packages/agent-pi/src/types.js +1 -0
- package/dist/packages/agent-pi/src/usage.js +30 -0
- package/dist/packages/agent-pi/src/worker.js +602 -0
- package/dist/packages/protocol/src/activity-summary.js +77 -0
- package/dist/packages/protocol/src/agents.js +301 -0
- package/dist/packages/protocol/src/browser-access.js +1 -0
- package/dist/packages/protocol/src/checks.js +40 -0
- package/dist/packages/protocol/src/child-sessions.js +28 -0
- package/dist/packages/protocol/src/command-policy.js +3 -0
- package/dist/packages/protocol/src/coordinator.js +135 -0
- package/dist/packages/protocol/src/delegation.js +40 -0
- package/dist/packages/protocol/src/git-diff.js +71 -0
- package/dist/packages/protocol/src/git-handoff.js +1 -0
- package/dist/packages/protocol/src/index.js +1 -0
- package/dist/packages/protocol/src/memory.js +46 -0
- package/dist/packages/protocol/src/metrics.js +70 -0
- package/dist/packages/protocol/src/planning.js +138 -0
- package/dist/packages/protocol/src/retrieval.js +36 -0
- package/dist/packages/protocol/src/reviewers.js +17 -0
- package/dist/packages/protocol/src/search.js +16 -0
- package/dist/packages/protocol/src/session-changes.js +29 -0
- package/dist/packages/protocol/src/session-router.js +123 -0
- package/dist/packages/protocol/src/shell-evaluator.js +43 -0
- package/dist/packages/protocol/src/task-graph.js +1 -0
- package/dist/packages/protocol/src/token-usage.js +17 -0
- package/dist/packages/protocol/src/user-questions.js +106 -0
- package/package.json +71 -0
- package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
- package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
- package/vendor/lancedb/dist/arrow.d.ts +296 -0
- package/vendor/lancedb/dist/arrow.js +1145 -0
- package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
- package/vendor/lancedb/dist/arrow_type.js +29 -0
- package/vendor/lancedb/dist/connection.d.ts +508 -0
- package/vendor/lancedb/dist/connection.js +298 -0
- package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
- package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
- package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
- package/vendor/lancedb/dist/embedding/index.js +108 -0
- package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
- package/vendor/lancedb/dist/embedding/openai.js +81 -0
- package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
- package/vendor/lancedb/dist/embedding/registry.js +221 -0
- package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
- package/vendor/lancedb/dist/embedding/transformers.js +110 -0
- package/vendor/lancedb/dist/header.d.ts +162 -0
- package/vendor/lancedb/dist/header.js +217 -0
- package/vendor/lancedb/dist/index.d.ts +246 -0
- package/vendor/lancedb/dist/index.js +176 -0
- package/vendor/lancedb/dist/indices.d.ts +721 -0
- package/vendor/lancedb/dist/indices.js +166 -0
- package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
- package/vendor/lancedb/dist/materialized_view.js +112 -0
- package/vendor/lancedb/dist/merge.d.ts +104 -0
- package/vendor/lancedb/dist/merge.js +120 -0
- package/vendor/lancedb/dist/native.d.ts +1193 -0
- package/vendor/lancedb/dist/native.js +688 -0
- package/vendor/lancedb/dist/oauth.d.ts +66 -0
- package/vendor/lancedb/dist/oauth.js +15 -0
- package/vendor/lancedb/dist/otel.d.ts +26 -0
- package/vendor/lancedb/dist/otel.js +114 -0
- package/vendor/lancedb/dist/permutation.d.ts +143 -0
- package/vendor/lancedb/dist/permutation.js +184 -0
- package/vendor/lancedb/dist/query.d.ts +663 -0
- package/vendor/lancedb/dist/query.js +966 -0
- package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
- package/vendor/lancedb/dist/rerankers/index.js +19 -0
- package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
- package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
- package/vendor/lancedb/dist/sanitize.d.ts +32 -0
- package/vendor/lancedb/dist/sanitize.js +544 -0
- package/vendor/lancedb/dist/scannable.d.ts +92 -0
- package/vendor/lancedb/dist/scannable.js +200 -0
- package/vendor/lancedb/dist/schema.d.ts +16 -0
- package/vendor/lancedb/dist/schema.js +387 -0
- package/vendor/lancedb/dist/table.d.ts +1069 -0
- package/vendor/lancedb/dist/table.js +498 -0
- package/vendor/lancedb/dist/util.d.ts +14 -0
- package/vendor/lancedb/dist/util.js +77 -0
- package/vendor/lancedb/license_header.txt +2 -0
- package/vendor/lancedb/package.json +56 -0
- package/vendor/transformers/LICENSE +202 -0
- package/vendor/transformers/dist/transformers.cjs +30844 -0
- package/vendor/transformers/dist/transformers.mjs +31404 -0
- package/vendor/transformers/package.json +55 -0
- package/vendor/transformers/types/backends/onnx.d.ts +30 -0
- package/vendor/transformers/types/configs.d.ts +85 -0
- package/vendor/transformers/types/env.d.ts +110 -0
- package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
- package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
- package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
- package/vendor/transformers/types/generation/parameters.d.ts +47 -0
- package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
- package/vendor/transformers/types/generation/streamers.d.ts +81 -0
- package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
- package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
- package/vendor/transformers/types/models.d.ts +3684 -0
- package/vendor/transformers/types/ops/registry.d.ts +11 -0
- package/vendor/transformers/types/pipelines.d.ts +2402 -0
- package/vendor/transformers/types/processors.d.ts +924 -0
- package/vendor/transformers/types/tokenizers.d.ts +990 -0
- package/vendor/transformers/types/transformers.d.ts +13 -0
- package/vendor/transformers/types/utils/audio.d.ts +130 -0
- package/vendor/transformers/types/utils/constants.d.ts +2 -0
- package/vendor/transformers/types/utils/core.d.ts +98 -0
- package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
- package/vendor/transformers/types/utils/devices.d.ts +18 -0
- package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
- package/vendor/transformers/types/utils/generic.d.ts +11 -0
- package/vendor/transformers/types/utils/hub.d.ts +152 -0
- package/vendor/transformers/types/utils/image.d.ts +119 -0
- package/vendor/transformers/types/utils/maths.d.ts +280 -0
- package/vendor/transformers/types/utils/tensor.d.ts +418 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { safetyPrompt } from '../../../packages/agent-pi/src/shell-safety.js';
|
|
5
|
+
import { parseShellEvaluatorSettings, } from '../../../packages/protocol/src/shell-evaluator.js';
|
|
6
|
+
function read(path) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code === 'ENOENT')
|
|
12
|
+
return { version: '0', providers: {} };
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function evaluatorState(authPath, provider, models = []) {
|
|
17
|
+
const saved = read(authPath + '.shell-evaluator');
|
|
18
|
+
return {
|
|
19
|
+
version: saved.version,
|
|
20
|
+
provider,
|
|
21
|
+
models: models.filter((model) => model.id.startsWith(`${provider}/`)),
|
|
22
|
+
settings: saved.providers[provider] ?? {},
|
|
23
|
+
defaultPrompt: safetyPrompt,
|
|
24
|
+
environmentModel: process.env.PRESUBMIT_SAFETY_MODEL || undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function saveEvaluator(authPath, provider, models, input) {
|
|
28
|
+
const path = authPath + '.shell-evaluator';
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
30
|
+
const lock = path + '.lock';
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(lock);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error('Evaluator settings are being saved elsewhere. Reload and retry.');
|
|
36
|
+
}
|
|
37
|
+
const temp = path + '.' + randomUUID();
|
|
38
|
+
try {
|
|
39
|
+
const saved = read(path);
|
|
40
|
+
if (input.version !== saved.version || input.provider !== provider)
|
|
41
|
+
throw new Error('Evaluator settings or provider changed. Reload before saving.');
|
|
42
|
+
const settings = parseShellEvaluatorSettings(input.settings, models.filter((model) => model.id.startsWith(`${provider}/`)));
|
|
43
|
+
saved.providers[provider] = settings;
|
|
44
|
+
saved.version = randomUUID();
|
|
45
|
+
writeFileSync(temp, JSON.stringify(saved), { mode: 0o600 });
|
|
46
|
+
renameSync(temp, path);
|
|
47
|
+
return evaluatorState(authPath, provider, models);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
rmSync(temp, { force: true });
|
|
51
|
+
rmSync(lock, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { activitySummary } from '../../../packages/protocol/src/activity-summary.js';
|
|
2
|
+
import { timed } from './diagnostics.js';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { mkdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { ReplySearchIndex } from './reply-search.js';
|
|
8
|
+
export class Store {
|
|
9
|
+
directory;
|
|
10
|
+
db;
|
|
11
|
+
replySearch;
|
|
12
|
+
sessionEventsCache;
|
|
13
|
+
summaries = new Map();
|
|
14
|
+
summaryBytes = 0;
|
|
15
|
+
constructor(directory) {
|
|
16
|
+
this.directory = directory;
|
|
17
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
18
|
+
this.db = new DatabaseSync(join(directory, 'state.sqlite'));
|
|
19
|
+
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
|
|
20
|
+
CREATE TABLE IF NOT EXISTS records(key TEXT PRIMARY KEY,value TEXT NOT NULL);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS events(seq INTEGER PRIMARY KEY AUTOINCREMENT,payload TEXT NOT NULL);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS events_tool_identity ON events(json_extract(payload, '$.runId'), json_extract(payload, '$.data.toolCallId'), seq) WHERE json_extract(payload, '$.type') IN ('tool_start','tool_update','tool_end');
|
|
23
|
+
CREATE TABLE IF NOT EXISTS documents(id TEXT PRIMARY KEY,bytes BLOB NOT NULL);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS commands(id TEXT PRIMARY KEY,response TEXT NOT NULL);`);
|
|
25
|
+
this.replySearch = new ReplySearchIndex(this.db);
|
|
26
|
+
}
|
|
27
|
+
get(key) {
|
|
28
|
+
const r = this.db.prepare('SELECT value FROM records WHERE key=?').get(key);
|
|
29
|
+
return r ? JSON.parse(r.value) : undefined;
|
|
30
|
+
}
|
|
31
|
+
put(key, value) {
|
|
32
|
+
const encoded = timed('store.encode', () => JSON.stringify(value));
|
|
33
|
+
timed('store.write', () => this.db.prepare('INSERT OR REPLACE INTO records VALUES(?,?)').run(key, encoded));
|
|
34
|
+
}
|
|
35
|
+
transaction(fn) {
|
|
36
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
37
|
+
try {
|
|
38
|
+
const result = fn();
|
|
39
|
+
this.db.exec('COMMIT');
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
this.db.exec('ROLLBACK');
|
|
44
|
+
this.sessionEventsCache = undefined;
|
|
45
|
+
this.summaries.clear();
|
|
46
|
+
this.summaryBytes = 0;
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
event(event) {
|
|
51
|
+
const data = { ...event, createdAt: new Date().toISOString() };
|
|
52
|
+
const r = this.db.prepare('INSERT INTO events(payload) VALUES(?)').run(JSON.stringify(data));
|
|
53
|
+
this.sessionEventsCache = undefined;
|
|
54
|
+
return { ...data, seq: Number(r.lastInsertRowid) };
|
|
55
|
+
}
|
|
56
|
+
events(after = 0, limit = 300) {
|
|
57
|
+
return this.db
|
|
58
|
+
.prepare('SELECT seq,payload FROM events WHERE seq>? ORDER BY seq ASC LIMIT ?')
|
|
59
|
+
.all(after, limit).map((r) => ({ ...JSON.parse(r.payload), seq: r.seq }));
|
|
60
|
+
}
|
|
61
|
+
recentEvents(limit = 300) {
|
|
62
|
+
return timed('store.recentEvents', () => this.db
|
|
63
|
+
.prepare('SELECT seq,payload FROM events ORDER BY seq DESC LIMIT ?')
|
|
64
|
+
.all(limit)
|
|
65
|
+
.reverse()
|
|
66
|
+
.map((r) => ({ ...JSON.parse(r.payload), seq: r.seq })));
|
|
67
|
+
}
|
|
68
|
+
summaryEvents(after = 0, limit = 500, recent = false) {
|
|
69
|
+
return timed('store.summaryEvents', () => {
|
|
70
|
+
const rows = this.db
|
|
71
|
+
.prepare(recent
|
|
72
|
+
? 'SELECT seq,payload FROM events ORDER BY seq DESC LIMIT ?'
|
|
73
|
+
: 'SELECT seq,payload FROM events WHERE seq>? ORDER BY seq ASC LIMIT ?')
|
|
74
|
+
.all(...(recent ? [limit] : [after, limit]));
|
|
75
|
+
if (recent)
|
|
76
|
+
rows.reverse();
|
|
77
|
+
return rows.map((row) => {
|
|
78
|
+
let event = this.summaries.get(row.seq)?.event;
|
|
79
|
+
if (!event) {
|
|
80
|
+
event = activitySummary({ ...JSON.parse(row.payload), seq: row.seq });
|
|
81
|
+
const size = JSON.stringify(event).length * 2;
|
|
82
|
+
if (size <= 4 * 1024 * 1024) {
|
|
83
|
+
this.summaries.set(row.seq, { event, size });
|
|
84
|
+
this.summaryBytes += size;
|
|
85
|
+
while (this.summaries.size > 1000 || this.summaryBytes > 4 * 1024 * 1024) {
|
|
86
|
+
const first = this.summaries.keys().next().value;
|
|
87
|
+
this.summaryBytes -= this.summaries.get(first).size;
|
|
88
|
+
this.summaries.delete(first);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return event;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
toolOutput(runId, toolCallId) {
|
|
97
|
+
return timed('store.toolOutput', () => {
|
|
98
|
+
const query = `SELECT seq,payload FROM events WHERE json_extract(payload, '$.runId')=?
|
|
99
|
+
AND json_extract(payload, '$.data.toolCallId')=? AND json_extract(payload, '$.type') IN ('tool_start','tool_update','tool_end')`;
|
|
100
|
+
const latest = this.db
|
|
101
|
+
.prepare(query + ' ORDER BY seq DESC LIMIT 1')
|
|
102
|
+
.get(runId, toolCallId);
|
|
103
|
+
if (!latest)
|
|
104
|
+
return;
|
|
105
|
+
const first = this.db.prepare(query + ' ORDER BY seq ASC LIMIT 1').get(runId, toolCallId);
|
|
106
|
+
const initial = JSON.parse(first.payload);
|
|
107
|
+
const current = JSON.parse(latest.payload);
|
|
108
|
+
return {
|
|
109
|
+
...current,
|
|
110
|
+
seq: latest.seq,
|
|
111
|
+
data: {
|
|
112
|
+
...initial.data,
|
|
113
|
+
...current.data,
|
|
114
|
+
args: current.data?.args ?? initial.data?.args,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Keep only the latest timestamp, displayable activity, and unread activity for session lists. */
|
|
120
|
+
sessionEvents() {
|
|
121
|
+
if (this.sessionEventsCache)
|
|
122
|
+
return this.sessionEventsCache;
|
|
123
|
+
const latest = this.recentEvents(1)[0];
|
|
124
|
+
if (!latest)
|
|
125
|
+
return (this.sessionEventsCache = []);
|
|
126
|
+
let display = latest.text && latest.type !== 'command' ? latest : undefined;
|
|
127
|
+
if (!display) {
|
|
128
|
+
// Match the full snapshot's 500-event window without decoding every tool payload in JS.
|
|
129
|
+
const row = this.db
|
|
130
|
+
.prepare(`SELECT seq,payload FROM (SELECT seq,payload FROM events ORDER BY seq DESC LIMIT 500)
|
|
131
|
+
WHERE json_extract(payload, '$.type') != 'command'
|
|
132
|
+
AND COALESCE(json_extract(payload, '$.text'), '') != ''
|
|
133
|
+
ORDER BY seq DESC LIMIT 1`)
|
|
134
|
+
.get();
|
|
135
|
+
if (row)
|
|
136
|
+
display = { ...JSON.parse(row.payload), seq: row.seq };
|
|
137
|
+
}
|
|
138
|
+
let activity = latest.type !== 'workspace_changed' ? latest : undefined;
|
|
139
|
+
if (!activity) {
|
|
140
|
+
// Watcher invalidations refresh Files/Changes, not unread state. Search past the
|
|
141
|
+
// snapshot window so a run's activity is not lost after many background refreshes.
|
|
142
|
+
const row = this.db
|
|
143
|
+
.prepare(`SELECT seq,payload FROM events
|
|
144
|
+
WHERE json_extract(payload, '$.type') != 'workspace_changed'
|
|
145
|
+
ORDER BY seq DESC LIMIT 1`)
|
|
146
|
+
.get();
|
|
147
|
+
if (row)
|
|
148
|
+
activity = { ...JSON.parse(row.payload), seq: row.seq };
|
|
149
|
+
}
|
|
150
|
+
const compact = ({ seq, type, text, createdAt }) => ({
|
|
151
|
+
seq,
|
|
152
|
+
type,
|
|
153
|
+
text: text?.slice(0, 180),
|
|
154
|
+
createdAt,
|
|
155
|
+
});
|
|
156
|
+
const events = new Map();
|
|
157
|
+
// The unread cursor may outlive the display window; do not resurrect old preview text.
|
|
158
|
+
if (activity)
|
|
159
|
+
events.set(activity.seq, { ...compact(activity), text: undefined });
|
|
160
|
+
for (const event of [display, latest])
|
|
161
|
+
if (event)
|
|
162
|
+
events.set(event.seq, compact(event));
|
|
163
|
+
return (this.sessionEventsCache = [...events.values()].sort((a, b) => a.seq - b.seq));
|
|
164
|
+
}
|
|
165
|
+
document(id) {
|
|
166
|
+
return this.db.prepare('SELECT bytes FROM documents WHERE id=?').get(id)?.bytes;
|
|
167
|
+
}
|
|
168
|
+
saveDocument(id, bytes) {
|
|
169
|
+
this.db.prepare('INSERT OR REPLACE INTO documents VALUES(?,?)').run(id, bytes);
|
|
170
|
+
}
|
|
171
|
+
response(id) {
|
|
172
|
+
const r = this.db.prepare('SELECT response FROM commands WHERE id=?').get(id);
|
|
173
|
+
return r ? JSON.parse(r.response) : undefined;
|
|
174
|
+
}
|
|
175
|
+
saveResponse(id, response) {
|
|
176
|
+
this.db.prepare('INSERT INTO commands VALUES(?,?)').run(id, JSON.stringify(response));
|
|
177
|
+
}
|
|
178
|
+
initial(workspace) {
|
|
179
|
+
const saved = this.get('state');
|
|
180
|
+
if (saved)
|
|
181
|
+
return {
|
|
182
|
+
...saved,
|
|
183
|
+
permissionMode: saved.permissionMode === 'auto' ? 'auto' : 'ask',
|
|
184
|
+
permissionVersion: saved.permissionVersion ?? 0,
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
workspace,
|
|
188
|
+
sessionId: randomUUID(),
|
|
189
|
+
generation: randomUUID(),
|
|
190
|
+
runs: [],
|
|
191
|
+
events: [],
|
|
192
|
+
approvals: [],
|
|
193
|
+
reviews: [],
|
|
194
|
+
configured: false,
|
|
195
|
+
permissionMode: 'ask',
|
|
196
|
+
permissionVersion: 0,
|
|
197
|
+
recoveredDrafts: [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
close() {
|
|
201
|
+
this.db.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile, fork } from 'node:child_process';
|
|
2
|
+
import { access, constants } from 'node:fs/promises';
|
|
3
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** No shell, provider credentials, or inherited Node hooks. */
|
|
6
|
+
export function tailscaleEnvironment() {
|
|
7
|
+
const env = { TAILSCALE_BE_CLI: '1' };
|
|
8
|
+
for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'TMPDIR', 'TMP', 'TEMP', 'SystemRoot'])
|
|
9
|
+
if (process.env[key] !== undefined)
|
|
10
|
+
env[key] = process.env[key];
|
|
11
|
+
return env;
|
|
12
|
+
}
|
|
13
|
+
async function findCli() {
|
|
14
|
+
const candidates = (process.env.PATH ?? '')
|
|
15
|
+
.split(delimiter)
|
|
16
|
+
.filter((path) => isAbsolute(path))
|
|
17
|
+
.map((path) => join(path, process.platform === 'win32' ? 'tailscale.exe' : 'tailscale'));
|
|
18
|
+
if (process.platform === 'darwin')
|
|
19
|
+
candidates.push('/Applications/Tailscale.app/Contents/MacOS/Tailscale');
|
|
20
|
+
for (const path of candidates) {
|
|
21
|
+
try {
|
|
22
|
+
await access(path, constants.X_OK);
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* Try the next installed CLI. */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Tailscale CLI was not found. Install Tailscale and enable its CLI integration.');
|
|
30
|
+
}
|
|
31
|
+
export function createTailscaleDriver() {
|
|
32
|
+
let command;
|
|
33
|
+
const cli = async () => (command ??= await findCli());
|
|
34
|
+
return {
|
|
35
|
+
async read(args, signal) {
|
|
36
|
+
const executable = await cli();
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
execFile(executable, args, {
|
|
39
|
+
env: tailscaleEnvironment(),
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: 5000,
|
|
42
|
+
maxBuffer: 1024 * 1024,
|
|
43
|
+
signal,
|
|
44
|
+
}, (error, stdout) => {
|
|
45
|
+
if (error) {
|
|
46
|
+
reject(new Error('Could not read Tailscale status. Check that Tailscale is running and the CLI is permitted to access it.'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
resolve(JSON.parse(stdout));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
reject(new Error('Tailscale returned an unreadable status. Check its CLI integration.'));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
async start(port, target) {
|
|
59
|
+
return startServeProcess(await cli(), port, target);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Exported for lifecycle fixtures using a disposable fake CLI, never a real tailnet. */
|
|
64
|
+
export function startServeProcess(command, port, target) {
|
|
65
|
+
const worker = new URL(import.meta.url.endsWith('.ts') ? './tailscale-serve-worker.ts' : './tailscale-serve-worker.js', import.meta.url);
|
|
66
|
+
const child = fork(fileURLToPath(worker), [command, port, target], {
|
|
67
|
+
env: tailscaleEnvironment(),
|
|
68
|
+
execArgv: worker.pathname.endsWith('.ts') ? ['--import', import.meta.resolve('tsx')] : [],
|
|
69
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
70
|
+
// A fallback group kill also cleans up the CLI if the supervisor itself gets stuck.
|
|
71
|
+
detached: process.platform !== 'win32',
|
|
72
|
+
});
|
|
73
|
+
let alive = true;
|
|
74
|
+
const exited = new Promise((resolve) => {
|
|
75
|
+
child.once('exit', () => {
|
|
76
|
+
alive = false;
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
child.once('error', () => {
|
|
80
|
+
alive = false;
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
let closing;
|
|
85
|
+
return {
|
|
86
|
+
get alive() {
|
|
87
|
+
return alive;
|
|
88
|
+
},
|
|
89
|
+
close() {
|
|
90
|
+
return (closing ??= (async () => {
|
|
91
|
+
if (!alive)
|
|
92
|
+
return;
|
|
93
|
+
// Disconnect is also used for intentional shutdown: it works before the
|
|
94
|
+
// supervisor has finished importing, unlike an early SIGTERM.
|
|
95
|
+
if (child.connected)
|
|
96
|
+
child.disconnect();
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
if (!alive || !child.pid)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
if (process.platform !== 'win32')
|
|
102
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
103
|
+
else
|
|
104
|
+
child.kill('SIGKILL');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* Already exited. */
|
|
108
|
+
}
|
|
109
|
+
}, 4000);
|
|
110
|
+
await exited;
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
})());
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
// This supervisor owns only a foreground Serve command. Its IPC connection ties the
|
|
3
|
+
// command's lifetime to Presubmit, including when Presubmit is killed unexpectedly.
|
|
4
|
+
const [command, port, target] = process.argv.slice(2);
|
|
5
|
+
if (!process.send || !command || !port || !target)
|
|
6
|
+
throw new Error('Missing Serve worker options');
|
|
7
|
+
let stopping = false;
|
|
8
|
+
let kill;
|
|
9
|
+
let child;
|
|
10
|
+
function stop() {
|
|
11
|
+
if (stopping)
|
|
12
|
+
return;
|
|
13
|
+
stopping = true;
|
|
14
|
+
if (!child?.pid)
|
|
15
|
+
process.exit(0);
|
|
16
|
+
child.kill('SIGINT');
|
|
17
|
+
kill = setTimeout(() => child?.kill('SIGKILL'), 2000);
|
|
18
|
+
}
|
|
19
|
+
process.once('disconnect', stop);
|
|
20
|
+
process.once('SIGTERM', stop);
|
|
21
|
+
process.once('SIGINT', stop);
|
|
22
|
+
process.once('exit', () => child?.kill('SIGKILL'));
|
|
23
|
+
if (!process.connected)
|
|
24
|
+
process.exit(0);
|
|
25
|
+
child = spawn(command, ['serve', '--https=' + port, target], {
|
|
26
|
+
env: process.env,
|
|
27
|
+
stdio: 'ignore',
|
|
28
|
+
});
|
|
29
|
+
child.once('error', () => process.exit(1));
|
|
30
|
+
child.once('exit', (code) => {
|
|
31
|
+
clearTimeout(kill);
|
|
32
|
+
process.exit(stopping ? 0 : (code ?? 1));
|
|
33
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { tailscaleOrigin } from './browser-access.js';
|
|
3
|
+
import { createTailscaleDriver, } from './tailscale-process.js';
|
|
4
|
+
function record(value) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
6
|
+
throw new Error('Tailscale returned an unsupported configuration. Inspect it with tailscale serve status.');
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function optionalRecord(value) {
|
|
10
|
+
return value == null ? {} : record(value);
|
|
11
|
+
}
|
|
12
|
+
/** Inspect the device listener only; named Tailscale Services have separate addresses. */
|
|
13
|
+
export function serveMapping(value, remote, local) {
|
|
14
|
+
const url = new URL(remote);
|
|
15
|
+
const port = url.port || '443';
|
|
16
|
+
const host = `${url.hostname}:${port}`;
|
|
17
|
+
let matches = 0;
|
|
18
|
+
let conflict = false;
|
|
19
|
+
function inspect(value, depth) {
|
|
20
|
+
if (depth > 1)
|
|
21
|
+
throw new Error('Unsupported nested Tailscale Serve configuration.');
|
|
22
|
+
const config = record(value);
|
|
23
|
+
const tcp = optionalRecord(config.TCP);
|
|
24
|
+
const web = optionalRecord(config.Web);
|
|
25
|
+
const funnel = optionalRecord(config.AllowFunnel);
|
|
26
|
+
if (Object.entries(funnel).some(([key, enabled]) => key.endsWith(':' + port) && enabled !== false))
|
|
27
|
+
conflict = true;
|
|
28
|
+
const webEntries = Object.entries(web).filter(([key]) => key.endsWith(':' + port));
|
|
29
|
+
if (tcp[port] !== undefined || webEntries.length) {
|
|
30
|
+
const listener = optionalRecord(tcp[port]);
|
|
31
|
+
const handlers = optionalRecord(optionalRecord(web[host]).Handlers);
|
|
32
|
+
const root = optionalRecord(handlers['/']);
|
|
33
|
+
if (Object.keys(listener).length === 1 &&
|
|
34
|
+
listener.HTTPS === true &&
|
|
35
|
+
webEntries.length === 1 &&
|
|
36
|
+
webEntries[0][0] === host &&
|
|
37
|
+
Object.keys(handlers).length === 1 &&
|
|
38
|
+
Object.keys(root).length === 1 &&
|
|
39
|
+
(root.Proxy === local || root.Proxy === local + '/'))
|
|
40
|
+
matches++;
|
|
41
|
+
else
|
|
42
|
+
conflict = true;
|
|
43
|
+
}
|
|
44
|
+
for (const child of Object.values(optionalRecord(config.Foreground)))
|
|
45
|
+
inspect(child, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
if (value !== null)
|
|
48
|
+
inspect(value, 0);
|
|
49
|
+
return conflict || matches > 1 ? 'conflict' : matches === 1 ? 'matching' : 'free';
|
|
50
|
+
}
|
|
51
|
+
export function servePreference(value) {
|
|
52
|
+
try {
|
|
53
|
+
const saved = record(value);
|
|
54
|
+
if (typeof saved.origin !== 'string' || typeof saved.autoStart !== 'boolean')
|
|
55
|
+
return;
|
|
56
|
+
return { origin: tailscaleOrigin(saved.origin), autoStart: saved.autoStart };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class TailscaleServe {
|
|
63
|
+
options;
|
|
64
|
+
remote;
|
|
65
|
+
detectedOrigin;
|
|
66
|
+
driver;
|
|
67
|
+
lifetime = new AbortController();
|
|
68
|
+
process;
|
|
69
|
+
operation;
|
|
70
|
+
read;
|
|
71
|
+
checkedAt = 0;
|
|
72
|
+
closed = false;
|
|
73
|
+
lastError = '';
|
|
74
|
+
state;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.options = options;
|
|
77
|
+
this.remote = options.remote ? tailscaleOrigin(options.remote) : undefined;
|
|
78
|
+
const local = new URL(options.local);
|
|
79
|
+
if (local.protocol !== 'http:' ||
|
|
80
|
+
local.hostname !== '127.0.0.1' ||
|
|
81
|
+
local.origin !== options.local)
|
|
82
|
+
throw new Error('Serve requires the actual loopback server origin.');
|
|
83
|
+
this.driver = options.driver ?? createTailscaleDriver();
|
|
84
|
+
this.state = {
|
|
85
|
+
tailscaleOrigin: this.remote ?? null,
|
|
86
|
+
phase: 'stopped',
|
|
87
|
+
autoStart: options.preference?.autoStart === true && options.preference.origin === this.remote,
|
|
88
|
+
message: 'Tailscale Serve is stopped.',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
update(phase, message) {
|
|
92
|
+
this.state = { ...this.state, phase, message };
|
|
93
|
+
return { ...this.state };
|
|
94
|
+
}
|
|
95
|
+
async inspect(signal) {
|
|
96
|
+
this.detectedOrigin = undefined;
|
|
97
|
+
const [rawStatus, config] = await Promise.all([
|
|
98
|
+
this.driver.read(['status', '--json', '--peers=false'], signal),
|
|
99
|
+
this.driver.read(['serve', 'status', '--json'], signal),
|
|
100
|
+
]);
|
|
101
|
+
const status = record(rawStatus);
|
|
102
|
+
if (status.BackendState !== 'Running' || optionalRecord(status.Self).Online === false)
|
|
103
|
+
return this.update('blocked', 'Connect and sign in to Tailscale on the host computer, then retry.');
|
|
104
|
+
const dnsName = optionalRecord(status.Self).DNSName;
|
|
105
|
+
if (typeof dnsName !== 'string' || !dnsName.endsWith('.'))
|
|
106
|
+
return this.update('blocked', 'This Tailscale device has no full DNS name. Enable MagicDNS in Tailscale’s DNS settings, then retry.');
|
|
107
|
+
const discovered = tailscaleOrigin('https://' + dnsName.slice(0, -1));
|
|
108
|
+
const remote = this.remote ?? discovered;
|
|
109
|
+
const host = new URL(remote).hostname;
|
|
110
|
+
if (dnsName !== host + '.')
|
|
111
|
+
return this.update('blocked', 'The saved Tailscale address does not match this device. Restart Presubmit with --tailscale and its current address.');
|
|
112
|
+
this.detectedOrigin = remote;
|
|
113
|
+
const mapping = serveMapping(config, remote, this.options.local);
|
|
114
|
+
if (mapping === 'conflict')
|
|
115
|
+
return this.update('blocked', 'This HTTPS port has another Serve or Funnel configuration. Stop that configuration yourself or restart Presubmit with a different HTTPS port.');
|
|
116
|
+
if (mapping === 'matching')
|
|
117
|
+
return this.update(!this.remote ? 'stopped' : this.process?.alive ? 'running' : 'external', !this.remote
|
|
118
|
+
? 'Serve already points here. Choose Start Serve to enable this device’s address in Presubmit.'
|
|
119
|
+
: this.process?.alive
|
|
120
|
+
? 'Serve is running. Presubmit will stop it when this host closes.'
|
|
121
|
+
: 'Serve already points to Presubmit and is managed outside this app.');
|
|
122
|
+
if (!Array.isArray(status.CertDomains) || !status.CertDomains.includes(host))
|
|
123
|
+
return this.update('blocked', 'Enable HTTPS certificates in Tailscale’s DNS settings, then retry. Funnel is not needed.');
|
|
124
|
+
if (this.lastError)
|
|
125
|
+
return this.update('error', this.lastError);
|
|
126
|
+
return this.update('stopped', this.remote
|
|
127
|
+
? 'Tailscale Serve is stopped.'
|
|
128
|
+
: 'Ready to connect. Start Serve to use this computer’s Tailscale address.');
|
|
129
|
+
}
|
|
130
|
+
configure() {
|
|
131
|
+
const origin = this.remote ?? this.detectedOrigin;
|
|
132
|
+
if (!origin)
|
|
133
|
+
throw new Error('Could not find this computer’s Tailscale address.');
|
|
134
|
+
this.options.save({ origin, autoStart: this.state.autoStart });
|
|
135
|
+
this.options.configure?.(origin);
|
|
136
|
+
this.remote = origin;
|
|
137
|
+
this.state = { ...this.state, tailscaleOrigin: origin };
|
|
138
|
+
return origin;
|
|
139
|
+
}
|
|
140
|
+
async status(force = false) {
|
|
141
|
+
if (this.closed || this.operation)
|
|
142
|
+
return { ...this.state };
|
|
143
|
+
if (!force && Date.now() - this.checkedAt < 3000)
|
|
144
|
+
return { ...this.state };
|
|
145
|
+
return (this.read ??= this.inspect(this.lifetime.signal)
|
|
146
|
+
.catch((error) => this.update('error', error instanceof Error ? error.message : 'Could not check Tailscale.'))
|
|
147
|
+
.finally(() => {
|
|
148
|
+
this.checkedAt = Date.now();
|
|
149
|
+
this.read = undefined;
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
async setAutoStart(enabled) {
|
|
153
|
+
if (this.closed)
|
|
154
|
+
throw new Error('Server is stopping.');
|
|
155
|
+
const previous = this.operation;
|
|
156
|
+
const operation = (async () => {
|
|
157
|
+
await previous;
|
|
158
|
+
await this.read;
|
|
159
|
+
this.lifetime.signal.throwIfAborted();
|
|
160
|
+
if (!this.remote) {
|
|
161
|
+
this.lastError = '';
|
|
162
|
+
const current = await this.inspect(this.lifetime.signal);
|
|
163
|
+
if (current.phase !== 'stopped')
|
|
164
|
+
return current;
|
|
165
|
+
}
|
|
166
|
+
this.lifetime.signal.throwIfAborted();
|
|
167
|
+
const origin = this.configure();
|
|
168
|
+
this.options.save({ origin, autoStart: enabled });
|
|
169
|
+
this.state = { ...this.state, autoStart: enabled };
|
|
170
|
+
return { ...this.state };
|
|
171
|
+
})().catch((error) => this.update('error', error instanceof Error ? error.message : 'Could not save startup preference.'));
|
|
172
|
+
this.operation = operation;
|
|
173
|
+
try {
|
|
174
|
+
return await operation;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
if (this.operation === operation)
|
|
178
|
+
this.operation = undefined;
|
|
179
|
+
this.checkedAt = 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
restore() {
|
|
183
|
+
if (this.state.autoStart)
|
|
184
|
+
void this.start();
|
|
185
|
+
}
|
|
186
|
+
start() {
|
|
187
|
+
if (this.closed)
|
|
188
|
+
return Promise.resolve({ ...this.state });
|
|
189
|
+
if (this.operation)
|
|
190
|
+
return this.operation;
|
|
191
|
+
const operation = this.startImpl().finally(() => {
|
|
192
|
+
if (this.operation === operation)
|
|
193
|
+
this.operation = undefined;
|
|
194
|
+
this.checkedAt = 0;
|
|
195
|
+
});
|
|
196
|
+
this.operation = operation;
|
|
197
|
+
return operation;
|
|
198
|
+
}
|
|
199
|
+
async startImpl() {
|
|
200
|
+
try {
|
|
201
|
+
await this.read;
|
|
202
|
+
this.lastError = '';
|
|
203
|
+
const signal = AbortSignal.any([
|
|
204
|
+
this.lifetime.signal,
|
|
205
|
+
AbortSignal.timeout(this.options.startupTimeoutMs ?? 15_000),
|
|
206
|
+
]);
|
|
207
|
+
const before = await this.inspect(signal);
|
|
208
|
+
if (before.phase !== 'stopped' && before.phase !== 'external' && before.phase !== 'running')
|
|
209
|
+
return before;
|
|
210
|
+
signal.throwIfAborted();
|
|
211
|
+
const remote = this.configure();
|
|
212
|
+
// Enabling an address can reuse an existing mapping without owning or replacing it.
|
|
213
|
+
const configured = await this.inspect(signal);
|
|
214
|
+
if (configured.phase !== 'stopped')
|
|
215
|
+
return configured;
|
|
216
|
+
signal.throwIfAborted();
|
|
217
|
+
// Never use --bg, reset, off, or Funnel. Only our foreground session is owned.
|
|
218
|
+
await this.process?.close();
|
|
219
|
+
this.update('starting', 'Starting Tailscale Serve…');
|
|
220
|
+
this.process = await this.driver.start(new URL(remote).port || '443', this.options.local);
|
|
221
|
+
for (;;) {
|
|
222
|
+
signal.throwIfAborted();
|
|
223
|
+
if (!this.process.alive)
|
|
224
|
+
throw new Error('Tailscale Serve exited. Check CLI permissions and HTTPS setup on the host, then retry.');
|
|
225
|
+
const current = await this.inspect(signal);
|
|
226
|
+
if (current.phase === 'running')
|
|
227
|
+
return current;
|
|
228
|
+
if (current.phase === 'blocked')
|
|
229
|
+
throw new Error(current.message);
|
|
230
|
+
this.update('starting', 'Starting Tailscale Serve…');
|
|
231
|
+
await delay(250, undefined, { signal });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
await this.process?.close();
|
|
236
|
+
this.process = undefined;
|
|
237
|
+
this.lastError = this.closed
|
|
238
|
+
? 'Presubmit is stopping.'
|
|
239
|
+
: error instanceof Error && error.name !== 'TimeoutError' && error.name !== 'AbortError'
|
|
240
|
+
? error.message
|
|
241
|
+
: 'Serve did not become ready. Check Tailscale and HTTPS setup on the host, then retry.';
|
|
242
|
+
return this.update('error', this.lastError);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async stop() {
|
|
246
|
+
if (this.closed)
|
|
247
|
+
return { ...this.state };
|
|
248
|
+
const previous = this.operation;
|
|
249
|
+
const operation = (async () => {
|
|
250
|
+
await previous;
|
|
251
|
+
await this.read;
|
|
252
|
+
await this.process?.close();
|
|
253
|
+
this.process = undefined;
|
|
254
|
+
this.lastError = '';
|
|
255
|
+
// An external Serve mapping is never stopped or adopted.
|
|
256
|
+
return this.inspect(this.lifetime.signal).catch((error) => this.update('error', error instanceof Error ? error.message : 'Could not check Tailscale.'));
|
|
257
|
+
})();
|
|
258
|
+
this.operation = operation;
|
|
259
|
+
try {
|
|
260
|
+
return await operation;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
if (this.operation === operation)
|
|
264
|
+
this.operation = undefined;
|
|
265
|
+
this.checkedAt = 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async close() {
|
|
269
|
+
this.closed = true;
|
|
270
|
+
this.lifetime.abort();
|
|
271
|
+
await this.operation;
|
|
272
|
+
await this.read;
|
|
273
|
+
await this.process?.close();
|
|
274
|
+
this.process = undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|