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.
Files changed (2) hide show
  1. package/dist/index.mjs +454 -135
  2. 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 http = (
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: http.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/http.mts
14124
- var ApiError = class extends Error {
14125
- status;
14126
- body;
14127
- requiredScope;
14128
- constructor(message, status, body) {
14129
- super(message);
14130
- this.status = status;
14131
- this.body = body;
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
- async function request(cfg, path5, opts = {}) {
14168
- const method = opts.method ?? "GET";
14169
- const retry = opts.retry ?? method === "GET";
14170
- const timeoutMs = opts.timeoutMs ?? 3e4;
14171
- const url = new URL(cfg.baseUrl + path5);
14172
- for (const [k, v] of Object.entries(opts.query ?? {})) {
14173
- if (v !== void 0) url.searchParams.set(k, String(v));
14174
- }
14175
- const headers = {};
14176
- if (cfg.token) headers.authorization = `Bearer ${cfg.token}`;
14177
- let bodyText;
14178
- if (opts.rawBody !== void 0) {
14179
- bodyText = opts.rawBody;
14180
- headers["content-type"] = "text/plain; charset=utf-8";
14181
- } else if (opts.body !== void 0) {
14182
- bodyText = JSON.stringify(opts.body);
14183
- headers["content-type"] = "application/json";
14184
- }
14185
- const maxAttempts = retry ? RETRY_DELAYS_MS.length + 1 : 1;
14186
- let lastError;
14187
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
14188
- if (attempt > 0) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt - 1]));
14189
- let res;
14190
- try {
14191
- res = await fetch(url, {
14192
- method,
14193
- headers,
14194
- body: bodyText,
14195
- signal: AbortSignal.timeout(timeoutMs)
14196
- });
14197
- } catch (err) {
14198
- lastError = err;
14199
- continue;
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
- return { status: res.status, json, text, headers: res.headers };
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
- // src/lib/config.mts
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 === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
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
- program3.command("login", { hidden: true }).description("OAuth login (not available yet \u2014 token-plane M5)").action(() => {
14791
- process.stderr.write(
14792
- "error: OAuth login is not available yet (token-plane M5, fellowship-dev/pylot#1889).\nGet a token instead:\n - inside a Pylot container the right token is already in the environment\n - an admin can mint one: pylot auth tokens create --org <org> --scopes dispatch,missions:read\n - then: export PYLOT_API_TOKEN=<token> (or: pylot config set token <token>)\n"
14793
- );
14794
- process.exit(1);
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 === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
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, promoteKey }) {
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
- let ok = status === "SUCCEEDED";
15376
- if (ok && promoteKey) {
15377
- const promote = body?.[promoteKey];
15378
- ok = promote != null && (promote.failed ?? []).length === 0;
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
- note(opts, `${buildId} ${status}${ok ? "" : " (failed)"}`);
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
- 'rebuild the operator image (POST /admin/build-operator). With --wait: SUCCEEDED alone is not "promoted" \u2014 completion requires operator_promote with no failures'
15431
- ).option("--source-version <ref>", "git ref to build (default: gateway's stage branch)").option("--wait", "poll until promoted (exit 0 only on SUCCEEDED + clean operator_promote)").option("--timeout <seconds>", "max seconds to wait (default 1500)", (v) => parseInt(v, 10), 1500).action(async (flags, cmd) => {
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, promoteKey: "operator_promote" });
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: completion requires worker_promote with no failures"
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 promoted (exit 0 only on SUCCEEDED + clean worker_promote)").option("--timeout <seconds>", "max seconds to wait (default 1500)", (v) => parseInt(v, 10), 1500).action(async (repo, flags, cmd) => {
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, promoteKey: "worker_promote" });
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 = readFileSync(0, "utf8").replace(/\r?\n$/, "");
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 (_flags, cmd) => {
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.2" : "0.0.0-dev";
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.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
- "description": "pylot first-class CLI for the Pylot gateway API. Replaces curl workflows for operators and humans.",
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
  },