@pdpp/cli 0.1.0-beta.1 → 0.1.0-beta.3
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 +49 -5
- package/package.json +2 -2
- package/src/cache-layout.js +7 -4
- package/src/collector/commands.js +94 -0
- package/src/collector/errors.js +7 -0
- package/src/collector/runner.js +193 -0
- package/src/connect/flow.js +114 -4
- package/src/index.js +115 -2
- package/src/package-info.js +2 -6
- package/src/ref/args.js +47 -0
- package/src/ref/commands/connectors.js +94 -0
- package/src/ref/commands/grant.js +29 -0
- package/src/ref/commands/login.js +167 -0
- package/src/ref/commands/run.js +29 -0
- package/src/ref/commands/trace.js +29 -0
- package/src/ref/errors.js +37 -0
- package/src/ref/fetch.js +75 -0
- package/src/ref/output.js +76 -0
- package/src/ref/session.js +107 -0
package/src/index.js
CHANGED
|
@@ -3,7 +3,14 @@ import {
|
|
|
3
3
|
getPdppCliPackageInfo,
|
|
4
4
|
PDPP_CLI_BIN_NAME,
|
|
5
5
|
} from './package-info.js';
|
|
6
|
-
import { ConnectError, connectProvider, normalizeProviderUrl } from './connect/flow.js';
|
|
6
|
+
import { ConnectError, connectProvider, normalizeProviderUrl, readStoredCredential } from './connect/flow.js';
|
|
7
|
+
import { runCollector } from './collector/commands.js';
|
|
8
|
+
import { runRefRun } from './ref/commands/run.js';
|
|
9
|
+
import { runRefGrant } from './ref/commands/grant.js';
|
|
10
|
+
import { runRefTrace } from './ref/commands/trace.js';
|
|
11
|
+
import { runRefLogin } from './ref/commands/login.js';
|
|
12
|
+
import { runRefConnectors } from './ref/commands/connectors.js';
|
|
13
|
+
import { PdppCliError, PdppUsageError } from './ref/errors.js';
|
|
7
14
|
|
|
8
15
|
const HELP = `PDPP CLI
|
|
9
16
|
|
|
@@ -11,12 +18,33 @@ Usage:
|
|
|
11
18
|
${PDPP_CLI_BIN_NAME} --help
|
|
12
19
|
${PDPP_CLI_BIN_NAME} package-info [--provider-url <url>]
|
|
13
20
|
${PDPP_CLI_BIN_NAME} connect <provider-url>
|
|
21
|
+
${PDPP_CLI_BIN_NAME} token <provider-url>
|
|
14
22
|
|
|
15
23
|
Agent access:
|
|
16
24
|
${createPdppCliCommand()}
|
|
17
25
|
|
|
26
|
+
Local collector (pair a host you control with a reference deployment):
|
|
27
|
+
${PDPP_CLI_BIN_NAME} collector advertise
|
|
28
|
+
${PDPP_CLI_BIN_NAME} collector enroll --base-url <url> --code <code>
|
|
29
|
+
${PDPP_CLI_BIN_NAME} collector run --base-url <url> --connector <id> ...
|
|
30
|
+
|
|
31
|
+
Reference diagnostics (reference server only):
|
|
32
|
+
${PDPP_CLI_BIN_NAME} ref login <reference-url>
|
|
33
|
+
${PDPP_CLI_BIN_NAME} ref run timeline <run-id> --as-url <url>
|
|
34
|
+
${PDPP_CLI_BIN_NAME} ref grant timeline <grant-id> --as-url <url>
|
|
35
|
+
${PDPP_CLI_BIN_NAME} ref trace show <trace-id> --as-url <url>
|
|
36
|
+
${PDPP_CLI_BIN_NAME} ref connectors list --as-url <url>
|
|
37
|
+
${PDPP_CLI_BIN_NAME} ref connectors show <connector-id> --as-url <url>
|
|
38
|
+
|
|
18
39
|
Notes:
|
|
19
40
|
Do not ask users for owner bearer tokens for routine delegated access.
|
|
41
|
+
"pdpp collector" is a thin @pdpp/cli shim. Install @pdpp/local-collector
|
|
42
|
+
once, or use "npx -y @pdpp/local-collector ..." directly, for filesystem
|
|
43
|
+
collectors like Claude Code and Codex.
|
|
44
|
+
"pdpp ref" commands require a running PDPP reference server and an owner session.
|
|
45
|
+
"pdpp ref login" caches an owner session in project-local .pdpp/ with mode 0600;
|
|
46
|
+
later "pdpp ref" commands use the cache when --owner-session and
|
|
47
|
+
PDPP_OWNER_SESSION_COOKIE are absent.
|
|
20
48
|
`;
|
|
21
49
|
|
|
22
50
|
export async function runCli(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
@@ -52,6 +80,79 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces
|
|
|
52
80
|
}
|
|
53
81
|
}
|
|
54
82
|
|
|
83
|
+
if (command === 'token') {
|
|
84
|
+
const providerUrl = readFirstPositional(rest);
|
|
85
|
+
if (!providerUrl) {
|
|
86
|
+
io.stderr.write('Usage: pdpp token <provider-url>\n');
|
|
87
|
+
return 64;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const { credential } = await readStoredCredential(providerUrl, {
|
|
92
|
+
cacheRoot: readOption(rest, '--cache-root'),
|
|
93
|
+
});
|
|
94
|
+
io.stdout.write(`${credential.access_token}\n`);
|
|
95
|
+
return 0;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof ConnectError) {
|
|
98
|
+
io.stderr.write(`${error.message}\n`);
|
|
99
|
+
return error.exitCode;
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (command === 'collector') {
|
|
106
|
+
return await runCollector(rest, io);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (command === 'ref') {
|
|
110
|
+
const [refCommand, ...refRest] = rest;
|
|
111
|
+
|
|
112
|
+
if (!refCommand || refCommand === '--help' || refCommand === '-h') {
|
|
113
|
+
io.stdout.write(`Reference diagnostics (reference server only):\n`);
|
|
114
|
+
io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref login <reference-url> [--password-stdin] [--cache-root <dir>]\n`);
|
|
115
|
+
io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref run timeline <run-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
|
|
116
|
+
io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref grant timeline <grant-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
|
|
117
|
+
io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref trace show <trace-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
|
|
118
|
+
io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref connectors list --as-url <url> [--owner-session <cookie>] [--format json|table] [--verbose]\n`);
|
|
119
|
+
io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref connectors show <connector-id> --as-url <url> [--owner-session <cookie>] [--format json|table] [--verbose]\n`);
|
|
120
|
+
io.stdout.write(`\nNotes:\n`);
|
|
121
|
+
io.stdout.write(` "ref login" prompts the reference server's owner-login route and caches the\n`);
|
|
122
|
+
io.stdout.write(` resulting session in .pdpp/owner-sessions/ (mode 0600). The cookie value is\n`);
|
|
123
|
+
io.stdout.write(` never printed. The password must come from --password-stdin or\n`);
|
|
124
|
+
io.stdout.write(` PDPP_OWNER_PASSWORD; it is not accepted on the command line.\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const refDispatch = {
|
|
129
|
+
login: runRefLogin,
|
|
130
|
+
run: runRefRun,
|
|
131
|
+
grant: runRefGrant,
|
|
132
|
+
trace: runRefTrace,
|
|
133
|
+
connectors: runRefConnectors,
|
|
134
|
+
};
|
|
135
|
+
const handler = refDispatch[refCommand];
|
|
136
|
+
if (!handler) {
|
|
137
|
+
io.stderr.write(`Unknown ref command: ${refCommand}\n`);
|
|
138
|
+
return 64;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
return await handler(refRest, io);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof PdppUsageError) {
|
|
145
|
+
io.stderr.write(`${error.message}\n`);
|
|
146
|
+
return error.exitCode;
|
|
147
|
+
}
|
|
148
|
+
if (error instanceof PdppCliError) {
|
|
149
|
+
io.stderr.write(`${error.message}\n`);
|
|
150
|
+
return error.exitCode;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
55
156
|
io.stderr.write(`Unknown command: ${command}\n\n${HELP}\n`);
|
|
56
157
|
return 64;
|
|
57
158
|
}
|
|
@@ -65,4 +166,16 @@ function readOption(argv, name) {
|
|
|
65
166
|
return argv[index + 1];
|
|
66
167
|
}
|
|
67
168
|
|
|
68
|
-
|
|
169
|
+
function readFirstPositional(argv) {
|
|
170
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
171
|
+
const value = argv[index];
|
|
172
|
+
if (value.startsWith('--')) {
|
|
173
|
+
index += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { connectProvider, normalizeProviderUrl, readStoredCredential };
|
package/src/package-info.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
|
-
|
|
5
|
-
export const PDPP_CLI_PACKAGE_NAME = manifest.name;
|
|
6
|
-
export const PDPP_CLI_BIN_NAME = Object.keys(manifest.bin)[0];
|
|
1
|
+
export const PDPP_CLI_PACKAGE_NAME = '@pdpp/cli';
|
|
2
|
+
export const PDPP_CLI_BIN_NAME = 'pdpp';
|
|
7
3
|
export const PDPP_CLI_VERSION_POLICY = 'beta';
|
|
8
4
|
export const PDPP_CLI_PACKAGE_SPECIFIER = `${PDPP_CLI_PACKAGE_NAME}@${PDPP_CLI_VERSION_POLICY}`;
|
|
9
5
|
export const PDPP_CLI_DEFAULT_CLIENT_ID = 'pdpp_cli';
|
package/src/ref/args.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PdppUsageError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export function parseArgs(argv) {
|
|
4
|
+
const flags = {};
|
|
5
|
+
const positionals = [];
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
8
|
+
const arg = argv[i];
|
|
9
|
+
if (arg === '--') {
|
|
10
|
+
positionals.push(...argv.slice(i + 1));
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
if (!arg.startsWith('--')) {
|
|
14
|
+
positionals.push(arg);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const [rawKey, inlineValue] = arg.slice(2).split('=', 2);
|
|
19
|
+
if (!rawKey) {
|
|
20
|
+
throw new PdppUsageError('Invalid empty flag');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (inlineValue !== undefined) {
|
|
24
|
+
flags[rawKey] = inlineValue;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const next = argv[i + 1];
|
|
29
|
+
if (next && !next.startsWith('--')) {
|
|
30
|
+
flags[rawKey] = next;
|
|
31
|
+
i += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
flags[rawKey] = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { flags, positionals };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function requirePositional(positionals, index, name) {
|
|
42
|
+
const value = positionals[index];
|
|
43
|
+
if (!value) {
|
|
44
|
+
throw new PdppUsageError(`Missing required argument: ${name}`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { parseArgs, requirePositional } from '../args.js';
|
|
2
|
+
import { PdppUsageError } from '../errors.js';
|
|
3
|
+
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
+
import { resolveFormat, writeData } from '../output.js';
|
|
5
|
+
|
|
6
|
+
// Operator-facing summary projection. Mirrors the evidence the dashboard renders
|
|
7
|
+
// in `apps/web/src/app/dashboard/lib/ref-client.ts` (RefConnectorSummary +
|
|
8
|
+
// RefConnectionHealthSnapshot + RefNextAction). The reference server has
|
|
9
|
+
// already redacted secret-bearing fields (e.g. `action_target` for
|
|
10
|
+
// `sensitivity: "secret"` attention rows); we surface what arrives, with no
|
|
11
|
+
// inference.
|
|
12
|
+
function projectSummaryRow(summary) {
|
|
13
|
+
const health = summary?.connection_health || {};
|
|
14
|
+
const axes = health.axes || {};
|
|
15
|
+
const badges = health.badges || {};
|
|
16
|
+
const nextAction = summary?.next_action || health.next_action || null;
|
|
17
|
+
const schedule = summary?.schedule || null;
|
|
18
|
+
const lastRun = summary?.last_run || null;
|
|
19
|
+
const lastSuccess = summary?.last_successful_run || null;
|
|
20
|
+
return {
|
|
21
|
+
connection_id: summary?.connection_id ?? null,
|
|
22
|
+
connector_id: summary?.connector_id ?? null,
|
|
23
|
+
display_name: summary?.display_name ?? null,
|
|
24
|
+
state: health.state ?? 'unknown',
|
|
25
|
+
coverage: axes.coverage ?? 'unknown',
|
|
26
|
+
freshness: axes.freshness ?? 'unknown',
|
|
27
|
+
attention: axes.attention ?? 'none',
|
|
28
|
+
outbox: axes.outbox ?? 'unknown',
|
|
29
|
+
syncing: badges.syncing === true,
|
|
30
|
+
stale: badges.stale === true,
|
|
31
|
+
reason_code: health.reason_code ?? null,
|
|
32
|
+
unknown_reasons: Array.isArray(health.unknown_reasons) ? health.unknown_reasons : [],
|
|
33
|
+
next_action_source: nextAction?.source ?? 'none',
|
|
34
|
+
next_action_reason: nextAction?.reason_code ?? null,
|
|
35
|
+
next_action_owner_action: nextAction?.owner_action ?? null,
|
|
36
|
+
next_action_target: nextAction?.action_target ?? null,
|
|
37
|
+
next_action_expires_at: nextAction?.expires_at ?? null,
|
|
38
|
+
last_run_at: lastRun?.last_at ?? null,
|
|
39
|
+
last_run_status: lastRun?.status ?? null,
|
|
40
|
+
last_success_at: health.last_success_at ?? lastSuccess?.last_at ?? null,
|
|
41
|
+
next_attempt_at: health.next_attempt_at ?? schedule?.next_due_at ?? null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runRefConnectors(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
46
|
+
const [subcommand, ...rest] = argv;
|
|
47
|
+
const { flags, positionals } = parseArgs(rest);
|
|
48
|
+
const out = io.stdout || process.stdout;
|
|
49
|
+
|
|
50
|
+
if (subcommand === 'list') {
|
|
51
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
52
|
+
const ownerSession = flags['owner-session'] || '';
|
|
53
|
+
const cacheRoot = flags['cache-root'];
|
|
54
|
+
const { body } = await fetchJson(
|
|
55
|
+
`${asUrl}/_ref/connectors`,
|
|
56
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
57
|
+
fetchImpl
|
|
58
|
+
);
|
|
59
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
60
|
+
const verbose = flags.verbose === true || flags.verbose === 'true';
|
|
61
|
+
if (verbose) {
|
|
62
|
+
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
const rows = Array.isArray(body?.data) ? body.data.map(projectSummaryRow) : [];
|
|
66
|
+
writeData(format === 'table' ? rows : { object: 'list', data: rows }, format, out);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (subcommand === 'show') {
|
|
71
|
+
const connectorId = requirePositional(positionals, 0, 'connector-id');
|
|
72
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
73
|
+
const ownerSession = flags['owner-session'] || '';
|
|
74
|
+
const cacheRoot = flags['cache-root'];
|
|
75
|
+
const { body } = await fetchJson(
|
|
76
|
+
`${asUrl}/_ref/connectors/${encodeURIComponent(connectorId)}`,
|
|
77
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
78
|
+
fetchImpl
|
|
79
|
+
);
|
|
80
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
81
|
+
const verbose = flags.verbose === true || flags.verbose === 'true';
|
|
82
|
+
if (verbose) {
|
|
83
|
+
writeData(body, format, out);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const row = projectSummaryRow(body);
|
|
87
|
+
writeData(format === 'table' ? [row] : row, format, out);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new PdppUsageError(
|
|
92
|
+
'Usage: pdpp ref connectors <list|show <connector-id>> [--as-url <url>] [--owner-session <cookie>] [--format json|table] [--verbose]'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseArgs, requirePositional } from '../args.js';
|
|
2
|
+
import { PdppUsageError } from '../errors.js';
|
|
3
|
+
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
+
import { resolveFormat, writeData } from '../output.js';
|
|
5
|
+
|
|
6
|
+
export async function runRefGrant(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
7
|
+
const [subcommand, ...rest] = argv;
|
|
8
|
+
const { flags, positionals } = parseArgs(rest);
|
|
9
|
+
const out = io.stdout || process.stdout;
|
|
10
|
+
|
|
11
|
+
if (subcommand === 'timeline') {
|
|
12
|
+
const grantId = requirePositional(positionals, 0, 'grant-id');
|
|
13
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
14
|
+
const ownerSession = flags['owner-session'] || '';
|
|
15
|
+
const cacheRoot = flags['cache-root'];
|
|
16
|
+
const { body } = await fetchJson(
|
|
17
|
+
`${asUrl}/_ref/grants/${encodeURIComponent(grantId)}/timeline`,
|
|
18
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
19
|
+
fetchImpl
|
|
20
|
+
);
|
|
21
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
22
|
+
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new PdppUsageError(
|
|
27
|
+
'Usage: pdpp ref grant timeline <grant-id> [--as-url <url>] [--owner-session <cookie>] [--format json|table]'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { parseArgs } from '../args.js';
|
|
2
|
+
import { PdppCliError, PdppUsageError } from '../errors.js';
|
|
3
|
+
import { OWNER_SESSION_COOKIE_NAME } from '../fetch.js';
|
|
4
|
+
import {
|
|
5
|
+
extractCookieFromSetCookie,
|
|
6
|
+
getOwnerSessionPaths,
|
|
7
|
+
writeOwnerSession,
|
|
8
|
+
} from '../session.js';
|
|
9
|
+
|
|
10
|
+
// Owner-session login UX for `pdpp ref ...`.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// pdpp ref login <reference-url> [--password-stdin] [--cache-root <dir>]
|
|
14
|
+
//
|
|
15
|
+
// Password sources, in precedence order:
|
|
16
|
+
// 1. --password-stdin (reads first line of stdin; CI-friendly)
|
|
17
|
+
// 2. PDPP_OWNER_PASSWORD env var
|
|
18
|
+
// We intentionally do NOT accept the password on argv to avoid leaking it
|
|
19
|
+
// into shell history, ps output, or logs.
|
|
20
|
+
//
|
|
21
|
+
// On success: persists the issued owner-session cookie to project-local
|
|
22
|
+
// `.pdpp/owner-sessions/<host>.json` with mode 0600. Cookie value is never
|
|
23
|
+
// printed.
|
|
24
|
+
export async function runRefLogin(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
25
|
+
const out = io.stdout || process.stdout;
|
|
26
|
+
|
|
27
|
+
const { flags, positionals } = parseArgs(argv);
|
|
28
|
+
const referenceUrlRaw = positionals[0];
|
|
29
|
+
if (!referenceUrlRaw) {
|
|
30
|
+
throw new PdppUsageError(
|
|
31
|
+
'Usage: pdpp ref login <reference-url> [--password-stdin] [--cache-root <dir>]'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const referenceUrl = referenceUrlRaw.replace(/\/$/, '');
|
|
35
|
+
|
|
36
|
+
const password = await resolvePassword(flags, io);
|
|
37
|
+
if (!password) {
|
|
38
|
+
throw new PdppUsageError(
|
|
39
|
+
'Owner password required. Pipe it via `--password-stdin` or set PDPP_OWNER_PASSWORD. ' +
|
|
40
|
+
'The password is never accepted on the command line.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const loginUrl = `${referenceUrl}/owner/login`;
|
|
45
|
+
let resp;
|
|
46
|
+
try {
|
|
47
|
+
resp = await fetchImpl(loginUrl, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Accept: 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ password }),
|
|
54
|
+
redirect: 'manual',
|
|
55
|
+
});
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new PdppCliError(`Network request to ${loginUrl} failed: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const status = resp.status;
|
|
61
|
+
// The reference server's POST /owner/login responds with a 302 redirect
|
|
62
|
+
// and Set-Cookie on success, or 401/403 on failure. Anything outside
|
|
63
|
+
// 200-399 means the login was rejected.
|
|
64
|
+
if (status >= 400) {
|
|
65
|
+
if (status === 401) {
|
|
66
|
+
throw new PdppCliError(
|
|
67
|
+
'Owner login rejected: incorrect password (HTTP 401).',
|
|
68
|
+
3
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (status === 403) {
|
|
72
|
+
throw new PdppCliError(
|
|
73
|
+
'Owner login rejected: CSRF/policy failure (HTTP 403).',
|
|
74
|
+
4
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (status === 404) {
|
|
78
|
+
throw new PdppCliError(
|
|
79
|
+
`Owner login route not found at ${loginUrl} (HTTP 404). Confirm the reference server URL.`,
|
|
80
|
+
5
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
throw new PdppCliError(`Owner login failed: HTTP ${status}.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const setCookie = readSetCookie(resp);
|
|
87
|
+
const cookieValue = extractCookieFromSetCookie(setCookie, OWNER_SESSION_COOKIE_NAME);
|
|
88
|
+
if (!cookieValue) {
|
|
89
|
+
throw new PdppCliError(
|
|
90
|
+
'Owner login succeeded but no owner-session cookie was returned. ' +
|
|
91
|
+
'Confirm that placeholder owner auth is enabled on the reference server.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cacheRoot = flags['cache-root'] || '.pdpp';
|
|
96
|
+
const file = writeOwnerSession({
|
|
97
|
+
referenceUrl,
|
|
98
|
+
cookie: `${OWNER_SESSION_COOKIE_NAME}=${cookieValue}`,
|
|
99
|
+
cacheRoot,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Never print the cookie value. Confirm location only.
|
|
103
|
+
out.write(`Saved owner session for ${referenceUrl}\n`);
|
|
104
|
+
out.write(` cache: ${file}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function resolvePassword(flags, io) {
|
|
109
|
+
if (flags['password-stdin']) {
|
|
110
|
+
return readFirstLine(io.stdin || process.stdin);
|
|
111
|
+
}
|
|
112
|
+
const fromEnv = process.env.PDPP_OWNER_PASSWORD;
|
|
113
|
+
if (typeof fromEnv === 'string' && fromEnv.length > 0) {
|
|
114
|
+
return fromEnv;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readFirstLine(stream) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
let buf = '';
|
|
122
|
+
if (!stream || typeof stream.on !== 'function') {
|
|
123
|
+
resolve('');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
stream.setEncoding?.('utf8');
|
|
127
|
+
const onData = (chunk) => {
|
|
128
|
+
buf += chunk;
|
|
129
|
+
const nl = buf.indexOf('\n');
|
|
130
|
+
if (nl !== -1) {
|
|
131
|
+
cleanup();
|
|
132
|
+
resolve(buf.slice(0, nl).replace(/\r$/, ''));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const onEnd = () => {
|
|
136
|
+
cleanup();
|
|
137
|
+
resolve(buf.replace(/\r?\n$/, ''));
|
|
138
|
+
};
|
|
139
|
+
const onError = (e) => {
|
|
140
|
+
cleanup();
|
|
141
|
+
reject(e);
|
|
142
|
+
};
|
|
143
|
+
function cleanup() {
|
|
144
|
+
stream.off?.('data', onData);
|
|
145
|
+
stream.off?.('end', onEnd);
|
|
146
|
+
stream.off?.('error', onError);
|
|
147
|
+
}
|
|
148
|
+
stream.on('data', onData);
|
|
149
|
+
stream.on('end', onEnd);
|
|
150
|
+
stream.on('error', onError);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readSetCookie(resp) {
|
|
155
|
+
const headers = resp.headers;
|
|
156
|
+
if (!headers) return null;
|
|
157
|
+
if (typeof headers.getSetCookie === 'function') {
|
|
158
|
+
const arr = headers.getSetCookie();
|
|
159
|
+
if (arr && arr.length) return arr;
|
|
160
|
+
}
|
|
161
|
+
if (typeof headers.get === 'function') {
|
|
162
|
+
return headers.get('set-cookie') || headers.get('Set-Cookie');
|
|
163
|
+
}
|
|
164
|
+
return headers['set-cookie'] || headers['Set-Cookie'] || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { getOwnerSessionPaths };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseArgs, requirePositional } from '../args.js';
|
|
2
|
+
import { PdppUsageError } from '../errors.js';
|
|
3
|
+
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
+
import { resolveFormat, writeData } from '../output.js';
|
|
5
|
+
|
|
6
|
+
export async function runRefRun(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
7
|
+
const [subcommand, ...rest] = argv;
|
|
8
|
+
const { flags, positionals } = parseArgs(rest);
|
|
9
|
+
const out = io.stdout || process.stdout;
|
|
10
|
+
|
|
11
|
+
if (subcommand === 'timeline') {
|
|
12
|
+
const runId = requirePositional(positionals, 0, 'run-id');
|
|
13
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
14
|
+
const ownerSession = flags['owner-session'] || '';
|
|
15
|
+
const cacheRoot = flags['cache-root'];
|
|
16
|
+
const { body } = await fetchJson(
|
|
17
|
+
`${asUrl}/_ref/runs/${encodeURIComponent(runId)}/timeline`,
|
|
18
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
19
|
+
fetchImpl
|
|
20
|
+
);
|
|
21
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
22
|
+
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new PdppUsageError(
|
|
27
|
+
'Usage: pdpp ref run timeline <run-id> [--as-url <url>] [--owner-session <cookie>] [--format json|table]'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseArgs, requirePositional } from '../args.js';
|
|
2
|
+
import { PdppUsageError } from '../errors.js';
|
|
3
|
+
import { fetchJson, ownerSessionHeaders, resolveReferenceUrl } from '../fetch.js';
|
|
4
|
+
import { resolveFormat, writeData } from '../output.js';
|
|
5
|
+
|
|
6
|
+
export async function runRefTrace(argv, io = {}, fetchImpl = globalThis.fetch) {
|
|
7
|
+
const [subcommand, ...rest] = argv;
|
|
8
|
+
const { flags, positionals } = parseArgs(rest);
|
|
9
|
+
const out = io.stdout || process.stdout;
|
|
10
|
+
|
|
11
|
+
if (subcommand === 'show') {
|
|
12
|
+
const traceId = requirePositional(positionals, 0, 'trace-id');
|
|
13
|
+
const asUrl = resolveReferenceUrl(flags);
|
|
14
|
+
const ownerSession = flags['owner-session'] || '';
|
|
15
|
+
const cacheRoot = flags['cache-root'];
|
|
16
|
+
const { body } = await fetchJson(
|
|
17
|
+
`${asUrl}/_ref/traces/${encodeURIComponent(traceId)}`,
|
|
18
|
+
{ headers: { ...ownerSessionHeaders({ ownerSession, referenceUrl: asUrl, cacheRoot }) } },
|
|
19
|
+
fetchImpl
|
|
20
|
+
);
|
|
21
|
+
const format = resolveFormat(flags, 'table', 'json');
|
|
22
|
+
writeData(format === 'table' ? (body.data || []) : body, format, out);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new PdppUsageError(
|
|
27
|
+
'Usage: pdpp ref trace show <trace-id> [--as-url <url>] [--owner-session <cookie>] [--format json|table]'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class PdppCliError extends Error {
|
|
2
|
+
constructor(message, exitCode = 1, details = null) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'PdppCliError';
|
|
5
|
+
this.exitCode = exitCode;
|
|
6
|
+
this.details = details;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class PdppUsageError extends PdppCliError {
|
|
11
|
+
constructor(message, details = null) {
|
|
12
|
+
super(message, 2, details);
|
|
13
|
+
this.name = 'PdppUsageError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class PdppHttpError extends PdppCliError {
|
|
18
|
+
constructor(message, status, body = null, responseMetadata = null) {
|
|
19
|
+
super(message, exitCodeForStatus(status), {
|
|
20
|
+
status,
|
|
21
|
+
body,
|
|
22
|
+
...(responseMetadata || {}),
|
|
23
|
+
});
|
|
24
|
+
this.name = 'PdppHttpError';
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.body = body;
|
|
27
|
+
this.requestId = responseMetadata?.request_id || null;
|
|
28
|
+
this.referenceTraceId = responseMetadata?.reference_trace_id || null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function exitCodeForStatus(status) {
|
|
33
|
+
if (status === 401) return 3;
|
|
34
|
+
if (status === 403) return 4;
|
|
35
|
+
if (status === 404) return 5;
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
package/src/ref/fetch.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PdppCliError, PdppHttpError } from './errors.js';
|
|
2
|
+
import { readOwnerSession } from './session.js';
|
|
3
|
+
|
|
4
|
+
export const OWNER_SESSION_COOKIE_NAME = 'pdpp_owner_session';
|
|
5
|
+
|
|
6
|
+
export async function fetchJson(url, opts = {}, fetchImpl = globalThis.fetch) {
|
|
7
|
+
let resp;
|
|
8
|
+
try {
|
|
9
|
+
resp = await fetchImpl(url, opts);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
throw new PdppCliError(`Network request failed: ${error.message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const text = await resp.text();
|
|
15
|
+
let body = null;
|
|
16
|
+
if (text) {
|
|
17
|
+
try {
|
|
18
|
+
body = JSON.parse(text);
|
|
19
|
+
} catch {
|
|
20
|
+
body = text;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
const message =
|
|
26
|
+
body?.error_description ||
|
|
27
|
+
body?.error?.message ||
|
|
28
|
+
body?.message ||
|
|
29
|
+
`HTTP ${resp.status} ${resp.statusText}`;
|
|
30
|
+
throw new PdppHttpError(message, resp.status, body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { status: resp.status, body, headers: resp.headers };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolves owner session cookie with precedence:
|
|
37
|
+
// 1. opts.ownerSession (e.g. --owner-session flag)
|
|
38
|
+
// 2. PDPP_OWNER_SESSION_COOKIE env var
|
|
39
|
+
// 3. project-local cached session (when opts.referenceUrl is provided)
|
|
40
|
+
// Returns headers object with Cookie set, or empty object if no session found.
|
|
41
|
+
export function ownerSessionHeaders(opts = {}) {
|
|
42
|
+
const fromOpts = typeof opts.ownerSession === 'string' ? opts.ownerSession : '';
|
|
43
|
+
const fromEnv =
|
|
44
|
+
typeof process.env.PDPP_OWNER_SESSION_COOKIE === 'string'
|
|
45
|
+
? process.env.PDPP_OWNER_SESSION_COOKIE
|
|
46
|
+
: '';
|
|
47
|
+
|
|
48
|
+
let value = (fromOpts || fromEnv).trim();
|
|
49
|
+
|
|
50
|
+
if (!value && opts.referenceUrl) {
|
|
51
|
+
const cached = readOwnerSession({
|
|
52
|
+
referenceUrl: opts.referenceUrl,
|
|
53
|
+
cacheRoot: opts.cacheRoot,
|
|
54
|
+
});
|
|
55
|
+
if (cached) value = cached.cookie;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!value) return {};
|
|
59
|
+
const cookie = value.includes('=') ? value : `${OWNER_SESSION_COOKIE_NAME}=${value}`;
|
|
60
|
+
return { Cookie: cookie };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolves the reference base URL from --as-url flag or PDPP_AS_URL / AS_URL env vars.
|
|
64
|
+
export function resolveReferenceUrl(flags) {
|
|
65
|
+
const url =
|
|
66
|
+
flags['as-url'] ||
|
|
67
|
+
process.env.PDPP_AS_URL ||
|
|
68
|
+
process.env.AS_URL;
|
|
69
|
+
if (!url) {
|
|
70
|
+
throw new PdppCliError(
|
|
71
|
+
'Missing reference server URL. Provide --as-url <url> or set PDPP_AS_URL.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return url.replace(/\/$/, '');
|
|
75
|
+
}
|