@secondlayer/cli 11.0.0 → 12.1.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/dist/cli.js CHANGED
@@ -10414,9 +10414,9 @@ function lowercaseKeys(object) {
10414
10414
 
10415
10415
  // ../../node_modules/responselike/index.js
10416
10416
  import { Readable as ReadableStream2 } from "node:stream";
10417
- var Response;
10417
+ var Response2;
10418
10418
  var init_responselike = __esm(() => {
10419
- Response = class Response extends ReadableStream2 {
10419
+ Response2 = class Response2 extends ReadableStream2 {
10420
10420
  statusCode;
10421
10421
  headers;
10422
10422
  body;
@@ -10854,7 +10854,7 @@ class CacheableRequest {
10854
10854
  response.once("end", resolve);
10855
10855
  });
10856
10856
  const headers = convertHeaders(revalidatedPolicy.policy.responseHeaders());
10857
- response = new Response({ statusCode: revalidate.statusCode, headers, body: revalidate.body, url: revalidate.url });
10857
+ response = new Response2({ statusCode: revalidate.statusCode, headers, body: revalidate.body, url: revalidate.url });
10858
10858
  response.cachePolicy = revalidatedPolicy.policy;
10859
10859
  response.fromCache = true;
10860
10860
  }
@@ -10930,7 +10930,7 @@ class CacheableRequest {
10930
10930
  const policy = import_http_cache_semantics.default.fromObject(cacheEntry.cachePolicy);
10931
10931
  if (policy.satisfiesWithoutRevalidation(options_) && !options_.forceRefresh) {
10932
10932
  const headers = convertHeaders(policy.responseHeaders());
10933
- const response = new Response({ statusCode: cacheEntry.statusCode, headers, body: cacheEntry.body, url: cacheEntry.url });
10933
+ const response = new Response2({ statusCode: cacheEntry.statusCode, headers, body: cacheEntry.body, url: cacheEntry.url });
10934
10934
  response.cachePolicy = policy;
10935
10935
  response.fromCache = true;
10936
10936
  ee.emit("response", response);
@@ -28878,23 +28878,8 @@ var init_require_auth = __esm(() => {
28878
28878
  });
28879
28879
 
28880
28880
  // src/templates/subgraph.ts
28881
- function generateSubgraphTemplate(name, slug = "basic") {
28882
- const body = (() => {
28883
- switch (slug) {
28884
- case "sip-010-balances":
28885
- return sip010Balances(name);
28886
- case "sbtc-flows":
28887
- return sbtcFlows(name);
28888
- case "pox-stacking":
28889
- return poxStacking(name);
28890
- case "bns-names":
28891
- return bnsNames(name);
28892
- default:
28893
- return basic(name);
28894
- }
28895
- })();
28896
- return `${nextStepsHeader(name)}
28897
- ${body}`;
28881
+ function generateSubgraphStarter(name) {
28882
+ return `${nextStepsHeader(name)}${starter(name)}`;
28898
28883
  }
28899
28884
  function nextStepsHeader(name) {
28900
28885
  return `// ───────────────────────────────────────────────────────────────────
@@ -28903,20 +28888,19 @@ function nextStepsHeader(name) {
28903
28888
  // 1. Edit the source filter + schema below to match what you want to track.
28904
28889
  // 2. Edit the handler at the bottom — it runs once per matching event.
28905
28890
  // 3. Deploy: secondlayer subgraphs deploy ${name}.ts
28906
- // (You'll be prompted to log in if this is your first remote deploy.)
28907
28891
  // 4. Wait for sync: secondlayer subgraphs status ${name}
28908
- // Mainnet backfill from genesis can take an hour or more depending on
28909
- // your filter scope. Use --start-block to skip ahead.
28892
+ // Backfill from genesis can take an hour or more depending on your
28893
+ // filter scope. Use --start-block to skip ahead.
28910
28894
  // 5. Query: secondlayer subgraphs query ${name} <table-name>
28911
- // Or hit the auto-generated REST endpoint listed in the deploy output.
28895
+ // Or hit the REST endpoint listed in the deploy output.
28912
28896
  //
28913
- // Bind a typed Subscription to any table you write here — see
28914
- // https://www.secondlayer.tools/docs/subscriptions
28897
+ // Prefer generating from a real contract:
28898
+ // secondlayer subgraphs create ${name} --from-contract SP....my-contract
28915
28899
  // ───────────────────────────────────────────────────────────────────
28916
28900
 
28917
28901
  `;
28918
28902
  }
28919
- function basic(name) {
28903
+ function starter(name) {
28920
28904
  return `import { defineSubgraph } from "@secondlayer/subgraphs";
28921
28905
 
28922
28906
  export default defineSubgraph({
@@ -28924,7 +28908,7 @@ export default defineSubgraph({
28924
28908
  version: "1.0.0",
28925
28909
  description: "TODO: describe what this subgraph tracks",
28926
28910
 
28927
- // Sources define what blockchain data this subgraph processes.
28911
+ // Sources define what chain data this subgraph processes.
28928
28912
  // Each source is named — the name becomes the handler key.
28929
28913
  //
28930
28914
  // Filter types:
@@ -28969,345 +28953,6 @@ export default defineSubgraph({
28969
28953
  });
28970
28954
  `;
28971
28955
  }
28972
- function sip010Balances(name) {
28973
- return `import { defineSchema, defineSubgraph, readContractAt, type TypedSubgraphContext } from "@secondlayer/subgraphs";
28974
-
28975
- /**
28976
- * Track SIP-010 token balances per (asset_identifier, holder).
28977
- * Mirrors the shape of the curated sBTC token-events view
28978
- * surface — but works for ANY SIP-010 token. Constrain to a single
28979
- * token by adding \`assetIdentifier: "SP...token::token-name"\` to each
28980
- * source filter.
28981
- *
28982
- * Balances are moved with \`ctx.increment\`, which applies a DELTA in one
28983
- * atomic statement. That matters: a read-modify-write (findOne → compute →
28984
- * upsert) loses concurrent updates to the same row, and it is not replay-safe
28985
- * under \`backfillMode: "concurrent"\`. Deltas commute, so order doesn't
28986
- * matter and a reorg rewind reverses cleanly via the journal.
28987
- *
28988
- * Token metadata (decimals/symbol) comes from the token contract itself via
28989
- * \`readContractAt\`, so \`amount\` stops being a bare integer nobody can
28990
- * render. The read is pinned to the block being processed — a handler stays a
28991
- * pure function of its block — and \`cache: "contract-constant"\` says these
28992
- * two values can never change for a given token, so each is fetched ONCE
28993
- * instead of once per block.
28994
- *
28995
- * Query examples once deployed:
28996
- * GET /v1/subgraphs/${name}/balances?_search=SP1...
28997
- * GET /v1/subgraphs/${name}/balances?holder=SP1...
28998
- * GET /v1/subgraphs/${name}/tokens
28999
- */
29000
-
29001
- /** The two SIP-010 read-only functions this subgraph calls. */
29002
- const SIP010_ABI = {
29003
- functions: [
29004
- { name: "get-decimals", access: "read-only", args: [], outputs: { response: { ok: "uint128", error: "uint128" } } },
29005
- { name: "get-symbol", access: "read-only", args: [], outputs: { response: { ok: { "string-ascii": { length: 32 } }, error: "uint128" } } },
29006
- ],
29007
- } as const;
29008
-
29009
- /** Tokens already labelled in this process — metadata is immutable, so one
29010
- * resolution per token is enough even across a backfill. */
29011
- const labelled = new Set<string>();
29012
-
29013
- // Hoisted so the helper below can be typed against it.
29014
- const schema = defineSchema({
29015
- balances: {
29016
- columns: {
29017
- asset_identifier: { type: "text", indexed: true, search: true },
29018
- holder: { type: "principal", indexed: true, search: true },
29019
- amount: { type: "uint" },
29020
- },
29021
- // increment() upserts on this key.
29022
- uniqueKeys: [["asset_identifier", "holder"]],
29023
- },
29024
- tokens: {
29025
- columns: {
29026
- asset_identifier: { type: "text", indexed: true },
29027
- contract_id: { type: "text" },
29028
- symbol: { type: "text" },
29029
- decimals: { type: "uint" },
29030
- },
29031
- uniqueKeys: [["asset_identifier"]],
29032
- },
29033
- });
29034
-
29035
- export default defineSubgraph({
29036
- name: "${name}",
29037
- version: "1.0.0",
29038
- description: "Per-token balance tracking for any SIP-010 asset",
29039
-
29040
- sources: {
29041
- transfer: { type: "ft_transfer" },
29042
- mint: { type: "ft_mint" },
29043
- burn: { type: "ft_burn" },
29044
- },
29045
-
29046
- schema,
29047
-
29048
- handlers: {
29049
- transfer: async (event, ctx) => {
29050
- await labelToken(event.assetIdentifier, ctx);
29051
- const amount = BigInt(event.amount ?? 0);
29052
- if (event.sender) {
29053
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.sender }, { amount: -amount });
29054
- }
29055
- if (event.recipient) {
29056
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.recipient }, { amount });
29057
- }
29058
- },
29059
- mint: (event, ctx) => {
29060
- if (event.recipient) {
29061
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.recipient }, { amount: BigInt(event.amount ?? 0) });
29062
- }
29063
- },
29064
- burn: (event, ctx) => {
29065
- if (event.sender) {
29066
- ctx.increment("balances", { asset_identifier: event.assetIdentifier, holder: event.sender }, { amount: -BigInt(event.amount ?? 0) });
29067
- }
29068
- },
29069
- },
29070
- });
29071
-
29072
- /** Resolve a token's symbol + decimals from its contract, once. */
29073
- async function labelToken(
29074
- assetIdentifier: string | undefined,
29075
- ctx: TypedSubgraphContext<typeof schema>,
29076
- ) {
29077
- if (!assetIdentifier || labelled.has(assetIdentifier)) return;
29078
- labelled.add(assetIdentifier);
29079
- const contractId = assetIdentifier.split("::")[0];
29080
- const token = readContractAt(ctx, contractId, SIP010_ABI, { cache: "contract-constant" });
29081
- const [decimals, symbol] = await Promise.all([
29082
- token.read.getDecimals({}),
29083
- token.read.getSymbol({}),
29084
- ]);
29085
- ctx.upsert("tokens", { asset_identifier: assetIdentifier }, { asset_identifier: assetIdentifier, contract_id: contractId, symbol, decimals });
29086
- }
29087
- `;
29088
- }
29089
- function sbtcFlows(name) {
29090
- return `import { defineSubgraph } from "@secondlayer/subgraphs";
29091
-
29092
- /**
29093
- * Track sBTC protocol flows: deposits, withdrawals, signer rotations,
29094
- * governance updates — your own indexed view of the sbtc-registry contract.
29095
- *
29096
- * Source contract: sbtc-registry (mainnet).
29097
- *
29098
- * Query examples once deployed:
29099
- * GET /v1/subgraphs/${name}/flows?topic=completed-deposit
29100
- * GET /v1/subgraphs/${name}/flows?topic=withdrawal-create
29101
- */
29102
- export default defineSubgraph({
29103
- name: "${name}",
29104
- version: "1.0.0",
29105
- description: "sBTC deposits, withdrawals, signer rotations, governance",
29106
-
29107
- // Skip pre-sBTC history. Raise this (e.g., to a recent block near tip) for
29108
- // a smaller backfill, or lower it if you need every sBTC event from genesis.
29109
- startBlock: 860000,
29110
-
29111
- sources: {
29112
- registry: {
29113
- type: "print_event",
29114
- contractId: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry",
29115
- },
29116
- },
29117
-
29118
- schema: {
29119
- flows: {
29120
- columns: {
29121
- topic: { type: "text", indexed: true, search: true },
29122
- request_id: { type: "uint", nullable: true, indexed: true },
29123
- amount: { type: "text", nullable: true },
29124
- sender: { type: "principal", nullable: true, indexed: true },
29125
- bitcoin_txid: { type: "text", nullable: true, search: true },
29126
- burn_height: { type: "uint", nullable: true },
29127
- },
29128
- },
29129
- },
29130
-
29131
- handlers: {
29132
- registry: (event, ctx) => {
29133
- if (!event.topic) return;
29134
- // event.data is untyped across topics — cast to the fields you read, or
29135
- // declare \`prints\` per topic on the source for fully typed event.data.
29136
- const data = event.data as {
29137
- requestId?: bigint;
29138
- amount?: bigint | string;
29139
- sender?: string;
29140
- bitcoinTxid?: string;
29141
- burnHeight?: bigint;
29142
- };
29143
- ctx.insert("flows", {
29144
- topic: event.topic,
29145
- request_id: data.requestId ?? null,
29146
- amount: data.amount != null ? String(data.amount) : null,
29147
- sender: data.sender ?? null,
29148
- bitcoin_txid: data.bitcoinTxid ?? null,
29149
- burn_height: data.burnHeight ?? null,
29150
- });
29151
- },
29152
- },
29153
- });
29154
- `;
29155
- }
29156
- function poxStacking(name) {
29157
- return `import { defineSubgraph } from "@secondlayer/subgraphs";
29158
- import { POX5_ABI } from "@secondlayer/stacks/pox5";
29159
-
29160
- /**
29161
- * Track PoX-5 \`stake\` calls with their arguments already decoded.
29162
- *
29163
- * pox-5 replaced pox-4 when Epoch 4.0 activated (Bitcoin block 960,230,
29164
- * 2026-07-30). SIP-045 reshaped the API: stacking became bonds + staking, so
29165
- * the entry point is \`stake\`, not \`stack-stx\`.
29166
- *
29167
- * The source pairs \`abi\` with \`functionName\`, which is what makes
29168
- * \`event.input\` a typed, named argument set instead of positional
29169
- * \`args[0] as bigint\`. Add one source per function you want to track.
29170
- *
29171
- * Query examples once deployed:
29172
- * GET /v1/subgraphs/${name}/calls?caller=SP1...
29173
- * GET /v1/subgraphs/${name}/calls?caller=SP1...
29174
- */
29175
- export default defineSubgraph({
29176
- name: "${name}",
29177
- version: "1.0.0",
29178
- description: "PoX-5 stake calls with decoded arguments",
29179
-
29180
- sources: {
29181
- pox: {
29182
- type: "contract_call",
29183
- // pox-5 is the live PoX contract since Epoch 4.0 activated at Bitcoin
29184
- // block 960,230 (2026-07-30). Stacking history before that is pox-4 —
29185
- // point a second source at \`.pox-4\` if you need it.
29186
- contractId: "SP000000000000000000002Q6VF78.pox-5",
29187
- // \`abi\` + \`functionName\` together are what type \`event.input\`: the
29188
- // arguments cannot be typed without knowing which function was called.
29189
- // POX5_ABI ships \`as const\` in @secondlayer/stacks, so there is no
29190
- // hand-copied ABI to drift. Add a source per function you care about.
29191
- functionName: "stake",
29192
- abi: POX5_ABI,
29193
- },
29194
- },
29195
-
29196
- schema: {
29197
- calls: {
29198
- columns: {
29199
- function_name: { type: "text", indexed: true, search: true },
29200
- caller: { type: "principal", indexed: true, search: true },
29201
- amount_ustx: { type: "uint" },
29202
- num_cycles: { type: "uint" },
29203
- result_ok: { type: "boolean" },
29204
- },
29205
- },
29206
- },
29207
-
29208
- handlers: {
29209
- pox: (event, ctx) => {
29210
- // \`event.input\` is the decoded, named, typed argument set — no
29211
- // \`args[0] as bigint\`. Hover \`amountUstx\` and it is a bigint.
29212
- const resultHex = event.resultHex ?? "";
29213
- ctx.insert("calls", {
29214
- function_name: event.functionName || ctx.tx.functionName || "",
29215
- caller: ctx.tx.sender,
29216
- amount_ustx: event.input.amountUstx,
29217
- num_cycles: event.input.numCycles,
29218
- result_ok: resultHex.startsWith("0x07"), // 0x07 = response-ok type tag
29219
- });
29220
- },
29221
- },
29222
- });
29223
- `;
29224
- }
29225
- function bnsNames(name) {
29226
- return `import { defineSubgraph } from "@secondlayer/subgraphs";
29227
-
29228
- /**
29229
- * Track BNS-V2 name lifecycle events — registrations, transfers,
29230
- * renewals, burns, airdrops.
29231
- *
29232
- * Source: BNS-V2 print events (topic-discriminated payloads).
29233
- *
29234
- * Query examples once deployed:
29235
- * GET /v1/subgraphs/${name}/names?owner=SP1...
29236
- * GET /v1/subgraphs/${name}/names?_search=alice
29237
- */
29238
- export default defineSubgraph({
29239
- name: "${name}",
29240
- version: "1.0.0",
29241
- description: "BNS-V2 name ownership and lifecycle",
29242
-
29243
- sources: {
29244
- bns: {
29245
- type: "print_event",
29246
- contractId: "SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF.BNS-V2",
29247
- },
29248
- },
29249
-
29250
- schema: {
29251
- names: {
29252
- columns: {
29253
- topic: { type: "text", indexed: true },
29254
- namespace: { type: "text", indexed: true, search: true },
29255
- name: { type: "text", indexed: true, search: true },
29256
- fqn: { type: "text", indexed: true, search: true },
29257
- owner: { type: "principal", nullable: true, indexed: true, search: true },
29258
- },
29259
- },
29260
- },
29261
-
29262
- handlers: {
29263
- bns: (event, ctx) => {
29264
- if (!event.topic) return;
29265
- const data = event.data as { namespace?: unknown; name?: unknown; owner?: string };
29266
- const namespace = decodeBuffUtf8(data.namespace);
29267
- const nameLabel = decodeBuffUtf8(data.name);
29268
- if (!namespace || !nameLabel) return;
29269
- ctx.insert("names", {
29270
- topic: event.topic,
29271
- namespace,
29272
- name: nameLabel,
29273
- fqn: \`\${nameLabel}.\${namespace}\`,
29274
- owner: event.topic === "burn-name" ? null : (data.owner ?? null),
29275
- });
29276
- },
29277
- },
29278
- });
29279
-
29280
- function decodeBuffUtf8(value: unknown): string | null {
29281
- if (typeof value !== "string") return null;
29282
- const hex = value.startsWith("0x") ? value.slice(2) : value;
29283
- if (hex.length === 0) return null;
29284
- const bytes = new Uint8Array(hex.length / 2);
29285
- for (let i = 0; i < bytes.length; i++) {
29286
- bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
29287
- }
29288
- let end = bytes.length;
29289
- while (end > 0 && bytes[end - 1] === 0) end -= 1;
29290
- return new TextDecoder("utf-8").decode(bytes.subarray(0, end));
29291
- }
29292
- `;
29293
- }
29294
- var SUBGRAPH_TEMPLATE_SLUGS, SUBGRAPH_TEMPLATE_DESCRIPTIONS;
29295
- var init_subgraph = __esm(() => {
29296
- SUBGRAPH_TEMPLATE_SLUGS = [
29297
- "basic",
29298
- "sip-010-balances",
29299
- "sbtc-flows",
29300
- "pox-stacking",
29301
- "bns-names"
29302
- ];
29303
- SUBGRAPH_TEMPLATE_DESCRIPTIONS = {
29304
- basic: "Empty starter — pick any source filter type",
29305
- "sip-010-balances": "SIP-010 token balances (transfers + mints + burns)",
29306
- "sbtc-flows": "sBTC protocol flows (deposits, withdrawals, signer rotations)",
29307
- "pox-stacking": "PoX-4 stacking lifecycle calls",
29308
- "bns-names": "BNS-V2 name ownership and lifecycle"
29309
- };
29310
- });
29311
28956
 
29312
28957
  // src/commands/subgraph-test.ts
29313
28958
  var exports_subgraph_test = {};
@@ -29969,27 +29614,15 @@ function formatSubgraphSync(sync) {
29969
29614
  }
29970
29615
  function registerSubgraphsCommand(program2) {
29971
29616
  const subgraphs = program2.command("subgraphs").description("Manage materialized subgraphs");
29972
- subgraphs.command("create <name>").description("Create a new subgraph definition file").option("--template <slug>", `Starter template (one of: ${SUBGRAPH_TEMPLATE_SLUGS.join(", ")})`).option("--from-contract <contractId>", "Generate sources/schema/handlers from the contract's observed print events (requires network)").option("--table-per-topic", "With --from-contract: one table per print topic instead of a single wide table").addHelpText("after", `
29617
+ subgraphs.command("create <name>").description("Create a new subgraph definition file").option("--from-contract <contractId>", "Generate sources/schema/handlers from the contract's observed print events (requires network)").option("--table-per-topic", "With --from-contract: one table per print topic instead of a single wide table").addHelpText("after", `
29973
29618
  Examples:
29974
29619
  $ secondlayer subgraphs create my-graph
29975
- $ secondlayer subgraphs create token-balances --template sip-010-balances
29976
29620
  $ secondlayer subgraphs create sbtc-flows --from-contract SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry
29977
29621
  $ secondlayer subgraphs create sbtc-flows --from-contract SM3....sbtc-registry --table-per-topic`).action(async (name, opts) => {
29978
- if (opts.fromContract && opts.template) {
29979
- error("--from-contract and --template are mutually exclusive");
29980
- process.exit(1);
29981
- }
29982
29622
  if (opts.tablePerTopic && !opts.fromContract) {
29983
29623
  error("--table-per-topic requires --from-contract");
29984
29624
  process.exit(1);
29985
29625
  }
29986
- const slug = opts.template ?? "basic";
29987
- if (!opts.fromContract && !SUBGRAPH_TEMPLATE_SLUGS.includes(slug)) {
29988
- error(`Unknown template "${opts.template}". Available templates:
29989
- ${SUBGRAPH_TEMPLATE_SLUGS.map((s2) => ` ${s2.padEnd(20)} ${SUBGRAPH_TEMPLATE_DESCRIPTIONS[s2]}`).join(`
29990
- `)}`);
29991
- process.exit(1);
29992
- }
29993
29626
  const dir = resolve3("subgraphs");
29994
29627
  const filePath = resolve3(dir, `${name}.ts`);
29995
29628
  if (existsSync3(filePath)) {
@@ -30018,7 +29651,7 @@ Check the contract id, and that the contract has emitted print events (schemas a
30018
29651
  sample: schema.sample
30019
29652
  }));
30020
29653
  } else {
30021
- content = generateSubgraphTemplate(name, slug);
29654
+ content = generateSubgraphStarter(name);
30022
29655
  }
30023
29656
  if (!existsSync3(dir)) {
30024
29657
  mkdirSync2(dir, { recursive: true });
@@ -30027,8 +29660,6 @@ Check the contract id, and that the contract has emitted print events (schemas a
30027
29660
  success(`Created ${filePath}`);
30028
29661
  if (opts.fromContract) {
30029
29662
  info(`Schema inferred from ${opts.fromContract} print events (${opts.tablePerTopic ? "table per topic" : "single wide table"})`);
30030
- } else if (slug !== "basic") {
30031
- info(`Template: ${slug} — ${SUBGRAPH_TEMPLATE_DESCRIPTIONS[slug]}`);
30032
29663
  }
30033
29664
  info(`Next: secondlayer subgraphs deploy subgraphs/${name}.ts`);
30034
29665
  });
@@ -30788,7 +30419,6 @@ var init_subgraphs2 = __esm(() => {
30788
30419
  init_resolve_auth();
30789
30420
  init_resolve_auth();
30790
30421
  init_clarity();
30791
- init_subgraph();
30792
30422
  init_api();
30793
30423
  init_format();
30794
30424
  ORM_ONLY_CODEGEN_FLAGS = {
@@ -34690,14 +34320,14 @@ init_esm();
34690
34320
  // package.json
34691
34321
  var package_default = {
34692
34322
  name: "@secondlayer/cli",
34693
- version: "11.0.0",
34323
+ version: "12.1.0",
34324
+ description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
34694
34325
  repository: {
34695
34326
  type: "git",
34696
34327
  url: "git+https://github.com/ryanwaits/secondlayer.git",
34697
34328
  directory: "packages/cli"
34698
34329
  },
34699
34330
  homepage: "https://github.com/ryanwaits/secondlayer/tree/main/packages/cli#readme",
34700
- description: "CLI for subgraphs and blockchain indexing on Stacks",
34701
34331
  type: "module",
34702
34332
  bin: {
34703
34333
  secondlayer: "./dist/cli.js",
@@ -34732,11 +34362,11 @@ var package_default = {
34732
34362
  "@dsnp/parquetjs": "^1.8.8",
34733
34363
  "@inquirer/prompts": "^8.2.0",
34734
34364
  "@secondlayer/bundler": "^0.5.1",
34735
- "@secondlayer/scaffold": "^1.5.3",
34736
- "@secondlayer/sdk": "^7.0.0",
34737
- "@secondlayer/shared": "^8.0.0",
34738
- "@secondlayer/stacks": "^3.3.1",
34739
- "@secondlayer/subgraphs": "^4.0.0",
34365
+ "@secondlayer/scaffold": "^1.5.4",
34366
+ "@secondlayer/sdk": "^8.0.1",
34367
+ "@secondlayer/shared": "^10.0.0",
34368
+ "@secondlayer/stacks": "^4.0.0",
34369
+ "@secondlayer/subgraphs": "^4.0.2",
34740
34370
  esbuild: "^0.27.4",
34741
34371
  "smol-toml": "^1.6.1",
34742
34372
  zod: "^4.3.6"
@@ -39063,6 +38693,7 @@ Exit codes:
39063
38693
  });
39064
38694
  }
39065
38695
  // src/commands/bootstrap.ts
38696
+ import { createHash as createHash3 } from "node:crypto";
39066
38697
  import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
39067
38698
  import { tmpdir as tmpdir3 } from "node:os";
39068
38699
  import { join as join16 } from "node:path";
@@ -39074,6 +38705,7 @@ import {
39074
38705
  } from "@secondlayer/shared/archive/copy-loader";
39075
38706
  import { computeRangeDigest as computeRangeDigest3 } from "@secondlayer/shared/archive/range-digest";
39076
38707
  import { getDb as getDb4, getRawClient, sql as sql3 } from "@secondlayer/shared/db";
38708
+ import { upsertSyncScope } from "@secondlayer/shared/db/queries/sync-scope";
39077
38709
 
39078
38710
  // src/lib/bootstrap-resume.ts
39079
38711
  function planTornImport(input4) {
@@ -39110,6 +38742,18 @@ var BOOTSTRAP_EXIT = {
39110
38742
  };
39111
38743
  var BLOCKS_PER_DAY_ESTIMATE = 5400;
39112
38744
  var RESTORE_ROWS_PER_SECOND = 30000;
38745
+ function archiveScopeBounds(partitions) {
38746
+ if (partitions.length === 0)
38747
+ return null;
38748
+ return {
38749
+ start_height: Math.min(...partitions.map((p2) => p2.from_block)),
38750
+ tip_height: Math.max(...partitions.map((p2) => p2.to_block))
38751
+ };
38752
+ }
38753
+ function manifestDigest(manifest) {
38754
+ const { signature: _signature, key_id: _keyId, ...payload } = manifest;
38755
+ return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
38756
+ }
39113
38757
  async function readNodeTip() {
39114
38758
  const url = process.env.STACKS_NODE_RPC_URL ?? "http://localhost:20443";
39115
38759
  try {
@@ -39164,10 +38808,11 @@ async function loadPartition(reference, partition, rawClient) {
39164
38808
  return written;
39165
38809
  }
39166
38810
  function attachBootstrapCommand(cmd) {
39167
- return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
38811
+ return cmd.requiredOption("--against <manifest>", "archive manifest: an https URL or a local file path").option("--to-block <n>", "stop at this height instead of the archive tip").option("--from-block <n>", "forward-only: restore from this height instead of genesis; earlier history is declared out of scope").option("--public-key <pem>", "pin the signing key instead of fetching it").option("-y, --yes", "skip the confirmation prompt").option("--json", "Output as JSON").addHelpText("after", `
39168
38812
  Examples:
39169
38813
  $ secondlayer bootstrap --against https://archive.secondlayer.tools/.../snapshots/<digest>.json
39170
38814
  $ secondlayer bootstrap --against ./snapshot.json --to-block 4000000 --yes
38815
+ $ secondlayer bootstrap --against ./snapshot.json --from-block 8000000 --yes
39171
38816
 
39172
38817
  Exit codes:
39173
38818
  0 restored and verified
@@ -39192,7 +38837,8 @@ Exit codes:
39192
38837
  const maxHeightRow = await db.selectFrom("blocks").select(({ fn }) => fn.max("height").as("max")).executeTakeFirst();
39193
38838
  const maxBlockHeight = maxHeightRow?.max === null || maxHeightRow?.max === undefined ? null : Number(maxHeightRow.max);
39194
38839
  const toBlock = opts.toBlock === undefined ? undefined : Number(opts.toBlock);
39195
- const declared = (reference.manifest.partitions ?? []).filter((p2) => toBlock === undefined || p2.to_block <= toBlock);
38840
+ const fromBlock = opts.fromBlock === undefined ? undefined : Number(opts.fromBlock);
38841
+ const declared = (reference.manifest.partitions ?? []).filter((p2) => (toBlock === undefined || p2.to_block <= toBlock) && (fromBlock === undefined || p2.from_block >= fromBlock));
39196
38842
  const resume = planTornImport({
39197
38843
  hasIndexProgress: !!progress,
39198
38844
  maxBlockHeight,
@@ -39219,6 +38865,8 @@ Exit codes:
39219
38865
  });
39220
38866
  process.exit(BOOTSTRAP_EXIT.REFUSED);
39221
38867
  }
38868
+ const scopeBounds = archiveScopeBounds(declared);
38869
+ const startHeight = scopeBounds?.start_height ?? 0;
39222
38870
  const totalRows = partitions.reduce((sum, p2) => sum + p2.row_count, 0);
39223
38871
  const totalBytes = partitions.reduce((sum, p2) => sum + p2.byte_size, 0);
39224
38872
  const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
@@ -39228,7 +38876,10 @@ Exit codes:
39228
38876
  console.error("");
39229
38877
  console.error(formatKeyValue([
39230
38878
  ["archive", reference.origin],
39231
- ["coverage", `genesis → ${tipHeight.toLocaleString()}`],
38879
+ [
38880
+ "coverage",
38881
+ `${startHeight === 0 ? "genesis" : startHeight.toLocaleString()} → ${tipHeight.toLocaleString()}`
38882
+ ],
39232
38883
  ["rows", totalRows.toLocaleString()],
39233
38884
  ["download", `${(totalBytes / 1e9).toFixed(1)} GB`],
39234
38885
  ["signature", "verified"]
@@ -39286,6 +38937,17 @@ Exit codes:
39286
38937
  highest_seen_block = GREATEST(index_progress.highest_seen_block, EXCLUDED.highest_seen_block),
39287
38938
  updated_at = NOW()
39288
38939
  `.execute(db);
38940
+ const genesisRow = startHeight === 0 ? await db.selectFrom("blocks").select("hash").where("canonical", "=", true).where("height", "=", 0).executeTakeFirst() : undefined;
38941
+ await upsertSyncScope(db, {
38942
+ network,
38943
+ start_height: startHeight,
38944
+ target_height: null,
38945
+ bootstrap: {
38946
+ source: "archive",
38947
+ manifest_digest: manifestDigest(reference.manifest),
38948
+ genesis_hash: genesisRow?.hash ?? null
38949
+ }
38950
+ });
39289
38951
  const seam = nodeTipAtStart !== null && nodeTipAtStart > tipHeight ? {
39290
38952
  nodeTip: nodeTipAtStart,
39291
38953
  gap: nodeTipAtStart - tipHeight
@@ -39294,6 +38956,7 @@ Exit codes:
39294
38956
  const report = {
39295
38957
  status: divergent === 0 ? "restored" : "divergent",
39296
38958
  archive: reference.origin,
38959
+ start_height: startHeight,
39297
38960
  tip_height: tipHeight,
39298
38961
  rows: loaded,
39299
38962
  ranges_verified: referenceDigests.length,
@@ -39316,6 +38979,9 @@ Exit codes:
39316
38979
  }
39317
38980
  console.error("");
39318
38981
  console.error(dim(`Your instance holds history through ${tipHeight.toLocaleString()} and will resume at ${(tipHeight + 1).toLocaleString()}.`));
38982
+ if (startHeight > 0) {
38983
+ console.error(dim(` Scope starts at ${startHeight.toLocaleString()} — earlier history is declared out of scope, not missing.`));
38984
+ }
39319
38985
  if (seam) {
39320
38986
  console.error(dim(`The chain advanced to ${seam.nodeTip.toLocaleString()} while restoring — ${seam.gap.toLocaleString()} blocks to catch up.`));
39321
38987
  console.error(dim(" Start the indexer; it backfills that range before following the tip."));
@@ -39340,10 +39006,339 @@ Exit codes:
39340
39006
  function registerBootstrapCommand(program2) {
39341
39007
  attachBootstrapCommand(program2.command("bootstrap").description("Restore chain history from a verified archive instead of syncing from genesis"));
39342
39008
  }
39009
+ // src/commands/uninstall.ts
39010
+ init_output();
39011
+ import { spawnSync as spawnSync3 } from "node:child_process";
39012
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
39013
+ import { join as join17, resolve as resolve13 } from "node:path";
39014
+ import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
39015
+ var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
39016
+ var DEFAULT_COMPOSE = "docker/oss/docker-compose.yml";
39017
+ function bundleHasKeys(bundleDir) {
39018
+ const manifest = join17(bundleDir, "manifest.json");
39019
+ if (!existsSync8(manifest))
39020
+ return false;
39021
+ try {
39022
+ const parsed = JSON.parse(readFileSync7(manifest, "utf8"));
39023
+ return parsed.secrets != null;
39024
+ } catch {
39025
+ return false;
39026
+ }
39027
+ }
39028
+ function registerUninstallCommand(program2) {
39029
+ 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) => {
39030
+ const dataDir = process.env.DATA_DIR ?? "./data";
39031
+ const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || existsSync8(resolve13(process.cwd(), ".env.local"));
39032
+ const keysBackedUp = opts.backup ? bundleHasKeys(resolve13(opts.backup)) : false;
39033
+ const decision = planUninstall({
39034
+ purge: opts.purge === true,
39035
+ confirmed: opts.yes === true,
39036
+ keysBackedUp,
39037
+ secretsPresent,
39038
+ dataDir
39039
+ });
39040
+ if (!decision.ok) {
39041
+ printError(decision.reason);
39042
+ process.exit(UNINSTALL_EXIT.REFUSED);
39043
+ }
39044
+ const cmd = uninstallCommand(decision.plan, opts.compose);
39045
+ if (!opts.apply) {
39046
+ output({
39047
+ json: opts.json,
39048
+ data: { dryRun: true, plan: decision.plan, command: cmd },
39049
+ human: () => {
39050
+ note("dry run — nothing removed. Pass --apply to proceed.");
39051
+ note(`would run: docker ${cmd.join(" ")}`);
39052
+ for (const p2 of decision.plan.preserves) {
39053
+ note(`preserved · ${p2.what}: ${p2.detail}`);
39054
+ }
39055
+ for (const d2 of decision.plan.destroys) {
39056
+ warn(`DESTROYED · volume ${d2}`);
39057
+ }
39058
+ }
39059
+ });
39060
+ return;
39061
+ }
39062
+ const res = spawnSync3("docker", cmd, { stdio: "inherit" });
39063
+ if ((res.status ?? 1) !== 0) {
39064
+ printError(`docker ${cmd.join(" ")} exited ${res.status}`);
39065
+ process.exit(UNINSTALL_EXIT.FAILED);
39066
+ }
39067
+ output({
39068
+ json: opts.json,
39069
+ data: { removed: true, plan: decision.plan },
39070
+ human: () => {
39071
+ success("stack removed");
39072
+ for (const w2 of decision.warnings)
39073
+ warn(w2);
39074
+ for (const p2 of decision.plan.preserves) {
39075
+ note(`preserved · ${p2.what}: ${p2.detail}`);
39076
+ }
39077
+ }
39078
+ });
39079
+ });
39080
+ }
39081
+ // src/commands/backup.ts
39082
+ init_output();
39083
+ import { createHash as createHash4 } from "node:crypto";
39084
+ import {
39085
+ chmodSync,
39086
+ existsSync as existsSync9,
39087
+ mkdirSync as mkdirSync6,
39088
+ readFileSync as readFileSync8,
39089
+ statSync as statSync3,
39090
+ writeFileSync as writeFileSync7
39091
+ } from "node:fs";
39092
+ import { join as join18, resolve as resolve14 } from "node:path";
39093
+ import { closeDb, getDb as getDb5, sql as sql4 } from "@secondlayer/shared/db";
39094
+ import {
39095
+ decryptBundle,
39096
+ encryptBundle,
39097
+ keyMatchesCanary,
39098
+ planBackup,
39099
+ precheckRestore,
39100
+ sealKeyCanary
39101
+ } from "@secondlayer/shared/runtime";
39102
+ var BACKUP_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
39103
+ var MANIFEST_FILE = "manifest.json";
39104
+ var DB_FILE = "db.dump";
39105
+ var SECRETS_FILE = "secrets.enc";
39106
+ var SECRET_ENV_KEYS = [
39107
+ "INSTANCE_TOKEN",
39108
+ "SECONDLAYER_SECRETS_KEY",
39109
+ "STREAMS_SIGNING_PRIVATE_KEY",
39110
+ "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY",
39111
+ "SL_API_KEY",
39112
+ "ARCHIVE_SIGNING_PUBLIC_KEY"
39113
+ ];
39114
+ function readSecretsKey() {
39115
+ const key = process.env.SECONDLAYER_SECRETS_KEY?.trim();
39116
+ return key ? key : undefined;
39117
+ }
39118
+ function sha256File(path11) {
39119
+ return createHash4("sha256").update(readFileSync8(path11)).digest("hex");
39120
+ }
39121
+ async function readScope() {
39122
+ const db = getDb5();
39123
+ const bounds = await sql4`SELECT MIN(height)::text AS min, MAX(height)::text AS max FROM blocks WHERE canonical = true`.execute(db);
39124
+ const row = bounds.rows[0];
39125
+ const network = process.env.STACKS_NETWORK ?? "mainnet";
39126
+ return {
39127
+ network,
39128
+ fromHeight: row?.min ? Number(row.min) : 0,
39129
+ toHeight: row?.max ? Number(row.max) : null
39130
+ };
39131
+ }
39132
+ function requireDatabaseUrl() {
39133
+ const url = process.env.DATABASE_URL?.trim();
39134
+ if (!url)
39135
+ throw new Error("DATABASE_URL is required");
39136
+ return url;
39137
+ }
39138
+ async function runProcess(cmd, label) {
39139
+ const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
39140
+ const stderr = await new Response(proc.stderr).text();
39141
+ const code = await proc.exited;
39142
+ if (code !== 0) {
39143
+ return { ok: false, stderr: `${label} exited ${code}: ${stderr.trim()}` };
39144
+ }
39145
+ return { ok: true, stderr };
39146
+ }
39147
+ function attachBackupCommand(cmd) {
39148
+ 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) => {
39149
+ try {
39150
+ await runBackup(opts);
39151
+ await closeDb();
39152
+ } catch (error2) {
39153
+ printError(error2 instanceof Error ? error2.message : String(error2));
39154
+ process.exit(BACKUP_EXIT.FAILED);
39155
+ }
39156
+ });
39157
+ }
39158
+ async function runBackup(opts) {
39159
+ const databaseUrl = requireDatabaseUrl();
39160
+ const includeSecrets = opts.secrets !== false;
39161
+ const passphrase = opts.passphrase ?? process.env.SECONDLAYER_BACKUP_PASSPHRASE ?? "";
39162
+ const secretsKey = readSecretsKey();
39163
+ if (includeSecrets && !secretsKey) {
39164
+ printError("SECONDLAYER_SECRETS_KEY is not set, so this backup cannot carry the keys that decrypt your data. Set it, or pass --no-secrets to take a data-only bundle.");
39165
+ process.exit(BACKUP_EXIT.REFUSED);
39166
+ }
39167
+ const scope = await readScope();
39168
+ const plan = planBackup({
39169
+ network: scope.network,
39170
+ fromHeight: scope.fromHeight,
39171
+ toHeight: scope.toHeight,
39172
+ includeSecrets,
39173
+ secretsEncrypted: includeSecrets && passphrase.length > 0,
39174
+ canary: secretsKey ? sealKeyCanary(secretsKey) : undefined
39175
+ });
39176
+ if (!plan.ok) {
39177
+ printError(plan.reason);
39178
+ process.exit(BACKUP_EXIT.REFUSED);
39179
+ }
39180
+ const outDir = resolve14(opts.out);
39181
+ mkdirSync6(outDir, { recursive: true });
39182
+ console.error("dumping database…");
39183
+ const dumpPath = join18(outDir, DB_FILE);
39184
+ const dump = await runProcess(["pg_dump", "-Fc", "--no-owner", "--no-acl", "-f", dumpPath, databaseUrl], "pg_dump");
39185
+ if (!dump.ok) {
39186
+ printError(dump.stderr);
39187
+ process.exit(BACKUP_EXIT.FAILED);
39188
+ }
39189
+ const manifest = {
39190
+ ...plan.manifest,
39191
+ db: {
39192
+ file: DB_FILE,
39193
+ sha256: sha256File(dumpPath),
39194
+ bytes: statSync3(dumpPath).size,
39195
+ format: "pg_dump-custom"
39196
+ }
39197
+ };
39198
+ if (includeSecrets && secretsKey) {
39199
+ const lines2 = SECRET_ENV_KEYS.filter((k2) => process.env[k2]).map((k2) => `${k2}=${process.env[k2]}`);
39200
+ const sealed = encryptBundle(lines2.join(`
39201
+ `), passphrase);
39202
+ const secretsPath = join18(outDir, SECRETS_FILE);
39203
+ writeFileSync7(secretsPath, sealed);
39204
+ chmodSync(secretsPath, 384);
39205
+ manifest.secrets = {
39206
+ file: SECRETS_FILE,
39207
+ encrypted: true,
39208
+ canary: sealKeyCanary(secretsKey)
39209
+ };
39210
+ }
39211
+ writeFileSync7(join18(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
39212
+ `);
39213
+ output({
39214
+ json: opts.json,
39215
+ data: { out: outDir, manifest },
39216
+ human: () => {
39217
+ success(`backup written to ${outDir}`);
39218
+ note(`blocks ${manifest.scope.from_height}–${manifest.scope.to_height ?? "?"} · ${(manifest.db?.bytes ?? 0) / 1024 ** 2 | 0} MB`);
39219
+ if (!manifest.secrets) {
39220
+ warn("no keys in this bundle — it can restore data, but not a working instance");
39221
+ } else {
39222
+ note("keys are encrypted; keep the passphrase somewhere else");
39223
+ }
39224
+ }
39225
+ });
39226
+ }
39227
+ function attachRestoreCommand(cmd) {
39228
+ 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) => {
39229
+ try {
39230
+ await runRestore(opts);
39231
+ await closeDb();
39232
+ } catch (error2) {
39233
+ printError(error2 instanceof Error ? error2.message : String(error2));
39234
+ process.exit(BACKUP_EXIT.FAILED);
39235
+ }
39236
+ });
39237
+ }
39238
+ async function runRestore(opts) {
39239
+ const databaseUrl = requireDatabaseUrl();
39240
+ const dir = resolve14(opts.from);
39241
+ const manifestPath = join18(dir, MANIFEST_FILE);
39242
+ if (!existsSync9(manifestPath)) {
39243
+ printError(`no ${MANIFEST_FILE} in ${dir}`);
39244
+ process.exit(BACKUP_EXIT.REFUSED);
39245
+ }
39246
+ const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
39247
+ const dumpPath = join18(dir, manifest.db?.file ?? DB_FILE);
39248
+ if (manifest.db && sha256File(dumpPath) !== manifest.db.sha256) {
39249
+ printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
39250
+ process.exit(BACKUP_EXIT.REFUSED);
39251
+ }
39252
+ const envKey = readSecretsKey();
39253
+ let bundleKey;
39254
+ if (manifest.secrets) {
39255
+ const passphrase = opts.passphrase ?? process.env.SECONDLAYER_BACKUP_PASSPHRASE ?? "";
39256
+ if (!passphrase) {
39257
+ printError("this bundle carries encrypted keys; supply --passphrase to read them");
39258
+ process.exit(BACKUP_EXIT.REFUSED);
39259
+ }
39260
+ try {
39261
+ const env2 = decryptBundle(readFileSync8(join18(dir, manifest.secrets.file)), passphrase);
39262
+ bundleKey = env2.split(`
39263
+ `).find((l2) => l2.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
39264
+ } catch {
39265
+ printError("could not decrypt the secrets bundle — wrong passphrase?");
39266
+ process.exit(BACKUP_EXIT.REFUSED);
39267
+ }
39268
+ }
39269
+ if (envKey && manifest.secrets && !keyMatchesCanary(manifest.secrets.canary, envKey)) {
39270
+ printError("SECONDLAYER_SECRETS_KEY in this environment is NOT the key this backup was taken with. Restoring would leave every encrypted column unreadable. Unset it to install the bundle's key, or point this restore at the matching instance.");
39271
+ process.exit(BACKUP_EXIT.REFUSED);
39272
+ }
39273
+ const db = getDb5();
39274
+ const present = await sql4`SELECT to_regclass('public.blocks') IS NOT NULL AS present`.execute(db);
39275
+ let targetIsEmpty = true;
39276
+ if (present.rows[0]?.present) {
39277
+ const existing = await sql4`SELECT COUNT(*)::text AS count FROM blocks`.execute(db);
39278
+ targetIsEmpty = (existing.rows[0]?.count ?? "0") === "0";
39279
+ }
39280
+ const check = precheckRestore({
39281
+ manifest,
39282
+ targetNetwork: process.env.STACKS_NETWORK ?? "mainnet",
39283
+ targetIsEmpty,
39284
+ force: opts.force === true,
39285
+ secretsKeyHex: envKey ?? bundleKey
39286
+ });
39287
+ if (!check.ok) {
39288
+ printError(check.reason);
39289
+ process.exit(BACKUP_EXIT.REFUSED);
39290
+ }
39291
+ if (!opts.apply) {
39292
+ output({
39293
+ json: opts.json,
39294
+ data: { dryRun: true, manifest, targetIsEmpty },
39295
+ human: () => {
39296
+ note("dry run — nothing written. Pass --apply to restore.");
39297
+ note(`would restore ${manifest.network} blocks ${manifest.scope.from_height}–${manifest.scope.to_height ?? "?"}`);
39298
+ if (bundleKey && !envKey) {
39299
+ note("after restoring, install the bundle's keys before starting the runtime");
39300
+ }
39301
+ }
39302
+ });
39303
+ return;
39304
+ }
39305
+ console.error("restoring database…");
39306
+ const restore = await runProcess([
39307
+ "pg_restore",
39308
+ "--no-owner",
39309
+ "--no-acl",
39310
+ "--clean",
39311
+ "--if-exists",
39312
+ "-d",
39313
+ databaseUrl,
39314
+ dumpPath
39315
+ ], "pg_restore");
39316
+ if (!restore.ok) {
39317
+ printError(restore.stderr);
39318
+ process.exit(BACKUP_EXIT.FAILED);
39319
+ }
39320
+ output({
39321
+ json: opts.json,
39322
+ data: { restored: true, manifest },
39323
+ human: () => {
39324
+ success("restore complete");
39325
+ if (bundleKey && !envKey) {
39326
+ warn("set SECONDLAYER_SECRETS_KEY from the bundle before starting the runtime, or it will generate a new one and the restored data will not decrypt");
39327
+ }
39328
+ note("verify with: secondlayer verify all --against <manifest>");
39329
+ }
39330
+ });
39331
+ }
39332
+ function registerBackupCommand(program2) {
39333
+ attachBackupCommand(program2.command("backup").description("Write a restorable bundle: index, keys, and scope"));
39334
+ }
39335
+ function registerRestoreCommand(program2) {
39336
+ attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
39337
+ }
39343
39338
  // src/lib/instance-init.ts
39344
39339
  import { randomBytes as randomBytes3 } from "node:crypto";
39345
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
39346
- import { resolve as resolve13 } from "node:path";
39340
+ import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
39341
+ import { resolve as resolve15 } from "node:path";
39347
39342
  import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
39348
39343
  import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
39349
39344
  var INSTANCE_ENV_FILE = ".env.local";
@@ -39377,9 +39372,9 @@ function unescapeEnvValue(raw) {
39377
39372
  `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
39378
39373
  }
39379
39374
  function readEnvValue(envPath, key) {
39380
- if (!existsSync8(envPath))
39375
+ if (!existsSync10(envPath))
39381
39376
  return null;
39382
- const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39377
+ const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39383
39378
  return match ? unescapeEnvValue(match[1]) : null;
39384
39379
  }
39385
39380
  function buildInstanceEnv(input4) {
@@ -39422,12 +39417,12 @@ function renderInstanceEnv(env2) {
39422
39417
  `);
39423
39418
  }
39424
39419
  function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
39425
- const path11 = resolve13(cwd, file);
39426
- writeFileSync7(path11, renderInstanceEnv(env2), { mode: 384 });
39420
+ const path11 = resolve15(cwd, file);
39421
+ writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
39427
39422
  return path11;
39428
39423
  }
39429
39424
  function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
39430
- const path11 = resolve13(cwd, file);
39425
+ const path11 = resolve15(cwd, file);
39431
39426
  return {
39432
39427
  INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
39433
39428
  SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
@@ -39581,7 +39576,7 @@ function registerStartCommand(program2) {
39581
39576
  }
39582
39577
  // src/cli.ts
39583
39578
  var { version } = package_default;
39584
- 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)");
39579
+ 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)");
39585
39580
  program.hook("preAction", (thisCommand) => {
39586
39581
  const { network, apiKey, apiUrl } = thisCommand.opts();
39587
39582
  if (network)
@@ -39595,7 +39590,7 @@ program.addHelpText("after", `
39595
39590
  Quickstart:
39596
39591
  $ secondlayer init --network mainnet
39597
39592
  $ secondlayer bootstrap --against <manifest>
39598
- $ secondlayer subgraphs create my-watcher --template sip-010-balances
39593
+ $ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
39599
39594
  $ secondlayer subgraphs deploy subgraphs/my-watcher.ts
39600
39595
  `);
39601
39596
  program.commandsGroup("Getting started:");
@@ -39607,7 +39602,7 @@ registerConsoleCommand(program);
39607
39602
  registerLoginCommand(program);
39608
39603
  registerLogoutCommand(program);
39609
39604
  registerWhoamiCommand(program);
39610
- program.commandsGroup("Data products:");
39605
+ program.commandsGroup("Your data:");
39611
39606
  registerSubgraphsCommand(program);
39612
39607
  registerSubscriptionsCommand(program);
39613
39608
  registerStreamsCommand(program);
@@ -39637,9 +39632,12 @@ registerConfigCommand(program);
39637
39632
  registerContextCommand(program);
39638
39633
  registerVerifyCommand(program);
39639
39634
  registerRepairCommand(program);
39640
- program.commandsGroup("Account:");
39635
+ registerBackupCommand(program);
39636
+ registerRestoreCommand(program);
39637
+ registerUninstallCommand(program);
39638
+ program.commandsGroup("Archive:");
39641
39639
  registerCreditsCommand(program);
39642
39640
  program.parse();
39643
39641
 
39644
- //# debugId=3107CF5F3E1F73F364756E2164756E21
39642
+ //# debugId=135579D3DFB23FDC64756E2164756E21
39645
39643
  //# sourceMappingURL=cli.js.map