runcloud 0.1.104 → 0.1.105
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/dist/diagnose.js +56 -20
- package/dist/sessionDiagnostics.js +13 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/diagnose.js
CHANGED
|
@@ -3,6 +3,28 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
import { GoogleAuth, Impersonated } from 'google-auth-library';
|
|
4
4
|
import { renderSessionDiagnosticTerminal, writeSessionDiagnosticBundle, } from './sessionDiagnostics.js';
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
|
+
const AUTH_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
7
|
+
const AUTH_DISCOVERY_PROJECT_ID = 'newly-diagnostics';
|
|
8
|
+
async function withDeadline(label, operation) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
let timer;
|
|
11
|
+
try {
|
|
12
|
+
return await Promise.race([
|
|
13
|
+
operation(controller.signal),
|
|
14
|
+
new Promise((_, reject) => {
|
|
15
|
+
timer = setTimeout(() => {
|
|
16
|
+
controller.abort();
|
|
17
|
+
reject(new Error(`${label} timed out after ${AUTH_ATTEMPT_TIMEOUT_MS}ms`));
|
|
18
|
+
}, AUTH_ATTEMPT_TIMEOUT_MS);
|
|
19
|
+
timer.unref?.();
|
|
20
|
+
}),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (timer)
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
6
28
|
const DEFAULT_URLS = {
|
|
7
29
|
dev: {
|
|
8
30
|
apiOps: 'https://cp-api-ops-dev-97683904813.us-east4.run.app',
|
|
@@ -102,26 +124,36 @@ function exactAudienceAuthorization(authorization, audience) {
|
|
|
102
124
|
return authorization;
|
|
103
125
|
}
|
|
104
126
|
async function impersonatedIdentityToken(audience, serviceAccount) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
return withDeadline('service-account impersonation', async (signal) => {
|
|
128
|
+
const sourceClient = await new GoogleAuth({
|
|
129
|
+
projectId: AUTH_DISCOVERY_PROJECT_ID,
|
|
130
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
131
|
+
clientOptions: { transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS } },
|
|
132
|
+
}).getClient();
|
|
133
|
+
const impersonated = new Impersonated({
|
|
134
|
+
sourceClient,
|
|
135
|
+
targetPrincipal: serviceAccount,
|
|
136
|
+
targetScopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
137
|
+
lifetime: 600,
|
|
138
|
+
transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS },
|
|
139
|
+
});
|
|
140
|
+
const token = await impersonated.fetchIdToken(audience, { includeEmail: true });
|
|
141
|
+
return exactAudienceAuthorization(`Bearer ${token}`, audience);
|
|
113
142
|
});
|
|
114
|
-
const token = await impersonated.fetchIdToken(audience, { includeEmail: true });
|
|
115
|
-
return exactAudienceAuthorization(`Bearer ${token}`, audience);
|
|
116
143
|
}
|
|
117
144
|
export async function resolveOpsAuthHeader(env = process.env, requestedAudience, requestedServiceAccount) {
|
|
118
145
|
const audience = requestedAudience ?? env.DIAGNOSE_OIDC_AUDIENCE ?? 'newly-diagnose-ops';
|
|
119
146
|
const serviceAccount = env.DIAGNOSE_OPS_IMPERSONATE_SERVICE_ACCOUNT ?? requestedServiceAccount;
|
|
120
147
|
let adcError = 'unknown ADC error';
|
|
121
148
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
149
|
+
return await withDeadline('ADC ID-token resolution', async (signal) => {
|
|
150
|
+
const auth = new GoogleAuth({
|
|
151
|
+
projectId: AUTH_DISCOVERY_PROJECT_ID,
|
|
152
|
+
clientOptions: { transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS } },
|
|
153
|
+
});
|
|
154
|
+
const client = await auth.getIdTokenClient(audience);
|
|
155
|
+
return exactAudienceAuthorization(extractAuthorization(await client.getRequestHeaders()), audience);
|
|
156
|
+
});
|
|
125
157
|
}
|
|
126
158
|
catch (error) {
|
|
127
159
|
adcError = error instanceof Error ? error.message : String(error);
|
|
@@ -251,6 +283,8 @@ export async function fetchSessionDiagnosticReport(input) {
|
|
|
251
283
|
params.set('session', input.session);
|
|
252
284
|
if (input.project)
|
|
253
285
|
params.set('project', projectIdFromInput(input.project));
|
|
286
|
+
if (input.user)
|
|
287
|
+
params.set('user', input.user);
|
|
254
288
|
if (input.at)
|
|
255
289
|
params.set('at', input.at);
|
|
256
290
|
const res = await fetch(`${input.target.url}/diagnose/session?${params}`, {
|
|
@@ -293,17 +327,18 @@ export function registerDiagnose(program) {
|
|
|
293
327
|
.option('--env <env>', 'dev, prod, or pr-<number>', 'dev')
|
|
294
328
|
.option('--session <id>', 'diagnose this exact session')
|
|
295
329
|
.option('--project <id-or-url>', 'diagnose the latest/nearest session for a project')
|
|
296
|
-
.option('--
|
|
330
|
+
.option('--user <email>', 'diagnose the latest/nearest session started by this user')
|
|
331
|
+
.option('--at <iso-time>', 'select the project/user session nearest this incident time')
|
|
297
332
|
.option('--out <dir>', 'write evidence, report, diagram, and agent repro/fix prompt')
|
|
298
333
|
.option('--json', 'emit the merged JSON document instead of the human rollup')
|
|
299
334
|
.action(async (opts) => {
|
|
300
335
|
try {
|
|
301
|
-
if (opts.session || opts.project) {
|
|
302
|
-
if (
|
|
303
|
-
throw new Error('Provide exactly one of --session or --
|
|
336
|
+
if (opts.session || opts.project || opts.user) {
|
|
337
|
+
if ([opts.session, opts.project, opts.user].filter(Boolean).length !== 1) {
|
|
338
|
+
throw new Error('Provide exactly one of --session, --project, or --user');
|
|
304
339
|
}
|
|
305
|
-
if (opts.at &&
|
|
306
|
-
throw new Error('--at requires --project because an exact --session needs no time selection');
|
|
340
|
+
if (opts.at && opts.session) {
|
|
341
|
+
throw new Error('--at requires --project or --user because an exact --session needs no time selection');
|
|
307
342
|
}
|
|
308
343
|
if (opts.at && Number.isNaN(Date.parse(opts.at))) {
|
|
309
344
|
throw new Error('--at must be an ISO timestamp');
|
|
@@ -315,6 +350,7 @@ export function registerDiagnose(program) {
|
|
|
315
350
|
authHeader,
|
|
316
351
|
...(opts.session ? { session: opts.session } : {}),
|
|
317
352
|
...(opts.project ? { project: opts.project } : {}),
|
|
353
|
+
...(opts.user ? { user: opts.user } : {}),
|
|
318
354
|
...(opts.at ? { at: opts.at } : {}),
|
|
319
355
|
});
|
|
320
356
|
if (opts.out) {
|
|
@@ -326,7 +362,7 @@ export function registerDiagnose(program) {
|
|
|
326
362
|
return;
|
|
327
363
|
}
|
|
328
364
|
if (opts.at || opts.out) {
|
|
329
|
-
throw new Error('--at and --out require --session or --
|
|
365
|
+
throw new Error('--at and --out require --session, --project, or --user');
|
|
330
366
|
}
|
|
331
367
|
if (opts.env !== 'dev' && opts.env !== 'prod') {
|
|
332
368
|
throw new Error('Global health diagnosis supports only --env dev or prod');
|
|
@@ -24,7 +24,16 @@ function nodeLabel(report, id) {
|
|
|
24
24
|
return `[${STATUS_TEXT[item.status]} ${item.label}]`;
|
|
25
25
|
}
|
|
26
26
|
export function renderAsciiDiagram(report) {
|
|
27
|
-
const main = [
|
|
27
|
+
const main = [
|
|
28
|
+
'session',
|
|
29
|
+
'venue',
|
|
30
|
+
'runner',
|
|
31
|
+
'command',
|
|
32
|
+
'metro',
|
|
33
|
+
'simulator_lease',
|
|
34
|
+
'simulator_app',
|
|
35
|
+
'preview',
|
|
36
|
+
];
|
|
28
37
|
const mainLine = main.map((id) => nodeLabel(report, id)).join(' -> ');
|
|
29
38
|
const runnerOffset = main.slice(0, 3).map((id) => nodeLabel(report, id)).join(' -> ').length;
|
|
30
39
|
const editor = nodeLabel(report, 'code_server');
|
|
@@ -42,7 +51,7 @@ function mermaidEscape(value) {
|
|
|
42
51
|
export function renderMermaidDiagram(report) {
|
|
43
52
|
const lines = [
|
|
44
53
|
'flowchart LR',
|
|
45
|
-
' session --> venue --> runner --> command --> metro --> preview',
|
|
54
|
+
' session --> venue --> runner --> command --> metro --> simulator_lease --> simulator_app --> preview',
|
|
46
55
|
' runner --> code_server',
|
|
47
56
|
];
|
|
48
57
|
for (const item of report.stages) {
|
|
@@ -172,6 +181,8 @@ Use the adjacent \`bundle.json\` as evidence, not as a conclusion to repeat blin
|
|
|
172
181
|
|
|
173
182
|
If the diagnosis is \`evidence_missing\`, repair evidence collection first: verify the \`session_diagnostic_events\` migration is applied, API/worker/launcher provenance matches, and the controlled PR preview smoke persists its full correlated timeline. Do not infer a product cause from absent telemetry.
|
|
174
183
|
|
|
184
|
+
For \`simulator_app_failed\`, inspect the persisted \`simulator.app.failed\` event first. In a live newly-desktop cloud session, call \`get_mobile_preview_logs\` to retrieve the retained native startup tail, Metro output, debugger status, and React Native log registry. Treat application exceptions as userland code to reproduce and fix in the project; do not relabel a successful \`simctl\`/\`adb\` launch as a healthy app.
|
|
185
|
+
|
|
175
186
|
1. Identify the first boundary whose recorded output does not match its required input.
|
|
176
187
|
2. Add or extend the smallest regression test that reproduces that exact failure. For cross-service delivery, use the normal PR environment smoke/e2e path instead of mocks alone.
|
|
177
188
|
3. Fix the production path durably in \`control-plane/\`, \`newly-desktop/\`, or \`newly-cli/\`. Do not add work to the frozen legacy Modal backend or Neon stack.
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.1.
|
|
1
|
+
export const CLI_VERSION = '0.1.105';
|