@secondlayer/cli 12.0.0 → 12.2.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.
package/README.md CHANGED
@@ -51,7 +51,44 @@ No account. Writes `.env.local`, restores history, prints the Stacks observer st
51
51
  | `secondlayer bootstrap --against <manifest> [--to-block <n>] [--public-key <pem>] [-y] [--json]` | Restore chain history from a verified archive into an empty database. Exit `0` restored, `1` diverged, `2` refused |
52
52
  | `secondlayer observer [--mode indexer\|signer-shared] [--endpoint host:port] [--recovery journal\|archive] [--network …]` | Print the `[[events_observer]]` stanza. Signer-shared requires `--recovery` |
53
53
  | `secondlayer verify [all\|raw\|decode:<name>\|subgraph:<name>] --against <manifest> [--quick\|--deep\|--anchor]` | Compare local data to a signed archive. Default target `raw`. Exit `0` clean, `1` diverged, `2` unanchored |
54
- | `secondlayer repair --against <archive> [--apply]` | Plan (default) or apply an archive repair |
54
+ | `secondlayer repair --against <archive> [--apply] [-y]` | Plan (default) or apply an archive repair |
55
+
56
+ Bootstrap and repair against the official hosted archive (`archive.secondlayer.tools`)
57
+ are metered per partition; against any other manifest (a mirror, a teammate's
58
+ box, a local file) they are free. See [Metered fetches](#metered-fetches).
59
+
60
+ ### Metered fetches
61
+
62
+ `secondlayer bootstrap` and `secondlayer repair` pull partitions from the
63
+ signed archive instead of replaying the chain, and that pull costs money only
64
+ when it targets the official hosted archive. Point `--against` at a mirror or
65
+ a local manifest and nothing is charged, nothing is even contacted beyond
66
+ that manifest, because self-hosting the archive is a supported way to use
67
+ these commands, not a workaround.
68
+
69
+ Against the official host, both commands quote before they charge:
70
+
71
+ 1. The manifest's partition list is priced with a free, no-write call
72
+ (`POST /api/archive/quote`).
73
+ 2. The quote prints into the existing plan output, for example
74
+ `metered: 528 partitions ≈ $44.00 · balance $50.00`, or for `repair`
75
+ inside its monthly allowance, `metered: free (4 of 6 monthly repair
76
+ fetches remaining)`.
77
+ 3. You confirm, or pass `-y` to skip the prompt. `-y` never skips the quote
78
+ or the balance check: if the balance is short, the command exits before
79
+ any partition is fetched and prints the shortfall and
80
+ `secondlayer credits buy`.
81
+ 4. Only then does the command fetch, and only the partitions it actually
82
+ reads are charged.
83
+
84
+ A partition already charged in the last 24 hours re-presigns for free, so a
85
+ resumed or retried bootstrap never pays twice for the same bytes. `repair`
86
+ gets 6 free range-bundles a month; `bootstrap` does not, since it is the
87
+ whole-chain operation the free tier exists to not subsidize.
88
+
89
+ `secondlayer verify` is unaffected: it reads manifests and digests, never
90
+ partition bytes, and stays free and anonymous no matter which archive it
91
+ points at.
55
92
 
56
93
  ### Subgraphs
57
94
 
@@ -181,8 +218,8 @@ const supply = await token.vars.totalSupply.get()
181
218
  ## Frozen commands
182
219
 
183
220
  Local-dev periphery: still functional, hidden from `secondlayer --help`, no further
184
- investment — may be removed in a future major. The hosted dev loop
185
- (`secondlayer subgraphs create/deploy`) is the supported path.
221
+ investment — may be removed in a future major. `secondlayer subgraphs
222
+ create/deploy` against your own instance is the supported path.
186
223
 
187
224
  - `secondlayer local up/down/start/stop/restart/status/logs` — local stack + dev services
188
225
  - `secondlayer local node ...` — local Stacks node management
package/dist/cli.js CHANGED
@@ -34320,14 +34320,14 @@ init_esm();
34320
34320
  // package.json
34321
34321
  var package_default = {
34322
34322
  name: "@secondlayer/cli",
34323
- version: "12.0.0",
34323
+ version: "12.2.0",
34324
+ description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
34324
34325
  repository: {
34325
34326
  type: "git",
34326
34327
  url: "git+https://github.com/ryanwaits/secondlayer.git",
34327
34328
  directory: "packages/cli"
34328
34329
  },
34329
34330
  homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/cli#readme",
34330
- description: "CLI for subgraphs and blockchain indexing on Stacks",
34331
34331
  type: "module",
34332
34332
  bin: {
34333
34333
  secondlayer: "./dist/cli.js",
@@ -34363,10 +34363,10 @@ var package_default = {
34363
34363
  "@inquirer/prompts": "^8.2.0",
34364
34364
  "@secondlayer/bundler": "^0.5.1",
34365
34365
  "@secondlayer/scaffold": "^1.5.4",
34366
- "@secondlayer/sdk": "^8.0.0",
34367
- "@secondlayer/shared": "^9.0.0",
34366
+ "@secondlayer/sdk": "^8.0.1",
34367
+ "@secondlayer/shared": "^10.0.1",
34368
34368
  "@secondlayer/stacks": "^4.0.0",
34369
- "@secondlayer/subgraphs": "^4.0.1",
34369
+ "@secondlayer/subgraphs": "^4.0.2",
34370
34370
  esbuild: "^0.27.4",
34371
34371
  "smol-toml": "^1.6.1",
34372
34372
  zod: "^4.3.6"
@@ -38217,9 +38217,22 @@ async function resolveArchivePublicKey(input4) {
38217
38217
  }
38218
38218
  return;
38219
38219
  }
38220
- async function fetchVerifiedPartition(reference, partition) {
38220
+ async function fetchViaGate(gate, partition, retried) {
38221
+ const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
38222
+ const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
38223
+ if (response2.status === 403 && !retried) {
38224
+ return fetchViaGate(gate, partition, true);
38225
+ }
38226
+ if (!response2.ok) {
38227
+ throw new Error(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`);
38228
+ }
38229
+ return Buffer.from(await response2.arrayBuffer());
38230
+ }
38231
+ async function fetchVerifiedPartition(reference, partition, gate) {
38221
38232
  let bytes;
38222
- if (reference.isRemote) {
38233
+ if (gate) {
38234
+ bytes = await fetchViaGate(gate, partition, false);
38235
+ } else if (reference.isRemote) {
38223
38236
  const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
38224
38237
  const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
38225
38238
  if (!response2.ok) {
@@ -38448,11 +38461,123 @@ import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
38448
38461
  import { tmpdir as tmpdir2 } from "node:os";
38449
38462
  import { join as join15 } from "node:path";
38450
38463
  import { ParquetReader } from "@dsnp/parquetjs";
38464
+ import { confirm as confirm7 } from "@inquirer/prompts";
38451
38465
  import {
38452
38466
  compareRangeDigests as compareRangeDigests2,
38453
38467
  computeRangeDigest as computeRangeDigest2
38454
38468
  } from "@secondlayer/shared/archive/range-digest";
38455
38469
  import { getDb as getDb3, sql as sql2 } from "@secondlayer/shared/db";
38470
+
38471
+ // src/lib/archive-gate.ts
38472
+ init_http();
38473
+ var OFFICIAL_ARCHIVE_HOST = "archive.secondlayer.tools";
38474
+ function isOfficialArchive(reference) {
38475
+ if (!reference.isRemote)
38476
+ return false;
38477
+ try {
38478
+ return new URL(reference.origin).hostname === OFFICIAL_ARCHIVE_HOST;
38479
+ } catch {
38480
+ return false;
38481
+ }
38482
+ }
38483
+ var defaultDeps = { httpArchiveOps };
38484
+ function quoteFromBody(body) {
38485
+ return {
38486
+ partitions: body.partitions,
38487
+ bundles: body.bundles,
38488
+ usdMicros: body.usd_micros,
38489
+ usd: body.usd,
38490
+ freeAllowanceAppliedMicros: body.free_allowance_applied_micros,
38491
+ allowanceRemainingBundles: body.allowance_remaining_bundles,
38492
+ balanceUsdMicros: body.balance_usd_micros,
38493
+ sufficient: body.sufficient
38494
+ };
38495
+ }
38496
+ async function quoteArchiveFetch(paths, flow, deps = defaultDeps) {
38497
+ try {
38498
+ const body = await deps.httpArchiveOps("/api/archive/quote", { method: "POST", body: { paths, flow } });
38499
+ return { ok: true, quote: quoteFromBody(body) };
38500
+ } catch (err) {
38501
+ if (err instanceof CliHttpError) {
38502
+ if (err.status === 503)
38503
+ return { ok: false, kind: "not_configured" };
38504
+ if (err.status === 401) {
38505
+ return { ok: false, kind: "not_authed", message: err.message };
38506
+ }
38507
+ return { ok: false, kind: "error", message: err.message };
38508
+ }
38509
+ throw err;
38510
+ }
38511
+ }
38512
+ var BUY_CREDITS_COMMAND = "secondlayer credits buy";
38513
+ function formatInsufficientMessage(quote) {
38514
+ const shortfallUsd = ((quote.usdMicros - quote.balanceUsdMicros) / 1e6).toFixed(2);
38515
+ const balanceUsd = (quote.balanceUsdMicros / 1e6).toFixed(2);
38516
+ return `Insufficient archive credits: quote $${quote.usd}, balance $${balanceUsd}, short $${shortfallUsd}. Buy more with \`${BUY_CREDITS_COMMAND}\`.`;
38517
+ }
38518
+ var ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE = "the archive gate is not configured on the server; contact the operator";
38519
+ var ALLOWANCE_MONTHLY_BUNDLES = 6;
38520
+ function formatQuoteValue(quote, flow) {
38521
+ if (flow === "repair" && quote.usdMicros === 0 && quote.freeAllowanceAppliedMicros > 0) {
38522
+ return `free (${quote.allowanceRemainingBundles} of ${ALLOWANCE_MONTHLY_BUNDLES} monthly repair fetches remaining)`;
38523
+ }
38524
+ const balance = `$${(quote.balanceUsdMicros / 1e6).toFixed(2)}`;
38525
+ return `${quote.partitions} partitions ≈ $${quote.usd} · balance ${balance}`;
38526
+ }
38527
+ function shouldPromptForGatedFetch(opts) {
38528
+ return !opts.yes && !opts.json;
38529
+ }
38530
+ var FETCH_BATCH_SIZE = 16;
38531
+ function createGatedFetcher(paths, flow, deps = defaultDeps) {
38532
+ const cache = new Map;
38533
+ const batchOf = new Map;
38534
+ for (const [index, path11] of paths.entries()) {
38535
+ batchOf.set(path11, Math.floor(index / FETCH_BATCH_SIZE));
38536
+ }
38537
+ const fetchedBatches = new Set;
38538
+ const inFlight = new Map;
38539
+ async function fetchAndCache(batchPaths) {
38540
+ const body = await deps.httpArchiveOps("/api/archive/fetch", { method: "POST", body: { paths: batchPaths, flow } });
38541
+ for (const u3 of body.urls)
38542
+ cache.set(u3.path, u3.url);
38543
+ }
38544
+ return {
38545
+ async getUrl(path11, opts) {
38546
+ if (opts?.forceRefresh) {
38547
+ if (process.env.DEBUG) {
38548
+ console.error(`[archive-gate] re-issuing expired URL for ${path11}`);
38549
+ }
38550
+ await fetchAndCache([path11]);
38551
+ const fresh = cache.get(path11);
38552
+ if (!fresh) {
38553
+ throw new Error(`archive gate: no URL returned for ${path11}`);
38554
+ }
38555
+ return fresh;
38556
+ }
38557
+ const batchIndex = batchOf.get(path11);
38558
+ if (batchIndex === undefined) {
38559
+ throw new Error(`archive gate: ${path11} was not in the quoted batch`);
38560
+ }
38561
+ if (!fetchedBatches.has(batchIndex)) {
38562
+ let inflight = inFlight.get(batchIndex);
38563
+ if (!inflight) {
38564
+ const batchPaths = paths.filter((p2) => batchOf.get(p2) === batchIndex);
38565
+ inflight = fetchAndCache(batchPaths).then(() => {
38566
+ fetchedBatches.add(batchIndex);
38567
+ });
38568
+ inFlight.set(batchIndex, inflight);
38569
+ }
38570
+ await inflight;
38571
+ }
38572
+ const url = cache.get(path11);
38573
+ if (!url)
38574
+ throw new Error(`archive gate: no URL returned for ${path11}`);
38575
+ return url;
38576
+ }
38577
+ };
38578
+ }
38579
+
38580
+ // src/commands/repair.ts
38456
38581
  init_output();
38457
38582
  var REPAIR_EXIT = {
38458
38583
  OK: 0,
@@ -38502,8 +38627,8 @@ function parseHeight3(value, flag) {
38502
38627
  }
38503
38628
  return parsed;
38504
38629
  }
38505
- async function planRange(db, reference, partition) {
38506
- const bytes = await fetchVerifiedPartition(reference, partition);
38630
+ async function planRange(db, reference, partition, gate) {
38631
+ const bytes = await fetchVerifiedPartition(reference, partition, gate);
38507
38632
  const archiveBlocks = await readBlocksPartition(bytes, `${partition.from_block}-${partition.to_block}`);
38508
38633
  const localRows = await db.selectFrom("blocks").select(["height", "hash"]).where("canonical", "=", true).where("height", ">=", partition.from_block).where("height", "<=", partition.to_block).execute();
38509
38634
  const localByHeight = new Map(localRows.map((r2) => [Number(r2.height), r2.hash]));
@@ -38530,10 +38655,10 @@ async function planRange(db, reference, partition) {
38530
38655
  const extraHeights = [...localByHeight.keys()].sort((a4, b3) => a4 - b3);
38531
38656
  return { fixes, extraHeights };
38532
38657
  }
38533
- async function applyFixes(db, reference, partition, fixes) {
38658
+ async function applyFixes(db, reference, partition, fixes, gate) {
38534
38659
  if (fixes.length === 0)
38535
38660
  return 0;
38536
- const bytes = await fetchVerifiedPartition(reference, partition);
38661
+ const bytes = await fetchVerifiedPartition(reference, partition, gate);
38537
38662
  const archiveBlocks = await readBlocksPartition(bytes, `apply-${partition.from_block}`);
38538
38663
  const byHeight = new Map(archiveBlocks.map((b3) => [b3.height, b3]));
38539
38664
  const targets = fixes.map((fix) => byHeight.get(fix.height)).filter((b3) => b3 !== undefined);
@@ -38562,7 +38687,7 @@ async function applyFixes(db, reference, partition, fixes) {
38562
38687
  return targets.length;
38563
38688
  }
38564
38689
  function registerRepairCommand(program2) {
38565
- program2.command("repair").description("Replace local chain data that diverges from a signed archive (dry-run by default)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to consider").option("--to-block <n>", "last height to consider").option("--apply", "write the repair (default is a dry-run plan)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("--json", "Output as JSON").addHelpText("after", `
38690
+ program2.command("repair").description("Replace local chain data that diverges from a signed archive (dry-run by default)").requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--from-block <n>", "first height to consider").option("--to-block <n>", "last height to consider").option("--apply", "write the repair (default is a dry-run plan)").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt for a metered fetch").option("--json", "Output as JSON").addHelpText("after", `
38566
38691
  Examples:
38567
38692
  $ secondlayer repair --against ./snapshot.json # plan only
38568
38693
  $ secondlayer repair --against ./snapshot.json --apply # write the fix
@@ -38608,12 +38733,37 @@ Exit codes:
38608
38733
  process.exit(REPAIR_EXIT.OK);
38609
38734
  }
38610
38735
  const partitions = (reference.manifest.partitions ?? []).filter((p2) => p2.dataset === "blocks" && divergent.some((d2) => d2.from_block === p2.from_block && d2.to_block === p2.to_block));
38736
+ let gate;
38737
+ if (isOfficialArchive(reference)) {
38738
+ const paths = partitions.map((p2) => p2.path);
38739
+ const result = await quoteArchiveFetch(paths, "repair");
38740
+ if (!result.ok) {
38741
+ printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
38742
+ process.exit(REPAIR_EXIT.UNANCHORED);
38743
+ }
38744
+ if (!result.quote.sufficient) {
38745
+ printError(formatInsufficientMessage(result.quote));
38746
+ process.exit(REPAIR_EXIT.UNANCHORED);
38747
+ }
38748
+ note(` metered: ${formatQuoteValue(result.quote, "repair")}`);
38749
+ if (shouldPromptForGatedFetch(opts)) {
38750
+ const proceed = await confirm7({
38751
+ message: `Fetch ${partitions.length} partition(s) from the archive?`,
38752
+ default: true
38753
+ });
38754
+ if (!proceed) {
38755
+ note("Nothing was fetched.");
38756
+ process.exit(REPAIR_EXIT.UNANCHORED);
38757
+ }
38758
+ }
38759
+ gate = createGatedFetcher(paths, "repair");
38760
+ }
38611
38761
  const allFixes = [];
38612
38762
  const allExtra = [];
38613
38763
  const planned = [];
38614
38764
  for (const partition of partitions) {
38615
38765
  note(` reading ${partition.dataset} ${partition.from_block}-${partition.to_block}`);
38616
- const { fixes, extraHeights } = await planRange(db, reference, partition);
38766
+ const { fixes, extraHeights } = await planRange(db, reference, partition, gate);
38617
38767
  planned.push({ partition, fixes });
38618
38768
  allFixes.push(...fixes);
38619
38769
  allExtra.push(...extraHeights);
@@ -38621,7 +38771,7 @@ Exit codes:
38621
38771
  let applied = 0;
38622
38772
  if (opts.apply) {
38623
38773
  for (const { partition, fixes } of planned) {
38624
- applied += await applyFixes(db, reference, partition, fixes);
38774
+ applied += await applyFixes(db, reference, partition, fixes, gate);
38625
38775
  }
38626
38776
  }
38627
38777
  let remaining = divergent.length;
@@ -38698,7 +38848,7 @@ import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
38698
38848
  import { tmpdir as tmpdir3 } from "node:os";
38699
38849
  import { join as join16 } from "node:path";
38700
38850
  import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
38701
- import { confirm as confirm7 } from "@inquirer/prompts";
38851
+ import { confirm as confirm8 } from "@inquirer/prompts";
38702
38852
  import {
38703
38853
  copyStatement,
38704
38854
  writeRowsToCopyStream
@@ -38794,8 +38944,8 @@ async function* readPartitionRows(bytes, label) {
38794
38944
  await unlink3(path11).catch(() => {});
38795
38945
  }
38796
38946
  }
38797
- async function loadPartition(reference, partition, rawClient) {
38798
- const bytes = await fetchVerifiedPartition(reference, partition);
38947
+ async function loadPartition(reference, partition, rawClient, gate) {
38948
+ const bytes = await fetchVerifiedPartition(reference, partition, gate);
38799
38949
  const writable2 = await rawClient.unsafe(copyStatement(partition.dataset)).writable();
38800
38950
  const written = await writeRowsToCopyStream({
38801
38951
  writable: writable2,
@@ -38872,6 +39022,23 @@ Exit codes:
38872
39022
  const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
38873
39023
  const restoreSeconds = totalRows / RESTORE_ROWS_PER_SECOND;
38874
39024
  const genesisSeconds = tipHeight / BLOCKS_PER_DAY_ESTIMATE * 86400;
39025
+ const gated = isOfficialArchive(reference);
39026
+ let quoteLine;
39027
+ let gate;
39028
+ if (gated) {
39029
+ const paths = partitions.map((p2) => p2.path);
39030
+ const result = await quoteArchiveFetch(paths, "bootstrap");
39031
+ if (!result.ok) {
39032
+ printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
39033
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
39034
+ }
39035
+ if (!result.quote.sufficient) {
39036
+ printError(formatInsufficientMessage(result.quote));
39037
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
39038
+ }
39039
+ quoteLine = formatQuoteValue(result.quote, "bootstrap");
39040
+ gate = createGatedFetcher(paths, "bootstrap");
39041
+ }
38875
39042
  if (!opts.json) {
38876
39043
  console.error("");
38877
39044
  console.error(formatKeyValue([
@@ -38882,15 +39049,16 @@ Exit codes:
38882
39049
  ],
38883
39050
  ["rows", totalRows.toLocaleString()],
38884
39051
  ["download", `${(totalBytes / 1e9).toFixed(1)} GB`],
38885
- ["signature", "verified"]
39052
+ ["signature", "verified"],
39053
+ ...quoteLine ? [["metered", quoteLine]] : []
38886
39054
  ]));
38887
39055
  console.error("");
38888
39056
  console.error(` sync from genesis ${bold(`~${formatDuration2(genesisSeconds)}`)} ${dim("(estimate)")}`);
38889
39057
  console.error(` restore from archive ${bold(`~${formatDuration2(restoreSeconds)}`)}`);
38890
39058
  console.error("");
38891
39059
  }
38892
- if (!opts.yes && !opts.json) {
38893
- const proceed = await confirm7({
39060
+ if (shouldPromptForGatedFetch(opts)) {
39061
+ const proceed = await confirm8({
38894
39062
  message: "Restore from the verified archive?",
38895
39063
  default: true
38896
39064
  });
@@ -38907,7 +39075,7 @@ Exit codes:
38907
39075
  for (const dataset of order) {
38908
39076
  const datasetPartitions = partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block);
38909
39077
  for (const [index, partition] of datasetPartitions.entries()) {
38910
- loaded[dataset] += await loadPartition(reference, partition, rawClient);
39078
+ loaded[dataset] += await loadPartition(reference, partition, rawClient, gate);
38911
39079
  const done = index + 1;
38912
39080
  if (done % 10 === 0 || done === datasetPartitions.length) {
38913
39081
  const elapsed2 = (Date.now() - startedAt) / 1000;
@@ -38964,7 +39132,8 @@ Exit codes:
38964
39132
  elapsed_seconds: Math.round(elapsed),
38965
39133
  resume_from: tipHeight + 1,
38966
39134
  node_tip_at_start: nodeTipAtStart,
38967
- catch_up_blocks: seam?.gap ?? null
39135
+ catch_up_blocks: seam?.gap ?? null,
39136
+ metered: quoteLine ?? null
38968
39137
  };
38969
39138
  output({
38970
39139
  json: opts.json,
@@ -39006,19 +39175,91 @@ Exit codes:
39006
39175
  function registerBootstrapCommand(program2) {
39007
39176
  attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
39008
39177
  }
39178
+ // src/commands/uninstall.ts
39179
+ init_output();
39180
+ import { spawnSync as spawnSync3 } from "node:child_process";
39181
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
39182
+ import { join as join17, resolve as resolve13 } from "node:path";
39183
+ import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
39184
+ var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
39185
+ var DEFAULT_COMPOSE = "docker/oss/docker-compose.yml";
39186
+ function bundleHasKeys(bundleDir) {
39187
+ const manifest = join17(bundleDir, "manifest.json");
39188
+ if (!existsSync8(manifest))
39189
+ return false;
39190
+ try {
39191
+ const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
39192
+ return parsed.secrets != null;
39193
+ } catch {
39194
+ return false;
39195
+ }
39196
+ }
39197
+ function registerUninstallCommand(program2) {
39198
+ program2.command("uninstall").description("Stop the stack; the index, chainstate, and keys survive").option("--compose <file>", "compose file to tear down", DEFAULT_COMPOSE).option("--purge", "also destroy the volumes (wipes the index)").option("--yes", "confirm a purge").option("--backup <dir>", "a bundle proving the keys exist elsewhere (required to purge)").option("--apply", "actually run it (default is a dry run)").option("--json", "Output as JSON").action(async (opts) => {
39199
+ const dataDir = process.env.DATA_DIR ?? "./data";
39200
+ const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || existsSync8(resolve13(process.cwd(), ".env.local"));
39201
+ const keysBackedUp = opts.backup ? bundleHasKeys(resolve13(opts.backup)) : false;
39202
+ const decision = planUninstall({
39203
+ purge: opts.purge === true,
39204
+ confirmed: opts.yes === true,
39205
+ keysBackedUp,
39206
+ secretsPresent,
39207
+ dataDir
39208
+ });
39209
+ if (!decision.ok) {
39210
+ printError(decision.reason);
39211
+ process.exit(UNINSTALL_EXIT.REFUSED);
39212
+ }
39213
+ const cmd = uninstallCommand(decision.plan, opts.compose);
39214
+ if (!opts.apply) {
39215
+ output({
39216
+ json: opts.json,
39217
+ data: { dryRun: true, plan: decision.plan, command: cmd },
39218
+ human: () => {
39219
+ note("dry run — nothing removed. Pass --apply to proceed.");
39220
+ note(`would run: docker ${cmd.join(" ")}`);
39221
+ for (const p2 of decision.plan.preserves) {
39222
+ note(`preserved · ${p2.what}: ${p2.detail}`);
39223
+ }
39224
+ for (const d2 of decision.plan.destroys) {
39225
+ warn(`DESTROYED · volume ${d2}`);
39226
+ }
39227
+ }
39228
+ });
39229
+ return;
39230
+ }
39231
+ const res = spawnSync3("docker", cmd, { stdio: "inherit" });
39232
+ if ((res.status ?? 1) !== 0) {
39233
+ printError(`docker ${cmd.join(" ")} exited ${res.status}`);
39234
+ process.exit(UNINSTALL_EXIT.FAILED);
39235
+ }
39236
+ output({
39237
+ json: opts.json,
39238
+ data: { removed: true, plan: decision.plan },
39239
+ human: () => {
39240
+ success("stack removed");
39241
+ for (const w2 of decision.warnings)
39242
+ warn(w2);
39243
+ for (const p2 of decision.plan.preserves) {
39244
+ note(`preserved · ${p2.what}: ${p2.detail}`);
39245
+ }
39246
+ }
39247
+ });
39248
+ });
39249
+ }
39009
39250
  // src/commands/backup.ts
39010
39251
  init_output();
39011
39252
  import { createHash as createHash4 } from "node:crypto";
39012
39253
  import {
39013
39254
  chmodSync,
39014
- existsSync as existsSync8,
39255
+ existsSync as existsSync9,
39015
39256
  mkdirSync as mkdirSync6,
39016
- readFileSync as readFileSync7,
39257
+ readFileSync as readFileSync8,
39017
39258
  statSync as statSync3,
39018
39259
  writeFileSync as writeFileSync7
39019
39260
  } from "node:fs";
39020
- import { join as join17, resolve as resolve13 } from "node:path";
39021
- import { getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
39261
+ import { join as join18, resolve as resolve14 } from "node:path";
39262
+ import { closeDb, getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
39022
39263
  import {
39023
39264
  decryptBundle,
39024
39265
  encryptBundle,
@@ -39044,7 +39285,7 @@ function readSecretsKey() {
39044
39285
  return key ? key : undefined;
39045
39286
  }
39046
39287
  function sha256File(path11) {
39047
- return createHash4("sha256").update(readFileSync7(path11)).digest("hex");
39288
+ return createHash4("sha256").update(readFileSync8(path11)).digest("hex");
39048
39289
  }
39049
39290
  async function readScope() {
39050
39291
  const db = getDb5();
@@ -39076,6 +39317,7 @@ function attachBackupCommand(cmd) {
39076
39317
  return cmd.requiredOption("--out <dir>", "directory to write the bundle into").option("--passphrase <passphrase>", "encrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--no-secrets", "omit keys; the bundle restores data only").option("--json", "Output as JSON").action(async (opts) => {
39077
39318
  try {
39078
39319
  await runBackup(opts);
39320
+ await closeDb();
39079
39321
  } catch (error2) {
39080
39322
  printError(error2 instanceof Error ? error2.message : String(error2));
39081
39323
  process.exit(BACKUP_EXIT.FAILED);
@@ -39104,10 +39346,10 @@ async function runBackup(opts) {
39104
39346
  printError(plan.reason);
39105
39347
  process.exit(BACKUP_EXIT.REFUSED);
39106
39348
  }
39107
- const outDir = resolve13(opts.out);
39349
+ const outDir = resolve14(opts.out);
39108
39350
  mkdirSync6(outDir, { recursive: true });
39109
39351
  console.error("dumping database…");
39110
- const dumpPath = join17(outDir, DB_FILE);
39352
+ const dumpPath = join18(outDir, DB_FILE);
39111
39353
  const dump = await runProcess(["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, databaseUrl], "pg_dump");
39112
39354
  if (!dump.ok) {
39113
39355
  printError(dump.stderr);
@@ -39126,7 +39368,7 @@ async function runBackup(opts) {
39126
39368
  const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
39127
39369
  const sealed = encryptBundle(lines2.join(`
39128
39370
  `), passphrase);
39129
- const secretsPath = join17(outDir, SECRETS_FILE);
39371
+ const secretsPath = join18(outDir, SECRETS_FILE);
39130
39372
  writeFileSync7(secretsPath, sealed);
39131
39373
  chmodSync(secretsPath, 384);
39132
39374
  manifest.secrets = {
@@ -39135,7 +39377,7 @@ async function runBackup(opts) {
39135
39377
  canary: sealKeyCanary(secretsKey)
39136
39378
  };
39137
39379
  }
39138
- writeFileSync7(join17(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
39380
+ writeFileSync7(join18(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
39139
39381
  `);
39140
39382
  output({
39141
39383
  json: opts.json,
@@ -39155,6 +39397,7 @@ function attachRestoreCommand(cmd) {
39155
39397
  return cmd.requiredOption("--from <dir>", "bundle directory to restore from").option("--passphrase <passphrase>", "decrypt bundled secrets (or set SECONDLAYER_BACKUP_PASSPHRASE)").option("--apply", "actually restore (default is a dry run)").option("--force", "restore over a database that already holds chain data").option("--json", "Output as JSON").action(async (opts) => {
39156
39398
  try {
39157
39399
  await runRestore(opts);
39400
+ await closeDb();
39158
39401
  } catch (error2) {
39159
39402
  printError(error2 instanceof Error ? error2.message : String(error2));
39160
39403
  process.exit(BACKUP_EXIT.FAILED);
@@ -39163,14 +39406,14 @@ function attachRestoreCommand(cmd) {
39163
39406
  }
39164
39407
  async function runRestore(opts) {
39165
39408
  const databaseUrl = requireDatabaseUrl();
39166
- const dir = resolve13(opts.from);
39167
- const manifestPath = join17(dir, MANIFEST_FILE);
39168
- if (!existsSync8(manifestPath)) {
39409
+ const dir = resolve14(opts.from);
39410
+ const manifestPath = join18(dir, MANIFEST_FILE);
39411
+ if (!existsSync9(manifestPath)) {
39169
39412
  printError(`no ${MANIFEST_FILE} in ${dir}`);
39170
39413
  process.exit(BACKUP_EXIT.REFUSED);
39171
39414
  }
39172
- const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
39173
- const dumpPath = join17(dir, manifest.db?.file ?? DB_FILE);
39415
+ const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
39416
+ const dumpPath = join18(dir, manifest.db?.file ?? DB_FILE);
39174
39417
  if (manifest.db && sha256File(dumpPath) !== manifest.db.sha256) {
39175
39418
  printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
39176
39419
  process.exit(BACKUP_EXIT.REFUSED);
@@ -39184,7 +39427,7 @@ async function runRestore(opts) {
39184
39427
  process.exit(BACKUP_EXIT.REFUSED);
39185
39428
  }
39186
39429
  try {
39187
- const env2 = decryptBundle(readFileSync7(join17(dir, manifest.secrets.file)), passphrase);
39430
+ const env2 = decryptBundle(readFileSync8(join18(dir, manifest.secrets.file)), passphrase);
39188
39431
  bundleKey = env2.split(`
39189
39432
  `).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
39190
39433
  } catch {
@@ -39263,8 +39506,8 @@ function registerRestoreCommand(program2) {
39263
39506
  }
39264
39507
  // src/lib/instance-init.ts
39265
39508
  import { randomBytes as randomBytes3 } from "node:crypto";
39266
- import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
39267
- import { resolve as resolve14 } from "node:path";
39509
+ import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
39510
+ import { resolve as resolve15 } from "node:path";
39268
39511
  import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
39269
39512
  import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
39270
39513
  var INSTANCE_ENV_FILE = ".env.local";
@@ -39298,9 +39541,9 @@ function unescapeEnvValue(raw) {
39298
39541
  `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
39299
39542
  }
39300
39543
  function readEnvValue(envPath, key) {
39301
- if (!existsSync9(envPath))
39544
+ if (!existsSync10(envPath))
39302
39545
  return null;
39303
- const match = readFileSync8(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39546
+ const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39304
39547
  return match ? unescapeEnvValue(match[1]) : null;
39305
39548
  }
39306
39549
  function buildInstanceEnv(input4) {
@@ -39343,12 +39586,12 @@ function renderInstanceEnv(env2) {
39343
39586
  `);
39344
39587
  }
39345
39588
  function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
39346
- const path11 = resolve14(cwd, file);
39589
+ const path11 = resolve15(cwd, file);
39347
39590
  writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
39348
39591
  return path11;
39349
39592
  }
39350
39593
  function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
39351
- const path11 = resolve14(cwd, file);
39594
+ const path11 = resolve15(cwd, file);
39352
39595
  return {
39353
39596
  INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
39354
39597
  SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
@@ -39502,7 +39745,7 @@ function registerStartCommand(program2) {
39502
39745
  }
39503
39746
  // src/cli.ts
39504
39747
  var { version } = package_default;
39505
- program.name("secondlayer").alias("sl").description("Secondlayer CLI — self-hosted Stacks data runtime").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides SL_API_KEY)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `secondlayer --help` to see available commands)");
39748
+ program.name("secondlayer").alias("sl").description("Secondlayer CLI — run a Stacks index on your own hardware").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides SL_API_KEY)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `secondlayer --help` to see available commands)");
39506
39749
  program.hook("preAction", (thisCommand) => {
39507
39750
  const { network, apiKey, apiUrl } = thisCommand.opts();
39508
39751
  if (network)
@@ -39516,7 +39759,7 @@ program.addHelpText("after", `
39516
39759
  Quickstart:
39517
39760
  $ secondlayer init --network mainnet
39518
39761
  $ secondlayer bootstrap --against <manifest>
39519
- $ secondlayer subgraphs create my-watcher --template sip-010-balances
39762
+ $ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
39520
39763
  $ secondlayer subgraphs deploy subgraphs/my-watcher.ts
39521
39764
  `);
39522
39765
  program.commandsGroup("Getting started:");
@@ -39528,7 +39771,7 @@ registerConsoleCommand(program);
39528
39771
  registerLoginCommand(program);
39529
39772
  registerLogoutCommand(program);
39530
39773
  registerWhoamiCommand(program);
39531
- program.commandsGroup("Data products:");
39774
+ program.commandsGroup("Your data:");
39532
39775
  registerSubgraphsCommand(program);
39533
39776
  registerSubscriptionsCommand(program);
39534
39777
  registerStreamsCommand(program);
@@ -39560,9 +39803,10 @@ registerVerifyCommand(program);
39560
39803
  registerRepairCommand(program);
39561
39804
  registerBackupCommand(program);
39562
39805
  registerRestoreCommand(program);
39563
- program.commandsGroup("Account:");
39806
+ registerUninstallCommand(program);
39807
+ program.commandsGroup("Archive:");
39564
39808
  registerCreditsCommand(program);
39565
39809
  program.parse();
39566
39810
 
39567
- //# debugId=D0F621CCA5D6F2E664756E2164756E21
39811
+ //# debugId=5BF91F30BB4466C964756E2164756E21
39568
39812
  //# sourceMappingURL=cli.js.map