pylot-cli 0.1.3 → 0.1.5
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 +459 -139
- 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) {
|
|
@@ -14287,13 +14362,43 @@ async function resolveConfig(opts) {
|
|
|
14287
14362
|
pickToken(env2.PYLOT_API_TOKEN, "PYLOT_API_TOKEN");
|
|
14288
14363
|
pickToken(env2.PYLOT_SESSION_JWT, "PYLOT_SESSION_JWT");
|
|
14289
14364
|
pickToken(env2.PYLOT_BROKER_TOKEN, "PYLOT_BROKER_TOKEN");
|
|
14290
|
-
pickToken(env2.PYLOT_DISPATCH_TOKEN, "PYLOT_DISPATCH_TOKEN");
|
|
14291
14365
|
pickUrl(env2.PYLOT_DISPATCH_URL, "PYLOT_DISPATCH_URL");
|
|
14292
14366
|
pickUrl(env2.PYLOT_API_URL, "PYLOT_API_URL");
|
|
14293
14367
|
pickUrl(env2.PYLOT_GATEWAY_URL, "PYLOT_GATEWAY_URL");
|
|
14294
14368
|
pickUrl(env2.PYLOT_API, "PYLOT_API");
|
|
14295
|
-
pickToken(await confGet("token"), "config file");
|
|
14296
14369
|
pickUrl(await confGet("url"), "config file");
|
|
14370
|
+
const selectedOrg = opts.org ?? await confGet("org");
|
|
14371
|
+
if (selectedOrg && token === null) {
|
|
14372
|
+
let entry;
|
|
14373
|
+
try {
|
|
14374
|
+
const creds = readCredentials();
|
|
14375
|
+
entry = creds.orgs[selectedOrg];
|
|
14376
|
+
if (entry?.token && entry.refresh_token && baseUrl && isNearExpiry(entry.expires_at)) {
|
|
14377
|
+
const rotated = await refreshOrgCredential(baseUrl, selectedOrg, entry).catch(() => null);
|
|
14378
|
+
if (rotated) {
|
|
14379
|
+
creds.orgs[selectedOrg] = rotated;
|
|
14380
|
+
try {
|
|
14381
|
+
writeCredentials(creds);
|
|
14382
|
+
} catch {
|
|
14383
|
+
}
|
|
14384
|
+
entry = rotated;
|
|
14385
|
+
} else if (new Date(entry.expires_at).getTime() <= Date.now()) {
|
|
14386
|
+
throw new UsageError(`stored token for ${selectedOrg} expired and refresh failed \u2014 run: pylot auth login`);
|
|
14387
|
+
}
|
|
14388
|
+
}
|
|
14389
|
+
} catch (err) {
|
|
14390
|
+
if (err instanceof UsageError) throw err;
|
|
14391
|
+
}
|
|
14392
|
+
if (!entry?.token) {
|
|
14393
|
+
throw new UsageError(
|
|
14394
|
+
`no credentials found for org '${selectedOrg}' \u2014 run: pylot auth login --org ${selectedOrg}`
|
|
14395
|
+
);
|
|
14396
|
+
}
|
|
14397
|
+
pickToken(entry.token, "credentials file");
|
|
14398
|
+
} else {
|
|
14399
|
+
pickToken(env2.PYLOT_DISPATCH_TOKEN, "PYLOT_DISPATCH_TOKEN");
|
|
14400
|
+
pickToken(await confGet("token"), "config file");
|
|
14401
|
+
}
|
|
14297
14402
|
if (!baseUrl) {
|
|
14298
14403
|
throw new UsageError(
|
|
14299
14404
|
"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 +14480,7 @@ async function ctx(cmd) {
|
|
|
14375
14480
|
}
|
|
14376
14481
|
|
|
14377
14482
|
// src/commands/core.mts
|
|
14483
|
+
init_http();
|
|
14378
14484
|
function registerCore(program3) {
|
|
14379
14485
|
program3.command("health").description("gateway liveness, version, and counters (GET /health)").action(async (_opts, cmd) => {
|
|
14380
14486
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -14386,6 +14492,12 @@ function registerCore(program3) {
|
|
|
14386
14492
|
});
|
|
14387
14493
|
}
|
|
14388
14494
|
|
|
14495
|
+
// src/commands/dispatch.mts
|
|
14496
|
+
init_http();
|
|
14497
|
+
|
|
14498
|
+
// src/commands/missions.mts
|
|
14499
|
+
init_http();
|
|
14500
|
+
|
|
14389
14501
|
// src/lib/poll.mts
|
|
14390
14502
|
var PollTimeoutError = class extends Error {
|
|
14391
14503
|
last;
|
|
@@ -14635,8 +14747,8 @@ function registerMissions(program3) {
|
|
|
14635
14747
|
async function bodyFromArgs(fields, file) {
|
|
14636
14748
|
let body = {};
|
|
14637
14749
|
if (file) {
|
|
14638
|
-
const { readFileSync } = await import("node:fs");
|
|
14639
|
-
const text = file === "-" ?
|
|
14750
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
14751
|
+
const text = file === "-" ? readFileSync2(0, "utf8") : readFileSync2(file, "utf8");
|
|
14640
14752
|
body = JSON.parse(text);
|
|
14641
14753
|
}
|
|
14642
14754
|
for (const field of fields) {
|
|
@@ -14712,6 +14824,64 @@ function registerDispatch(program3) {
|
|
|
14712
14824
|
}
|
|
14713
14825
|
|
|
14714
14826
|
// src/commands/auth.mts
|
|
14827
|
+
import * as http from "node:http";
|
|
14828
|
+
import * as net from "node:net";
|
|
14829
|
+
import { exec } from "node:child_process";
|
|
14830
|
+
init_http();
|
|
14831
|
+
function openBrowser(url) {
|
|
14832
|
+
const cmd = process.platform === "darwin" ? `open ${JSON.stringify(url)}` : process.platform === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
|
|
14833
|
+
exec(cmd, (err) => {
|
|
14834
|
+
if (err) process.stderr.write(`[auth] could not open browser automatically.
|
|
14835
|
+
Open manually: ${url}
|
|
14836
|
+
`);
|
|
14837
|
+
});
|
|
14838
|
+
}
|
|
14839
|
+
async function pickEphemeralPort() {
|
|
14840
|
+
return new Promise((resolve, reject) => {
|
|
14841
|
+
const srv = net.createServer();
|
|
14842
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
14843
|
+
const addr = srv.address();
|
|
14844
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
14845
|
+
srv.close((err) => {
|
|
14846
|
+
if (err) reject(err);
|
|
14847
|
+
else resolve(port);
|
|
14848
|
+
});
|
|
14849
|
+
});
|
|
14850
|
+
srv.on("error", reject);
|
|
14851
|
+
});
|
|
14852
|
+
}
|
|
14853
|
+
async function waitForCallback(port, timeoutMs = 3e5) {
|
|
14854
|
+
return new Promise((resolve, reject) => {
|
|
14855
|
+
const timer = setTimeout(() => {
|
|
14856
|
+
server.close();
|
|
14857
|
+
reject(new Error("timed out waiting for browser callback (5 min)"));
|
|
14858
|
+
}, timeoutMs);
|
|
14859
|
+
const server = http.createServer((req, res) => {
|
|
14860
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
14861
|
+
const code = url.searchParams.get("code");
|
|
14862
|
+
const error = url.searchParams.get("error");
|
|
14863
|
+
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>`;
|
|
14864
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
14865
|
+
res.end(html);
|
|
14866
|
+
clearTimeout(timer);
|
|
14867
|
+
server.close();
|
|
14868
|
+
if (error) {
|
|
14869
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
14870
|
+
return;
|
|
14871
|
+
}
|
|
14872
|
+
if (!code) {
|
|
14873
|
+
reject(new Error("no code in callback"));
|
|
14874
|
+
return;
|
|
14875
|
+
}
|
|
14876
|
+
resolve(code);
|
|
14877
|
+
});
|
|
14878
|
+
server.on("error", (err) => {
|
|
14879
|
+
clearTimeout(timer);
|
|
14880
|
+
reject(err);
|
|
14881
|
+
});
|
|
14882
|
+
server.listen(port, "127.0.0.1");
|
|
14883
|
+
});
|
|
14884
|
+
}
|
|
14715
14885
|
function registerAuth(program3) {
|
|
14716
14886
|
const auth = program3.command("auth").description("identity, tokens, and credentials");
|
|
14717
14887
|
auth.command("status").description("who am I \u2014 token identity, scopes, and CLI config resolution (GET /auth/me)").action(async (_flags, cmd) => {
|
|
@@ -14753,9 +14923,11 @@ function registerAuth(program3) {
|
|
|
14753
14923
|
);
|
|
14754
14924
|
});
|
|
14755
14925
|
const tokens = auth.command("tokens").description("scoped API tokens (plt_*) \u2014 token plane #1884");
|
|
14756
|
-
tokens.command("create").description("mint a scoped API token (POST /auth/tokens \u2014 requires admin)").
|
|
14926
|
+
tokens.command("create").description("mint a scoped API token (POST /auth/tokens \u2014 requires admin)").option("--org <org>", "org the token is scoped to (or use global --org before the subcommand)").option("--label <label>", "human-readable label").option("--scopes <a,b,c>", "comma-separated scopes (e.g. dispatch,missions:read)").option("--expires-at <iso>", "expiry timestamp (ISO 8601)").action(async (flags, cmd) => {
|
|
14757
14927
|
const { opts, cfg } = await ctx(cmd);
|
|
14758
|
-
const
|
|
14928
|
+
const org = cmd.optsWithGlobals().org;
|
|
14929
|
+
if (!org) throw new UsageError("--org <org> is required");
|
|
14930
|
+
const body = { org, label: flags.label, expires_at: flags.expiresAt };
|
|
14759
14931
|
if (flags.scopes) body.scopes = String(flags.scopes).split(",").map((s) => s.trim()).filter(Boolean);
|
|
14760
14932
|
for (const key of Object.keys(body)) if (body[key] === void 0) delete body[key];
|
|
14761
14933
|
emit(opts, await request(cfg, "/auth/tokens", { method: "POST", body }));
|
|
@@ -14787,16 +14959,133 @@ function registerAuth(program3) {
|
|
|
14787
14959
|
const { opts, cfg } = await ctx(cmd);
|
|
14788
14960
|
emit(opts, await request(cfg, "/bot-identity"));
|
|
14789
14961
|
});
|
|
14790
|
-
|
|
14791
|
-
process.
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14962
|
+
function isHeadless() {
|
|
14963
|
+
if (process.env.SSH_TTY) return true;
|
|
14964
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
|
|
14965
|
+
return false;
|
|
14966
|
+
}
|
|
14967
|
+
async function runDeviceLogin(flags, cmd) {
|
|
14968
|
+
const parentOpts = cmd.parent?.parent?.opts() ?? cmd.parent?.opts() ?? {};
|
|
14969
|
+
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 ?? "";
|
|
14970
|
+
if (!rawUrl) throw new UsageError("no gateway URL configured \u2014 pass --url or set PYLOT_API_URL");
|
|
14971
|
+
const baseUrl = normalizeBaseUrl(rawUrl);
|
|
14972
|
+
const cfg = { baseUrl, token: null, tokenSource: "none", urlSource: "--url" };
|
|
14973
|
+
const startResp = await request(cfg, "/auth/oauth/device/start", { method: "POST", body: {} });
|
|
14974
|
+
const { user_code, verification_uri, interval: rawInterval, device_session } = startResp.json ?? {};
|
|
14975
|
+
if (!user_code || !verification_uri || !device_session) throw new Error("gateway returned incomplete device-start response");
|
|
14976
|
+
process.stderr.write(`
|
|
14977
|
+
Open in your browser:
|
|
14978
|
+
${verification_uri}
|
|
14979
|
+
|
|
14980
|
+
Enter code: ${user_code}
|
|
14981
|
+
|
|
14982
|
+
Waiting for authorization\u2026
|
|
14983
|
+
`);
|
|
14984
|
+
let intervalSecs = typeof rawInterval === "number" ? rawInterval : 5;
|
|
14985
|
+
const timeoutAt = Date.now() + 15 * 60 * 1e3;
|
|
14986
|
+
while (Date.now() < timeoutAt) {
|
|
14987
|
+
await new Promise((r) => setTimeout(r, intervalSecs * 1e3));
|
|
14988
|
+
const pollResp = await request(
|
|
14989
|
+
cfg,
|
|
14990
|
+
"/auth/oauth/device/poll",
|
|
14991
|
+
{ method: "POST", body: { device_session } }
|
|
14992
|
+
).catch((err) => {
|
|
14993
|
+
throw new Error(`poll failed: ${err.message}`);
|
|
14994
|
+
});
|
|
14995
|
+
const data = pollResp.json;
|
|
14996
|
+
if (data?.slow_down) {
|
|
14997
|
+
intervalSecs += 5;
|
|
14998
|
+
continue;
|
|
14999
|
+
}
|
|
15000
|
+
if (data?.status === "pending") continue;
|
|
15001
|
+
if (Array.isArray(data?.tokens)) {
|
|
15002
|
+
const tokens2 = data.tokens;
|
|
15003
|
+
if (tokens2.length === 0) throw new Error("gateway returned no tokens");
|
|
15004
|
+
const creds = readCredentials();
|
|
15005
|
+
for (const t of tokens2) {
|
|
15006
|
+
creds.orgs[t.org] = {
|
|
15007
|
+
token_id: t.id,
|
|
15008
|
+
token: t.token,
|
|
15009
|
+
scopes: t.scopes,
|
|
15010
|
+
expires_at: t.expires_at,
|
|
15011
|
+
...t.refresh_token ? { refresh_token: t.refresh_token, refresh_expires_at: t.refresh_expires_at } : {}
|
|
15012
|
+
};
|
|
15013
|
+
}
|
|
15014
|
+
writeCredentials(creds);
|
|
15015
|
+
const orgList = tokens2.map((t) => t.org).join(", ");
|
|
15016
|
+
if (flags.org && !tokens2.find((t) => t.org === flags.org)) {
|
|
15017
|
+
process.stderr.write(`warning: --org ${flags.org} not found in returned tokens (got: ${orgList})
|
|
15018
|
+
`);
|
|
15019
|
+
}
|
|
15020
|
+
process.stdout.write(`Logged in. Tokens stored for: ${orgList}
|
|
15021
|
+
`);
|
|
15022
|
+
process.stdout.write(`Credentials written to ~/.pylot/credentials (mode 0600)
|
|
15023
|
+
`);
|
|
15024
|
+
if (flags.org) {
|
|
15025
|
+
process.stdout.write(`Active org: ${flags.org} \u2014 use --org ${flags.org} or: pylot config set org ${flags.org}
|
|
15026
|
+
`);
|
|
15027
|
+
}
|
|
15028
|
+
return;
|
|
15029
|
+
}
|
|
15030
|
+
throw new Error(`unexpected poll response: ${JSON.stringify(data)}`);
|
|
15031
|
+
}
|
|
15032
|
+
throw new Error("device code expired \u2014 run pylot auth login again");
|
|
15033
|
+
}
|
|
15034
|
+
async function runLogin(flags, cmd) {
|
|
15035
|
+
if (flags.device || isHeadless()) return runDeviceLogin(flags, cmd);
|
|
15036
|
+
const parentOpts = cmd.parent?.parent?.opts() ?? cmd.parent?.opts() ?? {};
|
|
15037
|
+
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 ?? "";
|
|
15038
|
+
if (!rawUrl) throw new UsageError("no gateway URL configured \u2014 pass --url or set PYLOT_API_URL");
|
|
15039
|
+
const baseUrl = normalizeBaseUrl(rawUrl);
|
|
15040
|
+
const port = await pickEphemeralPort();
|
|
15041
|
+
const loginUrl = `${baseUrl}/auth/oauth/cli/login?port=${port}`;
|
|
15042
|
+
process.stderr.write(`Opening browser for login\u2026
|
|
15043
|
+
`);
|
|
15044
|
+
openBrowser(loginUrl);
|
|
15045
|
+
process.stderr.write(`Waiting for browser callback on http://localhost:${port}/callback
|
|
15046
|
+
`);
|
|
15047
|
+
process.stderr.write(`If your browser did not open, visit:
|
|
15048
|
+
${loginUrl}
|
|
15049
|
+
`);
|
|
15050
|
+
const exchangeCode = await waitForCallback(port);
|
|
15051
|
+
const cfg = { baseUrl, token: null, tokenSource: "none", urlSource: "--url" };
|
|
15052
|
+
const resp = await request(cfg, "/auth/oauth/complete", {
|
|
15053
|
+
method: "POST",
|
|
15054
|
+
body: { code: exchangeCode }
|
|
15055
|
+
});
|
|
15056
|
+
const tokens2 = resp.json?.tokens ?? [];
|
|
15057
|
+
if (tokens2.length === 0) throw new Error("gateway returned no tokens");
|
|
15058
|
+
const creds = readCredentials();
|
|
15059
|
+
for (const t of tokens2) {
|
|
15060
|
+
creds.orgs[t.org] = {
|
|
15061
|
+
token_id: t.id,
|
|
15062
|
+
token: t.token,
|
|
15063
|
+
scopes: t.scopes,
|
|
15064
|
+
expires_at: t.expires_at,
|
|
15065
|
+
...t.refresh_token ? { refresh_token: t.refresh_token, refresh_expires_at: t.refresh_expires_at } : {}
|
|
15066
|
+
};
|
|
15067
|
+
}
|
|
15068
|
+
writeCredentials(creds);
|
|
15069
|
+
const orgList = tokens2.map((t) => t.org).join(", ");
|
|
15070
|
+
if (flags.org && !tokens2.find((t) => t.org === flags.org)) {
|
|
15071
|
+
process.stderr.write(`warning: --org ${flags.org} not found in returned tokens (got: ${orgList})
|
|
15072
|
+
`);
|
|
15073
|
+
}
|
|
15074
|
+
process.stdout.write(`Logged in. Tokens stored for: ${orgList}
|
|
15075
|
+
`);
|
|
15076
|
+
process.stdout.write(`Credentials written to ~/.pylot/credentials (mode 0600)
|
|
15077
|
+
`);
|
|
15078
|
+
if (flags.org) {
|
|
15079
|
+
process.stdout.write(`Active org: ${flags.org} \u2014 use --org ${flags.org} or: pylot config set org ${flags.org}
|
|
15080
|
+
`);
|
|
15081
|
+
}
|
|
15082
|
+
}
|
|
15083
|
+
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);
|
|
15084
|
+
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
15085
|
}
|
|
14797
15086
|
|
|
14798
15087
|
// src/commands/config-cmd.mts
|
|
14799
|
-
var KEYS = ["url", "token", "staging-url", "staging-token"];
|
|
15088
|
+
var KEYS = ["url", "token", "org", "staging-url", "staging-token"];
|
|
14800
15089
|
function redact(key, value) {
|
|
14801
15090
|
if (!key.includes("token") || typeof value !== "string" || value.length === 0) return value;
|
|
14802
15091
|
return value.length > 8 ? `${value.slice(0, 4)}\u2026${value.slice(-4)}` : "****";
|
|
@@ -14843,6 +15132,7 @@ function registerConfig(program3) {
|
|
|
14843
15132
|
}
|
|
14844
15133
|
|
|
14845
15134
|
// src/commands/observability.mts
|
|
15135
|
+
init_http();
|
|
14846
15136
|
function registerObservability(program3) {
|
|
14847
15137
|
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
15138
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -14943,6 +15233,7 @@ function registerObservability(program3) {
|
|
|
14943
15233
|
}
|
|
14944
15234
|
|
|
14945
15235
|
// src/commands/workers.mts
|
|
15236
|
+
init_http();
|
|
14946
15237
|
function scopedBase(flags) {
|
|
14947
15238
|
if (Boolean(flags.mission) === Boolean(flags.conversation)) {
|
|
14948
15239
|
throw new UsageError("pass exactly one of --mission <id> / --conversation <id>");
|
|
@@ -14995,6 +15286,7 @@ function registerWorkers(program3) {
|
|
|
14995
15286
|
}
|
|
14996
15287
|
|
|
14997
15288
|
// src/commands/devboxes.mts
|
|
15289
|
+
init_http();
|
|
14998
15290
|
function registerDevboxes(program3) {
|
|
14999
15291
|
const devboxes = program3.command("devboxes").description("devbox environments (Fargate dev containers)");
|
|
15000
15292
|
devboxes.command("projects").description("projects with devbox configs (GET /devboxes/projects)").action(async (_flags, cmd) => {
|
|
@@ -15005,9 +15297,16 @@ function registerDevboxes(program3) {
|
|
|
15005
15297
|
const { opts, cfg } = await ctx(cmd);
|
|
15006
15298
|
emit(opts, await request(cfg, `/devboxes/projects/${encodeURIComponent(project)}`));
|
|
15007
15299
|
});
|
|
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) => {
|
|
15300
|
+
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
15301
|
const { opts, cfg } = await ctx(cmd);
|
|
15010
15302
|
const body = fields.length || flags.file ? await bodyFromArgs(fields, flags.file) : {};
|
|
15303
|
+
if (flags.idleTtl != null) {
|
|
15304
|
+
const secs = Number(flags.idleTtl);
|
|
15305
|
+
if (!Number.isInteger(secs) || secs < 0 || secs > 604800) {
|
|
15306
|
+
throw new UsageError("--idle-ttl must be an integer between 0 and 604800 (0 = never idle-reap)");
|
|
15307
|
+
}
|
|
15308
|
+
body.idle_ttl_secs = secs;
|
|
15309
|
+
}
|
|
15011
15310
|
emit(opts, await request(cfg, `/devboxes/projects/${encodeURIComponent(project)}`, { method: "POST", body }));
|
|
15012
15311
|
});
|
|
15013
15312
|
devboxes.command("list").description("running devbox environments for a project (GET /devboxes/projects/:project/list)").argument("<org/repo>").action(async (project, _flags, cmd) => {
|
|
@@ -15034,19 +15333,22 @@ function registerDevboxes(program3) {
|
|
|
15034
15333
|
}
|
|
15035
15334
|
|
|
15036
15335
|
// src/commands/conversations.mts
|
|
15336
|
+
init_http();
|
|
15037
15337
|
function convPath(id, sub = "") {
|
|
15038
15338
|
return `/conversations/${encodeURIComponent(id)}${sub}`;
|
|
15039
15339
|
}
|
|
15040
15340
|
function registerConversations(program3) {
|
|
15041
15341
|
const conversations = program3.command("conversations").alias("convo").description("chat conversations: lifecycle, messages, wakes, resources");
|
|
15042
|
-
conversations.command("create").description("create a conversation (POST /conversations)").
|
|
15342
|
+
conversations.command("create").description("create a conversation (POST /conversations)").option("--org <org>", "owning org (required; or use global --org before the subcommand)").option("--title <title>", "conversation title").option("--team <team>", "team name (must belong to --org)").option("--repos <a,b>", "comma-separated repo list").option("--branch <branch>", "working branch").option("--user-handle <handle>", "user handle").action(async (flags, cmd) => {
|
|
15043
15343
|
const { opts, cfg } = await ctx(cmd);
|
|
15344
|
+
const org = cmd.optsWithGlobals().org;
|
|
15345
|
+
if (!org) throw new UsageError("--org <org> is required");
|
|
15044
15346
|
emit(
|
|
15045
15347
|
opts,
|
|
15046
15348
|
await request(cfg, "/conversations", {
|
|
15047
15349
|
method: "POST",
|
|
15048
15350
|
body: {
|
|
15049
|
-
org
|
|
15351
|
+
org,
|
|
15050
15352
|
title: flags.title,
|
|
15051
15353
|
team: flags.team,
|
|
15052
15354
|
repos: flags.repos ? String(flags.repos).split(",").map((r) => r.trim()).filter(Boolean) : void 0,
|
|
@@ -15178,9 +15480,10 @@ function registerConversations(program3) {
|
|
|
15178
15480
|
}
|
|
15179
15481
|
|
|
15180
15482
|
// src/commands/teams.mts
|
|
15483
|
+
init_http();
|
|
15181
15484
|
async function readText(file) {
|
|
15182
|
-
const { readFileSync } = await import("node:fs");
|
|
15183
|
-
return file === "-" ?
|
|
15485
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
15486
|
+
return file === "-" ? readFileSync2(0, "utf8") : readFileSync2(file, "utf8");
|
|
15184
15487
|
}
|
|
15185
15488
|
function registerTeams(program3) {
|
|
15186
15489
|
const teams = program3.command("teams").description("team config, operators, playbooks, goals, ledger");
|
|
@@ -15309,6 +15612,7 @@ function registerTeams(program3) {
|
|
|
15309
15612
|
}
|
|
15310
15613
|
|
|
15311
15614
|
// src/commands/admin.mts
|
|
15615
|
+
init_http();
|
|
15312
15616
|
function registerAdmin(program3) {
|
|
15313
15617
|
const admin = program3.command("admin").description("system config, audit, org caps, DB migrations");
|
|
15314
15618
|
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 +15654,7 @@ function registerAdmin(program3) {
|
|
|
15350
15654
|
}
|
|
15351
15655
|
|
|
15352
15656
|
// src/commands/deploy.mts
|
|
15657
|
+
init_http();
|
|
15353
15658
|
var TERMINAL_BUILD_STATUSES = /* @__PURE__ */ new Set(["SUCCEEDED", "FAILED", "FAULT", "STOPPED", "TIMED_OUT"]);
|
|
15354
15659
|
function printNewPhases(opts, body, seen) {
|
|
15355
15660
|
for (const p of body?.phases ?? []) {
|
|
@@ -15404,7 +15709,7 @@ function withParentBuildOpts(flags, cmd) {
|
|
|
15404
15709
|
function registerDeploy(program3) {
|
|
15405
15710
|
const deploy = program3.command("deploy").description(
|
|
15406
15711
|
"trigger a cdk deploy via CodeBuild (POST /admin/deploy) \u2014 a 409 means a deploy is already running (deploy lock held: {stage, held_by, since, build_id}); wait for it or poll `pylot deploy status`"
|
|
15407
|
-
).option("--source-version <ref>", "git ref to deploy (default: gateway's stage branch)").option("--wait", "poll the build to a terminal state (exit 0 only on SUCCEEDED)").option("--timeout <seconds>", "max seconds to wait (default 1500 = 25min)", (v) => parseInt(v, 10), 1500).action(async (flags, cmd) => {
|
|
15712
|
+
).allowExcessArguments(false).showHelpAfterError("(run 'pylot deploy --help' to list valid subcommands)").option("--source-version <ref>", "git ref to deploy (default: gateway's stage branch)").option("--wait", "poll the build to a terminal state (exit 0 only on SUCCEEDED)").option("--timeout <seconds>", "max seconds to wait (default 1500 = 25min)", (v) => parseInt(v, 10), 1500).action(async (flags, cmd) => {
|
|
15408
15713
|
const { opts, cfg } = await ctx(cmd);
|
|
15409
15714
|
const body = {};
|
|
15410
15715
|
if (flags.sourceVersion) body.source_version = flags.sourceVersion;
|
|
@@ -15518,6 +15823,7 @@ function registerDeploy(program3) {
|
|
|
15518
15823
|
}
|
|
15519
15824
|
|
|
15520
15825
|
// src/commands/secrets.mts
|
|
15826
|
+
init_http();
|
|
15521
15827
|
function registerSecrets(program3) {
|
|
15522
15828
|
const secrets = program3.command("secrets").description("ASM secret bundles (path-based, admin-scoped)");
|
|
15523
15829
|
secrets.command("tree").description("full secrets tree (GET /admin/secrets)").action(async (_flags, cmd) => {
|
|
@@ -15553,8 +15859,8 @@ function registerSecrets(program3) {
|
|
|
15553
15859
|
}
|
|
15554
15860
|
let value;
|
|
15555
15861
|
if (flags.stdin) {
|
|
15556
|
-
const { readFileSync } = await import("node:fs");
|
|
15557
|
-
value =
|
|
15862
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
15863
|
+
value = readFileSync2(0, "utf8").replace(/\r?\n$/, "");
|
|
15558
15864
|
} else {
|
|
15559
15865
|
value = String(flags.value);
|
|
15560
15866
|
}
|
|
@@ -15585,6 +15891,7 @@ function registerSecrets(program3) {
|
|
|
15585
15891
|
}
|
|
15586
15892
|
|
|
15587
15893
|
// src/commands/skills.mts
|
|
15894
|
+
init_http();
|
|
15588
15895
|
function registerSkills(program3) {
|
|
15589
15896
|
const skills = program3.command("skills").description("skills catalog, sync pipeline, procedures");
|
|
15590
15897
|
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) => {
|
|
@@ -15600,21 +15907,24 @@ function registerSkills(program3) {
|
|
|
15600
15907
|
const { opts, cfg } = await ctx(cmd);
|
|
15601
15908
|
emit(opts, await request(cfg, "/admin/skills/lock"));
|
|
15602
15909
|
});
|
|
15603
|
-
skills.command("staleness").description("SHA-mismatch report vs origin HEAD (GET /admin/skills/staleness?org=<org>)").
|
|
15910
|
+
skills.command("staleness").description("SHA-mismatch report vs origin HEAD (GET /admin/skills/staleness?org=<org>)").option("--org <org>", "GitHub org to check (staleness is per-org, #1919; or use global --org before the subcommand)").action(async (flags, cmd) => {
|
|
15604
15911
|
const { opts, cfg } = await ctx(cmd);
|
|
15605
|
-
|
|
15912
|
+
const org = cmd.optsWithGlobals().org;
|
|
15913
|
+
if (!org) throw new UsageError("--org <org> is required");
|
|
15914
|
+
emit(opts, await request(cfg, "/admin/skills/staleness", { query: { org } }));
|
|
15606
15915
|
});
|
|
15607
15916
|
skills.command("sync").description(
|
|
15608
15917
|
"trigger the CodeBuild skills-sync job for one org (POST /admin/skills/sync, body {org}) or every installed org (--all \u2192 POST /admin/skills/sync-all)"
|
|
15609
15918
|
).option("--all", "fan out one sync per installed org").option("--org <org>", "GitHub org to sync (required unless --all)").action(async (flags, cmd) => {
|
|
15610
15919
|
const { opts, cfg } = await ctx(cmd);
|
|
15920
|
+
const org = cmd.optsWithGlobals().org;
|
|
15611
15921
|
if (flags.all) {
|
|
15612
|
-
if (
|
|
15922
|
+
if (org) throw new UsageError("pass either --all or --org <org>, not both");
|
|
15613
15923
|
emit(opts, await request(cfg, "/admin/skills/sync-all", { method: "POST", body: {} }));
|
|
15614
15924
|
return;
|
|
15615
15925
|
}
|
|
15616
|
-
if (!
|
|
15617
|
-
emit(opts, await request(cfg, "/admin/skills/sync", { method: "POST", body: { org
|
|
15926
|
+
if (!org) throw new UsageError("--org <org> is required (or --all to sync every installed org)");
|
|
15927
|
+
emit(opts, await request(cfg, "/admin/skills/sync", { method: "POST", body: { org } }));
|
|
15618
15928
|
});
|
|
15619
15929
|
skills.command("sync-status").description("skills-sync build status + logs (GET /admin/skills/sync/:id)").argument("[build_id]", "CodeBuild build id (from `skills sync`)").action(async (buildId, _flags, cmd) => {
|
|
15620
15930
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -15626,26 +15936,30 @@ function registerSkills(program3) {
|
|
|
15626
15936
|
const body = await bodyFromArgs(fields, flags.file);
|
|
15627
15937
|
emit(opts, await request(cfg, "/admin/skills/reconcile-org", { method: "POST", body }));
|
|
15628
15938
|
});
|
|
15629
|
-
skills.command("update").description("update catalog metadata (PATCH /admin/skills/:name?org=<org> \u2014 tags, origin_repo, version, commit_sha, needs_worker)").argument("<name>").argument("[fields...]", "key=value pairs").
|
|
15939
|
+
skills.command("update").description("update catalog metadata (PATCH /admin/skills/:name?org=<org> \u2014 tags, origin_repo, version, commit_sha, needs_worker)").argument("<name>").argument("[fields...]", "key=value pairs").option("--org <org>", "owning org (catalog rows are keyed (org, name)); or use global --org before the subcommand)").option("--file <path>", "JSON body from file ('-' for stdin)").action(async (name, fields, flags, cmd) => {
|
|
15630
15940
|
const { opts, cfg } = await ctx(cmd);
|
|
15941
|
+
const org = cmd.optsWithGlobals().org;
|
|
15942
|
+
if (!org) throw new UsageError("--org <org> is required");
|
|
15631
15943
|
const body = await bodyFromArgs(fields, flags.file);
|
|
15632
15944
|
emit(
|
|
15633
15945
|
opts,
|
|
15634
15946
|
await request(cfg, `/admin/skills/${encodeURIComponent(name)}`, {
|
|
15635
15947
|
method: "PATCH",
|
|
15636
|
-
query: { org
|
|
15948
|
+
query: { org },
|
|
15637
15949
|
body
|
|
15638
15950
|
})
|
|
15639
15951
|
);
|
|
15640
15952
|
});
|
|
15641
|
-
skills.command("delete").description("remove a skill from the catalog (DELETE /admin/skills/:name?org=<org>)").argument("<name>").
|
|
15953
|
+
skills.command("delete").description("remove a skill from the catalog (DELETE /admin/skills/:name?org=<org>)").argument("<name>").option("--org <org>", "owning org (catalog rows are keyed (org, name)); or use global --org before the subcommand)").option("--force", "required to confirm deletion").action(async (name, flags, cmd) => {
|
|
15642
15954
|
const { opts, cfg } = await ctx(cmd);
|
|
15643
15955
|
if (!flags.force) throw new UsageError("deleting a catalog skill is destructive \u2014 re-run with --force");
|
|
15956
|
+
const org = cmd.optsWithGlobals().org;
|
|
15957
|
+
if (!org) throw new UsageError("--org <org> is required");
|
|
15644
15958
|
emit(
|
|
15645
15959
|
opts,
|
|
15646
15960
|
await request(cfg, `/admin/skills/${encodeURIComponent(name)}`, {
|
|
15647
15961
|
method: "DELETE",
|
|
15648
|
-
query: { org
|
|
15962
|
+
query: { org }
|
|
15649
15963
|
})
|
|
15650
15964
|
);
|
|
15651
15965
|
});
|
|
@@ -15656,6 +15970,7 @@ function registerSkills(program3) {
|
|
|
15656
15970
|
}
|
|
15657
15971
|
|
|
15658
15972
|
// src/commands/automations.mts
|
|
15973
|
+
init_http();
|
|
15659
15974
|
function registerAutomations(program3) {
|
|
15660
15975
|
const automations = program3.command("automations").description("event-driven automation rules");
|
|
15661
15976
|
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,11 +16017,14 @@ function registerAutomations(program3) {
|
|
|
15702
16017
|
}
|
|
15703
16018
|
|
|
15704
16019
|
// src/commands/providers.mts
|
|
16020
|
+
init_http();
|
|
15705
16021
|
function registerProviders(program3) {
|
|
15706
16022
|
const providers = program3.command("providers").description("LLM provider instances and org purpose-chains");
|
|
15707
|
-
providers.command("list").description("provider instances for an org (GET /admin/providers?org=)").
|
|
16023
|
+
providers.command("list").description("provider instances for an org (GET /admin/providers?org=)").option("--org <org>", "org to list (required by the gateway; or use global --org before the subcommand)").action(async (flags, cmd) => {
|
|
15708
16024
|
const { opts, cfg } = await ctx(cmd);
|
|
15709
|
-
|
|
16025
|
+
const org = cmd.optsWithGlobals().org;
|
|
16026
|
+
if (!org) throw new UsageError("--org <org> is required");
|
|
16027
|
+
emit(opts, await request(cfg, "/admin/providers", { query: { org } }));
|
|
15710
16028
|
});
|
|
15711
16029
|
providers.command("create").description("create a provider instance (POST /admin/providers, body {org, type, model, label?, credentials?})").argument("[fields...]", "key=value pairs (e.g. org=my-org type=anthropic-api model=opus)").option("--file <path>", "JSON body from file ('-' for stdin)").action(async (fields, flags, cmd) => {
|
|
15712
16030
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -15758,6 +16076,7 @@ function registerProviders(program3) {
|
|
|
15758
16076
|
}
|
|
15759
16077
|
|
|
15760
16078
|
// src/commands/orgs.mts
|
|
16079
|
+
init_http();
|
|
15761
16080
|
function registerOrgs(program3) {
|
|
15762
16081
|
const orgs = program3.command("orgs").description("GitHub orgs and repo onboarding");
|
|
15763
16082
|
orgs.command("list").description("orgs visible to this token (GET /orgs)").action(async (_flags, cmd) => {
|
|
@@ -15774,6 +16093,7 @@ function registerOrgs(program3) {
|
|
|
15774
16093
|
}
|
|
15775
16094
|
|
|
15776
16095
|
// src/commands/assets.mts
|
|
16096
|
+
init_http();
|
|
15777
16097
|
function registerAssets(program3) {
|
|
15778
16098
|
const assets = program3.command("assets").description("S3 asset presign, view, publish");
|
|
15779
16099
|
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,12 +16131,12 @@ function registerAssets(program3) {
|
|
|
15811
16131
|
}
|
|
15812
16132
|
|
|
15813
16133
|
// src/index.mts
|
|
15814
|
-
var VERSION = true ? "0.1.
|
|
16134
|
+
var VERSION = true ? "0.1.5" : "0.0.0-dev";
|
|
15815
16135
|
installEpipeGuard();
|
|
15816
16136
|
var program2 = new Command();
|
|
15817
16137
|
program2.name("pylot").description(
|
|
15818
16138
|
"Pylot gateway CLI \u2014 dispatch missions, tail logs, manage teams/admin/secrets without curl.\nJSON on non-TTY stdout (pipe-friendly for agents and jq); tables on TTY."
|
|
15819
|
-
).allowExcessArguments(false).configureHelp({ sortSubcommands: true }).version(VERSION).option("--url <url>", "gateway base URL (default: PYLOT_DISPATCH_URL / PYLOT_API_URL / PYLOT_GATEWAY_URL)").option("--token <token>", "bearer token (default: PYLOT_API_TOKEN / PYLOT_SESSION_JWT / PYLOT_BROKER_TOKEN / PYLOT_DISPATCH_TOKEN)").option("--env <env>", "target environment: prod | staging (staging uses PYLOT_STAGING_URL + PYLOT_STAGING_DISPATCH_TOKEN)").option("--json", "force JSON output even on a TTY").option("-q, --quiet", "suppress progress notes on stderr");
|
|
16139
|
+
).allowExcessArguments(false).configureHelp({ sortSubcommands: true }).version(VERSION).option("--url <url>", "gateway base URL (default: PYLOT_DISPATCH_URL / PYLOT_API_URL / PYLOT_GATEWAY_URL)").option("--token <token>", "bearer token (default: PYLOT_API_TOKEN / PYLOT_SESSION_JWT / PYLOT_BROKER_TOKEN / PYLOT_DISPATCH_TOKEN)").option("--org <org>", "default org for credential resolution (selects ~/.pylot/credentials entry; overrides pylot config set org)").option("--env <env>", "target environment: prod | staging (staging uses PYLOT_STAGING_URL + PYLOT_STAGING_DISPATCH_TOKEN)").option("--json", "force JSON output even on a TTY").option("-q, --quiet", "suppress progress notes on stderr");
|
|
15820
16140
|
program2.exitOverride();
|
|
15821
16141
|
program2.configureOutput({ outputError: (str, write) => write(str) });
|
|
15822
16142
|
registerCore(program2);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pylot-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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
|
},
|