pylot-cli 0.1.3 → 0.1.5

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