@pdpp/cli 0.1.0-beta.7 → 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/README.md +51 -13
- package/package.json +2 -2
- package/src/collector/commands.js +5 -6
- package/src/collector/runner.js +3 -3
- package/src/index.js +54 -3
- package/src/owner-agent/command.js +368 -0
- package/src/owner-agent/control.js +138 -0
- package/src/owner-agent/credential-store.js +126 -0
- package/src/owner-agent/device-flow.js +145 -0
- package/src/owner-agent/discovery.js +233 -0
- package/src/owner-agent/errors.js +13 -0
- package/src/owner-agent/lifecycle.js +126 -0
- package/src/owner-agent/setup.js +378 -0
- package/src/package-info.d.ts +2 -2
- package/src/package-info.js +4 -2
- package/src/read/commands.js +250 -0
- package/src/ref/auth.js +179 -0
- package/src/ref/commands/call.js +168 -0
- package/src/ref/commands/connectors.js +44 -4
- package/src/ref/commands/event-subscriptions.js +190 -0
- package/src/ref/commands/grant.js +3 -1
- package/src/ref/commands/run.js +3 -1
- package/src/ref/commands/trace.js +3 -1
- package/src/ref/output.js +44 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseArgs, requirePositional } from '../args.js';
|
|
2
2
|
import { PdppUsageError } from '../errors.js';
|
|
3
3
|
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
-
import { resolveFormat, writeData } from '../output.js';
|
|
4
|
+
import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js';
|
|
5
5
|
|
|
6
6
|
// Operator-facing summary projection. Mirrors the evidence the dashboard renders
|
|
7
|
-
// in `apps/
|
|
7
|
+
// in `apps/console/src/app/dashboard/lib/ref-client.ts` (RefConnectorSummary +
|
|
8
8
|
// RefConnectionHealthSnapshot + RefNextAction). The reference server has
|
|
9
9
|
// already redacted secret-bearing fields (e.g. `action_target` for
|
|
10
10
|
// `sensitivity: "secret"` attention rows); we surface what arrives, with no
|
|
@@ -17,6 +17,7 @@ function projectSummaryRow(summary) {
|
|
|
17
17
|
const schedule = summary?.schedule || null;
|
|
18
18
|
const lastRun = summary?.last_run || null;
|
|
19
19
|
const lastSuccess = summary?.last_successful_run || null;
|
|
20
|
+
const dominantCondition = findConditionById(health.conditions, health.dominant_condition_id);
|
|
20
21
|
return {
|
|
21
22
|
connection_id: summary?.connection_id ?? null,
|
|
22
23
|
connector_id: summary?.connector_id ?? null,
|
|
@@ -29,6 +30,13 @@ function projectSummaryRow(summary) {
|
|
|
29
30
|
syncing: badges.syncing === true,
|
|
30
31
|
stale: badges.stale === true,
|
|
31
32
|
reason_code: health.reason_code ?? null,
|
|
33
|
+
dominant_condition_id: health.dominant_condition_id ?? null,
|
|
34
|
+
dominant_condition_type: dominantCondition?.type ?? null,
|
|
35
|
+
dominant_condition_reason: dominantCondition?.reason ?? null,
|
|
36
|
+
dominant_condition_severity: dominantCondition?.severity ?? null,
|
|
37
|
+
dominant_condition_message: dominantCondition?.message ?? null,
|
|
38
|
+
dominant_condition_origin: dominantCondition?.origin ?? null,
|
|
39
|
+
supporting_condition_ids: Array.isArray(health.supporting_condition_ids) ? health.supporting_condition_ids : [],
|
|
32
40
|
unknown_reasons: Array.isArray(health.unknown_reasons) ? health.unknown_reasons : [],
|
|
33
41
|
next_action_source: nextAction?.source ?? 'none',
|
|
34
42
|
next_action_reason: nextAction?.reason_code ?? null,
|
|
@@ -42,10 +50,18 @@ function projectSummaryRow(summary) {
|
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
function findConditionById(conditions, id) {
|
|
54
|
+
if (!id || !Array.isArray(conditions)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return conditions.find((condition) => condition?.id === id) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
export async function runRefConnectors(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
46
61
|
const [subcommand, ...rest] = argv;
|
|
47
62
|
const { flags, positionals } = parseArgs(rest);
|
|
48
63
|
const out = io.stdout || process.stdout;
|
|
64
|
+
const err = io.stderr || process.stderr;
|
|
49
65
|
|
|
50
66
|
if (subcommand === 'list') {
|
|
51
67
|
const asUrl = resolveReferenceUrl(flags);
|
|
@@ -60,10 +76,12 @@ export async function runRefConnectors(argv, io = {}, fetchImpl = globalThis.fet
|
|
|
60
76
|
const verbose = flags.verbose === true || flags.verbose === 'true';
|
|
61
77
|
if (verbose) {
|
|
62
78
|
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
79
|
+
writeEnvelopeWarnings(body, err);
|
|
63
80
|
return 0;
|
|
64
81
|
}
|
|
65
82
|
const rows = Array.isArray(body?.data) ? body.data.map(projectSummaryRow) : [];
|
|
66
|
-
writeData(format === 'table' ? rows : { object: 'list', data: rows }, format, out);
|
|
83
|
+
writeData(format === 'table' ? rows.map(projectSummaryTableRow) : { object: 'list', data: rows }, format, out);
|
|
84
|
+
writeEnvelopeWarnings(body, err);
|
|
67
85
|
return 0;
|
|
68
86
|
}
|
|
69
87
|
|
|
@@ -81,10 +99,12 @@ export async function runRefConnectors(argv, io = {}, fetchImpl = globalThis.fet
|
|
|
81
99
|
const verbose = flags.verbose === true || flags.verbose === 'true';
|
|
82
100
|
if (verbose) {
|
|
83
101
|
writeData(body, format, out);
|
|
102
|
+
writeEnvelopeWarnings(body, err);
|
|
84
103
|
return 0;
|
|
85
104
|
}
|
|
86
105
|
const row = projectSummaryRow(body);
|
|
87
|
-
writeData(format === 'table' ? [row] : row, format, out);
|
|
106
|
+
writeData(format === 'table' ? [projectSummaryTableRow(row)] : row, format, out);
|
|
107
|
+
writeEnvelopeWarnings(body, err);
|
|
88
108
|
return 0;
|
|
89
109
|
}
|
|
90
110
|
|
|
@@ -92,3 +112,23 @@ export async function runRefConnectors(argv, io = {}, fetchImpl = globalThis.fet
|
|
|
92
112
|
'Usage: pdpp ref connectors <list|show <connector-id>> [--as-url <url>] [--owner-session <cookie>] [--format json|table] [--verbose]'
|
|
93
113
|
);
|
|
94
114
|
}
|
|
115
|
+
|
|
116
|
+
function projectSummaryTableRow(row) {
|
|
117
|
+
return {
|
|
118
|
+
connection_id: row.connection_id,
|
|
119
|
+
connector_id: row.connector_id,
|
|
120
|
+
display_name: row.display_name,
|
|
121
|
+
state: row.state,
|
|
122
|
+
coverage: row.coverage,
|
|
123
|
+
freshness: row.freshness,
|
|
124
|
+
attention: row.attention,
|
|
125
|
+
outbox: row.outbox,
|
|
126
|
+
syncing: row.syncing,
|
|
127
|
+
stale: row.stale,
|
|
128
|
+
reason_code: row.reason_code,
|
|
129
|
+
dominant_condition_reason: row.dominant_condition_reason,
|
|
130
|
+
next_action_reason: row.next_action_reason,
|
|
131
|
+
last_success_at: row.last_success_at,
|
|
132
|
+
next_attempt_at: row.next_attempt_at,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pdpp ref event-subscriptions` — operator oversight of client event
|
|
3
|
+
* subscriptions. Mirrors the `_ref/event-subscriptions*` HTTP routes
|
|
4
|
+
* one-to-one. Owner-session-only; the CLI never sends or receives
|
|
5
|
+
* subscription secret material because the `_ref` projection strips it.
|
|
6
|
+
*
|
|
7
|
+
* Spec: openspec/changes/add-client-event-subscription-management/specs/
|
|
8
|
+
* reference-implementation-architecture/spec.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseArgs, requirePositional } from '../args.js';
|
|
12
|
+
import { PdppCliError, PdppUsageError } from '../errors.js';
|
|
13
|
+
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
14
|
+
import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js';
|
|
15
|
+
|
|
16
|
+
function projectListRow(row) {
|
|
17
|
+
return {
|
|
18
|
+
subscription_id: row.subscription_id,
|
|
19
|
+
authority: row.authority_kind || 'client_grant',
|
|
20
|
+
client_id: row.client_id,
|
|
21
|
+
grant_id: row.grant_id || '',
|
|
22
|
+
status: row.status,
|
|
23
|
+
callback_host: row.callback_host,
|
|
24
|
+
disabled_reason: row.disabled_reason ?? '',
|
|
25
|
+
pending: row.pending_queue_count ?? 0,
|
|
26
|
+
final_failures: row.final_failure_count ?? 0,
|
|
27
|
+
last_attempt_at: row.last_attempted_at ?? '',
|
|
28
|
+
last_attempt_ok: row.last_attempt_ok === null || row.last_attempt_ok === undefined ? '' : row.last_attempt_ok ? 'ok' : 'fail',
|
|
29
|
+
last_attempt_code: row.last_attempt_status_code ?? '',
|
|
30
|
+
updated_at: row.updated_at,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function projectDetail(detail) {
|
|
35
|
+
return {
|
|
36
|
+
subscription_id: detail.subscription_id,
|
|
37
|
+
authority: detail.authority_kind || 'client_grant',
|
|
38
|
+
client_id: detail.client_id,
|
|
39
|
+
grant_id: detail.grant_id || '',
|
|
40
|
+
status: detail.status,
|
|
41
|
+
disabled_reason: detail.disabled_reason ?? '',
|
|
42
|
+
callback_url: detail.callback_url,
|
|
43
|
+
created_at: detail.created_at,
|
|
44
|
+
updated_at: detail.updated_at,
|
|
45
|
+
disabled_at: detail.disabled_at ?? '',
|
|
46
|
+
pending_queue_count: detail.pending_queue_count,
|
|
47
|
+
final_failure_count: detail.final_failure_count,
|
|
48
|
+
last_attempt_at: detail.last_attempted_at ?? '',
|
|
49
|
+
last_attempt_ok: detail.last_attempt_ok ?? '',
|
|
50
|
+
last_attempt_code: detail.last_attempt_status_code ?? '',
|
|
51
|
+
recent_attempts: (detail.recent_attempts || []).length,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readConfirmation(io) {
|
|
56
|
+
const stdin = io.stdin || process.stdin;
|
|
57
|
+
if (!stdin || stdin.isTTY === false) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let buf = '';
|
|
62
|
+
const onData = (chunk) => {
|
|
63
|
+
buf += chunk;
|
|
64
|
+
const newlineIdx = buf.indexOf('\n');
|
|
65
|
+
if (newlineIdx !== -1) {
|
|
66
|
+
stdin.removeListener('data', onData);
|
|
67
|
+
if (typeof stdin.setRawMode === 'function') {
|
|
68
|
+
try { stdin.setRawMode(false); } catch { /* ignore */ }
|
|
69
|
+
}
|
|
70
|
+
try { stdin.pause(); } catch { /* ignore */ }
|
|
71
|
+
resolve(buf.slice(0, newlineIdx).trim());
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
try { stdin.setEncoding('utf8'); } catch { /* ignore */ }
|
|
75
|
+
stdin.on('data', onData);
|
|
76
|
+
try { stdin.resume(); } catch { /* ignore */ }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runRefEventSubscriptions(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
81
|
+
const [subcommand, ...rest] = argv;
|
|
82
|
+
const { flags, positionals } = parseArgs(rest);
|
|
83
|
+
const out = io.stdout || process.stdout;
|
|
84
|
+
const err = io.stderr || process.stderr;
|
|
85
|
+
|
|
86
|
+
if (subcommand === 'list') {
|
|
87
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
88
|
+
const ownerSession = flags['owner-session'] || '';
|
|
89
|
+
const cacheRoot = flags['cache-root'];
|
|
90
|
+
const query = new URLSearchParams();
|
|
91
|
+
if (flags['client-id']) query.set('client_id', String(flags['client-id']));
|
|
92
|
+
if (flags['grant-id']) query.set('grant_id', String(flags['grant-id']));
|
|
93
|
+
if (flags.status) query.set('status', String(flags.status));
|
|
94
|
+
const queryString = query.toString();
|
|
95
|
+
const url = `${asUrl}/_ref/event-subscriptions${queryString ? `?${queryString}` : ''}`;
|
|
96
|
+
const { body } = await fetchJson(
|
|
97
|
+
url,
|
|
98
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
99
|
+
fetchImpl,
|
|
100
|
+
);
|
|
101
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
102
|
+
const rows = body?.data || [];
|
|
103
|
+
if (format === 'table') {
|
|
104
|
+
writeData(rows.map(projectListRow), 'table', out);
|
|
105
|
+
} else {
|
|
106
|
+
writeData(body, format, out);
|
|
107
|
+
}
|
|
108
|
+
writeEnvelopeWarnings(body, err);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (subcommand === 'show') {
|
|
113
|
+
const subscriptionId = requirePositional(positionals, 0, 'subscription-id');
|
|
114
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
115
|
+
const ownerSession = flags['owner-session'] || '';
|
|
116
|
+
const cacheRoot = flags['cache-root'];
|
|
117
|
+
const { body } = await fetchJson(
|
|
118
|
+
`${asUrl}/_ref/event-subscriptions/${encodeURIComponent(subscriptionId)}`,
|
|
119
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
120
|
+
fetchImpl,
|
|
121
|
+
);
|
|
122
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
123
|
+
if (format === 'table') {
|
|
124
|
+
writeData(projectDetail(body), 'table', out);
|
|
125
|
+
} else {
|
|
126
|
+
writeData(body, format, out);
|
|
127
|
+
}
|
|
128
|
+
writeEnvelopeWarnings(body, err);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (subcommand === 'disable') {
|
|
133
|
+
const subscriptionId = requirePositional(positionals, 0, 'subscription-id');
|
|
134
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
135
|
+
const ownerSession = flags['owner-session'] || '';
|
|
136
|
+
const cacheRoot = flags['cache-root'];
|
|
137
|
+
const reason = typeof flags.reason === 'string' ? flags.reason : null;
|
|
138
|
+
const explicitYes = flags.yes === true || flags.yes === 'true';
|
|
139
|
+
|
|
140
|
+
if (!explicitYes) {
|
|
141
|
+
const headers = { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) };
|
|
142
|
+
const { body } = await fetchJson(
|
|
143
|
+
`${asUrl}/_ref/event-subscriptions/${encodeURIComponent(subscriptionId)}`,
|
|
144
|
+
{ headers },
|
|
145
|
+
fetchImpl,
|
|
146
|
+
);
|
|
147
|
+
const authority = body.authority_kind || 'client_grant';
|
|
148
|
+
const grant = body.grant_id || 'none';
|
|
149
|
+
err.write(`Subscription ${body.subscription_id} (authority=${authority}, client=${body.client_id}, grant=${grant}, status=${body.status})\n`);
|
|
150
|
+
err.write(`Callback: ${body.callback_url}\n`);
|
|
151
|
+
err.write(`Disable subscription? Type 'yes' to confirm: `);
|
|
152
|
+
const answer = await readConfirmation(io);
|
|
153
|
+
if (!answer || answer.toLowerCase() !== 'yes') {
|
|
154
|
+
err.write('Aborted.\n');
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const body = reason ? { reason } : {};
|
|
160
|
+
const { body: detail } = await fetchJson(
|
|
161
|
+
`${asUrl}/_ref/event-subscriptions/${encodeURIComponent(subscriptionId)}/disable`,
|
|
162
|
+
{
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: {
|
|
165
|
+
'Content-Type': 'application/json',
|
|
166
|
+
...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }),
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
},
|
|
170
|
+
fetchImpl,
|
|
171
|
+
);
|
|
172
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
173
|
+
if (format === 'table') {
|
|
174
|
+
writeData(projectDetail(detail), 'table', out);
|
|
175
|
+
} else {
|
|
176
|
+
writeData(detail, format, out);
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new PdppUsageError(
|
|
182
|
+
'Usage:\n' +
|
|
183
|
+
' pdpp ref event-subscriptions list [--client-id <id>] [--grant-id <id>] [--status <status>] [--as-url <url>] [--owner-session <cookie>] [--format json|table]\n' +
|
|
184
|
+
' pdpp ref event-subscriptions show <subscription-id> [--as-url <url>] [--owner-session <cookie>] [--format json|table]\n' +
|
|
185
|
+
' pdpp ref event-subscriptions disable <subscription-id> [--reason <text>] [--yes] [--as-url <url>] [--owner-session <cookie>]'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Re-export for shaped error surface in case the CLI dispatcher needs it.
|
|
190
|
+
export { PdppCliError };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseArgs, requirePositional } from '../args.js';
|
|
2
2
|
import { PdppUsageError } from '../errors.js';
|
|
3
3
|
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
-
import { resolveFormat, writeData } from '../output.js';
|
|
4
|
+
import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js';
|
|
5
5
|
|
|
6
6
|
export async function runRefGrant(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
7
7
|
const [subcommand, ...rest] = argv;
|
|
8
8
|
const { flags, positionals } = parseArgs(rest);
|
|
9
9
|
const out = io.stdout || process.stdout;
|
|
10
|
+
const err = io.stderr || process.stderr;
|
|
10
11
|
|
|
11
12
|
if (subcommand === 'timeline') {
|
|
12
13
|
const grantId = requirePositional(positionals, 0, 'grant-id');
|
|
@@ -20,6 +21,7 @@ export async function runRefGrant(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
|
20
21
|
);
|
|
21
22
|
const format = resolveFormat(flags, 'table', 'json');
|
|
22
23
|
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
24
|
+
writeEnvelopeWarnings(body, err);
|
|
23
25
|
return 0;
|
|
24
26
|
}
|
|
25
27
|
|
package/src/ref/commands/run.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseArgs, requirePositional } from '../args.js';
|
|
2
2
|
import { PdppUsageError } from '../errors.js';
|
|
3
3
|
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
-
import { resolveFormat, writeData } from '../output.js';
|
|
4
|
+
import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js';
|
|
5
5
|
|
|
6
6
|
export async function runRefRun(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
7
7
|
const [subcommand, ...rest] = argv;
|
|
8
8
|
const { flags, positionals } = parseArgs(rest);
|
|
9
9
|
const out = io.stdout || process.stdout;
|
|
10
|
+
const err = io.stderr || process.stderr;
|
|
10
11
|
|
|
11
12
|
if (subcommand === 'timeline') {
|
|
12
13
|
const runId = requirePositional(positionals, 0, 'run-id');
|
|
@@ -20,6 +21,7 @@ export async function runRefRun(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
|
20
21
|
);
|
|
21
22
|
const format = resolveFormat(flags, 'table', 'json');
|
|
22
23
|
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
24
|
+
writeEnvelopeWarnings(body, err);
|
|
23
25
|
return 0;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseArgs, requirePositional } from '../args.js';
|
|
2
2
|
import { PdppUsageError } from '../errors.js';
|
|
3
3
|
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
-
import { resolveFormat, writeData } from '../output.js';
|
|
4
|
+
import { resolveFormat, writeData, writeEnvelopeWarnings } from '../output.js';
|
|
5
5
|
|
|
6
6
|
export async function runRefTrace(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
7
7
|
const [subcommand, ...rest] = argv;
|
|
8
8
|
const { flags, positionals } = parseArgs(rest);
|
|
9
9
|
const out = io.stdout || process.stdout;
|
|
10
|
+
const err = io.stderr || process.stderr;
|
|
10
11
|
|
|
11
12
|
if (subcommand === 'show') {
|
|
12
13
|
const traceId = requirePositional(positionals, 0, 'trace-id');
|
|
@@ -20,6 +21,7 @@ export async function runRefTrace(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
|
20
21
|
);
|
|
21
22
|
const format = resolveFormat(flags, 'table', 'json');
|
|
22
23
|
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
24
|
+
writeEnvelopeWarnings(body, err);
|
|
23
25
|
return 0;
|
|
24
26
|
}
|
|
25
27
|
|
package/src/ref/output.js
CHANGED
|
@@ -2,6 +2,50 @@ export function resolveFormat(flags, defaultWhenTty = 'json', defaultWhenPipe =
|
|
|
2
2
|
return flags.format || (process.stdout.isTTY ? defaultWhenTty : defaultWhenPipe);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Extract the canonical `meta.warnings` array from a public read response
|
|
7
|
+
* body. Returns `[]` when the field is missing or malformed. The canonical
|
|
8
|
+
* envelope (canonicalize-public-read-contract) puts non-fatal lossiness,
|
|
9
|
+
* deprecated alias use, and count downgrades here; the CLI must surface
|
|
10
|
+
* them so operators are not silently misled by a lossy read. Pre-canonical
|
|
11
|
+
* responses have no `meta.warnings`, so this returns an empty array and
|
|
12
|
+
* the renderer prints nothing.
|
|
13
|
+
*/
|
|
14
|
+
export function extractEnvelopeWarnings(body) {
|
|
15
|
+
if (!body || typeof body !== 'object') {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const meta = body.meta;
|
|
19
|
+
if (!meta || typeof meta !== 'object') {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const warnings = Array.isArray(meta.warnings) ? meta.warnings : [];
|
|
23
|
+
return warnings.filter((w) => w && typeof w === 'object' && typeof w.code === 'string');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Write canonical `meta.warnings` to stderr in a human-readable form.
|
|
28
|
+
* Stays on stderr so machine-readable stdout (JSON, JSONL, table) is not
|
|
29
|
+
* polluted; operators piping to jq/grep keep their parseable output and
|
|
30
|
+
* still see the warning.
|
|
31
|
+
*/
|
|
32
|
+
export function writeEnvelopeWarnings(body, err = process.stderr) {
|
|
33
|
+
const warnings = extractEnvelopeWarnings(body);
|
|
34
|
+
if (warnings.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const warning of warnings) {
|
|
38
|
+
const parts = [`warning: ${warning.code}`];
|
|
39
|
+
if (warning.message) {
|
|
40
|
+
parts.push(warning.message);
|
|
41
|
+
}
|
|
42
|
+
if (warning.dropped_parameter) {
|
|
43
|
+
parts.push(`(dropped: ${warning.dropped_parameter})`);
|
|
44
|
+
}
|
|
45
|
+
err.write(`${parts.join(' — ')}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
5
49
|
export function writeData(data, format = 'json', out = process.stdout) {
|
|
6
50
|
if (format === 'json') {
|
|
7
51
|
out.write(`${JSON.stringify(data, null, 2)}\n`);
|