pylot-cli 0.1.2 → 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 +454 -135
- 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);
|
|
@@ -14921,7 +15204,7 @@ function registerObservability(program3) {
|
|
|
14921
15204
|
const { opts, cfg } = await ctx(cmd);
|
|
14922
15205
|
emit(opts, await request(cfg, "/logs/router", { query: { lines: flags.lines } }));
|
|
14923
15206
|
});
|
|
14924
|
-
logs.command("admin").description("gateway Lambda CloudWatch logs by source (GET /admin/logs/:source)").argument("<source>", "api | ingress | processor | executor | notifier | scheduler | chat").option("--minutes <n>", "lookback minutes (max 180)", (v) => parseInt(v, 10)).option("--grep <pattern>", "filter pattern").option("--limit <n>", "max entries (max 1000)", (v) => parseInt(v, 10)).action(async (source, flags, cmd) => {
|
|
15207
|
+
logs.command("admin").description("gateway Lambda CloudWatch logs by source (GET /admin/logs/:source)").argument("<source>", "api | ingress | processor | executor | notifier | scheduler | chat | proxy | slack-events | slack-processor").option("--minutes <n>", "lookback minutes (max 180)", (v) => parseInt(v, 10)).option("--grep <pattern>", "filter pattern").option("--limit <n>", "max entries (max 1000)", (v) => parseInt(v, 10)).action(async (source, flags, cmd) => {
|
|
14925
15208
|
const { opts, cfg } = await ctx(cmd);
|
|
14926
15209
|
emit(
|
|
14927
15210
|
opts,
|
|
@@ -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 ?? []) {
|
|
@@ -15358,7 +15654,7 @@ function printNewPhases(opts, body, seen) {
|
|
|
15358
15654
|
note(opts, `phase ${p.phase}${p.status ? ` ${p.status}` : ""}`);
|
|
15359
15655
|
}
|
|
15360
15656
|
}
|
|
15361
|
-
async function waitForBuild(opts, cfg, buildId, { timeoutMs
|
|
15657
|
+
async function waitForBuild(opts, cfg, buildId, { timeoutMs }) {
|
|
15362
15658
|
const seenPhases = /* @__PURE__ */ new Set();
|
|
15363
15659
|
const resp = await pollUntil(
|
|
15364
15660
|
() => request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}`),
|
|
@@ -15372,13 +15668,21 @@ async function waitForBuild(opts, cfg, buildId, { timeoutMs, promoteKey }) {
|
|
|
15372
15668
|
printNewPhases(opts, resp.json, seenPhases);
|
|
15373
15669
|
const body = resp.json;
|
|
15374
15670
|
const status = String(body?.status ?? "");
|
|
15375
|
-
|
|
15376
|
-
if (
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
if (!ok) note(opts, `${buildId} SUCCEEDED but ${promoteKey} reported failures \u2014 NOT promoted`);
|
|
15671
|
+
note(opts, `${buildId} ${status}${status === "SUCCEEDED" ? "" : " (failed)"}`);
|
|
15672
|
+
if (status !== "SUCCEEDED") {
|
|
15673
|
+
emit(opts, resp);
|
|
15674
|
+
process.exit(1);
|
|
15380
15675
|
}
|
|
15381
|
-
|
|
15676
|
+
return resp;
|
|
15677
|
+
}
|
|
15678
|
+
async function promoteBuild(opts, cfg, buildId, { promoteKey, smoke = false }) {
|
|
15679
|
+
const resp = await request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}/promote`, {
|
|
15680
|
+
method: "POST",
|
|
15681
|
+
body: promoteKey === "worker_promote" ? { smoke } : {}
|
|
15682
|
+
});
|
|
15683
|
+
const promote = resp.json?.[promoteKey];
|
|
15684
|
+
const ok = resp.status >= 200 && resp.status < 300 && promote != null && (promote.failed ?? []).length === 0;
|
|
15685
|
+
if (!ok) note(opts, `${buildId} ${promoteKey} failed`);
|
|
15382
15686
|
emit(opts, resp);
|
|
15383
15687
|
process.exit(ok ? 0 : 1);
|
|
15384
15688
|
}
|
|
@@ -15417,6 +15721,13 @@ function registerDeploy(program3) {
|
|
|
15417
15721
|
const { opts, cfg } = await ctx(cmd);
|
|
15418
15722
|
emit(opts, await request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}`));
|
|
15419
15723
|
});
|
|
15724
|
+
deploy.command("promote").description("promote a successful operator/worker image build (POST /admin/build-worker/:build_id/promote)").argument("<build_id>").option("--smoke", "for worker builds, run the live Fargate smoke gate before pinning").action(async (buildId, flags, cmd) => {
|
|
15725
|
+
const { opts, cfg } = await ctx(cmd);
|
|
15726
|
+
emit(opts, await request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}/promote`, {
|
|
15727
|
+
method: "POST",
|
|
15728
|
+
body: flags.smoke ? { smoke: true } : {}
|
|
15729
|
+
}));
|
|
15730
|
+
});
|
|
15420
15731
|
deploy.command("logs").description("CodeBuild log tail via CloudWatch (GET /admin/deploy/logs/:build_id)").argument("<build_id>").option("--stage <stage>", "staging | production (default staging)").action(async (buildId, flags, cmd) => {
|
|
15421
15732
|
const { opts, cfg } = await ctx(cmd);
|
|
15422
15733
|
emit(
|
|
@@ -15427,8 +15738,8 @@ function registerDeploy(program3) {
|
|
|
15427
15738
|
);
|
|
15428
15739
|
});
|
|
15429
15740
|
deploy.command("build-operator").description(
|
|
15430
|
-
|
|
15431
|
-
).option("--source-version <ref>", "git ref to build (default: gateway's stage branch)").option("--wait", "poll until
|
|
15741
|
+
"rebuild the operator image (POST /admin/build-operator). With --wait: poll to SUCCEEDED, then explicitly promote"
|
|
15742
|
+
).option("--source-version <ref>", "git ref to build (default: gateway's stage branch)").option("--wait", "poll until SUCCEEDED, then promote (exit 0 only on clean operator_promote)").option("--timeout <seconds>", "max seconds to wait (default 1500)", (v) => parseInt(v, 10), 1500).action(async (flags, cmd) => {
|
|
15432
15743
|
flags = withParentBuildOpts(flags, cmd);
|
|
15433
15744
|
const { opts, cfg } = await ctx(cmd);
|
|
15434
15745
|
const body = {};
|
|
@@ -15444,7 +15755,8 @@ function registerDeploy(program3) {
|
|
|
15444
15755
|
process.exit(1);
|
|
15445
15756
|
}
|
|
15446
15757
|
note(opts, `operator build started: ${buildId}`);
|
|
15447
|
-
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3
|
|
15758
|
+
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15759
|
+
await promoteBuild(opts, cfg, buildId, { promoteKey: "operator_promote" });
|
|
15448
15760
|
});
|
|
15449
15761
|
deploy.command("publish-cli").description(
|
|
15450
15762
|
"publish modules/cli to npm as pylot-cli (POST /admin/publish-cli). Idempotent: no-ops when the tree's version is already on npm. Staging builders always dry-run"
|
|
@@ -15468,8 +15780,8 @@ function registerDeploy(program3) {
|
|
|
15468
15780
|
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15469
15781
|
});
|
|
15470
15782
|
deploy.command("build-worker").description(
|
|
15471
|
-
"build a repo's worker image (POST /admin/build-worker, body {repo}); --batch --file posts {repos: [...]} to /admin/build-worker/batch. With --wait:
|
|
15472
|
-
).argument("[org/repo]", "repo to build (omit with --batch)").option("--source-version <ref>", "git ref to build the worker overlay from").option("--batch", "batch mode (POST /admin/build-worker/batch)").option("--file <path>", "JSON body from file ('-' for stdin; required with --batch)").option("--wait", "poll until
|
|
15783
|
+
"build a repo's worker image (POST /admin/build-worker, body {repo}); --batch --file posts {repos: [...]} to /admin/build-worker/batch. With --wait: poll to SUCCEEDED, then explicitly promote"
|
|
15784
|
+
).argument("[org/repo]", "repo to build (omit with --batch)").option("--source-version <ref>", "git ref to build the worker overlay from").option("--batch", "batch mode (POST /admin/build-worker/batch)").option("--file <path>", "JSON body from file ('-' for stdin; required with --batch)").option("--wait", "poll until SUCCEEDED, then promote (exit 0 only on clean worker_promote)").option("--smoke", "with --wait, run the live Fargate smoke gate before pinning").option("--timeout <seconds>", "max seconds to wait (default 1500)", (v) => parseInt(v, 10), 1500).action(async (repo, flags, cmd) => {
|
|
15473
15785
|
flags = withParentBuildOpts(flags, cmd);
|
|
15474
15786
|
const { opts, cfg } = await ctx(cmd);
|
|
15475
15787
|
if (flags.batch) {
|
|
@@ -15492,7 +15804,8 @@ function registerDeploy(program3) {
|
|
|
15492
15804
|
process.exit(1);
|
|
15493
15805
|
}
|
|
15494
15806
|
note(opts, `worker build started: ${buildId}`);
|
|
15495
|
-
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3
|
|
15807
|
+
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15808
|
+
await promoteBuild(opts, cfg, buildId, { promoteKey: "worker_promote", smoke: flags.smoke === true });
|
|
15496
15809
|
});
|
|
15497
15810
|
deploy.command("ensure-taskdefs").description("backfill per-role operator task definitions (POST /admin/operators/ensure-taskdefs)").action(async (_flags, cmd) => {
|
|
15498
15811
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -15501,6 +15814,7 @@ function registerDeploy(program3) {
|
|
|
15501
15814
|
}
|
|
15502
15815
|
|
|
15503
15816
|
// src/commands/secrets.mts
|
|
15817
|
+
init_http();
|
|
15504
15818
|
function registerSecrets(program3) {
|
|
15505
15819
|
const secrets = program3.command("secrets").description("ASM secret bundles (path-based, admin-scoped)");
|
|
15506
15820
|
secrets.command("tree").description("full secrets tree (GET /admin/secrets)").action(async (_flags, cmd) => {
|
|
@@ -15536,8 +15850,8 @@ function registerSecrets(program3) {
|
|
|
15536
15850
|
}
|
|
15537
15851
|
let value;
|
|
15538
15852
|
if (flags.stdin) {
|
|
15539
|
-
const { readFileSync } = await import("node:fs");
|
|
15540
|
-
value =
|
|
15853
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
15854
|
+
value = readFileSync2(0, "utf8").replace(/\r?\n$/, "");
|
|
15541
15855
|
} else {
|
|
15542
15856
|
value = String(flags.value);
|
|
15543
15857
|
}
|
|
@@ -15568,6 +15882,7 @@ function registerSecrets(program3) {
|
|
|
15568
15882
|
}
|
|
15569
15883
|
|
|
15570
15884
|
// src/commands/skills.mts
|
|
15885
|
+
init_http();
|
|
15571
15886
|
function registerSkills(program3) {
|
|
15572
15887
|
const skills = program3.command("skills").description("skills catalog, sync pipeline, procedures");
|
|
15573
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) => {
|
|
@@ -15583,9 +15898,9 @@ function registerSkills(program3) {
|
|
|
15583
15898
|
const { opts, cfg } = await ctx(cmd);
|
|
15584
15899
|
emit(opts, await request(cfg, "/admin/skills/lock"));
|
|
15585
15900
|
});
|
|
15586
|
-
skills.command("staleness").description("SHA-mismatch report vs origin HEAD (GET /admin/skills/staleness)").action(async (
|
|
15901
|
+
skills.command("staleness").description("SHA-mismatch report vs origin HEAD (GET /admin/skills/staleness?org=<org>)").requiredOption("--org <org>", "GitHub org to check (staleness is per-org, #1919)").action(async (flags, cmd) => {
|
|
15587
15902
|
const { opts, cfg } = await ctx(cmd);
|
|
15588
|
-
emit(opts, await request(cfg, "/admin/skills/staleness"));
|
|
15903
|
+
emit(opts, await request(cfg, "/admin/skills/staleness", { query: { org: flags.org } }));
|
|
15589
15904
|
});
|
|
15590
15905
|
skills.command("sync").description(
|
|
15591
15906
|
"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)"
|
|
@@ -15639,6 +15954,7 @@ function registerSkills(program3) {
|
|
|
15639
15954
|
}
|
|
15640
15955
|
|
|
15641
15956
|
// src/commands/automations.mts
|
|
15957
|
+
init_http();
|
|
15642
15958
|
function registerAutomations(program3) {
|
|
15643
15959
|
const automations = program3.command("automations").description("event-driven automation rules");
|
|
15644
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) => {
|
|
@@ -15685,6 +16001,7 @@ function registerAutomations(program3) {
|
|
|
15685
16001
|
}
|
|
15686
16002
|
|
|
15687
16003
|
// src/commands/providers.mts
|
|
16004
|
+
init_http();
|
|
15688
16005
|
function registerProviders(program3) {
|
|
15689
16006
|
const providers = program3.command("providers").description("LLM provider instances and org purpose-chains");
|
|
15690
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) => {
|
|
@@ -15741,6 +16058,7 @@ function registerProviders(program3) {
|
|
|
15741
16058
|
}
|
|
15742
16059
|
|
|
15743
16060
|
// src/commands/orgs.mts
|
|
16061
|
+
init_http();
|
|
15744
16062
|
function registerOrgs(program3) {
|
|
15745
16063
|
const orgs = program3.command("orgs").description("GitHub orgs and repo onboarding");
|
|
15746
16064
|
orgs.command("list").description("orgs visible to this token (GET /orgs)").action(async (_flags, cmd) => {
|
|
@@ -15757,6 +16075,7 @@ function registerOrgs(program3) {
|
|
|
15757
16075
|
}
|
|
15758
16076
|
|
|
15759
16077
|
// src/commands/assets.mts
|
|
16078
|
+
init_http();
|
|
15760
16079
|
function registerAssets(program3) {
|
|
15761
16080
|
const assets = program3.command("assets").description("S3 asset presign, view, publish");
|
|
15762
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) => {
|
|
@@ -15794,7 +16113,7 @@ function registerAssets(program3) {
|
|
|
15794
16113
|
}
|
|
15795
16114
|
|
|
15796
16115
|
// src/index.mts
|
|
15797
|
-
var VERSION = true ? "0.1.
|
|
16116
|
+
var VERSION = true ? "0.1.4" : "0.0.0-dev";
|
|
15798
16117
|
installEpipeGuard();
|
|
15799
16118
|
var program2 = new Command();
|
|
15800
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
|
},
|