pylot-cli 0.1.3 → 0.1.4
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/index.mjs +421 -119
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -3032,6 +3032,123 @@ var require_commander = __commonJS({
|
|
|
3032
3032
|
}
|
|
3033
3033
|
});
|
|
3034
3034
|
|
|
3035
|
+
// src/lib/http.mts
|
|
3036
|
+
var http_exports = {};
|
|
3037
|
+
__export(http_exports, {
|
|
3038
|
+
ApiError: () => ApiError,
|
|
3039
|
+
NetworkError: () => NetworkError,
|
|
3040
|
+
request: () => request
|
|
3041
|
+
});
|
|
3042
|
+
function errorMessage(status, body, cfg) {
|
|
3043
|
+
const err = body && typeof body === "object" ? body : {};
|
|
3044
|
+
if (status === 401) {
|
|
3045
|
+
return `unauthorized \u2014 the gateway rejected your token (source: ${cfg.tokenSource}).
|
|
3046
|
+
hint: check 'pylot auth status'; inside a Pylot container the right token is already in the environment, otherwise export PYLOT_API_TOKEN or run 'pylot config set token <token>'`;
|
|
3047
|
+
}
|
|
3048
|
+
if (status === 403 && err.error === "insufficient_scope" && typeof err.required === "string") {
|
|
3049
|
+
return `forbidden \u2014 your token (source: ${cfg.tokenSource}) lacks the '${err.required}' scope.
|
|
3050
|
+
hint: mission broker tokens can only heartbeat and mint git tokens; session tokens cannot use admin routes. Use a token with the '${err.required}' scope (an admin can mint one: pylot auth tokens create --scopes ${err.required})`;
|
|
3051
|
+
}
|
|
3052
|
+
const parts = [`${status}${err.error ? ` ${err.error}` : ""}`];
|
|
3053
|
+
if (err.detail) parts.push(String(err.detail));
|
|
3054
|
+
if (err.hint) parts.push(`hint: ${err.hint}`);
|
|
3055
|
+
const extras = Object.entries(err).filter(([k]) => !["error", "detail", "hint"].includes(k));
|
|
3056
|
+
if (extras.length > 0) {
|
|
3057
|
+
parts.push(extras.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" "));
|
|
3058
|
+
}
|
|
3059
|
+
if (!err.error && !err.detail && extras.length === 0) {
|
|
3060
|
+
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
3061
|
+
if (text && text !== "null") parts.push(text.slice(0, 300));
|
|
3062
|
+
}
|
|
3063
|
+
return `API error ${parts.join(" \u2014 ")}`;
|
|
3064
|
+
}
|
|
3065
|
+
async function request(cfg, path5, opts = {}) {
|
|
3066
|
+
const method = opts.method ?? "GET";
|
|
3067
|
+
const retry = opts.retry ?? method === "GET";
|
|
3068
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
3069
|
+
const url = new URL(cfg.baseUrl + path5);
|
|
3070
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
3071
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
3072
|
+
}
|
|
3073
|
+
const headers = {};
|
|
3074
|
+
if (cfg.token) headers.authorization = `Bearer ${cfg.token}`;
|
|
3075
|
+
let bodyText;
|
|
3076
|
+
if (opts.rawBody !== void 0) {
|
|
3077
|
+
bodyText = opts.rawBody;
|
|
3078
|
+
headers["content-type"] = "text/plain; charset=utf-8";
|
|
3079
|
+
} else if (opts.body !== void 0) {
|
|
3080
|
+
bodyText = JSON.stringify(opts.body);
|
|
3081
|
+
headers["content-type"] = "application/json";
|
|
3082
|
+
}
|
|
3083
|
+
const maxAttempts = retry ? RETRY_DELAYS_MS.length + 1 : 1;
|
|
3084
|
+
let lastError;
|
|
3085
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3086
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt - 1]));
|
|
3087
|
+
let res;
|
|
3088
|
+
try {
|
|
3089
|
+
res = await fetch(url, {
|
|
3090
|
+
method,
|
|
3091
|
+
headers,
|
|
3092
|
+
body: bodyText,
|
|
3093
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3094
|
+
});
|
|
3095
|
+
} catch (err) {
|
|
3096
|
+
lastError = err;
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
if (retry && RETRYABLE_STATUSES.has(res.status) && attempt < maxAttempts - 1) {
|
|
3100
|
+
lastError = new ApiError(`API error ${res.status}`, res.status, null);
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
const text = await res.text();
|
|
3104
|
+
let json = null;
|
|
3105
|
+
try {
|
|
3106
|
+
json = JSON.parse(text);
|
|
3107
|
+
} catch {
|
|
3108
|
+
}
|
|
3109
|
+
const ok = res.status >= 200 && res.status < 300 || (opts.okStatuses ?? []).includes(res.status);
|
|
3110
|
+
if (!ok) {
|
|
3111
|
+
const apiErr = new ApiError(errorMessage(res.status, json ?? text, cfg), res.status, json ?? text);
|
|
3112
|
+
const errBody = json;
|
|
3113
|
+
if (res.status === 403 && errBody?.error === "insufficient_scope") apiErr.requiredScope = errBody.required;
|
|
3114
|
+
throw apiErr;
|
|
3115
|
+
}
|
|
3116
|
+
return { status: res.status, json, text, headers: res.headers };
|
|
3117
|
+
}
|
|
3118
|
+
const cause = lastError instanceof Error ? lastError.message : String(lastError);
|
|
3119
|
+
const maybeDelivered = method !== "GET";
|
|
3120
|
+
throw new NetworkError(
|
|
3121
|
+
`request failed: ${method} ${url.pathname} \u2014 ${cause}` + (maybeDelivered ? "\nhint: the request may still have been received \u2014 verify before retrying (e.g. pylot ps)" : ""),
|
|
3122
|
+
maybeDelivered
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
var ApiError, NetworkError, RETRYABLE_STATUSES, RETRY_DELAYS_MS;
|
|
3126
|
+
var init_http = __esm({
|
|
3127
|
+
"src/lib/http.mts"() {
|
|
3128
|
+
"use strict";
|
|
3129
|
+
ApiError = class extends Error {
|
|
3130
|
+
status;
|
|
3131
|
+
body;
|
|
3132
|
+
requiredScope;
|
|
3133
|
+
constructor(message, status, body) {
|
|
3134
|
+
super(message);
|
|
3135
|
+
this.status = status;
|
|
3136
|
+
this.body = body;
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
NetworkError = class extends Error {
|
|
3140
|
+
/** True when the request may have reached the server (sent, then failed). */
|
|
3141
|
+
maybeDelivered;
|
|
3142
|
+
constructor(message, maybeDelivered) {
|
|
3143
|
+
super(message);
|
|
3144
|
+
this.maybeDelivered = maybeDelivered;
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
RETRYABLE_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
3148
|
+
RETRY_DELAYS_MS = [500, 1500];
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3035
3152
|
// node_modules/dot-prop/index.js
|
|
3036
3153
|
function getPathSegments(path5) {
|
|
3037
3154
|
const parts = [];
|
|
@@ -7322,7 +7439,7 @@ var require_schemes = __commonJS({
|
|
|
7322
7439
|
urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
|
|
7323
7440
|
return urnComponent;
|
|
7324
7441
|
}
|
|
7325
|
-
var
|
|
7442
|
+
var http2 = (
|
|
7326
7443
|
/** @type {SchemeHandler} */
|
|
7327
7444
|
{
|
|
7328
7445
|
scheme: "http",
|
|
@@ -7335,7 +7452,7 @@ var require_schemes = __commonJS({
|
|
|
7335
7452
|
/** @type {SchemeHandler} */
|
|
7336
7453
|
{
|
|
7337
7454
|
scheme: "https",
|
|
7338
|
-
domainHost:
|
|
7455
|
+
domainHost: http2.domainHost,
|
|
7339
7456
|
parse: httpParse,
|
|
7340
7457
|
serialize: httpSerialize
|
|
7341
7458
|
}
|
|
@@ -7379,7 +7496,7 @@ var require_schemes = __commonJS({
|
|
|
7379
7496
|
var SCHEMES = (
|
|
7380
7497
|
/** @type {Record<SchemeName, SchemeHandler>} */
|
|
7381
7498
|
{
|
|
7382
|
-
http,
|
|
7499
|
+
http: http2,
|
|
7383
7500
|
https,
|
|
7384
7501
|
ws,
|
|
7385
7502
|
wss,
|
|
@@ -14120,112 +14237,70 @@ var {
|
|
|
14120
14237
|
Help
|
|
14121
14238
|
} = import_index.default;
|
|
14122
14239
|
|
|
14123
|
-
// src/lib/
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
}
|
|
14133
|
-
};
|
|
14134
|
-
var NetworkError = class extends Error {
|
|
14135
|
-
/** True when the request may have reached the server (sent, then failed). */
|
|
14136
|
-
maybeDelivered;
|
|
14137
|
-
constructor(message, maybeDelivered) {
|
|
14138
|
-
super(message);
|
|
14139
|
-
this.maybeDelivered = maybeDelivered;
|
|
14140
|
-
}
|
|
14141
|
-
};
|
|
14142
|
-
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
14143
|
-
var RETRY_DELAYS_MS = [500, 1500];
|
|
14144
|
-
function errorMessage(status, body, cfg) {
|
|
14145
|
-
const err = body && typeof body === "object" ? body : {};
|
|
14146
|
-
if (status === 401) {
|
|
14147
|
-
return `unauthorized \u2014 the gateway rejected your token (source: ${cfg.tokenSource}).
|
|
14148
|
-
hint: check 'pylot auth status'; inside a Pylot container the right token is already in the environment, otherwise export PYLOT_API_TOKEN or run 'pylot config set token <token>'`;
|
|
14149
|
-
}
|
|
14150
|
-
if (status === 403 && err.error === "insufficient_scope" && typeof err.required === "string") {
|
|
14151
|
-
return `forbidden \u2014 your token (source: ${cfg.tokenSource}) lacks the '${err.required}' scope.
|
|
14152
|
-
hint: mission broker tokens can only heartbeat and mint git tokens; session tokens cannot use admin routes. Use a token with the '${err.required}' scope (an admin can mint one: pylot auth tokens create --scopes ${err.required})`;
|
|
14153
|
-
}
|
|
14154
|
-
const parts = [`${status}${err.error ? ` ${err.error}` : ""}`];
|
|
14155
|
-
if (err.detail) parts.push(String(err.detail));
|
|
14156
|
-
if (err.hint) parts.push(`hint: ${err.hint}`);
|
|
14157
|
-
const extras = Object.entries(err).filter(([k]) => !["error", "detail", "hint"].includes(k));
|
|
14158
|
-
if (extras.length > 0) {
|
|
14159
|
-
parts.push(extras.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" "));
|
|
14160
|
-
}
|
|
14161
|
-
if (!err.error && !err.detail && extras.length === 0) {
|
|
14162
|
-
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
14163
|
-
if (text && text !== "null") parts.push(text.slice(0, 300));
|
|
14164
|
-
}
|
|
14165
|
-
return `API error ${parts.join(" \u2014 ")}`;
|
|
14240
|
+
// src/lib/output.mts
|
|
14241
|
+
init_http();
|
|
14242
|
+
|
|
14243
|
+
// src/lib/config.mts
|
|
14244
|
+
import { readFileSync, writeFileSync as writeFileSync2, chmodSync, mkdirSync, renameSync } from "node:fs";
|
|
14245
|
+
import { homedir as homedir2 } from "node:os";
|
|
14246
|
+
import { join, dirname } from "node:path";
|
|
14247
|
+
function credentialsPath() {
|
|
14248
|
+
return join(homedir2(), ".pylot", "credentials");
|
|
14166
14249
|
}
|
|
14167
|
-
|
|
14168
|
-
|
|
14169
|
-
const
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
}
|
|
14175
|
-
const
|
|
14176
|
-
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
}
|
|
14201
|
-
if (retry && RETRYABLE_STATUSES.has(res.status) && attempt < maxAttempts - 1) {
|
|
14202
|
-
lastError = new ApiError(`API error ${res.status}`, res.status, null);
|
|
14203
|
-
continue;
|
|
14204
|
-
}
|
|
14205
|
-
const text = await res.text();
|
|
14206
|
-
let json = null;
|
|
14207
|
-
try {
|
|
14208
|
-
json = JSON.parse(text);
|
|
14209
|
-
} catch {
|
|
14210
|
-
}
|
|
14211
|
-
const ok = res.status >= 200 && res.status < 300 || (opts.okStatuses ?? []).includes(res.status);
|
|
14212
|
-
if (!ok) {
|
|
14213
|
-
const apiErr = new ApiError(errorMessage(res.status, json ?? text, cfg), res.status, json ?? text);
|
|
14214
|
-
const errBody = json;
|
|
14215
|
-
if (res.status === 403 && errBody?.error === "insufficient_scope") apiErr.requiredScope = errBody.required;
|
|
14216
|
-
throw apiErr;
|
|
14250
|
+
function isNearExpiry(expiresAt) {
|
|
14251
|
+
if (!expiresAt) return false;
|
|
14252
|
+
const t = new Date(expiresAt).getTime();
|
|
14253
|
+
if (Number.isNaN(t)) return false;
|
|
14254
|
+
return t - Date.now() <= 5 * 60 * 1e3;
|
|
14255
|
+
}
|
|
14256
|
+
async function refreshOrgCredential(baseUrl, org, entry) {
|
|
14257
|
+
const { request: request2, ApiError: ApiError2 } = await Promise.resolve().then(() => (init_http(), http_exports));
|
|
14258
|
+
const cfg = { baseUrl, token: null, tokenSource: "none", urlSource: "credentials-refresh" };
|
|
14259
|
+
try {
|
|
14260
|
+
const resp = await request2(cfg, "/auth/oauth/refresh", {
|
|
14261
|
+
method: "POST",
|
|
14262
|
+
body: { refresh_token: entry.refresh_token },
|
|
14263
|
+
retry: false
|
|
14264
|
+
});
|
|
14265
|
+
const d = resp.json;
|
|
14266
|
+
if (!d?.token || !d.refresh_token) return null;
|
|
14267
|
+
return {
|
|
14268
|
+
token_id: d.id,
|
|
14269
|
+
token: d.token,
|
|
14270
|
+
scopes: d.scopes,
|
|
14271
|
+
expires_at: d.expires_at,
|
|
14272
|
+
refresh_token: d.refresh_token,
|
|
14273
|
+
refresh_expires_at: d.refresh_expires_at
|
|
14274
|
+
};
|
|
14275
|
+
} catch (err) {
|
|
14276
|
+
if (err instanceof ApiError2 && err.status === 409) {
|
|
14277
|
+
try {
|
|
14278
|
+
const latest = readCredentials().orgs[org];
|
|
14279
|
+
if (latest && latest.token !== entry.token) return latest;
|
|
14280
|
+
} catch {
|
|
14281
|
+
}
|
|
14282
|
+
return null;
|
|
14217
14283
|
}
|
|
14218
|
-
|
|
14284
|
+
throw err;
|
|
14219
14285
|
}
|
|
14220
|
-
const cause = lastError instanceof Error ? lastError.message : String(lastError);
|
|
14221
|
-
const maybeDelivered = method !== "GET";
|
|
14222
|
-
throw new NetworkError(
|
|
14223
|
-
`request failed: ${method} ${url.pathname} \u2014 ${cause}` + (maybeDelivered ? "\nhint: the request may still have been received \u2014 verify before retrying (e.g. pylot ps)" : ""),
|
|
14224
|
-
maybeDelivered
|
|
14225
|
-
);
|
|
14226
14286
|
}
|
|
14227
|
-
|
|
14228
|
-
|
|
14287
|
+
function readCredentials() {
|
|
14288
|
+
try {
|
|
14289
|
+
const raw = readFileSync(credentialsPath(), "utf8");
|
|
14290
|
+
const parsed = JSON.parse(raw);
|
|
14291
|
+
if (parsed && typeof parsed.orgs === "object") return parsed;
|
|
14292
|
+
} catch {
|
|
14293
|
+
}
|
|
14294
|
+
return { orgs: {} };
|
|
14295
|
+
}
|
|
14296
|
+
function writeCredentials(data) {
|
|
14297
|
+
const path5 = credentialsPath();
|
|
14298
|
+
mkdirSync(dirname(path5), { recursive: true });
|
|
14299
|
+
const tmp = `${path5}.${process.pid}.tmp`;
|
|
14300
|
+
writeFileSync2(tmp, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
|
|
14301
|
+
chmodSync(tmp, 384);
|
|
14302
|
+
renameSync(tmp, path5);
|
|
14303
|
+
}
|
|
14229
14304
|
var UsageError = class extends Error {
|
|
14230
14305
|
};
|
|
14231
14306
|
function normalizeBaseUrl(raw) {
|
|
@@ -14294,6 +14369,31 @@ async function resolveConfig(opts) {
|
|
|
14294
14369
|
pickUrl(env2.PYLOT_API, "PYLOT_API");
|
|
14295
14370
|
pickToken(await confGet("token"), "config file");
|
|
14296
14371
|
pickUrl(await confGet("url"), "config file");
|
|
14372
|
+
if (token === null) {
|
|
14373
|
+
const org = opts.org ?? await confGet("org");
|
|
14374
|
+
if (org) {
|
|
14375
|
+
try {
|
|
14376
|
+
const creds = readCredentials();
|
|
14377
|
+
let entry = creds.orgs[org];
|
|
14378
|
+
if (entry?.token && entry.refresh_token && baseUrl && isNearExpiry(entry.expires_at)) {
|
|
14379
|
+
const rotated = await refreshOrgCredential(baseUrl, org, entry).catch(() => null);
|
|
14380
|
+
if (rotated) {
|
|
14381
|
+
creds.orgs[org] = rotated;
|
|
14382
|
+
try {
|
|
14383
|
+
writeCredentials(creds);
|
|
14384
|
+
} catch {
|
|
14385
|
+
}
|
|
14386
|
+
entry = rotated;
|
|
14387
|
+
} else if (new Date(entry.expires_at).getTime() <= Date.now()) {
|
|
14388
|
+
throw new UsageError(`stored token for ${org} expired and refresh failed \u2014 run: pylot auth login`);
|
|
14389
|
+
}
|
|
14390
|
+
}
|
|
14391
|
+
if (entry?.token) pickToken(entry.token, "credentials file");
|
|
14392
|
+
} catch (err) {
|
|
14393
|
+
if (err instanceof UsageError) throw err;
|
|
14394
|
+
}
|
|
14395
|
+
}
|
|
14396
|
+
}
|
|
14297
14397
|
if (!baseUrl) {
|
|
14298
14398
|
throw new UsageError(
|
|
14299
14399
|
"no gateway URL configured \u2014 set PYLOT_DISPATCH_URL / PYLOT_API_URL / PYLOT_GATEWAY_URL, pass --url, or run: pylot config set url <url>"
|
|
@@ -14375,6 +14475,7 @@ async function ctx(cmd) {
|
|
|
14375
14475
|
}
|
|
14376
14476
|
|
|
14377
14477
|
// src/commands/core.mts
|
|
14478
|
+
init_http();
|
|
14378
14479
|
function registerCore(program3) {
|
|
14379
14480
|
program3.command("health").description("gateway liveness, version, and counters (GET /health)").action(async (_opts, cmd) => {
|
|
14380
14481
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -14386,6 +14487,12 @@ function registerCore(program3) {
|
|
|
14386
14487
|
});
|
|
14387
14488
|
}
|
|
14388
14489
|
|
|
14490
|
+
// src/commands/dispatch.mts
|
|
14491
|
+
init_http();
|
|
14492
|
+
|
|
14493
|
+
// src/commands/missions.mts
|
|
14494
|
+
init_http();
|
|
14495
|
+
|
|
14389
14496
|
// src/lib/poll.mts
|
|
14390
14497
|
var PollTimeoutError = class extends Error {
|
|
14391
14498
|
last;
|
|
@@ -14635,8 +14742,8 @@ function registerMissions(program3) {
|
|
|
14635
14742
|
async function bodyFromArgs(fields, file) {
|
|
14636
14743
|
let body = {};
|
|
14637
14744
|
if (file) {
|
|
14638
|
-
const { readFileSync } = await import("node:fs");
|
|
14639
|
-
const text = file === "-" ?
|
|
14745
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
14746
|
+
const text = file === "-" ? readFileSync2(0, "utf8") : readFileSync2(file, "utf8");
|
|
14640
14747
|
body = JSON.parse(text);
|
|
14641
14748
|
}
|
|
14642
14749
|
for (const field of fields) {
|
|
@@ -14712,6 +14819,64 @@ function registerDispatch(program3) {
|
|
|
14712
14819
|
}
|
|
14713
14820
|
|
|
14714
14821
|
// src/commands/auth.mts
|
|
14822
|
+
import * as http from "node:http";
|
|
14823
|
+
import * as net from "node:net";
|
|
14824
|
+
import { exec } from "node:child_process";
|
|
14825
|
+
init_http();
|
|
14826
|
+
function openBrowser(url) {
|
|
14827
|
+
const cmd = process.platform === "darwin" ? `open ${JSON.stringify(url)}` : process.platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
|
|
14828
|
+
exec(cmd, (err) => {
|
|
14829
|
+
if (err) process.stderr.write(`[auth] could not open browser automatically.
|
|
14830
|
+
Open manually: ${url}
|
|
14831
|
+
`);
|
|
14832
|
+
});
|
|
14833
|
+
}
|
|
14834
|
+
async function pickEphemeralPort() {
|
|
14835
|
+
return new Promise((resolve, reject) => {
|
|
14836
|
+
const srv = net.createServer();
|
|
14837
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
14838
|
+
const addr = srv.address();
|
|
14839
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
14840
|
+
srv.close((err) => {
|
|
14841
|
+
if (err) reject(err);
|
|
14842
|
+
else resolve(port);
|
|
14843
|
+
});
|
|
14844
|
+
});
|
|
14845
|
+
srv.on("error", reject);
|
|
14846
|
+
});
|
|
14847
|
+
}
|
|
14848
|
+
async function waitForCallback(port, timeoutMs = 3e5) {
|
|
14849
|
+
return new Promise((resolve, reject) => {
|
|
14850
|
+
const timer = setTimeout(() => {
|
|
14851
|
+
server.close();
|
|
14852
|
+
reject(new Error("timed out waiting for browser callback (5 min)"));
|
|
14853
|
+
}, timeoutMs);
|
|
14854
|
+
const server = http.createServer((req, res) => {
|
|
14855
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
14856
|
+
const code = url.searchParams.get("code");
|
|
14857
|
+
const error = url.searchParams.get("error");
|
|
14858
|
+
const html = `<!doctype html><html><body style="font-family:sans-serif;padding:2em"><h2>You can close this tab</h2><p>Return to your terminal.</p></body></html>`;
|
|
14859
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
14860
|
+
res.end(html);
|
|
14861
|
+
clearTimeout(timer);
|
|
14862
|
+
server.close();
|
|
14863
|
+
if (error) {
|
|
14864
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
14865
|
+
return;
|
|
14866
|
+
}
|
|
14867
|
+
if (!code) {
|
|
14868
|
+
reject(new Error("no code in callback"));
|
|
14869
|
+
return;
|
|
14870
|
+
}
|
|
14871
|
+
resolve(code);
|
|
14872
|
+
});
|
|
14873
|
+
server.on("error", (err) => {
|
|
14874
|
+
clearTimeout(timer);
|
|
14875
|
+
reject(err);
|
|
14876
|
+
});
|
|
14877
|
+
server.listen(port, "127.0.0.1");
|
|
14878
|
+
});
|
|
14879
|
+
}
|
|
14715
14880
|
function registerAuth(program3) {
|
|
14716
14881
|
const auth = program3.command("auth").description("identity, tokens, and credentials");
|
|
14717
14882
|
auth.command("status").description("who am I \u2014 token identity, scopes, and CLI config resolution (GET /auth/me)").action(async (_flags, cmd) => {
|
|
@@ -14787,12 +14952,129 @@ function registerAuth(program3) {
|
|
|
14787
14952
|
const { opts, cfg } = await ctx(cmd);
|
|
14788
14953
|
emit(opts, await request(cfg, "/bot-identity"));
|
|
14789
14954
|
});
|
|
14790
|
-
|
|
14791
|
-
process.
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14955
|
+
function isHeadless() {
|
|
14956
|
+
if (process.env.SSH_TTY) return true;
|
|
14957
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
|
|
14958
|
+
return false;
|
|
14959
|
+
}
|
|
14960
|
+
async function runDeviceLogin(flags, cmd) {
|
|
14961
|
+
const parentOpts = cmd.parent?.parent?.opts() ?? cmd.parent?.opts() ?? {};
|
|
14962
|
+
const rawUrl = flags.url ?? parentOpts.url ?? process.env.PYLOT_DISPATCH_URL ?? process.env.PYLOT_API_URL ?? process.env.PYLOT_GATEWAY_URL ?? process.env.PYLOT_API ?? "";
|
|
14963
|
+
if (!rawUrl) throw new UsageError("no gateway URL configured \u2014 pass --url or set PYLOT_API_URL");
|
|
14964
|
+
const baseUrl = normalizeBaseUrl(rawUrl);
|
|
14965
|
+
const cfg = { baseUrl, token: null, tokenSource: "none", urlSource: "--url" };
|
|
14966
|
+
const startResp = await request(cfg, "/auth/oauth/device/start", { method: "POST", body: {} });
|
|
14967
|
+
const { user_code, verification_uri, interval: rawInterval, device_session } = startResp.json ?? {};
|
|
14968
|
+
if (!user_code || !verification_uri || !device_session) throw new Error("gateway returned incomplete device-start response");
|
|
14969
|
+
process.stderr.write(`
|
|
14970
|
+
Open in your browser:
|
|
14971
|
+
${verification_uri}
|
|
14972
|
+
|
|
14973
|
+
Enter code: ${user_code}
|
|
14974
|
+
|
|
14975
|
+
Waiting for authorization\u2026
|
|
14976
|
+
`);
|
|
14977
|
+
let intervalSecs = typeof rawInterval === "number" ? rawInterval : 5;
|
|
14978
|
+
const timeoutAt = Date.now() + 15 * 60 * 1e3;
|
|
14979
|
+
while (Date.now() < timeoutAt) {
|
|
14980
|
+
await new Promise((r) => setTimeout(r, intervalSecs * 1e3));
|
|
14981
|
+
const pollResp = await request(
|
|
14982
|
+
cfg,
|
|
14983
|
+
"/auth/oauth/device/poll",
|
|
14984
|
+
{ method: "POST", body: { device_session } }
|
|
14985
|
+
).catch((err) => {
|
|
14986
|
+
throw new Error(`poll failed: ${err.message}`);
|
|
14987
|
+
});
|
|
14988
|
+
const data = pollResp.json;
|
|
14989
|
+
if (data?.slow_down) {
|
|
14990
|
+
intervalSecs += 5;
|
|
14991
|
+
continue;
|
|
14992
|
+
}
|
|
14993
|
+
if (data?.status === "pending") continue;
|
|
14994
|
+
if (Array.isArray(data?.tokens)) {
|
|
14995
|
+
const tokens2 = data.tokens;
|
|
14996
|
+
if (tokens2.length === 0) throw new Error("gateway returned no tokens");
|
|
14997
|
+
const creds = readCredentials();
|
|
14998
|
+
for (const t of tokens2) {
|
|
14999
|
+
creds.orgs[t.org] = {
|
|
15000
|
+
token_id: t.id,
|
|
15001
|
+
token: t.token,
|
|
15002
|
+
scopes: t.scopes,
|
|
15003
|
+
expires_at: t.expires_at,
|
|
15004
|
+
...t.refresh_token ? { refresh_token: t.refresh_token, refresh_expires_at: t.refresh_expires_at } : {}
|
|
15005
|
+
};
|
|
15006
|
+
}
|
|
15007
|
+
writeCredentials(creds);
|
|
15008
|
+
const orgList = tokens2.map((t) => t.org).join(", ");
|
|
15009
|
+
if (flags.org && !tokens2.find((t) => t.org === flags.org)) {
|
|
15010
|
+
process.stderr.write(`warning: --org ${flags.org} not found in returned tokens (got: ${orgList})
|
|
15011
|
+
`);
|
|
15012
|
+
}
|
|
15013
|
+
process.stdout.write(`Logged in. Tokens stored for: ${orgList}
|
|
15014
|
+
`);
|
|
15015
|
+
process.stdout.write(`Credentials written to ~/.pylot/credentials (mode 0600)
|
|
15016
|
+
`);
|
|
15017
|
+
if (flags.org) {
|
|
15018
|
+
process.stdout.write(`Active org: ${flags.org} \u2014 use PYLOT_API_TOKEN or pylot config set org ${flags.org}
|
|
15019
|
+
`);
|
|
15020
|
+
}
|
|
15021
|
+
return;
|
|
15022
|
+
}
|
|
15023
|
+
throw new Error(`unexpected poll response: ${JSON.stringify(data)}`);
|
|
15024
|
+
}
|
|
15025
|
+
throw new Error("device code expired \u2014 run pylot auth login again");
|
|
15026
|
+
}
|
|
15027
|
+
async function runLogin(flags, cmd) {
|
|
15028
|
+
if (flags.device || isHeadless()) return runDeviceLogin(flags, cmd);
|
|
15029
|
+
const parentOpts = cmd.parent?.parent?.opts() ?? cmd.parent?.opts() ?? {};
|
|
15030
|
+
const rawUrl = flags.url ?? parentOpts.url ?? process.env.PYLOT_DISPATCH_URL ?? process.env.PYLOT_API_URL ?? process.env.PYLOT_GATEWAY_URL ?? process.env.PYLOT_API ?? "";
|
|
15031
|
+
if (!rawUrl) throw new UsageError("no gateway URL configured \u2014 pass --url or set PYLOT_API_URL");
|
|
15032
|
+
const baseUrl = normalizeBaseUrl(rawUrl);
|
|
15033
|
+
const port = await pickEphemeralPort();
|
|
15034
|
+
const loginUrl = `${baseUrl}/auth/oauth/cli/login?port=${port}`;
|
|
15035
|
+
process.stderr.write(`Opening browser for login\u2026
|
|
15036
|
+
`);
|
|
15037
|
+
openBrowser(loginUrl);
|
|
15038
|
+
process.stderr.write(`Waiting for browser callback on http://localhost:${port}/callback
|
|
15039
|
+
`);
|
|
15040
|
+
process.stderr.write(`If your browser did not open, visit:
|
|
15041
|
+
${loginUrl}
|
|
15042
|
+
`);
|
|
15043
|
+
const exchangeCode = await waitForCallback(port);
|
|
15044
|
+
const cfg = { baseUrl, token: null, tokenSource: "none", urlSource: "--url" };
|
|
15045
|
+
const resp = await request(cfg, "/auth/oauth/complete", {
|
|
15046
|
+
method: "POST",
|
|
15047
|
+
body: { code: exchangeCode }
|
|
15048
|
+
});
|
|
15049
|
+
const tokens2 = resp.json?.tokens ?? [];
|
|
15050
|
+
if (tokens2.length === 0) throw new Error("gateway returned no tokens");
|
|
15051
|
+
const creds = readCredentials();
|
|
15052
|
+
for (const t of tokens2) {
|
|
15053
|
+
creds.orgs[t.org] = {
|
|
15054
|
+
token_id: t.id,
|
|
15055
|
+
token: t.token,
|
|
15056
|
+
scopes: t.scopes,
|
|
15057
|
+
expires_at: t.expires_at,
|
|
15058
|
+
...t.refresh_token ? { refresh_token: t.refresh_token, refresh_expires_at: t.refresh_expires_at } : {}
|
|
15059
|
+
};
|
|
15060
|
+
}
|
|
15061
|
+
writeCredentials(creds);
|
|
15062
|
+
const orgList = tokens2.map((t) => t.org).join(", ");
|
|
15063
|
+
if (flags.org && !tokens2.find((t) => t.org === flags.org)) {
|
|
15064
|
+
process.stderr.write(`warning: --org ${flags.org} not found in returned tokens (got: ${orgList})
|
|
15065
|
+
`);
|
|
15066
|
+
}
|
|
15067
|
+
process.stdout.write(`Logged in. Tokens stored for: ${orgList}
|
|
15068
|
+
`);
|
|
15069
|
+
process.stdout.write(`Credentials written to ~/.pylot/credentials (mode 0600)
|
|
15070
|
+
`);
|
|
15071
|
+
if (flags.org) {
|
|
15072
|
+
process.stdout.write(`Active org: ${flags.org} \u2014 use PYLOT_API_TOKEN or pylot config set org ${flags.org}
|
|
15073
|
+
`);
|
|
15074
|
+
}
|
|
15075
|
+
}
|
|
15076
|
+
auth.command("login").description("authenticate and store per-org tokens in ~/.pylot/credentials (browser or --device for headless)").option("--org <org>", "filter stored credentials to a single org").option("--url <url>", "gateway base URL (overrides env / config)").option("--device", "use device-code flow (auto-selected in headless/SSH environments)").action(runLogin);
|
|
15077
|
+
program3.command("login", { hidden: true }).description("authenticate \u2014 alias for `pylot auth login` (M5a #2145, M5b #2146)").option("--org <org>", "filter stored credentials to a single org").option("--url <url>", "gateway base URL").option("--device", "use device-code flow (auto-selected in headless/SSH environments)").action(runLogin);
|
|
14796
15078
|
}
|
|
14797
15079
|
|
|
14798
15080
|
// src/commands/config-cmd.mts
|
|
@@ -14843,6 +15125,7 @@ function registerConfig(program3) {
|
|
|
14843
15125
|
}
|
|
14844
15126
|
|
|
14845
15127
|
// src/commands/observability.mts
|
|
15128
|
+
init_http();
|
|
14846
15129
|
function registerObservability(program3) {
|
|
14847
15130
|
program3.command("events").description("event ledger entries (GET /events)").option("--type <type>", "filter by event type").option("--since <iso>", "events since this timestamp").option("--org <org>", "filter by org").option("--limit <n>", "max rows (server caps at 200)", (v) => parseInt(v, 10)).action(async (flags, cmd) => {
|
|
14848
15131
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -14943,6 +15226,7 @@ function registerObservability(program3) {
|
|
|
14943
15226
|
}
|
|
14944
15227
|
|
|
14945
15228
|
// src/commands/workers.mts
|
|
15229
|
+
init_http();
|
|
14946
15230
|
function scopedBase(flags) {
|
|
14947
15231
|
if (Boolean(flags.mission) === Boolean(flags.conversation)) {
|
|
14948
15232
|
throw new UsageError("pass exactly one of --mission <id> / --conversation <id>");
|
|
@@ -14995,6 +15279,7 @@ function registerWorkers(program3) {
|
|
|
14995
15279
|
}
|
|
14996
15280
|
|
|
14997
15281
|
// src/commands/devboxes.mts
|
|
15282
|
+
init_http();
|
|
14998
15283
|
function registerDevboxes(program3) {
|
|
14999
15284
|
const devboxes = program3.command("devboxes").description("devbox environments (Fargate dev containers)");
|
|
15000
15285
|
devboxes.command("projects").description("projects with devbox configs (GET /devboxes/projects)").action(async (_flags, cmd) => {
|
|
@@ -15005,9 +15290,16 @@ function registerDevboxes(program3) {
|
|
|
15005
15290
|
const { opts, cfg } = await ctx(cmd);
|
|
15006
15291
|
emit(opts, await request(cfg, `/devboxes/projects/${encodeURIComponent(project)}`));
|
|
15007
15292
|
});
|
|
15008
|
-
devboxes.command("spawn").description("spawn a devbox environment (POST /devboxes/projects/:project)").argument("<org/repo>").argument("[fields...]", "key=value pairs").option("--file <path>", "JSON body from file ('-' for stdin)").action(async (project, fields, flags, cmd) => {
|
|
15293
|
+
devboxes.command("spawn").description("spawn a devbox environment (POST /devboxes/projects/:project)").argument("<org/repo>").argument("[fields...]", "key=value pairs").option("--file <path>", "JSON body from file ('-' for stdin)").option("--idle-ttl <secs>", "idle-reap TTL in seconds (0\u2013604800); 0 = never idle-reap").action(async (project, fields, flags, cmd) => {
|
|
15009
15294
|
const { opts, cfg } = await ctx(cmd);
|
|
15010
15295
|
const body = fields.length || flags.file ? await bodyFromArgs(fields, flags.file) : {};
|
|
15296
|
+
if (flags.idleTtl != null) {
|
|
15297
|
+
const secs = Number(flags.idleTtl);
|
|
15298
|
+
if (!Number.isInteger(secs) || secs < 0 || secs > 604800) {
|
|
15299
|
+
throw new UsageError("--idle-ttl must be an integer between 0 and 604800 (0 = never idle-reap)");
|
|
15300
|
+
}
|
|
15301
|
+
body.idle_ttl_secs = secs;
|
|
15302
|
+
}
|
|
15011
15303
|
emit(opts, await request(cfg, `/devboxes/projects/${encodeURIComponent(project)}`, { method: "POST", body }));
|
|
15012
15304
|
});
|
|
15013
15305
|
devboxes.command("list").description("running devbox environments for a project (GET /devboxes/projects/:project/list)").argument("<org/repo>").action(async (project, _flags, cmd) => {
|
|
@@ -15034,6 +15326,7 @@ function registerDevboxes(program3) {
|
|
|
15034
15326
|
}
|
|
15035
15327
|
|
|
15036
15328
|
// src/commands/conversations.mts
|
|
15329
|
+
init_http();
|
|
15037
15330
|
function convPath(id, sub = "") {
|
|
15038
15331
|
return `/conversations/${encodeURIComponent(id)}${sub}`;
|
|
15039
15332
|
}
|
|
@@ -15178,9 +15471,10 @@ function registerConversations(program3) {
|
|
|
15178
15471
|
}
|
|
15179
15472
|
|
|
15180
15473
|
// src/commands/teams.mts
|
|
15474
|
+
init_http();
|
|
15181
15475
|
async function readText(file) {
|
|
15182
|
-
const { readFileSync } = await import("node:fs");
|
|
15183
|
-
return file === "-" ?
|
|
15476
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
15477
|
+
return file === "-" ? readFileSync2(0, "utf8") : readFileSync2(file, "utf8");
|
|
15184
15478
|
}
|
|
15185
15479
|
function registerTeams(program3) {
|
|
15186
15480
|
const teams = program3.command("teams").description("team config, operators, playbooks, goals, ledger");
|
|
@@ -15309,6 +15603,7 @@ function registerTeams(program3) {
|
|
|
15309
15603
|
}
|
|
15310
15604
|
|
|
15311
15605
|
// src/commands/admin.mts
|
|
15606
|
+
init_http();
|
|
15312
15607
|
function registerAdmin(program3) {
|
|
15313
15608
|
const admin = program3.command("admin").description("system config, audit, org caps, DB migrations");
|
|
15314
15609
|
admin.command("config").description("read the config table (GET /admin/config) or one runtime key (GET /admin/config/:key)").argument("[key]", "system_state key, e.g. snapshot.mission_workers_enabled").action(async (key, _flags, cmd) => {
|
|
@@ -15350,6 +15645,7 @@ function registerAdmin(program3) {
|
|
|
15350
15645
|
}
|
|
15351
15646
|
|
|
15352
15647
|
// src/commands/deploy.mts
|
|
15648
|
+
init_http();
|
|
15353
15649
|
var TERMINAL_BUILD_STATUSES = /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED", "FAULT", "STOPPED", "TIMED_OUT"]);
|
|
15354
15650
|
function printNewPhases(opts, body, seen) {
|
|
15355
15651
|
for (const p of body?.phases ?? []) {
|
|
@@ -15518,6 +15814,7 @@ function registerDeploy(program3) {
|
|
|
15518
15814
|
}
|
|
15519
15815
|
|
|
15520
15816
|
// src/commands/secrets.mts
|
|
15817
|
+
init_http();
|
|
15521
15818
|
function registerSecrets(program3) {
|
|
15522
15819
|
const secrets = program3.command("secrets").description("ASM secret bundles (path-based, admin-scoped)");
|
|
15523
15820
|
secrets.command("tree").description("full secrets tree (GET /admin/secrets)").action(async (_flags, cmd) => {
|
|
@@ -15553,8 +15850,8 @@ function registerSecrets(program3) {
|
|
|
15553
15850
|
}
|
|
15554
15851
|
let value;
|
|
15555
15852
|
if (flags.stdin) {
|
|
15556
|
-
const { readFileSync } = await import("node:fs");
|
|
15557
|
-
value =
|
|
15853
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
15854
|
+
value = readFileSync2(0, "utf8").replace(/\r?\n$/, "");
|
|
15558
15855
|
} else {
|
|
15559
15856
|
value = String(flags.value);
|
|
15560
15857
|
}
|
|
@@ -15585,6 +15882,7 @@ function registerSecrets(program3) {
|
|
|
15585
15882
|
}
|
|
15586
15883
|
|
|
15587
15884
|
// src/commands/skills.mts
|
|
15885
|
+
init_http();
|
|
15588
15886
|
function registerSkills(program3) {
|
|
15589
15887
|
const skills = program3.command("skills").description("skills catalog, sync pipeline, procedures");
|
|
15590
15888
|
skills.command("list").description("skills catalog (GET /admin/skills)").option("--org <org>", "scope to one org (default: all orgs)").option("--tag <tag>", "filter by tag").option("--origin <org/repo>", "filter by origin repo").option("--search <text>", "search name/description").action(async (flags, cmd) => {
|
|
@@ -15656,6 +15954,7 @@ function registerSkills(program3) {
|
|
|
15656
15954
|
}
|
|
15657
15955
|
|
|
15658
15956
|
// src/commands/automations.mts
|
|
15957
|
+
init_http();
|
|
15659
15958
|
function registerAutomations(program3) {
|
|
15660
15959
|
const automations = program3.command("automations").description("event-driven automation rules");
|
|
15661
15960
|
automations.command("list").description("automation rules with run stats (GET /automations); --db raw DB view; --snapshots snapshot history").option("--db", "raw DB view (GET /automations/_db)").option("--snapshots", "snapshot history (GET /automations/_snapshots)").option("--limit <n>", "max snapshot rows (with --snapshots, default 20)", (v) => parseInt(v, 10)).action(async (flags, cmd) => {
|
|
@@ -15702,6 +16001,7 @@ function registerAutomations(program3) {
|
|
|
15702
16001
|
}
|
|
15703
16002
|
|
|
15704
16003
|
// src/commands/providers.mts
|
|
16004
|
+
init_http();
|
|
15705
16005
|
function registerProviders(program3) {
|
|
15706
16006
|
const providers = program3.command("providers").description("LLM provider instances and org purpose-chains");
|
|
15707
16007
|
providers.command("list").description("provider instances for an org (GET /admin/providers?org=)").requiredOption("--org <org>", "org to list (required by the gateway)").action(async (flags, cmd) => {
|
|
@@ -15758,6 +16058,7 @@ function registerProviders(program3) {
|
|
|
15758
16058
|
}
|
|
15759
16059
|
|
|
15760
16060
|
// src/commands/orgs.mts
|
|
16061
|
+
init_http();
|
|
15761
16062
|
function registerOrgs(program3) {
|
|
15762
16063
|
const orgs = program3.command("orgs").description("GitHub orgs and repo onboarding");
|
|
15763
16064
|
orgs.command("list").description("orgs visible to this token (GET /orgs)").action(async (_flags, cmd) => {
|
|
@@ -15774,6 +16075,7 @@ function registerOrgs(program3) {
|
|
|
15774
16075
|
}
|
|
15775
16076
|
|
|
15776
16077
|
// src/commands/assets.mts
|
|
16078
|
+
init_http();
|
|
15777
16079
|
function registerAssets(program3) {
|
|
15778
16080
|
const assets = program3.command("assets").description("S3 asset presign, view, publish");
|
|
15779
16081
|
assets.command("presign").description("mint a presigned upload URL (POST /assets/presign) \u2014 needs one of --conversation/--job/--repo").requiredOption("--content-type <ct>", "asset content type, e.g. image/png").requiredOption("--size <bytes>", "upload size in bytes", (v) => parseInt(v, 10)).option("--conversation <id>", "attach to a conversation").option("--job <id>", "attach to a mission job").option("--repo <org/repo>", "attach to a repo").action(async (flags, cmd) => {
|
|
@@ -15811,7 +16113,7 @@ function registerAssets(program3) {
|
|
|
15811
16113
|
}
|
|
15812
16114
|
|
|
15813
16115
|
// src/index.mts
|
|
15814
|
-
var VERSION = true ? "0.1.
|
|
16116
|
+
var VERSION = true ? "0.1.4" : "0.0.0-dev";
|
|
15815
16117
|
installEpipeGuard();
|
|
15816
16118
|
var program2 = new Command();
|
|
15817
16119
|
program2.name("pylot").description(
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pylot-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "pylot
|
|
5
|
+
"description": "pylot \u2014 first-class CLI for the Pylot gateway API. Replaces curl workflows for operators and humans.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pylot": "dist/index.mjs"
|
|
8
8
|
},
|