@whittlelabs/sifter 0.29.0 → 0.30.0

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 (4) hide show
  1. package/README.md +25 -5
  2. package/bin.js +210 -13
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -21,15 +21,35 @@ Visit Sift's **Settings → Pair a Sifter**. Sift will display a pairing code an
21
21
  whittle-sifter init --pairing-code=ABCD-1234
22
22
  ```
23
23
 
24
- The Sifter defaults to production (`https://keep.whittlelabs.com`). To pair against stage, test, or any other environment, pass `--keep-url`:
24
+ The Sifter defaults to production. To pair against another Whittle Labs environment, name it with `--profile`: the name selects the backend as well as the directory the pairing lands in.
25
25
 
26
26
  ```bash
27
- whittle-sifter init \
28
- --keep-url=https://keep.stage.whittlelabs.com \
29
- --pairing-code=ABCD-1234
27
+ whittle-sifter init --profile stage # keep.stage / jobs.stage
28
+ whittle-sifter init --profile test # keep.test / jobs.test
29
+ whittle-sifter init --profile studio # a dev stack on the Studio
30
30
  ```
31
31
 
32
- Pairing writes credentials to `~/.sifter/config.json` (mode 0600).
32
+ The known names are `prod`, `stage`, `test` and `studio`. For any other backend, keep the profile as a label and pass the URLs yourself; a flag always beats the environment it would otherwise have resolved:
33
+
34
+ ```bash
35
+ whittle-sifter init --profile my-box \
36
+ --keep-url=http://box.local:3007 \
37
+ --jobs-url=http://box.local:3005
38
+ ```
39
+
40
+ Whenever a profile or a URL flag is in play, `init` prints what it resolved before it pairs, so a host that has moved is something you see rather than something you debug:
41
+
42
+ ```
43
+ Pairing environment
44
+ Profile: stage
45
+ Environment: stage (named by this build)
46
+ Keep: https://keep.stage.whittlelabs.com from the "stage" environment
47
+ Jobs: https://jobs.stage.whittlelabs.com from the "stage" environment
48
+ ```
49
+
50
+ Once the backend answers, one more line names the page it sent you to, which is its decision and not the CLI's. Add `--json` to get those two facts as JSON objects, one per line.
51
+
52
+ Pairing writes credentials to `~/.sifter/<profile>/config.json` (mode 0600), or `~/.sifter/config.json` for the default profile.
33
53
 
34
54
  ## Run
35
55
 
package/bin.js CHANGED
@@ -11604,6 +11604,120 @@ var require_version_check = __commonJS({
11604
11604
  }
11605
11605
  });
11606
11606
 
11607
+ // ../../packages/shuttle/dist/branding/environment.js
11608
+ var require_environment = __commonJS({
11609
+ "../../packages/shuttle/dist/branding/environment.js"(exports2) {
11610
+ "use strict";
11611
+ Object.defineProperty(exports2, "__esModule", { value: true });
11612
+ exports2.resolveEnvironment = resolveEnvironment;
11613
+ exports2.applyEnvironment = applyEnvironment;
11614
+ exports2.isWorthReporting = isWorthReporting;
11615
+ exports2.renderEnvironmentReport = renderEnvironmentReport;
11616
+ exports2.renderVerificationLine = renderVerificationLine;
11617
+ exports2.environmentReportJson = environmentReportJson;
11618
+ exports2.verificationJson = verificationJson;
11619
+ exports2.hostOf = hostOf;
11620
+ function resolveEnvironment(brand, profile, overrides = {}) {
11621
+ const table = brand.environments ?? {};
11622
+ const known = Object.keys(table).sort();
11623
+ const matched = Object.prototype.hasOwnProperty.call(table, profile) ? profile : null;
11624
+ const entry = matched === null ? void 0 : table[matched];
11625
+ return {
11626
+ profile,
11627
+ environment: matched,
11628
+ known,
11629
+ keep: pick(overrides.keepUrl, entry?.keepApiUrl, brand.keepRegistration.keepApiUrl),
11630
+ jobs: pick(overrides.jobsUrl, entry?.jobsApiUrl, brand.keepRegistration.jobsApiUrl)
11631
+ };
11632
+ }
11633
+ function pick(flag, fromEnvironment, brandDefault) {
11634
+ if (flag)
11635
+ return { url: flag, source: "flag" };
11636
+ if (fromEnvironment)
11637
+ return { url: fromEnvironment, source: "environment" };
11638
+ return { url: brandDefault, source: "brand-default" };
11639
+ }
11640
+ function applyEnvironment(brand, resolved) {
11641
+ const reg = brand.keepRegistration;
11642
+ if (resolved.keep.url === reg.keepApiUrl && resolved.jobs.url === reg.jobsApiUrl) {
11643
+ return brand;
11644
+ }
11645
+ return {
11646
+ ...brand,
11647
+ keepRegistration: {
11648
+ ...reg,
11649
+ keepApiUrl: resolved.keep.url,
11650
+ jobsApiUrl: resolved.jobs.url
11651
+ }
11652
+ };
11653
+ }
11654
+ function isWorthReporting(resolved) {
11655
+ return resolved.environment !== null || resolved.keep.source === "flag" || resolved.jobs.source === "flag";
11656
+ }
11657
+ function renderEnvironmentReport(resolved) {
11658
+ return [
11659
+ "",
11660
+ "Pairing environment",
11661
+ field("Profile", resolved.profile),
11662
+ field("Environment", describeEnvironment(resolved)),
11663
+ field("Keep", `${resolved.keep.url} ${describeSource(resolved, resolved.keep)}`),
11664
+ field("Jobs", `${resolved.jobs.url} ${describeSource(resolved, resolved.jobs)}`),
11665
+ ""
11666
+ ];
11667
+ }
11668
+ function renderVerificationLine(verificationUri) {
11669
+ return field("Verify at", `${hostOf(verificationUri)} as Keep reported it`);
11670
+ }
11671
+ function environmentReportJson(resolved) {
11672
+ return JSON.stringify({
11673
+ kind: "pairing-environment",
11674
+ profile: resolved.profile,
11675
+ environment: resolved.environment,
11676
+ known: resolved.known,
11677
+ keep: resolved.keep,
11678
+ jobs: resolved.jobs
11679
+ });
11680
+ }
11681
+ function verificationJson(verificationUri) {
11682
+ return JSON.stringify({
11683
+ kind: "pairing-verification",
11684
+ verificationUri,
11685
+ host: hostOf(verificationUri),
11686
+ source: "keep"
11687
+ });
11688
+ }
11689
+ function field(label, value) {
11690
+ return ` ${`${label}:`.padEnd(13)}${value}`;
11691
+ }
11692
+ function describeEnvironment(resolved) {
11693
+ if (resolved.environment !== null) {
11694
+ return `${resolved.environment} (named by this build)`;
11695
+ }
11696
+ if (resolved.known.length === 0) {
11697
+ return "none (this build declares no environments)";
11698
+ }
11699
+ return `none (this build knows ${resolved.known.join(", ")})`;
11700
+ }
11701
+ function describeSource(resolved, url) {
11702
+ switch (url.source) {
11703
+ case "flag":
11704
+ return "from the flag you passed";
11705
+ case "environment":
11706
+ return `from the "${resolved.environment ?? ""}" environment`;
11707
+ default:
11708
+ return "from the brand default";
11709
+ }
11710
+ }
11711
+ function hostOf(url) {
11712
+ try {
11713
+ return new URL(url).host;
11714
+ } catch {
11715
+ return url;
11716
+ }
11717
+ }
11718
+ }
11719
+ });
11720
+
11607
11721
  // ../../packages/shuttle/dist/preflight/checks.js
11608
11722
  var require_checks = __commonJS({
11609
11723
  "../../packages/shuttle/dist/preflight/checks.js"(exports2) {
@@ -11619,6 +11733,7 @@ var require_checks = __commonJS({
11619
11733
  var loom_1 = require_dist();
11620
11734
  var version_check_1 = require_version_check();
11621
11735
  var apply_1 = require_apply();
11736
+ var environment_1 = require_environment();
11622
11737
  var NODE_FLOOR = "20.0.0";
11623
11738
  var DISK_FLOOR_BYTES = 1024 * 1024 * 1024;
11624
11739
  var WORKSPACE_EXECUTOR = "claude-code";
@@ -11745,7 +11860,7 @@ var require_checks = __commonJS({
11745
11860
  if (strays.length > 0) {
11746
11861
  return fail("pairing-environment", `the config claims from ${strays.join(", ")}, which this pairing is not rostered into`, `run \`${subject.brand.product.cliBinary} init\` to rewrite the config from this pairing`);
11747
11862
  }
11748
- return pass("pairing-environment", `config and pairing both use ${pairing.jobsApiUrl}`);
11863
+ return pass("pairing-environment", `paired with Keep at ${(0, environment_1.hostOf)(pairing.keepApiUrl)}, claiming from Jobs at ${(0, environment_1.hostOf)(pairing.jobsApiUrl)}`);
11749
11864
  });
11750
11865
  }
11751
11866
  };
@@ -22369,6 +22484,7 @@ var require_device_code = __commonJS({
22369
22484
  throw err;
22370
22485
  throw new Error(webFirstRemedy(opts.brand, err));
22371
22486
  }
22487
+ opts.onVerificationUri?.(device.verificationUri);
22372
22488
  const invitation = (0, terminal_1.renderPairingInvitation)({
22373
22489
  header: opts.brand.strings?.pairingHeader ?? `Pair your ${opts.brand.product.title}`,
22374
22490
  verificationUri: device.verificationUri,
@@ -22712,24 +22828,31 @@ var require_init = __commonJS({
22712
22828
  var fsp = __importStar(require("fs/promises"));
22713
22829
  var commander_1 = require_commander();
22714
22830
  var apply_1 = require_apply();
22831
+ var environment_1 = require_environment();
22715
22832
  var device_code_1 = require_device_code();
22716
22833
  var store_1 = require_store();
22717
22834
  var spend_store_1 = require_spend_store();
22718
22835
  var typed_exit_1 = require_typed_exit();
22719
22836
  var preflight_1 = require_preflight();
22720
22837
  function buildInitCommand(brand) {
22721
- return new commander_1.Command("init").description(`Pair this ${brand.product.title} with Keep and seed local state`).option("--pairing-code <code>", "Pairing code from the product UI").option("--keep-url <url>", "Override the Keep API URL from BrandConfig").option("--jobs-url <url>", "Override the Jobs API URL from BrandConfig").option("--executor <type>", `Executor to write into ${brand.product.id}.yaml (e.g. claude-code). Must be in the brand's executor allowlist.`).option("--poll-interval <ms>", "Run-loop poll interval in milliseconds").option("--daily-cap <tokens>", "Daily token cap; overrides the brand default").option("--monthly-cap <tokens>", "Monthly token cap; overrides the brand default").option("--tmp-dir <path>", "Base directory for each job's throwaway checkout (defaults to the OS temp dir; set only when that is unsuitable)").option("-p, --profile <name>", 'Config profile to pair into (isolates this pairing under its own dir; default "default")').action(async (options) => {
22838
+ return new commander_1.Command("init").description(`Pair this ${brand.product.title} with Keep and seed local state`).option("--pairing-code <code>", "Pairing code from the product UI").option("--keep-url <url>", "Override the Keep API URL, beating both the profile's environment and the brand default").option("--jobs-url <url>", "Override the Jobs API URL, beating both the profile's environment and the brand default").option("--json", "Print the resolved pairing environment as JSON, one object per line; the rest of the output is unchanged").option("--executor <type>", `Executor to write into ${brand.product.id}.yaml (e.g. claude-code). Must be in the brand's executor allowlist.`).option("--poll-interval <ms>", "Run-loop poll interval in milliseconds").option("--daily-cap <tokens>", "Daily token cap; overrides the brand default").option("--monthly-cap <tokens>", "Monthly token cap; overrides the brand default").option("--tmp-dir <path>", "Base directory for each job's throwaway checkout (defaults to the OS temp dir; set only when that is unsuitable)").option("-p, --profile <name>", profileFlagDescription(brand)).action(async (options) => {
22722
22839
  try {
22723
22840
  const profile = (0, apply_1.resolveProfile)(brand, options.profile);
22724
22841
  const setupOptions = parseSetupOptions(brand, options);
22725
- const effectiveBrand = options.keepUrl || options.jobsUrl ? {
22726
- ...brand,
22727
- keepRegistration: {
22728
- ...brand.keepRegistration,
22729
- ...options.keepUrl ? { keepApiUrl: options.keepUrl } : {},
22730
- ...options.jobsUrl ? { jobsApiUrl: options.jobsUrl } : {}
22731
- }
22732
- } : brand;
22842
+ const asJson = options.json === true;
22843
+ const environment = (0, environment_1.resolveEnvironment)(brand, profile, {
22844
+ keepUrl: options.keepUrl,
22845
+ jobsUrl: options.jobsUrl
22846
+ });
22847
+ const effectiveBrand = (0, environment_1.applyEnvironment)(brand, environment);
22848
+ const report = (0, environment_1.isWorthReporting)(environment);
22849
+ if (report) {
22850
+ if (asJson)
22851
+ console.log((0, environment_1.environmentReportJson)(environment));
22852
+ else
22853
+ for (const line of (0, environment_1.renderEnvironmentReport)(environment))
22854
+ console.log(line);
22855
+ }
22733
22856
  const paths = (0, apply_1.resolvePaths)(effectiveBrand, profile);
22734
22857
  await fsp.mkdir(paths.configDir, { recursive: true });
22735
22858
  for (const step of effectiveBrand.setupSteps ?? []) {
@@ -22759,7 +22882,14 @@ var require_init = __commonJS({
22759
22882
  }
22760
22883
  const pairing = await (0, device_code_1.pair)({
22761
22884
  brand: effectiveBrand,
22762
- pairingCode: options.pairingCode
22885
+ pairingCode: options.pairingCode,
22886
+ // Keep's answer is the half no local table can predict: it
22887
+ // builds `verification_uri` from its OWN configuration, so a
22888
+ // tier pointed at a page nobody serves looks exactly like a
22889
+ // healthy one until an operator opens the link.
22890
+ ...report ? {
22891
+ onVerificationUri: (uri) => console.log(asJson ? (0, environment_1.verificationJson)(uri) : (0, environment_1.renderVerificationLine)(uri))
22892
+ } : {}
22763
22893
  });
22764
22894
  const store = new store_1.PairingConfigStore(paths.configFile);
22765
22895
  await store.write(pairing);
@@ -22799,6 +22929,13 @@ var require_init = __commonJS({
22799
22929
  }
22800
22930
  });
22801
22931
  }
22932
+ function profileFlagDescription(brand) {
22933
+ const known = Object.keys(brand.environments ?? {}).sort();
22934
+ const base = 'Config profile to pair into (isolates this pairing under its own dir; default "default")';
22935
+ if (known.length === 0)
22936
+ return base;
22937
+ return `${base}. These names also select the backend: ${known.join(", ")}`;
22938
+ }
22802
22939
  function parseSetupOptions(brand, raw) {
22803
22940
  const out = {};
22804
22941
  if (raw.executor !== void 0) {
@@ -25096,7 +25233,8 @@ var require_dist5 = __commonJS({
25096
25233
  "../../packages/shuttle/dist/index.js"(exports2) {
25097
25234
  "use strict";
25098
25235
  Object.defineProperty(exports2, "__esModule", { value: true });
25099
- exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
25236
+ exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.environmentReportJson = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
25237
+ exports2.revokePairing = void 0;
25100
25238
  var cli_1 = require_cli();
25101
25239
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
25102
25240
  return cli_1.createCli;
@@ -25220,6 +25358,31 @@ var require_dist5 = __commonJS({
25220
25358
  Object.defineProperty(exports2, "renderTemplate", { enumerable: true, get: function() {
25221
25359
  return apply_1.renderTemplate;
25222
25360
  } });
25361
+ var environment_1 = require_environment();
25362
+ Object.defineProperty(exports2, "applyEnvironment", { enumerable: true, get: function() {
25363
+ return environment_1.applyEnvironment;
25364
+ } });
25365
+ Object.defineProperty(exports2, "environmentReportJson", { enumerable: true, get: function() {
25366
+ return environment_1.environmentReportJson;
25367
+ } });
25368
+ Object.defineProperty(exports2, "hostOf", { enumerable: true, get: function() {
25369
+ return environment_1.hostOf;
25370
+ } });
25371
+ Object.defineProperty(exports2, "isWorthReporting", { enumerable: true, get: function() {
25372
+ return environment_1.isWorthReporting;
25373
+ } });
25374
+ Object.defineProperty(exports2, "renderEnvironmentReport", { enumerable: true, get: function() {
25375
+ return environment_1.renderEnvironmentReport;
25376
+ } });
25377
+ Object.defineProperty(exports2, "renderVerificationLine", { enumerable: true, get: function() {
25378
+ return environment_1.renderVerificationLine;
25379
+ } });
25380
+ Object.defineProperty(exports2, "resolveEnvironment", { enumerable: true, get: function() {
25381
+ return environment_1.resolveEnvironment;
25382
+ } });
25383
+ Object.defineProperty(exports2, "verificationJson", { enumerable: true, get: function() {
25384
+ return environment_1.verificationJson;
25385
+ } });
25223
25386
  var defaults_1 = require_defaults2();
25224
25387
  Object.defineProperty(exports2, "SUBSTRATE_BRAND", { enumerable: true, get: function() {
25225
25388
  return defaults_1.SUBSTRATE_BRAND;
@@ -25259,7 +25422,7 @@ var import_path = require("path");
25259
25422
  var import_promises = require("fs/promises");
25260
25423
  var import_yaml = __toESM(require_dist4());
25261
25424
  var import_shuttle = __toESM(require_dist5());
25262
- var buildVersion = true ? "0.29.0" : pkg.version;
25425
+ var buildVersion = true ? "0.30.0" : pkg.version;
25263
25426
  var sifterBrand = {
25264
25427
  product: {
25265
25428
  id: "sifter",
@@ -25307,6 +25470,40 @@ var sifterBrand = {
25307
25470
  ]
25308
25471
  }
25309
25472
  },
25473
+ // The environments Whittle Labs operates, keyed by the profile name that
25474
+ // selects one. `init --profile stage` resolves both URLs from here, so a
25475
+ // non-prod pairing is one flag instead of three and there is no host to
25476
+ // mistype. `--keep-url` / `--jobs-url` still win, which is what keeps a
25477
+ // backend Whittle Labs does not run reachable.
25478
+ //
25479
+ // `prod` is stated even though it duplicates the defaults below: a person
25480
+ // reading `--help` should see the whole set, and a profile literally named
25481
+ // `prod` should resolve rather than fall through to a default that only
25482
+ // happens to agree. The DEFAULT profile ("default") is deliberately not a
25483
+ // key — it is not an environment name, and it keeps its historic meaning
25484
+ // of "the brand's own defaults", which are production's.
25485
+ //
25486
+ // `studio` is the local dev fleet on this machine, reachable over Tailscale
25487
+ // at the `studio.whittlelabs.com` hostname rather than `localhost`, because
25488
+ // a pairing made from a phone or a second machine has to resolve too.
25489
+ environments: {
25490
+ prod: {
25491
+ keepApiUrl: "https://keep.whittlelabs.com",
25492
+ jobsApiUrl: "https://jobs.whittlelabs.com"
25493
+ },
25494
+ stage: {
25495
+ keepApiUrl: "https://keep.stage.whittlelabs.com",
25496
+ jobsApiUrl: "https://jobs.stage.whittlelabs.com"
25497
+ },
25498
+ test: {
25499
+ keepApiUrl: "https://keep.test.whittlelabs.com",
25500
+ jobsApiUrl: "https://jobs.test.whittlelabs.com"
25501
+ },
25502
+ studio: {
25503
+ keepApiUrl: "http://studio.whittlelabs.com:3007",
25504
+ jobsApiUrl: "http://studio.whittlelabs.com:3005"
25505
+ }
25506
+ },
25310
25507
  keepRegistration: {
25311
25508
  keepApiUrl: process.env.KEEP_API_URL ?? "https://keep.whittlelabs.com",
25312
25509
  jobsApiUrl: process.env.JOBS_API_URL ?? "https://jobs.whittlelabs.com",