pylot-cli 0.1.1 → 0.1.2

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 (3) hide show
  1. package/README.md +27 -12
  2. package/dist/index.mjs +36 -2
  3. 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`. To cut a release:
120
-
121
- 1. Bump `version` in `modules/cli/package.json` (semver — the bundle embeds it
122
- via `__CLI_VERSION__`, so `pylot --version` reports whatever you publish).
123
- 2. From `modules/cli/`: `npm publish --access public`. The `prepublishOnly`
124
- hook runs the typecheck and a fresh build, and the `files` allowlist keeps
125
- the tarball to `dist/index.mjs` + README only no source, no lockfile.
126
- 3. Publishing requires an npm token with publish rights on `pylot-cli` use
127
- an **Automation** token (or a granular token with 2FA bypass) in CI;
128
- interactive publishes prompt for the account OTP.
129
- 4. Verify: `npm view pylot-cli version` and
130
- `npx -y pylot-cli@latest --version`.
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
@@ -15382,6 +15382,17 @@ async function waitForBuild(opts, cfg, buildId, { timeoutMs, promoteKey }) {
15382
15382
  emit(opts, resp);
15383
15383
  process.exit(ok ? 0 : 1);
15384
15384
  }
15385
+ function withParentBuildOpts(flags, cmd) {
15386
+ const parent = cmd.parent;
15387
+ if (!parent) return flags;
15388
+ const parentOpts = parent.opts();
15389
+ for (const key of ["sourceVersion", "wait", "timeout"]) {
15390
+ if (cmd.getOptionValueSource(key) !== "cli" && parent.getOptionValueSource(key) === "cli") {
15391
+ flags[key] = parentOpts[key];
15392
+ }
15393
+ }
15394
+ return flags;
15395
+ }
15385
15396
  function registerDeploy(program3) {
15386
15397
  const deploy = program3.command("deploy").description(
15387
15398
  "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`"
@@ -15418,6 +15429,7 @@ function registerDeploy(program3) {
15418
15429
  deploy.command("build-operator").description(
15419
15430
  'rebuild the operator image (POST /admin/build-operator). With --wait: SUCCEEDED alone is not "promoted" \u2014 completion requires operator_promote with no failures'
15420
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) => {
15432
+ flags = withParentBuildOpts(flags, cmd);
15421
15433
  const { opts, cfg } = await ctx(cmd);
15422
15434
  const body = {};
15423
15435
  if (flags.sourceVersion) body.source_version = flags.sourceVersion;
@@ -15434,9 +15446,31 @@ function registerDeploy(program3) {
15434
15446
  note(opts, `operator build started: ${buildId}`);
15435
15447
  await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3, promoteKey: "operator_promote" });
15436
15448
  });
15449
+ deploy.command("publish-cli").description(
15450
+ "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"
15451
+ ).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) => {
15452
+ flags = withParentBuildOpts(flags, cmd);
15453
+ const { opts, cfg } = await ctx(cmd);
15454
+ const body = {};
15455
+ if (flags.sourceVersion) body.source_version = flags.sourceVersion;
15456
+ if (flags.dryRun) body.dry_run = true;
15457
+ const resp = await request(cfg, "/admin/publish-cli", { method: "POST", body });
15458
+ if (!flags.wait) {
15459
+ emit(opts, resp);
15460
+ return;
15461
+ }
15462
+ const buildId = resp.json?.build_id;
15463
+ if (!buildId) {
15464
+ emit(opts, resp);
15465
+ process.exit(1);
15466
+ }
15467
+ note(opts, `pylot-cli publish build started: ${buildId}`);
15468
+ await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
15469
+ });
15437
15470
  deploy.command("build-worker").description(
15438
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"
15439
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) => {
15473
+ flags = withParentBuildOpts(flags, cmd);
15440
15474
  const { opts, cfg } = await ctx(cmd);
15441
15475
  if (flags.batch) {
15442
15476
  if (!flags.file) throw new UsageError('--batch requires --file <path|-> with {repos: ["org/repo", ...]}');
@@ -15760,12 +15794,12 @@ function registerAssets(program3) {
15760
15794
  }
15761
15795
 
15762
15796
  // src/index.mts
15763
- var VERSION = true ? "0.1.1" : "0.0.0-dev";
15797
+ var VERSION = true ? "0.1.2" : "0.0.0-dev";
15764
15798
  installEpipeGuard();
15765
15799
  var program2 = new Command();
15766
15800
  program2.name("pylot").description(
15767
15801
  "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");
15802
+ ).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
15803
  program2.exitOverride();
15770
15804
  program2.configureOutput({ outputError: (str, write) => write(str) });
15771
15805
  registerCore(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pylot-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "pylot — first-class CLI for the Pylot gateway API. Replaces curl workflows for operators and humans.",
6
6
  "bin": {