@venturewild/workspace 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +112 -73
- package/package.json +75 -69
- package/server/bin/wild-workspace.mjs +725 -95
- package/server/src/account.mjs +114 -0
- package/server/src/agent-identity.mjs +65 -0
- package/server/src/agent-readiness.mjs +200 -0
- package/server/src/agent.mjs +356 -335
- package/server/src/config.mjs +302 -236
- package/server/src/daemon-bin.mjs +6 -2
- package/server/src/daemon-supervisor.mjs +216 -0
- package/server/src/daemon.mjs +6 -0
- package/server/src/doctor.mjs +246 -0
- package/server/src/error-reporter.mjs +86 -0
- package/server/src/inbox.mjs +86 -81
- package/server/src/index.mjs +1330 -635
- package/server/src/logpaths.mjs +97 -0
- package/server/src/observability.mjs +45 -0
- package/server/src/operator.mjs +65 -0
- package/server/src/service.mjs +127 -0
- package/server/src/session-reporter.mjs +201 -0
- package/server/src/supervisor.mjs +217 -0
- package/server/src/sync.mjs +248 -176
- package/server/src/transcript.mjs +121 -0
- package/web/dist/assets/index-Bj-mdLGj.css +1 -0
- package/web/dist/assets/index-DLRgyr9j.js +89 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DOwej8U4.js +0 -89
- package/web/dist/assets/index-DZkyDo10.css +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Per-install bmo-sync account — the user's identity in the wild-* ecosystem.
|
|
2
|
+
//
|
|
3
|
+
// Distinct from `agent-identity.mjs` (which holds the agent's name/tone/color)
|
|
4
|
+
// and from `secrets.mjs` (per-install partner/share tokens). An account binds
|
|
5
|
+
// this installation to:
|
|
6
|
+
// - a `slug` (the `<slug>.venturewild.llc` namespace, claimed at signup)
|
|
7
|
+
// - an `email` (human identification — NOT used as a password)
|
|
8
|
+
// - an `accountId` (uuid)
|
|
9
|
+
// - an `accountToken` (long-random secret that proves ownership to
|
|
10
|
+
// `sync.venturewild.llc`; never sent to the browser)
|
|
11
|
+
//
|
|
12
|
+
// Persisted at `<dataDir>/account.json` (mode 0600). Absence means the user
|
|
13
|
+
// has not run `wild-workspace login` yet; the server still works in localhost
|
|
14
|
+
// mode but `<slug>.venturewild.llc` isn't configured.
|
|
15
|
+
//
|
|
16
|
+
// Login flow:
|
|
17
|
+
// 1. User registers at `workspace.venturewild.llc` (landing).
|
|
18
|
+
// 2. Landing displays a single opaque payload (base64url-encoded JSON).
|
|
19
|
+
// 3. User runs `wild-workspace login <payload>`.
|
|
20
|
+
// 4. We decode + persist here.
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const FILE = 'account.json';
|
|
26
|
+
|
|
27
|
+
function accountPath(dataDir) {
|
|
28
|
+
return path.join(dataDir, FILE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadAccount(dataDir) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(fs.readFileSync(accountPath(dataDir), 'utf8'));
|
|
34
|
+
return sanitize(parsed);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sanitize(raw) {
|
|
41
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
42
|
+
const slug = typeof raw.slug === 'string' ? raw.slug.trim().toLowerCase() : '';
|
|
43
|
+
const email = typeof raw.email === 'string' ? raw.email.trim().toLowerCase() : '';
|
|
44
|
+
const accountId = typeof raw.accountId === 'string' ? raw.accountId.trim() : '';
|
|
45
|
+
const accountToken = typeof raw.accountToken === 'string' ? raw.accountToken.trim() : '';
|
|
46
|
+
if (!slug || !email || !accountId || !accountToken) return null;
|
|
47
|
+
return {
|
|
48
|
+
slug,
|
|
49
|
+
email,
|
|
50
|
+
accountId,
|
|
51
|
+
accountToken,
|
|
52
|
+
displayName: typeof raw.displayName === 'string' ? raw.displayName : null,
|
|
53
|
+
loggedInAt: Number(raw.loggedInAt) || Date.now(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function saveAccount(dataDir, account) {
|
|
58
|
+
const merged = sanitize({ loggedInAt: Date.now(), ...account });
|
|
59
|
+
if (!merged) throw new Error('account-invalid');
|
|
60
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
61
|
+
fs.writeFileSync(accountPath(dataDir), JSON.stringify(merged, null, 2), {
|
|
62
|
+
mode: 0o600,
|
|
63
|
+
});
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function clearAccount(dataDir) {
|
|
68
|
+
try {
|
|
69
|
+
fs.unlinkSync(accountPath(dataDir));
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Parse a single opaque token-blob the user pastes from the landing page.
|
|
77
|
+
// The landing emits base64url(JSON({slug,email,accountId,token,displayName?})).
|
|
78
|
+
// We accept either base64url or raw JSON for forward-compat / curl debugging.
|
|
79
|
+
export function decodeLoginPayload(input) {
|
|
80
|
+
const raw = String(input || '').trim();
|
|
81
|
+
if (!raw) throw new Error('Empty login payload — paste the blob from your signup page.');
|
|
82
|
+
// 1. Try base64url decode.
|
|
83
|
+
const tryBase64 = () => {
|
|
84
|
+
let b = raw.replace(/-/g, '+').replace(/_/g, '/');
|
|
85
|
+
while (b.length % 4 !== 0) b += '=';
|
|
86
|
+
return Buffer.from(b, 'base64').toString('utf8');
|
|
87
|
+
};
|
|
88
|
+
// 2. Try raw JSON (if it starts with `{`).
|
|
89
|
+
let jsonText;
|
|
90
|
+
if (raw[0] === '{') {
|
|
91
|
+
jsonText = raw;
|
|
92
|
+
} else {
|
|
93
|
+
try {
|
|
94
|
+
jsonText = tryBase64();
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error('Login payload is not base64url-encoded JSON.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(jsonText);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('Login payload decoded but is not valid JSON.');
|
|
104
|
+
}
|
|
105
|
+
// Accept both `token` (what the landing emits) and `accountToken`.
|
|
106
|
+
if (parsed.token && !parsed.accountToken) parsed.accountToken = parsed.token;
|
|
107
|
+
if (!parsed.slug || !parsed.email || !parsed.accountId || !parsed.accountToken) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Login payload is missing one or more required fields ' +
|
|
110
|
+
'(slug, email, accountId, token). Re-copy from the signup page?',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Agent identity — the user-given name, voice/tone, and accent color the agent
|
|
2
|
+
// adopts during onboarding (see docs/onboarding-flow-plan in memory). Persisted
|
|
3
|
+
// to <dataDir>/agent-identity.json so it survives restarts. Absence of this
|
|
4
|
+
// file is how the server detects "first run" and triggers the onboarding UI.
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const FILE = 'agent-identity.json';
|
|
10
|
+
|
|
11
|
+
export const TONES = Object.freeze(['concise', 'playful', 'formal', 'dry']);
|
|
12
|
+
|
|
13
|
+
function identityPath(dataDir) {
|
|
14
|
+
return path.join(dataDir, FILE);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadIdentity(dataDir) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(fs.readFileSync(identityPath(dataDir), 'utf8'));
|
|
20
|
+
return sanitize(parsed);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sanitize(raw) {
|
|
27
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
28
|
+
const name = typeof raw.name === 'string' ? raw.name.trim().slice(0, 40) : '';
|
|
29
|
+
if (!name) return null;
|
|
30
|
+
const tone = TONES.includes(raw.tone) ? raw.tone : 'concise';
|
|
31
|
+
const color =
|
|
32
|
+
typeof raw.color === 'string' && /^#[0-9a-fA-F]{6}$/.test(raw.color)
|
|
33
|
+
? raw.color
|
|
34
|
+
: '#22d3ee';
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
tone,
|
|
38
|
+
color,
|
|
39
|
+
createdAt: Number(raw.createdAt) || Date.now(),
|
|
40
|
+
onboardedAt: raw.onboardedAt ? Number(raw.onboardedAt) : null,
|
|
41
|
+
workspaceFirstSeen: raw.workspaceFirstSeen || null,
|
|
42
|
+
connectedServices: Array.isArray(raw.connectedServices)
|
|
43
|
+
? raw.connectedServices.filter((s) => typeof s === 'string')
|
|
44
|
+
: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function saveIdentity(dataDir, partial) {
|
|
49
|
+
const existing = loadIdentity(dataDir) || {};
|
|
50
|
+
const merged = sanitize({
|
|
51
|
+
createdAt: Date.now(),
|
|
52
|
+
...existing,
|
|
53
|
+
...partial,
|
|
54
|
+
});
|
|
55
|
+
if (!merged) throw new Error('identity-requires-name');
|
|
56
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
57
|
+
fs.writeFileSync(identityPath(dataDir), JSON.stringify(merged, null, 2));
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function markOnboarded(dataDir) {
|
|
62
|
+
const existing = loadIdentity(dataDir);
|
|
63
|
+
if (!existing) throw new Error('identity-not-set');
|
|
64
|
+
return saveIdentity(dataDir, { onboardedAt: Date.now() });
|
|
65
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Agent readiness — "is the wrapped agent actually able to talk?"
|
|
2
|
+
//
|
|
3
|
+
// THE GAP THIS CLOSES: detectAgents() only proves the `claude` binary is on the
|
|
4
|
+
// PATH. It does NOT prove the user has signed in. A brand-new user who installed
|
|
5
|
+
// the CLI but never ran `claude auth login` has a binary that resolves but every
|
|
6
|
+
// `claude -p` turn fails with an auth error. Onboarding's folder-peek (step 2)
|
|
7
|
+
// would then surface a scary error bubble — the exact "makes them feel stupid"
|
|
8
|
+
// failure docs/user-experience.md §4 forbids, and the open question in §3.2.
|
|
9
|
+
//
|
|
10
|
+
// THE SECOND GAP (added 2026-06-01): being signed in is NOT the same as being
|
|
11
|
+
// ABLE to run turns. A free claude.ai account can complete `claude auth login`
|
|
12
|
+
// (so `loggedIn:true`) yet Claude Code still refuses every prompt — a paid plan
|
|
13
|
+
// (Pro/Max/Team/Enterprise) or API billing is required. If we only checked
|
|
14
|
+
// `loggedIn` we'd wave a free user straight into the folder-peek, where the
|
|
15
|
+
// FIRST `claude -p` blows up with a billing error = the same scary bubble. So we
|
|
16
|
+
// also read `subscriptionType` / `apiProvider` and raise a distinct `subscribe`
|
|
17
|
+
// verdict ("you're in — you just need an active plan") instead.
|
|
18
|
+
//
|
|
19
|
+
// THE PROBE: `claude auth status` — a built-in, ZERO-TOKEN, cross-platform check
|
|
20
|
+
// (verified on Claude Code 2.1.158). It reads whatever credential store the
|
|
21
|
+
// platform uses (macOS Keychain, Windows ~/.claude/.credentials.json, Linux
|
|
22
|
+
// config), so a naive credentials-file check (which would false-negative on a
|
|
23
|
+
// signed-in Mac user like Mel) is wrong — we ask the CLI itself. It prints JSON:
|
|
24
|
+
// - signed in → exit 0, {"loggedIn":true,"authMethod":"claude.ai",
|
|
25
|
+
// "apiProvider":"firstParty","email":"…",
|
|
26
|
+
// "subscriptionType":"max",…}
|
|
27
|
+
// - signed out → exit 1, {"loggedIn":false,"authMethod":"none",…}
|
|
28
|
+
// We parse the JSON (authoritative) and fall back to the exit code if the shape
|
|
29
|
+
// ever changes.
|
|
30
|
+
//
|
|
31
|
+
// API-KEY / TOKEN BILLING: if ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN /
|
|
32
|
+
// CLAUDE_CODE_OAUTH_TOKEN) is set, `claude -p` bills through that and turns run
|
|
33
|
+
// even without a claude.ai OAuth session — so we treat a configured key as
|
|
34
|
+
// `ready` regardless of `auth status` (the sponsored / managed-key path).
|
|
35
|
+
//
|
|
36
|
+
// Only `claude` has this contract today. Other agents (gemini/glm/codex) report
|
|
37
|
+
// `unknown` — we never block on a readiness we can't measure (fail-open), so a
|
|
38
|
+
// Codex user is never gated by a Claude-shaped check.
|
|
39
|
+
|
|
40
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
41
|
+
import { promisify } from 'node:util';
|
|
42
|
+
|
|
43
|
+
const execFile = promisify(execFileCb);
|
|
44
|
+
|
|
45
|
+
// `claude auth status` is local-only (no network), but give it room on a cold
|
|
46
|
+
// cache / slow disk. Comfortably under the 5s onboarding "feels instant" budget.
|
|
47
|
+
const AUTH_PROBE_TIMEOUT_MS = 8000;
|
|
48
|
+
|
|
49
|
+
// Subscription tiers that CAN run Claude Code turns vs. ones that cannot. Lower-
|
|
50
|
+
// cased for comparison. An unknown, non-empty tier on a firstParty account is
|
|
51
|
+
// treated leniently (assumed paid) so a future tier name never false-gates a
|
|
52
|
+
// paying user behind a non-skippable wall — the empty/known-free cases are the
|
|
53
|
+
// only ones we positively block.
|
|
54
|
+
const PAID_TIERS = new Set(['pro', 'max', 'team', 'enterprise']);
|
|
55
|
+
const UNPAID_TIERS = new Set(['free', 'none', 'inactive', 'expired', 'trial_expired']);
|
|
56
|
+
|
|
57
|
+
// An API key / long-lived token configures billing directly — turns will run
|
|
58
|
+
// even with no OAuth session. Checked before the auth probe.
|
|
59
|
+
function hasApiBillingKey(env) {
|
|
60
|
+
const keys = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN'];
|
|
61
|
+
return keys.some((k) => typeof env?.[k] === 'string' && env[k].trim() !== '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Given a parsed `auth status` object for a signed-in user, can turns actually
|
|
65
|
+
// run? true = yes (paid plan or API billing), false = no (free / no plan),
|
|
66
|
+
// null = unknown shape → caller treats as ready (lenient, never false-gates).
|
|
67
|
+
export function canRunTurns(parsed) {
|
|
68
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
69
|
+
const sub =
|
|
70
|
+
typeof parsed.subscriptionType === 'string' ? parsed.subscriptionType.toLowerCase().trim() : '';
|
|
71
|
+
const provider =
|
|
72
|
+
typeof parsed.apiProvider === 'string' ? parsed.apiProvider.toLowerCase().trim() : '';
|
|
73
|
+
// A non-firstParty provider (console / bedrock / vertex) bills via API → runs.
|
|
74
|
+
if (provider && provider !== 'firstparty') return true;
|
|
75
|
+
if (PAID_TIERS.has(sub)) return true;
|
|
76
|
+
if (UNPAID_TIERS.has(sub)) return false;
|
|
77
|
+
// Signed in via claude.ai with NO subscription field at all → no active plan
|
|
78
|
+
// (the current CLI emits subscriptionType for paid accounts, so absence here
|
|
79
|
+
// is the free-account fingerprint).
|
|
80
|
+
if (sub === '') return false;
|
|
81
|
+
// Unknown, non-empty tier on a firstParty account — lean paid (don't gate).
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Readiness verdict for one agent.
|
|
87
|
+
* status: 'ready' — installed AND able to run turns (signed-in + plan, or API key).
|
|
88
|
+
* 'subscribe' — installed AND signed in, but no active plan → the user
|
|
89
|
+
* needs a Claude Pro (or higher) plan before turns run.
|
|
90
|
+
* 'login' — installed but NOT signed in; the user must auth.
|
|
91
|
+
* 'missing' — binary not on PATH (detect step already knew).
|
|
92
|
+
* 'unknown' — installed, but this agent has no auth-status probe
|
|
93
|
+
* we understand → never gate on it (fail-open).
|
|
94
|
+
* email: parsed sign-in identity when known (claude only), else null.
|
|
95
|
+
* hint: a short, human, NON-technical line the UI can show as-is.
|
|
96
|
+
*/
|
|
97
|
+
export async function probeClaudeAuth(agent, runner = defaultRunner, env = process.env) {
|
|
98
|
+
if (!agent || !agent.available) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'missing',
|
|
101
|
+
email: null,
|
|
102
|
+
hint: 'Claude Code isn’t installed yet. Install it, then come back.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Sponsored / managed billing: a configured key runs turns regardless of the
|
|
106
|
+
// OAuth session state, so don't gate.
|
|
107
|
+
if (hasApiBillingKey(env)) {
|
|
108
|
+
return { status: 'ready', email: null, hint: null };
|
|
109
|
+
}
|
|
110
|
+
const command = agent.resolvedPath || agent.binary;
|
|
111
|
+
try {
|
|
112
|
+
const { code, stdout } = await runner(command, ['auth', 'status']);
|
|
113
|
+
const parsed = parseAuthStatus(stdout);
|
|
114
|
+
// Authoritative: the JSON `loggedIn` flag. Fall back to the exit code
|
|
115
|
+
// (0 = signed in) only if the JSON is unparseable / shape-changed.
|
|
116
|
+
const loggedIn = parsed ? parsed.loggedIn === true : code === 0;
|
|
117
|
+
if (loggedIn) {
|
|
118
|
+
// Signed in — but can they actually run a turn? A free account can't.
|
|
119
|
+
if (canRunTurns(parsed) === false) {
|
|
120
|
+
return {
|
|
121
|
+
status: 'subscribe',
|
|
122
|
+
email: parsed?.email || null,
|
|
123
|
+
hint: 'You’re signed in — you just need an active Claude plan (Pro or higher) so your agent can run.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { status: 'ready', email: parsed?.email || null, hint: null };
|
|
127
|
+
}
|
|
128
|
+
// Signed out (or any non-ready shape). Treat as "needs login" — safer than
|
|
129
|
+
// claiming ready and then failing the first real turn mid-onboarding.
|
|
130
|
+
return {
|
|
131
|
+
status: 'login',
|
|
132
|
+
email: null,
|
|
133
|
+
hint: 'One quick thing: sign in to Claude so your agent can think.',
|
|
134
|
+
};
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// The probe itself failed to run (timeout, spawn error, unexpected CLI
|
|
137
|
+
// version with no `auth` subcommand). Don't hard-block onboarding on our
|
|
138
|
+
// own probe breaking — report unknown and let the turn-runner be the
|
|
139
|
+
// ground truth. Surfaced for logging, not shown as a scary error.
|
|
140
|
+
return {
|
|
141
|
+
status: 'unknown',
|
|
142
|
+
email: null,
|
|
143
|
+
hint: null,
|
|
144
|
+
error: String(err?.message || err),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Readiness for the active agent. Claude gets the real probe; every other agent
|
|
151
|
+
* is 'unknown' (we have no auth-status contract for them, so we never gate).
|
|
152
|
+
*/
|
|
153
|
+
export async function probeAgentReadiness(agent, runner = defaultRunner, env = process.env) {
|
|
154
|
+
if (agent && agent.id === 'claude') {
|
|
155
|
+
return probeClaudeAuth(agent, runner, env);
|
|
156
|
+
}
|
|
157
|
+
return { status: 'unknown', email: null, hint: null };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Parse `claude auth status` stdout. It prints a JSON object; tolerate leading/
|
|
161
|
+
// trailing noise by extracting the first {...} span. Returns null if nothing
|
|
162
|
+
// parseable is found (caller falls back to the exit code).
|
|
163
|
+
export function parseAuthStatus(stdout) {
|
|
164
|
+
const text = String(stdout || '');
|
|
165
|
+
const start = text.indexOf('{');
|
|
166
|
+
const end = text.lastIndexOf('}');
|
|
167
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
168
|
+
try {
|
|
169
|
+
const obj = JSON.parse(text.slice(start, end + 1));
|
|
170
|
+
return obj && typeof obj === 'object' ? obj : null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// execFile wrapper that resolves (never rejects) on a non-zero exit, so the
|
|
177
|
+
// caller can branch on { code, stdout } uniformly. Only a genuine spawn/timeout
|
|
178
|
+
// failure rejects — that's the 'unknown' path above.
|
|
179
|
+
function defaultRunner(command, args) {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
execFile(
|
|
182
|
+
command,
|
|
183
|
+
args,
|
|
184
|
+
{ timeout: AUTH_PROBE_TIMEOUT_MS, windowsHide: true },
|
|
185
|
+
(err, stdout, stderr) => {
|
|
186
|
+
if (err && typeof err.code !== 'number') {
|
|
187
|
+
// No numeric exit code => process never ran to completion
|
|
188
|
+
// (ENOENT, ETIMEDOUT, killed). That's a probe failure, not a verdict.
|
|
189
|
+
reject(err);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
resolve({
|
|
193
|
+
code: err ? err.code : 0,
|
|
194
|
+
stdout: stdout || '',
|
|
195
|
+
stderr: stderr || '',
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
}
|