pylot-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/dist/index.mjs +69 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,18 +116,33 @@ The operator image bakes the bundle in as `/usr/local/bin/pylot`
|
|
|
116
116
|
|
|
117
117
|
## Publishing to npm (maintainers)
|
|
118
118
|
|
|
119
|
-
The package is public on npm as `pylot-cli`.
|
|
120
|
-
|
|
121
|
-
1.
|
|
122
|
-
via `__CLI_VERSION__`, so `pylot --version` reports
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
The package is public on npm as `pylot-cli`. Releases are pipeline-driven:
|
|
120
|
+
|
|
121
|
+
1. In your PR, bump `version` in `modules/cli/package.json` (semver — the
|
|
122
|
+
bundle embeds it via `__CLI_VERSION__`, so `pylot --version` reports
|
|
123
|
+
whatever you publish). A PreToolUse hook (`.claude/doc-coverage.json`)
|
|
124
|
+
reminds any agent editing gateway route files to do this.
|
|
125
|
+
2. Merge → deploy. Every **prod** deploy ends with a drift check
|
|
126
|
+
(`scripts/ci-cli-publish-trigger.sh` in `buildspec-cdk-deploy.yml`): if the
|
|
127
|
+
deployed tree's version isn't on npm, it triggers the publish build
|
|
128
|
+
automatically. Nothing to do.
|
|
129
|
+
3. Manual trigger (same build): `pylot deploy publish-cli --wait`
|
|
130
|
+
(POST `/admin/publish-cli` → `buildspec-cli-publish.yml` →
|
|
131
|
+
`scripts/publish-cli.sh` on the pylot-builder CodeBuild project).
|
|
132
|
+
Idempotent — if the version is already on npm it no-ops. `--dry-run`
|
|
133
|
+
rehearses without the token; staging builders are always forced dry-run
|
|
134
|
+
(npm `latest` tracks prod/main only).
|
|
135
|
+
4. Verify: `npm view pylot-cli version` and `npx -y pylot-cli@latest --version`.
|
|
136
|
+
|
|
137
|
+
Credentials: `NPM_TOKEN` (npm **Automation** token — bypasses 2FA, publish
|
|
138
|
+
rights on `pylot-cli`) lives in ASM **`pylot/npm`**; the CodeBuild service
|
|
139
|
+
role reads it at run time. That bundle is deliberately outside the gateway
|
|
140
|
+
secrets hierarchy (the 4-layer path validator rejects `pylot/npm`), so no
|
|
141
|
+
team/role/repo scope — and therefore no mission container — can ever resolve
|
|
142
|
+
it. It is never a repo secret, never an env override, and never logged. Local manual publish still works from `modules/cli/` with
|
|
143
|
+
`NPM_TOKEN` in your env (`npm publish --access public` — `prepublishOnly`
|
|
144
|
+
runs typecheck + fresh build; the `files` allowlist keeps the tarball to
|
|
145
|
+
`dist/index.mjs` + README).
|
|
131
146
|
|
|
132
147
|
Version discipline: any change to gateway routes that the CLI wraps should
|
|
133
148
|
land with a CLI bump in the same PR train, so the published CLI never lags
|
package/dist/index.mjs
CHANGED
|
@@ -14921,7 +14921,7 @@ function registerObservability(program3) {
|
|
|
14921
14921
|
const { opts, cfg } = await ctx(cmd);
|
|
14922
14922
|
emit(opts, await request(cfg, "/logs/router", { query: { lines: flags.lines } }));
|
|
14923
14923
|
});
|
|
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) => {
|
|
14924
|
+
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
14925
|
const { opts, cfg } = await ctx(cmd);
|
|
14926
14926
|
emit(
|
|
14927
14927
|
opts,
|
|
@@ -15358,7 +15358,7 @@ function printNewPhases(opts, body, seen) {
|
|
|
15358
15358
|
note(opts, `phase ${p.phase}${p.status ? ` ${p.status}` : ""}`);
|
|
15359
15359
|
}
|
|
15360
15360
|
}
|
|
15361
|
-
async function waitForBuild(opts, cfg, buildId, { timeoutMs
|
|
15361
|
+
async function waitForBuild(opts, cfg, buildId, { timeoutMs }) {
|
|
15362
15362
|
const seenPhases = /* @__PURE__ */ new Set();
|
|
15363
15363
|
const resp = await pollUntil(
|
|
15364
15364
|
() => request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}`),
|
|
@@ -15372,16 +15372,35 @@ async function waitForBuild(opts, cfg, buildId, { timeoutMs, promoteKey }) {
|
|
|
15372
15372
|
printNewPhases(opts, resp.json, seenPhases);
|
|
15373
15373
|
const body = resp.json;
|
|
15374
15374
|
const status = String(body?.status ?? "");
|
|
15375
|
-
|
|
15376
|
-
if (
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
if (!ok) note(opts, `${buildId} SUCCEEDED but ${promoteKey} reported failures \u2014 NOT promoted`);
|
|
15375
|
+
note(opts, `${buildId} ${status}${status === "SUCCEEDED" ? "" : " (failed)"}`);
|
|
15376
|
+
if (status !== "SUCCEEDED") {
|
|
15377
|
+
emit(opts, resp);
|
|
15378
|
+
process.exit(1);
|
|
15380
15379
|
}
|
|
15381
|
-
|
|
15380
|
+
return resp;
|
|
15381
|
+
}
|
|
15382
|
+
async function promoteBuild(opts, cfg, buildId, { promoteKey, smoke = false }) {
|
|
15383
|
+
const resp = await request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}/promote`, {
|
|
15384
|
+
method: "POST",
|
|
15385
|
+
body: promoteKey === "worker_promote" ? { smoke } : {}
|
|
15386
|
+
});
|
|
15387
|
+
const promote = resp.json?.[promoteKey];
|
|
15388
|
+
const ok = resp.status >= 200 && resp.status < 300 && promote != null && (promote.failed ?? []).length === 0;
|
|
15389
|
+
if (!ok) note(opts, `${buildId} ${promoteKey} failed`);
|
|
15382
15390
|
emit(opts, resp);
|
|
15383
15391
|
process.exit(ok ? 0 : 1);
|
|
15384
15392
|
}
|
|
15393
|
+
function withParentBuildOpts(flags, cmd) {
|
|
15394
|
+
const parent = cmd.parent;
|
|
15395
|
+
if (!parent) return flags;
|
|
15396
|
+
const parentOpts = parent.opts();
|
|
15397
|
+
for (const key of ["sourceVersion", "wait", "timeout"]) {
|
|
15398
|
+
if (cmd.getOptionValueSource(key) !== "cli" && parent.getOptionValueSource(key) === "cli") {
|
|
15399
|
+
flags[key] = parentOpts[key];
|
|
15400
|
+
}
|
|
15401
|
+
}
|
|
15402
|
+
return flags;
|
|
15403
|
+
}
|
|
15385
15404
|
function registerDeploy(program3) {
|
|
15386
15405
|
const deploy = program3.command("deploy").description(
|
|
15387
15406
|
"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`"
|
|
@@ -15406,6 +15425,13 @@ function registerDeploy(program3) {
|
|
|
15406
15425
|
const { opts, cfg } = await ctx(cmd);
|
|
15407
15426
|
emit(opts, await request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}`));
|
|
15408
15427
|
});
|
|
15428
|
+
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) => {
|
|
15429
|
+
const { opts, cfg } = await ctx(cmd);
|
|
15430
|
+
emit(opts, await request(cfg, `/admin/build-worker/${encodeURIComponent(buildId)}/promote`, {
|
|
15431
|
+
method: "POST",
|
|
15432
|
+
body: flags.smoke ? { smoke: true } : {}
|
|
15433
|
+
}));
|
|
15434
|
+
});
|
|
15409
15435
|
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) => {
|
|
15410
15436
|
const { opts, cfg } = await ctx(cmd);
|
|
15411
15437
|
emit(
|
|
@@ -15416,8 +15442,9 @@ function registerDeploy(program3) {
|
|
|
15416
15442
|
);
|
|
15417
15443
|
});
|
|
15418
15444
|
deploy.command("build-operator").description(
|
|
15419
|
-
|
|
15420
|
-
).option("--source-version <ref>", "git ref to build (default: gateway's stage branch)").option("--wait", "poll until
|
|
15445
|
+
"rebuild the operator image (POST /admin/build-operator). With --wait: poll to SUCCEEDED, then explicitly promote"
|
|
15446
|
+
).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) => {
|
|
15447
|
+
flags = withParentBuildOpts(flags, cmd);
|
|
15421
15448
|
const { opts, cfg } = await ctx(cmd);
|
|
15422
15449
|
const body = {};
|
|
15423
15450
|
if (flags.sourceVersion) body.source_version = flags.sourceVersion;
|
|
@@ -15432,11 +15459,34 @@ function registerDeploy(program3) {
|
|
|
15432
15459
|
process.exit(1);
|
|
15433
15460
|
}
|
|
15434
15461
|
note(opts, `operator build started: ${buildId}`);
|
|
15435
|
-
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3
|
|
15462
|
+
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15463
|
+
await promoteBuild(opts, cfg, buildId, { promoteKey: "operator_promote" });
|
|
15464
|
+
});
|
|
15465
|
+
deploy.command("publish-cli").description(
|
|
15466
|
+
"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"
|
|
15467
|
+
).option("--source-version <ref>", "git ref to publish from (default: gateway's stage branch)").option("--dry-run", "typecheck + build + npm publish --dry-run (no token, nothing published)").option("--wait", "poll until the build finishes (exit 0 only on SUCCEEDED)").option("--timeout <seconds>", "max seconds to wait (default 900)", (v) => parseInt(v, 10), 900).action(async (flags, cmd) => {
|
|
15468
|
+
flags = withParentBuildOpts(flags, cmd);
|
|
15469
|
+
const { opts, cfg } = await ctx(cmd);
|
|
15470
|
+
const body = {};
|
|
15471
|
+
if (flags.sourceVersion) body.source_version = flags.sourceVersion;
|
|
15472
|
+
if (flags.dryRun) body.dry_run = true;
|
|
15473
|
+
const resp = await request(cfg, "/admin/publish-cli", { method: "POST", body });
|
|
15474
|
+
if (!flags.wait) {
|
|
15475
|
+
emit(opts, resp);
|
|
15476
|
+
return;
|
|
15477
|
+
}
|
|
15478
|
+
const buildId = resp.json?.build_id;
|
|
15479
|
+
if (!buildId) {
|
|
15480
|
+
emit(opts, resp);
|
|
15481
|
+
process.exit(1);
|
|
15482
|
+
}
|
|
15483
|
+
note(opts, `pylot-cli publish build started: ${buildId}`);
|
|
15484
|
+
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15436
15485
|
});
|
|
15437
15486
|
deploy.command("build-worker").description(
|
|
15438
|
-
"build a repo's worker image (POST /admin/build-worker, body {repo}); --batch --file posts {repos: [...]} to /admin/build-worker/batch. With --wait:
|
|
15439
|
-
).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
|
|
15487
|
+
"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"
|
|
15488
|
+
).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) => {
|
|
15489
|
+
flags = withParentBuildOpts(flags, cmd);
|
|
15440
15490
|
const { opts, cfg } = await ctx(cmd);
|
|
15441
15491
|
if (flags.batch) {
|
|
15442
15492
|
if (!flags.file) throw new UsageError('--batch requires --file <path|-> with {repos: ["org/repo", ...]}');
|
|
@@ -15458,7 +15508,8 @@ function registerDeploy(program3) {
|
|
|
15458
15508
|
process.exit(1);
|
|
15459
15509
|
}
|
|
15460
15510
|
note(opts, `worker build started: ${buildId}`);
|
|
15461
|
-
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3
|
|
15511
|
+
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15512
|
+
await promoteBuild(opts, cfg, buildId, { promoteKey: "worker_promote", smoke: flags.smoke === true });
|
|
15462
15513
|
});
|
|
15463
15514
|
deploy.command("ensure-taskdefs").description("backfill per-role operator task definitions (POST /admin/operators/ensure-taskdefs)").action(async (_flags, cmd) => {
|
|
15464
15515
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -15549,9 +15600,9 @@ function registerSkills(program3) {
|
|
|
15549
15600
|
const { opts, cfg } = await ctx(cmd);
|
|
15550
15601
|
emit(opts, await request(cfg, "/admin/skills/lock"));
|
|
15551
15602
|
});
|
|
15552
|
-
skills.command("staleness").description("SHA-mismatch report vs origin HEAD (GET /admin/skills/staleness)").action(async (
|
|
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) => {
|
|
15553
15604
|
const { opts, cfg } = await ctx(cmd);
|
|
15554
|
-
emit(opts, await request(cfg, "/admin/skills/staleness"));
|
|
15605
|
+
emit(opts, await request(cfg, "/admin/skills/staleness", { query: { org: flags.org } }));
|
|
15555
15606
|
});
|
|
15556
15607
|
skills.command("sync").description(
|
|
15557
15608
|
"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)"
|
|
@@ -15760,12 +15811,12 @@ function registerAssets(program3) {
|
|
|
15760
15811
|
}
|
|
15761
15812
|
|
|
15762
15813
|
// src/index.mts
|
|
15763
|
-
var VERSION = true ? "0.1.
|
|
15814
|
+
var VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
15764
15815
|
installEpipeGuard();
|
|
15765
15816
|
var program2 = new Command();
|
|
15766
15817
|
program2.name("pylot").description(
|
|
15767
15818
|
"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."
|
|
15768
|
-
).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");
|
|
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");
|
|
15769
15820
|
program2.exitOverride();
|
|
15770
15821
|
program2.configureOutput({ outputError: (str, write) => write(str) });
|
|
15771
15822
|
registerCore(program2);
|