@secondlayer/cli 12.1.0 → 13.0.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 +69 -17
  2. package/dist/cli.js +2520 -509
  3. package/dist/cli.js.map +40 -33
  4. package/package.json +8 -6
package/dist/cli.js CHANGED
@@ -2481,25 +2481,6 @@ var require_commander = __commonJS((exports) => {
2481
2481
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2482
2482
  });
2483
2483
 
2484
- // ../../node_modules/commander/esm.mjs
2485
- var import__, program, createCommand, createArgument, createOption, CommanderError, InvalidArgumentError, InvalidOptionArgumentError, Command, Argument, Option, Help;
2486
- var init_esm = __esm(() => {
2487
- import__ = __toESM(require_commander(), 1);
2488
- ({
2489
- program,
2490
- createCommand,
2491
- createArgument,
2492
- createOption,
2493
- CommanderError,
2494
- InvalidArgumentError,
2495
- InvalidOptionArgumentError,
2496
- Command,
2497
- Argument,
2498
- Option,
2499
- Help
2500
- } = import__.default);
2501
- });
2502
-
2503
2484
  // src/lib/session.ts
2504
2485
  import {
2505
2486
  chmod,
@@ -2550,11 +2531,12 @@ var init_session = __esm(() => {
2550
2531
  });
2551
2532
 
2552
2533
  // src/lib/resolve-auth.ts
2534
+ import { resolveApiKey } from "@secondlayer/sdk";
2553
2535
  function resolveApiUrl() {
2554
2536
  return (process.env.SL_API_URL ?? process.env.SL_PLATFORM_API_URL ?? LOCAL_API_URL).replace(/\/+$/, "");
2555
2537
  }
2556
2538
  function resolveEnvKey() {
2557
- return process.env.SL_API_KEY;
2539
+ return resolveApiKey();
2558
2540
  }
2559
2541
  async function resolveAuth() {
2560
2542
  const apiUrl = resolveApiUrl();
@@ -2619,7 +2601,7 @@ async function httpPlatform(path2, opts = {}) {
2619
2601
  try {
2620
2602
  auth = await resolveAuth();
2621
2603
  } catch {
2622
- throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, isOssMode() ? "Not authenticated — set SL_API_KEY from `secondlayer init`" : "Not logged in — run `secondlayer login`");
2604
+ throw new CliHttpError(401, "SESSION_EXPIRED", { error: "Not logged in" }, isOssMode() ? "Not authenticated — set INSTANCE_TOKEN from `secondlayer init`" : "Not logged in — run `secondlayer login`");
2623
2605
  }
2624
2606
  return request(`${auth.apiUrl}${path2}`, {
2625
2607
  ...opts,
@@ -2725,6 +2707,24 @@ function output(opts) {
2725
2707
  }
2726
2708
  opts.human();
2727
2709
  }
2710
+ function wrapText(text, width = 76) {
2711
+ const words = text.split(/\s+/).filter(Boolean);
2712
+ const lines = [];
2713
+ let line = "";
2714
+ for (const word of words) {
2715
+ if (line.length === 0)
2716
+ line = word;
2717
+ else if (line.length + 1 + word.length <= width)
2718
+ line += ` ${word}`;
2719
+ else {
2720
+ lines.push(line);
2721
+ line = word;
2722
+ }
2723
+ }
2724
+ if (line.length > 0)
2725
+ lines.push(line);
2726
+ return lines;
2727
+ }
2728
2728
  function stripAnsi(text) {
2729
2729
  return text.replace(ANSI_ESCAPE_PATTERN, "");
2730
2730
  }
@@ -28309,7 +28309,7 @@ function handleApiError(err, action2) {
28309
28309
  const status = err instanceof ApiError || err instanceof CliHttpError ? err.status : undefined;
28310
28310
  if (err instanceof CliHttpError && err.code === "SESSION_EXPIRED" || status === 401) {
28311
28311
  printError("Authentication required.", {
28312
- hint: isOssMode() ? "Set SL_API_KEY / INSTANCE_TOKEN from `secondlayer init`." : "Run `secondlayer login` to re-authenticate."
28312
+ hint: isOssMode() ? "Set INSTANCE_TOKEN from `secondlayer init` (SL_API_KEY is a legacy alias)." : "Run `secondlayer login` to re-authenticate."
28313
28313
  });
28314
28314
  process.exit(1);
28315
28315
  }
@@ -28317,6 +28317,19 @@ function handleApiError(err, action2) {
28317
28317
  printError(`Failed to ${action2}: ${detail}`, { hint: nextStepHint(status) });
28318
28318
  process.exit(1);
28319
28319
  }
28320
+ function withErrorHandling(fn, options3) {
28321
+ return async (...args) => {
28322
+ try {
28323
+ await fn(...args);
28324
+ } catch (err) {
28325
+ if (options3?.onError) {
28326
+ options3.onError(err);
28327
+ } else {
28328
+ handleApiError(err, options3?.action ?? "execute command");
28329
+ }
28330
+ }
28331
+ };
28332
+ }
28320
28333
  async function getPlatformClient() {
28321
28334
  const { apiUrl, ephemeralKey } = await resolveAuth();
28322
28335
  return new SecondLayer({ baseUrl: apiUrl, apiKey: ephemeralKey });
@@ -28325,7 +28338,7 @@ async function listSubgraphsApi() {
28325
28338
  return (await getPlatformClient()).subgraphs.list();
28326
28339
  }
28327
28340
  async function getSubgraphApi(name) {
28328
- return (await getPlatformClient()).subgraphs.get(name);
28341
+ return (await getPlatformClient()).subgraphs.status(name);
28329
28342
  }
28330
28343
  async function getSubgraphOpenApi(name, options3) {
28331
28344
  return (await getPlatformClient()).subgraphs.openapi(name, options3);
@@ -28360,6 +28373,15 @@ async function querySubgraphTableCount(name, table, params = {}) {
28360
28373
  async function getSubgraphGaps(name, opts) {
28361
28374
  return (await getPlatformClient()).subgraphs.gaps(name, opts);
28362
28375
  }
28376
+ async function listSubgraphOperationsApi(name) {
28377
+ return (await getPlatformClient()).subgraphs.operations(name);
28378
+ }
28379
+ async function getSubgraphOperationApi(name, operationId) {
28380
+ return (await getPlatformClient()).subgraphs.getOperation(name, operationId);
28381
+ }
28382
+ async function getSubgraphSourceApi(name) {
28383
+ return (await getPlatformClient()).subgraphs.getSource(name);
28384
+ }
28363
28385
  async function getContractPrintSchema(contractId) {
28364
28386
  let client;
28365
28387
  try {
@@ -28761,6 +28783,7 @@ async function runTokenLogin() {
28761
28783
  error('No token on stdin. Usage: echo "$KEY" | secondlayer login --with-token');
28762
28784
  process.exit(1);
28763
28785
  }
28786
+ process.env.INSTANCE_TOKEN = token;
28764
28787
  process.env.SL_API_KEY = token;
28765
28788
  let account;
28766
28789
  try {
@@ -28852,7 +28875,7 @@ function registerLoginCommand(program2) {
28852
28875
  program2.command("login", { hidden: true }).description("Log in to Secondlayer (magic-link email)").option("--force", "Skip the already-logged-in check and re-run the flow").option("--with-token", "Read an API key from stdin (non-interactive)").addHelpText("after", `
28853
28876
  Examples:
28854
28877
  $ secondlayer login
28855
- $ echo "$SL_API_KEY" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
28878
+ $ echo "$INSTANCE_TOKEN" | secondlayer login --with-token`).action((opts) => opts.withToken ? runTokenLogin() : runLoginFlow({ force: opts.force }));
28856
28879
  }
28857
28880
  var init_login = __esm(() => {
28858
28881
  init_http();
@@ -28959,13 +28982,14 @@ var exports_subgraph_test = {};
28959
28982
  __export(exports_subgraph_test, {
28960
28983
  runSubgraphTest: () => runSubgraphTest,
28961
28984
  loadCassette: () => loadCassette,
28985
+ indexReadFailure: () => indexReadFailure,
28962
28986
  filterHashOf: () => filterHashOf,
28963
28987
  DECODER_VERSION: () => DECODER_VERSION
28964
28988
  });
28965
28989
  import { createHash } from "node:crypto";
28966
28990
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
28967
28991
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
28968
- import { Index } from "@secondlayer/sdk";
28992
+ import { Index, resolveApiKey as resolveApiKey2, resolveBaseUrl } from "@secondlayer/sdk";
28969
28993
  import {
28970
28994
  DECODED_EVENT_TYPES
28971
28995
  } from "@secondlayer/stacks/filters";
@@ -29001,6 +29025,53 @@ function eventTypeFor(filter) {
29001
29025
  const candidate = filter.type === "print_event" ? "print" : filter.type;
29002
29026
  return DECODED_EVENT_TYPES.includes(candidate) ? candidate : null;
29003
29027
  }
29028
+ function indexApiUrl() {
29029
+ return resolveBaseUrl();
29030
+ }
29031
+ function indexReadFailure(err, ctx) {
29032
+ const e2 = err;
29033
+ const status = typeof e2?.status === "number" ? e2.status : undefined;
29034
+ const range = `blocks ${ctx.fromHeight}–${ctx.toHeight}`;
29035
+ const where = `reading source "${ctx.source}" (${range}) from ${ctx.apiUrl}`;
29036
+ const offlineHint = `Replay a recorded cassette instead of reading: secondlayer subgraphs test ${ctx.file} --offline`;
29037
+ if (status === 402) {
29038
+ const details = e2.body?.details;
29039
+ return {
29040
+ message: `Blocks ${ctx.fromHeight}–${ctx.toHeight} are below the free read window.`,
29041
+ hint: details?.oldest_seekable_height ? `Try --from ${details.oldest_seekable_height}, or add a paid API key (INSTANCE_TOKEN) to test against older history.` : "Add a paid API key (INSTANCE_TOKEN) to test against older history."
29042
+ };
29043
+ }
29044
+ if (status === 401 || status === 403) {
29045
+ const credential = resolveApiKey2() ? "INSTANCE_TOKEN is set in this shell" : "no INSTANCE_TOKEN is set in this shell";
29046
+ return {
29047
+ message: `Index read rejected (HTTP ${status}) while ${where}.`,
29048
+ hint: `${credential}. This read uses the same SL_API_URL + INSTANCE_TOKEN pair as \`secondlayer subgraphs deploy\` — confirm both point at the instance you deployed to (\`secondlayer status\`), then retry. ${offlineHint}`
29049
+ };
29050
+ }
29051
+ if (status === 404) {
29052
+ return {
29053
+ message: `Index read returned 404 while ${where}.`,
29054
+ hint: `${ctx.apiUrl} answered but serves no /v1/index/events — check SL_API_URL points at a Secondlayer instance, not a Stacks node.`
29055
+ };
29056
+ }
29057
+ if (status === 429) {
29058
+ return {
29059
+ message: `Rate limited while ${where}.`,
29060
+ hint: `Wait a moment and retry, or narrow the range with --to. ${offlineHint}`
29061
+ };
29062
+ }
29063
+ if (status !== undefined && status >= 500) {
29064
+ return {
29065
+ message: `Index read failed with HTTP ${status} while ${where}.`,
29066
+ hint: `The instance is reachable but erroring — check it with \`secondlayer status\`, then retry. ${offlineHint}`
29067
+ };
29068
+ }
29069
+ const detail = err instanceof Error ? err.message : String(err);
29070
+ return {
29071
+ message: `Index read failed while ${where}: ${detail}`,
29072
+ hint: `Confirm the instance is up and reachable: secondlayer status. ${offlineHint}`
29073
+ };
29074
+ }
29004
29075
  async function runSubgraphTest(file, options3) {
29005
29076
  const absPath = resolve2(file);
29006
29077
  if (!existsSync2(absPath)) {
@@ -29070,13 +29141,15 @@ async function runSubgraphTest(file, options3) {
29070
29141
  });
29071
29142
  events[name] = envelope.events;
29072
29143
  } catch (err) {
29073
- const e2 = err;
29074
- if (e2?.status === 402) {
29075
- const details = e2.body?.details;
29076
- error(`Blocks ${fromHeight}–${toHeight} are below the free read window.${details?.oldest_seekable_height ? ` Try --from ${details.oldest_seekable_height}, or add a paid API key (SL_API_KEY) to test against older history.` : " Add a paid API key (SL_API_KEY) to test against older history."}`);
29077
- process.exit(1);
29078
- }
29079
- throw err;
29144
+ const failure = indexReadFailure(err, {
29145
+ source: name,
29146
+ fromHeight,
29147
+ toHeight,
29148
+ apiUrl: indexApiUrl(),
29149
+ file
29150
+ });
29151
+ printError(failure.message, { hint: failure.hint });
29152
+ process.exit(1);
29080
29153
  }
29081
29154
  }
29082
29155
  if (options3.record !== false) {
@@ -29183,11 +29256,13 @@ __export(exports_subgraphs, {
29183
29256
  runSubgraphClientCodegen: () => runSubgraphClientCodegen,
29184
29257
  runPayloadsCodegen: () => runPayloadsCodegen,
29185
29258
  registerSubgraphsCommand: () => registerSubgraphsCommand,
29186
- parseVisibilityOption: () => parseVisibilityOption,
29187
29259
  parseSubgraphSpecFormat: () => parseSubgraphSpecFormat,
29188
29260
  parseStartBlockOption: () => parseStartBlockOption,
29189
29261
  ormFlagsConflictingWithPayloads: () => ormFlagsConflictingWithPayloads,
29262
+ operationDetailPairs: () => operationDetailPairs,
29190
29263
  installScaffoldDependencies: () => installScaffoldDependencies,
29264
+ formatOperationRange: () => formatOperationRange,
29265
+ formatOperationProgress: () => formatOperationProgress,
29191
29266
  ensureScaffoldPackageJson: () => ensureScaffoldPackageJson,
29192
29267
  createSubgraphDeployPreview: () => createSubgraphDeployPreview,
29193
29268
  collectPinnedPrintSources: () => collectPinnedPrintSources
@@ -29259,13 +29334,6 @@ function parseStartBlockOption(value) {
29259
29334
  }
29260
29335
  return parsed;
29261
29336
  }
29262
- function parseVisibilityOption(value) {
29263
- if (value === undefined)
29264
- return;
29265
- if (value === "public" || value === "private")
29266
- return value;
29267
- throw new Error('--visibility must be "public" or "private"');
29268
- }
29269
29337
  function collectPinnedPrintSources(sources) {
29270
29338
  const out = [];
29271
29339
  for (const [sourceName, raw] of Object.entries(sources ?? {})) {
@@ -29559,6 +29627,40 @@ ${dim("Columns")}`);
29559
29627
  const deployTarget = context.bundled ? "tenant API" : "local database";
29560
29628
  info(`Dry run only. No ${deployTarget} changes were made.`);
29561
29629
  }
29630
+ function formatOperationRange(op) {
29631
+ if (op.fromBlock === null && op.toBlock === null)
29632
+ return "whole subgraph";
29633
+ return `${op.fromBlock ?? "start"} → ${op.toBlock ?? "tip"}`;
29634
+ }
29635
+ function formatOperationProgress(progress) {
29636
+ if (progress === null)
29637
+ return "—";
29638
+ return `${(progress * 100).toFixed(1)}%`;
29639
+ }
29640
+ function colorOperationStatus(status) {
29641
+ if (status === "completed")
29642
+ return green(status);
29643
+ if (status === "failed")
29644
+ return red(status);
29645
+ if (status === "cancelled")
29646
+ return yellow(status);
29647
+ return status;
29648
+ }
29649
+ function operationDetailPairs(op) {
29650
+ return [
29651
+ ["ID", op.id],
29652
+ ["Subgraph", op.subgraphName],
29653
+ ["Kind", op.kind],
29654
+ ["Status", op.status],
29655
+ ["Range", formatOperationRange(op)],
29656
+ ["Progress", formatOperationProgress(op.progress)],
29657
+ ["Processed Blocks", op.processedBlocks?.toLocaleString() ?? "—"],
29658
+ ["Error", op.error ?? "none"],
29659
+ ["Created", op.createdAt],
29660
+ ["Started", op.startedAt ?? "—"],
29661
+ ["Finished", op.finishedAt ?? "—"]
29662
+ ];
29663
+ }
29562
29664
  function formatDuration(seconds2) {
29563
29665
  if (seconds2 < 90)
29564
29666
  return `${Math.max(1, Math.round(seconds2))}s`;
@@ -29614,7 +29716,16 @@ function formatSubgraphSync(sync) {
29614
29716
  }
29615
29717
  function registerSubgraphsCommand(program2) {
29616
29718
  const subgraphs = program2.command("subgraphs").description("Manage materialized subgraphs");
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", `
29719
+ subgraphs.command("create <name>").description("Start here: create a new subgraph definition file (see also: subgraphs scaffold)").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", `
29720
+ create is the default way to start a subgraph. It writes subgraphs/<name>.ts —
29721
+ an empty starter, or with --from-contract a schema inferred from the print
29722
+ events that contract has already emitted into your index.
29723
+
29724
+ Use "secondlayer subgraphs scaffold" instead when you need what create cannot
29725
+ infer: typed contract_call tables generated from a contract's ABI (read from a
29726
+ Stacks node, so it works before your index holds that contract's history), or a
29727
+ trait-wide source with --trait sip-010.
29728
+
29618
29729
  Examples:
29619
29730
  $ secondlayer subgraphs create my-graph
29620
29731
  $ secondlayer subgraphs create sbtc-flows --from-contract SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry
@@ -29724,7 +29835,10 @@ Stopped watching.`);
29724
29835
  });
29725
29836
  await new Promise(() => {});
29726
29837
  });
29727
- subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").addOption(new Option("--visibility <visibility>", "Hosted only. Public/private namespace.").hideHelp()).option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").action(async (file, options3) => {
29838
+ subgraphs.command("deploy <file>").alias("update").description("Deploy a subgraph definition file (alias: update — deploy is create-or-update)").option("--start-block <n>", "Override the subgraph definition startBlock for this deploy").option("--tip-first", "Go live at chain tip immediately; history backfills in the background (requires order-tolerant handlers — commutative or insert-only writes)").option("--dry-run", "Validate and preview deploy without writing changes").option("-y, --yes", "Skip the reindex confirmation prompt").option("--strict", "Run `tsc --noEmit` against the handler before deploy (slower; catches TS type errors)").option("--allow-uncommitted", "Deploy even though the source file is not committed to git (the deployed definition will exist only in the database)").addHelpText("after", `
29839
+ By default the source file must be committed to git — otherwise the
29840
+ deployed definition's only copy is the row in the database. Pass
29841
+ --allow-uncommitted to deploy anyway.`).action(async (file, options3) => {
29728
29842
  try {
29729
29843
  const absPath = resolve3(file);
29730
29844
  if (!options3.dryRun) {
@@ -29756,7 +29870,6 @@ Stopped watching.`);
29756
29870
  await requireAuth();
29757
29871
  }
29758
29872
  const dryRun = options3.dryRun;
29759
- const visibility = parseVisibilityOption(options3.visibility);
29760
29873
  const startBlock = parseStartBlockOption(options3.startBlock);
29761
29874
  if (startBlock !== undefined) {
29762
29875
  warn(`--start-block ${startBlock} overrides the definition's startBlock for this deploy.`);
@@ -29829,8 +29942,7 @@ Stopped watching.`);
29829
29942
  schema: effectiveDef.schema,
29830
29943
  handlerCode,
29831
29944
  sourceCode: source,
29832
- ...deployStartBlock !== undefined ? { startBlock: deployStartBlock } : {},
29833
- ...visibility ? { visibility } : {}
29945
+ ...deployStartBlock !== undefined ? { startBlock: deployStartBlock } : {}
29834
29946
  });
29835
29947
  for (const w2 of result.warnings ?? [])
29836
29948
  warn(w2);
@@ -29838,12 +29950,8 @@ Stopped watching.`);
29838
29950
  try {
29839
29951
  const { apiUrl } = await resolveAuth();
29840
29952
  const firstTable = Object.keys(effectiveDef.schema ?? {})[0];
29841
- const isPublic = result.visibility === "public";
29842
- if (isPublic && firstTable) {
29843
- info(` Read: ${apiUrl}/v1/subgraphs/${effectiveDef.name}/${firstTable}`);
29844
- info(` Share: ${apiUrl}/v1/subgraphs/${effectiveDef.name} (public — no key needed)`);
29845
- } else if (firstTable) {
29846
- info(` REST: ${apiUrl}/api/subgraphs/${effectiveDef.name}/${firstTable}`);
29953
+ if (firstTable) {
29954
+ info(` REST: ${apiUrl}/v1/subgraphs/${effectiveDef.name}/${firstTable}`);
29847
29955
  }
29848
29956
  if (result.reindexStarted) {
29849
29957
  const denom = result.estimatedEvents != null ? ` ~${result.estimatedEvents.toLocaleString()} events —` : "";
@@ -30023,14 +30131,17 @@ Synced — exiting watch.`));
30023
30131
  subgraphs.command("spec <nameOrFile>").description("Output API documentation for a deployed subgraph or a local subgraph file").option("--format <format>", "Output format: openapi, agent, markdown", "openapi").option("-o, --output <path>", "Write output to a file").option("--server <url>", "Override the server URL in generated docs").action(async (nameOrFile, options3) => {
30024
30132
  try {
30025
30133
  const format2 = parseSubgraphSpecFormat(options3.format);
30026
- const specOptions = options3.server ? { serverUrl: options3.server } : undefined;
30134
+ const specOptions = { serverUrl: options3.server ?? resolveApiUrl() };
30027
30135
  const absPath = resolve3(nameOrFile);
30028
30136
  if (nameOrFile.endsWith(".ts") && !existsSync3(absPath)) {
30029
30137
  error(`File not found: ${absPath}`);
30030
30138
  process.exit(1);
30031
30139
  }
30032
30140
  const isLocalFile = nameOrFile.endsWith(".ts");
30033
- const spec = isLocalFile ? await specFromLocalFile(absPath, format2, specOptions) : format2 === "openapi" ? await getSubgraphOpenApi(nameOrFile, specOptions) : format2 === "agent" ? await getSubgraphAgentSchema(nameOrFile, specOptions) : await getSubgraphMarkdown(nameOrFile, specOptions);
30141
+ const spec = isLocalFile ? await specFromLocalFile(absPath, format2, {
30142
+ ...specOptions,
30143
+ forcePublicRead: true
30144
+ }) : format2 === "openapi" ? await getSubgraphOpenApi(nameOrFile, specOptions) : format2 === "agent" ? await getSubgraphAgentSchema(nameOrFile, specOptions) : await getSubgraphMarkdown(nameOrFile, specOptions);
30034
30145
  await writeOrPrintSubgraphSpec(spec, format2, options3.output);
30035
30146
  } catch (err) {
30036
30147
  if (err instanceof Error && err.message.startsWith("--format")) {
@@ -30040,16 +30151,14 @@ Synced — exiting watch.`));
30040
30151
  handleApiError(err, "generate subgraph spec");
30041
30152
  }
30042
30153
  });
30043
- subgraphs.command("codegen <file>").description("Generate an ORM schema (Prisma, Drizzle, or Kysely) for a subgraph's tables — point it at your BYO database").option("--target <orm>", "ORM target: prisma | drizzle | kysely", "prisma").option("--schema <name>", "Postgres schema name (defaults to subgraph_<name>)").option("--env <var>", "datasource url env var", "DATABASE_URL").option("--models-only", "Emit only Prisma models (compose via prismaSchemaFolder)").option("--payloads", "Emit a .d.ts of print payload types for pinned print_event sources, inferred from observed on-chain events (requires network)").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (file, options3, command) => {
30044
- deprecatedCodegenNotice("secondlayer subgraphs codegen", options3.payloads ? "secondlayer codegen prints" : "secondlayer codegen subgraph");
30045
- await runSubgraphSchemaCodegen(file, options3, (key) => command.getOptionValueSource(key));
30046
- });
30047
30154
  subgraphs.command("test <file>").description("Run a subgraph's handlers against real chain data without deploying").option("--from <height>", "Start block height (required on first run)").option("--to <height>", "End block height (defaults to --from + 100)").option("--offline", "Replay the recorded cassette; never hit the network").option("--no-record", "Don't write a cassette").option("--limit <n>", "Max events per source", "500").addHelpText("after", `
30048
30155
  Examples:
30049
30156
  $ secondlayer subgraphs test subgraphs/bns-names.ts --from 167484 --to 167600
30050
30157
  $ secondlayer subgraphs test subgraphs/bns-names.ts --offline`).action(async (file, options3) => {
30051
30158
  const { runSubgraphTest: runSubgraphTest2 } = await Promise.resolve().then(() => (init_subgraph_test(), exports_subgraph_test));
30052
- await runSubgraphTest2(file, options3);
30159
+ await withErrorHandling(runSubgraphTest2, {
30160
+ action: `test ${file}`
30161
+ })(file, options3);
30053
30162
  });
30054
30163
  subgraphs.command("reindex <name>").description("Drop a subgraph's data and rebuild it from its start block (destructive)").option("-y, --yes", "Skip confirmation").addHelpText("after", `
30055
30164
  Reindex is always whole-subgraph: it drops every row and rebuilds from the
@@ -30119,7 +30228,13 @@ Examples:
30119
30228
  handleApiError(err, "backfill subgraph");
30120
30229
  }
30121
30230
  });
30122
- subgraphs.command("cancel <name>").description("Cancel a running reindex or backfill operation").action(async (name) => {
30231
+ subgraphs.command("stop <name>").description("Stop a running reindex or backfill operation").addHelpText("after", `
30232
+ Stopping leaves the rows already written in place — it cancels the operation,
30233
+ it does not roll it back. Re-run reindex or backfill to finish the work.
30234
+
30235
+ Examples:
30236
+ $ secondlayer subgraphs stop my-graph
30237
+ $ secondlayer subgraphs operations my-graph # confirm it went to cancelled`).action(async (name) => {
30123
30238
  try {
30124
30239
  info(`Stopping operation for subgraph "${name}"...`);
30125
30240
  const result = await stopSubgraphApi(name);
@@ -30128,6 +30243,87 @@ Examples:
30128
30243
  handleApiError(err, "stop subgraph operation");
30129
30244
  }
30130
30245
  });
30246
+ subgraphs.command("operations <name> [operationId]").description("Show a subgraph's operation history — the verify step for deploy, reindex, and backfill").option("--json", "Output as JSON").addHelpText("after", `
30247
+ Every deploy that reindexes, every reindex, and every backfill enqueues a
30248
+ tracked operation. This is where you check whether it ran, is still queued, or
30249
+ failed — and why. Pass an operation id for a single one.
30250
+
30251
+ Examples:
30252
+ $ secondlayer subgraphs operations my-graph
30253
+ $ secondlayer subgraphs operations my-graph --json
30254
+ $ secondlayer subgraphs operations my-graph 0f1c9d2e-4b7a-...`).action(async (name, operationId, options3) => {
30255
+ try {
30256
+ if (operationId) {
30257
+ const op = await getSubgraphOperationApi(name, operationId);
30258
+ output({
30259
+ json: options3.json,
30260
+ data: op,
30261
+ human: () => console.log(formatKeyValue(operationDetailPairs(op)))
30262
+ });
30263
+ return;
30264
+ }
30265
+ const { operations } = await listSubgraphOperationsApi(name);
30266
+ output({
30267
+ json: options3.json,
30268
+ data: { operations },
30269
+ human: () => {
30270
+ if (operations.length === 0) {
30271
+ console.log(dim("No operations recorded"));
30272
+ return;
30273
+ }
30274
+ console.log(formatTable(["ID", "Kind", "Status", "Range", "Progress", "Started"], operations.map((op) => [
30275
+ op.id,
30276
+ op.kind,
30277
+ colorOperationStatus(op.status),
30278
+ formatOperationRange(op),
30279
+ formatOperationProgress(op.progress),
30280
+ op.startedAt ? op.startedAt.replace("T", " ").slice(0, 19) : dim("—")
30281
+ ])));
30282
+ console.log(dim(`
30283
+ ${operations.length} operation(s)`));
30284
+ const failed = operations.filter((op) => op.error);
30285
+ for (const op of failed) {
30286
+ console.log(dim(`
30287
+ ${op.id}: ${op.error}`));
30288
+ }
30289
+ }
30290
+ });
30291
+ } catch (err) {
30292
+ handleApiError(err, "get subgraph operations");
30293
+ }
30294
+ });
30295
+ subgraphs.command("source <name>").description("Print the source of a deployed subgraph definition").option("-o, --output <path>", "Write the source to a file").option("--json", "Output the full response as JSON").addHelpText("after", `
30296
+ Recovers the definition that is actually deployed, which is not always what is
30297
+ on disk. Subgraphs deployed before source capture return no source — redeploy
30298
+ to make them recoverable.
30299
+
30300
+ Examples:
30301
+ $ secondlayer subgraphs source my-graph
30302
+ $ secondlayer subgraphs source my-graph -o subgraphs/my-graph.ts`).action(async (name, options3) => {
30303
+ try {
30304
+ const result = await getSubgraphSourceApi(name);
30305
+ if (options3.json) {
30306
+ writeData(JSON.stringify(result, null, 2));
30307
+ return;
30308
+ }
30309
+ if (result.sourceCode === null) {
30310
+ error(`No source stored for "${name}"${result.reason ? ` — ${result.reason}` : ""}`);
30311
+ process.exit(1);
30312
+ }
30313
+ if (options3.output) {
30314
+ const outPath = resolve3(options3.output);
30315
+ const dir = resolve3(outPath, "..");
30316
+ if (!existsSync3(dir))
30317
+ mkdirSync2(dir, { recursive: true });
30318
+ await writeTextFile(outPath, result.sourceCode);
30319
+ success(`Wrote ${name} v${result.version} source to ${outPath}`);
30320
+ return;
30321
+ }
30322
+ writeData(result.sourceCode);
30323
+ } catch (err) {
30324
+ handleApiError(err, "get subgraph source");
30325
+ }
30326
+ });
30131
30327
  subgraphs.command("gaps <name>").description("Show block gaps for a subgraph").option("--resolved", "Include resolved gaps").option("--limit <n>", "Max gaps to return", "50").option("--json", "Output as JSON").action(async (name, options3) => {
30132
30328
  try {
30133
30329
  const result = await getSubgraphGaps(name, {
@@ -30259,7 +30455,14 @@ ${rows.length} row(s)`));
30259
30455
  handleApiError(err, "delete subgraph");
30260
30456
  }
30261
30457
  });
30262
- subgraphs.command("scaffold [contractAddress]").description("Scaffold a defineSubgraph() file from a contract or trait").option("-o, --output <path>", "Output file path (required)").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("--functions <names>", "Comma-separated public functions to index as typed contract_call tables").option("--trait <std>", "Scaffold a trait-scoped source (sip-009|sip-010|sip-013) — no contract needed").option("--no-install", "Skip bun install after writing package.json").addHelpText("after", `
30458
+ subgraphs.command("scaffold [contractAddress]").description("Generate a defineSubgraph() file from a contract ABI or trait (start with: subgraphs create)").option("-o, --output <path>", "Output file path (required)").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("--functions <names>", "Comma-separated public functions to index as typed contract_call tables").option("--trait <std>", "Scaffold a trait-scoped source (sip-009|sip-010|sip-013) — no contract needed").option("--no-install", "Skip bun install after writing package.json").addHelpText("after", `
30459
+ "secondlayer subgraphs create <name>" is the documented starting point. Reach
30460
+ for scaffold when you need what create cannot infer: typed contract_call tables
30461
+ from the contract's ABI, or a trait-wide source (--trait sip-010). scaffold
30462
+ reads the ABI from a Stacks node, so it does not need your index to hold that
30463
+ contract's history yet, and it writes a package.json and installs dependencies
30464
+ next to the output file.
30465
+
30263
30466
  Examples:
30264
30467
  $ secondlayer subgraphs scaffold SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.megapont-ape-club-nft -o subgraphs/apes.ts
30265
30468
  $ secondlayer subgraphs scaffold SP00...token --functions transfer,mint -o subgraphs/token.ts
@@ -30322,10 +30525,6 @@ Examples:
30322
30525
  process.exit(1);
30323
30526
  }
30324
30527
  });
30325
- subgraphs.command("client <subgraphName>").description("Generate a typed query client for a deployed subgraph").option("-o, --output <path>", "Output file path (required)").action(async (subgraphName, options3) => {
30326
- deprecatedCodegenNotice("secondlayer subgraphs client", "secondlayer codegen client");
30327
- await runSubgraphClientCodegen(subgraphName, options3);
30328
- });
30329
30528
  }
30330
30529
  async function runSubgraphClientCodegen(subgraphName, options3) {
30331
30530
  try {
@@ -30347,7 +30546,7 @@ async function runSubgraphClientCodegen(subgraphName, options3) {
30347
30546
  handleApiError(err, "generate subgraph client");
30348
30547
  }
30349
30548
  }
30350
- async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTarget = "prisma") {
30549
+ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTarget = "kysely") {
30351
30550
  if (options3.payloads) {
30352
30551
  const conflicts = ormFlagsConflictingWithPayloads(optionSource);
30353
30552
  if (conflicts.length > 0) {
@@ -30405,11 +30604,9 @@ async function runSubgraphSchemaCodegen(file, options3, optionSource, defaultTar
30405
30604
  }
30406
30605
  var ORM_ONLY_CODEGEN_FLAGS;
30407
30606
  var init_subgraphs2 = __esm(() => {
30408
- init_esm();
30409
30607
  init_subgraph_scaffold();
30410
30608
  init_subgraphs();
30411
30609
  init_api_client();
30412
- init_codegen();
30413
30610
  init_config2();
30414
30611
  init_filter_params();
30415
30612
  init_fs();
@@ -30447,7 +30644,7 @@ function fail(action2, err) {
30447
30644
  const message = err instanceof Error ? err.message : String(err);
30448
30645
  error(`Failed to ${action2}: ${message}`);
30449
30646
  if (/\b403\b/.test(message) || /forbidden/i.test(message)) {
30450
- note("The local API refused this read. Check INSTANCE_TOKEN / SL_API_KEY.");
30647
+ note("The local API refused this read. Check INSTANCE_TOKEN.");
30451
30648
  }
30452
30649
  process.exit(1);
30453
30650
  }
@@ -30486,10 +30683,6 @@ function emitOne(doc, label, json) {
30486
30683
  }
30487
30684
  function registerIndexCommand(program2) {
30488
30685
  const index = program2.command("index").description("Query the decoded Index layer (events + contract calls)");
30489
- index.command("codegen").description("Generate a typed schema (Prisma, Kysely, Drizzle, or JSON-Schema) for the Index domain tables — point it at your BYO database mirror").option("--target <orm>", "prisma | kysely | drizzle | json-schema", "kysely").option("--schema <name>", "Postgres schema to qualify table names with").option("--tables <list>", "Comma-separated subset of Index tables (default: all)").option("--env <var>", "Prisma datasource url env var", "DATABASE_URL").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (o3) => {
30490
- deprecatedCodegenNotice("secondlayer index codegen", "secondlayer codegen index");
30491
- await runIndexCodegen(o3);
30492
- });
30493
30686
  const rangeFlags = (cmd) => cmd.option("--contract-id <id>", "Filter by contract id").option("--from-height <n>", "Start block height (inclusive)").option("--to-height <n>", "End block height (inclusive)").option("--cursor <cursor>", "Resume token from a previous next_cursor").option("--limit <n>", "Rows per page").option("--json", "Print the full envelope as JSON");
30494
30687
  const heightFlags = (cmd) => cmd.option("--from-height <n>", "Start block height (inclusive)").option("--to-height <n>", "End block height (inclusive)").option("--cursor <cursor>", "Resume token from a previous next_cursor").option("--limit <n>", "Rows per page").option("--json", "Print the full envelope as JSON");
30495
30688
  rangeFlags(index.command("ft-transfers")).description("List decoded SIP-010 fungible-token transfers").option("--sender <principal>", "Filter by sender").option("--recipient <principal>", "Filter by recipient").action(async (o3) => {
@@ -30642,57 +30835,22 @@ var init_index_api = __esm(() => {
30642
30835
  init_fs();
30643
30836
  init_output();
30644
30837
  init_resolve_auth();
30645
- init_codegen();
30646
- });
30647
-
30648
- // src/commands/codegen.ts
30649
- var exports_codegen = {};
30650
- __export(exports_codegen, {
30651
- registerCodegenCommand: () => registerCodegenCommand,
30652
- deprecatedCodegenNotice: () => deprecatedCodegenNotice
30653
- });
30654
- function registerCodegenCommand(program2) {
30655
- const codegen = program2.command("codegen").description("Generate typed code: contracts, subgraphs, Index, clients");
30656
- codegen.command("contracts [files...]").description("Generate TypeScript interfaces from Clarity contracts").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output file path").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("-w, --watch", "Watch for changes").action(async (files, options3) => {
30657
- const { generate: generate2 } = await Promise.resolve().then(() => (init_generate(), exports_generate));
30658
- await generate2(files, { ...options3, out: options3.output });
30659
- });
30660
- codegen.command("subgraph <file>").description("Generate an ORM schema (Kysely, Prisma, or Drizzle) for a subgraph's tables").option("--target <orm>", "ORM target: kysely | prisma | drizzle", "kysely").option("--schema <name>", "Postgres schema name (defaults to subgraph_<name>)").option("--env <var>", "datasource url env var", "DATABASE_URL").option("--models-only", "Emit only Prisma models (compose via prismaSchemaFolder)").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (file, options3, command) => {
30661
- const { runSubgraphSchemaCodegen: runSubgraphSchemaCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
30662
- await runSubgraphSchemaCodegen2(file, options3, (key) => command.getOptionValueSource(key), "kysely");
30663
- });
30664
- codegen.command("index").description("Generate a typed schema (Kysely, Prisma, Drizzle, or JSON-Schema) for the Index domain tables").option("--target <orm>", "kysely | prisma | drizzle | json-schema", "kysely").option("--schema <name>", "Postgres schema to qualify table names with").option("--tables <list>", "Comma-separated subset of Index tables (default: all)").option("--env <var>", "Prisma datasource url env var", "DATABASE_URL").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (options3) => {
30665
- const { runIndexCodegen: runIndexCodegen2 } = await Promise.resolve().then(() => (init_index_api(), exports_index_api));
30666
- await runIndexCodegen2(options3);
30667
- });
30668
- codegen.command("client <subgraphName>").description("Generate a typed query client for a deployed subgraph").option("-o, --output <path>", "Output file path (required)").action(async (subgraphName, options3) => {
30669
- const { runSubgraphClientCodegen: runSubgraphClientCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
30670
- await runSubgraphClientCodegen2(subgraphName, options3);
30671
- });
30672
- codegen.command("prints <file>").description("Emit a .d.ts of print payload types for a subgraph's pinned print_event sources (inferred from observed on-chain events; requires network)").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (file, options3) => {
30673
- const { runPayloadsCodegen: runPayloadsCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
30674
- await runPayloadsCodegen2(file, options3.output);
30675
- });
30676
- }
30677
- function deprecatedCodegenNotice(oldPath, newPath) {
30678
- warn(`\`${oldPath}\` is deprecated — use \`${newPath}\`. (The new verb defaults --target to kysely everywhere; this alias keeps its original default so your output does not change.)`);
30679
- }
30680
- var init_codegen = __esm(() => {
30681
- init_output();
30682
30838
  });
30683
30839
 
30684
30840
  // src/lib/docker.ts
30841
+ import { spawnSync as spawnSync2 } from "node:child_process";
30842
+ function run2(command, args) {
30843
+ const result = spawnSync2(command, args, { encoding: "utf8" });
30844
+ return {
30845
+ ok: !result.error && result.status === 0,
30846
+ stdout: result.stdout ?? ""
30847
+ };
30848
+ }
30685
30849
  async function isDockerAvailable() {
30686
- try {
30687
- const result = await Bun.$`docker info`.quiet().nothrow();
30688
- return result.exitCode === 0;
30689
- } catch {
30690
- return false;
30691
- }
30850
+ return run2("docker", ["info"]).ok;
30692
30851
  }
30693
30852
  async function requireDocker() {
30694
- const whichResult = await Bun.$`which docker`.quiet().nothrow();
30695
- if (whichResult.exitCode !== 0) {
30853
+ if (!run2("which", ["docker"]).ok) {
30696
30854
  throw new DockerNotAvailableError(`Docker is not installed.
30697
30855
 
30698
30856
  ` + `Install Docker:
@@ -32711,6 +32869,155 @@ var init_src = __esm(() => {
32711
32869
  src_default = Postgres;
32712
32870
  });
32713
32871
 
32872
+ // src/lib/devnet-compose.ts
32873
+ function buildDevnetCompose(opts = {}) {
32874
+ const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
32875
+ const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
32876
+ const apiPort = opts.apiPort ?? 3800;
32877
+ const indexerPort = opts.indexerPort ?? 3700;
32878
+ const apiPublish = `127.0.0.1:${apiPort}`;
32879
+ const apiImage = `ghcr.io/${owner}/secondlayer-api:${tag}`;
32880
+ const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
32881
+ const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
32882
+ const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
32883
+ return `# Generated by \`secondlayer devnet connect\` — do not edit by hand.
32884
+ # Secondlayer app services for local clarinet devnet development.
32885
+ name: secondlayer-devnet
32886
+
32887
+ services:
32888
+ postgres:
32889
+ image: postgres:17-alpine
32890
+ environment:
32891
+ POSTGRES_USER: secondlayer
32892
+ POSTGRES_PASSWORD: secondlayer
32893
+ POSTGRES_DB: secondlayer
32894
+ volumes:
32895
+ - postgres_data:/var/lib/postgresql/data
32896
+ healthcheck:
32897
+ test: ["CMD-SHELL", "pg_isready -U secondlayer"]
32898
+ interval: 5s
32899
+ timeout: 5s
32900
+ retries: 10
32901
+ restart: unless-stopped
32902
+
32903
+ migrate:
32904
+ image: ${apiImage}
32905
+ command: ["bun", "run", "packages/shared/src/db/migrate.ts"]
32906
+ environment:
32907
+ DATABASE_URL: ${dbUrl}
32908
+ depends_on:
32909
+ postgres:
32910
+ condition: service_healthy
32911
+ restart: "no"
32912
+
32913
+ api:
32914
+ image: ${apiImage}
32915
+ environment:
32916
+ INSTANCE_MODE: oss
32917
+ DATABASE_URL: ${dbUrl}
32918
+ PORT: 3800
32919
+ # 0.0.0.0 is forced: a container process that binds loopback is
32920
+ # unreachable through a published port. The real reach is the publish
32921
+ # spec below, handed over verbatim so /v1 reads stay keyless on devnet.
32922
+ LISTEN_HOST: "0.0.0.0"
32923
+ API_PUBLISH_ADDR: "${apiPublish}"
32924
+ # Boot guard reads LISTEN_HOST, so a token is required to start at all.
32925
+ # It gates writes only — reads are open on this loopback publish.
32926
+ INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
32927
+ NODE_ENV: production
32928
+ DATA_DIR: /data
32929
+ # Shared across api + subgraph-processor so the secret the api encrypts
32930
+ # (e.g. a subscription's signing secret) can be decrypted by the emitter.
32931
+ # OSS auto-generates a per-process key when unset — that splits them.
32932
+ # Local-dev fixed value; not a production secret.
32933
+ SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
32934
+ ports:
32935
+ - "${apiPublish}:3800"
32936
+ volumes:
32937
+ - subgraphs_data:/data/subgraphs
32938
+ depends_on:
32939
+ migrate:
32940
+ condition: service_completed_successfully
32941
+ postgres:
32942
+ condition: service_healthy
32943
+ healthcheck:
32944
+ test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
32945
+ interval: 10s
32946
+ timeout: 3s
32947
+ start_period: 10s
32948
+ retries: 5
32949
+ restart: unless-stopped
32950
+
32951
+ indexer:
32952
+ image: ${indexerImage}
32953
+ environment:
32954
+ DATABASE_URL: ${dbUrl}
32955
+ PORT: 3700
32956
+ NODE_ENV: production
32957
+ # Keep every network key aligned: the indexer writes index_progress under
32958
+ # STACKS_NETWORK, the subgraph processor reads it under NETWORK. If they
32959
+ # differ, catch-up finds no progress row and subgraphs never advance.
32960
+ NETWORKS: testnet
32961
+ STACKS_NETWORK: testnet
32962
+ NETWORK: testnet
32963
+ # Fed by the devnet node's event observer — never reach out to Hiro mainnet.
32964
+ AUTO_BACKFILL_ENABLED: "false"
32965
+ TIP_FOLLOWER_ENABLED: "false"
32966
+ # Deliberately published on every interface, unlike the api: the devnet's
32967
+ # stacks-node runs in its own container and POSTs events to
32968
+ # host.docker.internal:3700, which resolves to the host gateway address,
32969
+ # not 127.0.0.1. A loopback publish here would drop every block.
32970
+ ports:
32971
+ - "${indexerPort}:3700"
32972
+ extra_hosts:
32973
+ - "host.docker.internal:host-gateway"
32974
+ depends_on:
32975
+ migrate:
32976
+ condition: service_completed_successfully
32977
+ postgres:
32978
+ condition: service_healthy
32979
+ healthcheck:
32980
+ test: ["CMD", "curl", "-f", "http://localhost:3700/health"]
32981
+ interval: 10s
32982
+ timeout: 3s
32983
+ start_period: 10s
32984
+ retries: 5
32985
+ restart: unless-stopped
32986
+
32987
+ subgraph-processor:
32988
+ image: ${apiImage}
32989
+ command: ["bun", "run", "packages/subgraphs/src/service.ts"]
32990
+ environment:
32991
+ DATABASE_URL: ${dbUrl}
32992
+ NODE_ENV: production
32993
+ DATA_DIR: /data
32994
+ # Must match the indexer's network key (see indexer note) — catch-up reads
32995
+ # index_progress under NETWORK.
32996
+ NETWORK: testnet
32997
+ STACKS_NETWORK: testnet
32998
+ # The emitter blocks webhook delivery to private/loopback IPs by default.
32999
+ # On a local devnet you deliver to your own localhost receiver, so allow it.
33000
+ SECONDLAYER_ALLOW_PRIVATE_EGRESS: "true"
33001
+ # Must match the api's key so the emitter can decrypt signing secrets.
33002
+ SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
33003
+ volumes:
33004
+ - subgraphs_data:/data/subgraphs
33005
+ depends_on:
33006
+ migrate:
33007
+ condition: service_completed_successfully
33008
+ postgres:
33009
+ condition: service_healthy
33010
+ healthcheck:
33011
+ disable: true
33012
+ restart: unless-stopped
33013
+
33014
+ volumes:
33015
+ postgres_data:
33016
+ subgraphs_data:
33017
+ `;
33018
+ }
33019
+ var DEFAULT_IMAGE_OWNER = "ryanwaits", DEFAULT_IMAGE_TAG = "latest", DEV_INSTANCE_TOKEN = "dev-instance-token";
33020
+
32714
33021
  // src/lib/dev-state.ts
32715
33022
  var exports_dev_state = {};
32716
33023
  __export(exports_dev_state, {
@@ -34315,12 +34622,1417 @@ var init_node_impl = __esm(() => {
34315
34622
  init_output();
34316
34623
  });
34317
34624
 
34318
- // src/cli.ts
34319
- init_esm();
34625
+ // src/lib/instance-init.ts
34626
+ import { randomBytes as randomBytes3 } from "node:crypto";
34627
+ import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
34628
+ import { resolve as resolve15 } from "node:path";
34629
+ import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
34630
+ import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
34631
+ function parseInstanceNetwork(value) {
34632
+ const network = value.trim().toLowerCase();
34633
+ if (INSTANCE_NETWORKS.includes(network)) {
34634
+ return network;
34635
+ }
34636
+ throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
34637
+ }
34638
+ function generateInstanceToken() {
34639
+ return randomBytes3(32).toString("hex");
34640
+ }
34641
+ function generateSecretsKey() {
34642
+ return randomBytes3(32).toString("hex");
34643
+ }
34644
+ function generateSigningPrivateKey() {
34645
+ return generateEd25519KeyPair().privateKeyPem;
34646
+ }
34647
+ function escapeEnvValue(value) {
34648
+ if (!/[\s#"']/.test(value) && !value.includes("\\"))
34649
+ return value;
34650
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
34651
+ }
34652
+ function unescapeEnvValue(raw) {
34653
+ let value = raw.trim();
34654
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
34655
+ value = value.slice(1, -1);
34656
+ }
34657
+ return value.replace(/\\n/g, `
34658
+ `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
34659
+ }
34660
+ function readEnvValue(envPath, key) {
34661
+ if (!existsSync10(envPath))
34662
+ return null;
34663
+ const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
34664
+ return match ? unescapeEnvValue(match[1]) : null;
34665
+ }
34666
+ function buildInstanceEnv(input4) {
34667
+ const token = input4.existing?.INSTANCE_TOKEN || input4.existing?.SL_API_KEY || generateInstanceToken();
34668
+ const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
34669
+ const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
34670
+ const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
34671
+ const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
34672
+ return {
34673
+ INSTANCE_MODE: "oss",
34674
+ STACKS_NETWORK: input4.network,
34675
+ INSTANCE_TOKEN: token,
34676
+ SECONDLAYER_SECRETS_KEY: secrets,
34677
+ STREAMS_SIGNING_PRIVATE_KEY: signing,
34678
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
34679
+ ALLOW_UNSIGNED_WEBHOOKS: "false",
34680
+ SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
34681
+ SL_API_KEY: token,
34682
+ ...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
34683
+ };
34684
+ }
34685
+ function renderInstanceEnv(env2) {
34686
+ const lines2 = [
34687
+ "# Generated by secondlayer init. Do not commit.",
34688
+ `INSTANCE_MODE=${env2.INSTANCE_MODE}`,
34689
+ `STACKS_NETWORK=${env2.STACKS_NETWORK}`,
34690
+ "# The instance credential. The API validates bearer tokens against it,",
34691
+ "# and the CLI/SDK/MCP read it first when authenticating.",
34692
+ `INSTANCE_TOKEN=${env2.INSTANCE_TOKEN}`,
34693
+ `SECONDLAYER_SECRETS_KEY=${env2.SECONDLAYER_SECRETS_KEY}`,
34694
+ `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.STREAMS_SIGNING_PRIVATE_KEY)}`,
34695
+ `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
34696
+ `ALLOW_UNSIGNED_WEBHOOKS=${env2.ALLOW_UNSIGNED_WEBHOOKS}`,
34697
+ `SL_API_URL=${env2.SL_API_URL}`,
34698
+ "# Legacy alias of INSTANCE_TOKEN, same value. Kept for existing setups;",
34699
+ "# INSTANCE_TOKEN wins if the two ever disagree.",
34700
+ `SL_API_KEY=${env2.SL_API_KEY}`
34701
+ ];
34702
+ if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
34703
+ lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
34704
+ }
34705
+ lines2.push("");
34706
+ return lines2.join(`
34707
+ `);
34708
+ }
34709
+ function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
34710
+ const path11 = resolve15(cwd, file);
34711
+ writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
34712
+ return path11;
34713
+ }
34714
+ function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
34715
+ const path11 = resolve15(cwd, file);
34716
+ return {
34717
+ INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
34718
+ SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
34719
+ STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path11, "STREAMS_SIGNING_PRIVATE_KEY"),
34720
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path11, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
34721
+ SL_API_URL: readEnvValue(path11, "SL_API_URL"),
34722
+ SL_API_KEY: readEnvValue(path11, "SL_API_KEY"),
34723
+ ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path11, "ARCHIVE_SIGNING_PUBLIC_KEY")
34724
+ };
34725
+ }
34726
+ var INSTANCE_ENV_FILE = ".env.local";
34727
+ var init_instance_init = () => {};
34728
+
34729
+ // src/lib/observer-stanza.ts
34730
+ function parseObserverMode(value) {
34731
+ const mode = value.trim().toLowerCase();
34732
+ if (OBSERVER_MODES.includes(mode)) {
34733
+ return mode;
34734
+ }
34735
+ throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
34736
+ }
34737
+ function parseRecoverySource(value) {
34738
+ const source = value.trim().toLowerCase();
34739
+ if (RECOVERY_SOURCES.includes(source)) {
34740
+ return source;
34741
+ }
34742
+ throw new UnsupportedObserverError(`recovery source must be journal or archive (got ${value})`);
34743
+ }
34744
+ function parseObserverEndpoint(value) {
34745
+ const trimmed = value.trim();
34746
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
34747
+ throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
34748
+ }
34749
+ const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
34750
+ if (!match) {
34751
+ throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
34752
+ }
34753
+ const port = Number(match[2]);
34754
+ if (port < 1 || port > 65535) {
34755
+ throw new UnsupportedObserverError(`observer port out of range (${port})`);
34756
+ }
34757
+ return trimmed;
34758
+ }
34759
+ function validateObserverStanza(input4) {
34760
+ const endpoint = parseObserverEndpoint(input4.endpoint);
34761
+ const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
34762
+ if (input4.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
34763
+ throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input4.network}; use a docker DNS name such as indexer:3700`);
34764
+ }
34765
+ if (input4.mode === "signer-shared" && !input4.recovery) {
34766
+ throw new UnsupportedObserverError("signer-shared mode requires --recovery journal or --recovery archive");
34767
+ }
34768
+ return { endpoint };
34769
+ }
34770
+ function renderObserverStanza(input4) {
34771
+ const { endpoint } = validateObserverStanza(input4);
34772
+ const indexer = input4.mode === "indexer";
34773
+ const timeoutMs = indexer ? 2000 : 500;
34774
+ const disableRetries = !indexer;
34775
+ const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. Missed blocks refill from the journal/archive.";
34776
+ return [
34777
+ comment,
34778
+ "[[events_observer]]",
34779
+ `endpoint = "${endpoint}"`,
34780
+ 'events_keys = ["*"]',
34781
+ `timeout_ms = ${timeoutMs}`,
34782
+ `disable_retries = ${disableRetries}`,
34783
+ ""
34784
+ ].join(`
34785
+ `);
34786
+ }
34787
+ function defaultObserverEndpoint(network) {
34788
+ if (network === "devnet")
34789
+ return "127.0.0.1:3700";
34790
+ return "indexer:3700";
34791
+ }
34792
+ var OBSERVER_MODES, RECOVERY_SOURCES, UnsupportedObserverError, LOOPBACK_HOSTS;
34793
+ var init_observer_stanza = __esm(() => {
34794
+ OBSERVER_MODES = ["indexer", "signer-shared"];
34795
+ RECOVERY_SOURCES = ["journal", "archive"];
34796
+ UnsupportedObserverError = class UnsupportedObserverError extends Error {
34797
+ name = "UnsupportedObserverError";
34798
+ };
34799
+ LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
34800
+ });
34801
+
34802
+ // src/lib/oss-compose.ts
34803
+ function buildOssCompose(opts = {}) {
34804
+ const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
34805
+ const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
34806
+ const runtimeImage = `ghcr.io/${owner}/secondlayer-runtime:${tag}`;
34807
+ const consoleImage = `ghcr.io/${owner}/secondlayer-console:${tag}`;
34808
+ return `# Generated by \`secondlayer setup\` — do not edit by hand.
34809
+ # Mirrors docker/oss/docker-compose.yml; see that file for the source of truth
34810
+ # on env vars, profiles, and third-party digest pins. \`secondlayer\`/\`console\`
34811
+ # are swapped from a repo build to the published OSS images (no checkout here).
34812
+ name: secondlayer
34813
+
34814
+ services:
34815
+ postgres:
34816
+ image: postgres:17-alpine@sha256:d4bb0a8c1b7bb2e29f976d099e7bfb9a5d8858cffe9e46b35cd302cd1f1f8168
34817
+ environment:
34818
+ POSTGRES_USER: \${POSTGRES_USER:-secondlayer}
34819
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-secondlayer}
34820
+ POSTGRES_DB: \${POSTGRES_DB:-secondlayer}
34821
+ volumes:
34822
+ - postgres_data:/var/lib/postgresql/data
34823
+ ports:
34824
+ - "\${POSTGRES_PORT:-127.0.0.1:5432}:5432"
34825
+ healthcheck:
34826
+ test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-secondlayer}"]
34827
+ interval: 10s
34828
+ timeout: 5s
34829
+ retries: 5
34830
+ restart: unless-stopped
34831
+
34832
+ secondlayer:
34833
+ image: ${runtimeImage}
34834
+ environment:
34835
+ INSTANCE_MODE: oss
34836
+ NETWORK: \${NETWORK:-\${STACKS_NETWORK:-mainnet}}
34837
+ STACKS_NETWORK: \${STACKS_NETWORK:-mainnet}
34838
+ NODE_MODE: \${NODE_MODE:-external}
34839
+ SECONDLAYER_ALLOW_UNDERSIZED: \${SECONDLAYER_ALLOW_UNDERSIZED:-false}
34840
+ DATABASE_URL: postgres://\${POSTGRES_USER:-secondlayer}:\${POSTGRES_PASSWORD:-secondlayer}@postgres:5432/\${POSTGRES_DB:-secondlayer}
34841
+ DATA_DIR: /data
34842
+ API_PORT: "3800"
34843
+ INDEXER_PORT: "3700"
34844
+ # See docker/oss/docker-compose.yml: API_PORT above is the *container*
34845
+ # port; \${API_PORT} in ports:/here is the *publish spec* from .env.
34846
+ LISTEN_HOST: "0.0.0.0"
34847
+ API_PUBLISH_ADDR: \${API_PORT:-127.0.0.1:3800}
34848
+ INSTANCE_TOKEN: \${INSTANCE_TOKEN:-}
34849
+ SECONDLAYER_SECRETS_KEY: \${SECONDLAYER_SECRETS_KEY:-}
34850
+ STREAMS_SIGNING_PRIVATE_KEY: \${STREAMS_SIGNING_PRIVATE_KEY:-}
34851
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: \${SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY:-}
34852
+ ALLOW_UNSIGNED_WEBHOOKS: \${ALLOW_UNSIGNED_WEBHOOKS:-false}
34853
+ LOG_LEVEL: \${LOG_LEVEL:-info}
34854
+ NODE_ENV: production
34855
+ STACKS_NODE_RPC_URL: \${STACKS_NODE_RPC_URL:-}
34856
+ OBSERVER_JOURNAL_ENABLED: \${OBSERVER_JOURNAL_ENABLED:-true}
34857
+ STREAMS_API_URL: http://127.0.0.1:3800
34858
+ STREAMS_INTERNAL_API_KEY: \${STREAMS_INTERNAL_API_KEY:-}
34859
+ SBTC_DECODER_ENABLED: \${SBTC_DECODER_ENABLED:-true}
34860
+ POX4_DECODER_ENABLED: \${POX4_DECODER_ENABLED:-true}
34861
+ BNS_DECODER_ENABLED: \${BNS_DECODER_ENABLED:-false}
34862
+ SUBGRAPH_SOURCE: \${SUBGRAPH_SOURCE:-streams-index}
34863
+ SUBGRAPH_INDEX_API_URL: http://127.0.0.1:3800
34864
+ BITCOIN_RPC_PASSWORD: \${BITCOIN_RPC_PASSWORD:-}
34865
+ ports:
34866
+ - "\${API_PORT:-127.0.0.1:3800}:3800"
34867
+ - "\${INDEXER_PORT:-127.0.0.1:3700}:3700"
34868
+ # \`secondlayer observer\`'s default endpoint is \`indexer:3700\` — this
34869
+ # alias is what makes that default actually resolve on this network,
34870
+ # without renaming the service. Mirrors docker/oss/docker-compose.yml.
34871
+ networks:
34872
+ default:
34873
+ aliases:
34874
+ - indexer
34875
+ volumes:
34876
+ - subgraphs_data:/data/subgraphs
34877
+ depends_on:
34878
+ postgres:
34879
+ condition: service_healthy
34880
+ healthcheck:
34881
+ test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
34882
+ interval: 30s
34883
+ timeout: 5s
34884
+ start_period: 40s
34885
+ retries: 3
34886
+ restart: unless-stopped
34887
+
34888
+ console:
34889
+ image: ${consoleImage}
34890
+ environment:
34891
+ SL_API_URL: http://secondlayer:3800
34892
+ INSTANCE_TOKEN: \${INSTANCE_TOKEN:-}
34893
+ CONSOLE_TOKEN: \${CONSOLE_TOKEN:-}
34894
+ NODE_ENV: production
34895
+ ports:
34896
+ - "\${CONSOLE_PORT:-3801}:3801"
34897
+ depends_on:
34898
+ secondlayer:
34899
+ condition: service_started
34900
+ healthcheck:
34901
+ test: ["CMD", "curl", "-f", "http://localhost:3801/console"]
34902
+ interval: 30s
34903
+ timeout: 5s
34904
+ start_period: 15s
34905
+ retries: 3
34906
+ restart: unless-stopped
34907
+ profiles: ["console"]
34908
+
34909
+ bitcoind:
34910
+ # Digest = kylemanna/bitcoind:latest as published 2026-07-20 — see
34911
+ # docker/oss/docker-compose.yml for why the digest is the real pin here.
34912
+ image: kylemanna/bitcoind:latest@sha256:86bbcaa99bf3bf3d5df7fd6d9217b3a41194db4c4e34385064f2cfc9fa7bdf91
34913
+ volumes:
34914
+ - \${BITCOIN_DATA_DIR:-./data/bitcoin}:/bitcoin/.bitcoin
34915
+ ports:
34916
+ - "127.0.0.1:8332:8332"
34917
+ - "8333:8333"
34918
+ healthcheck:
34919
+ test: ["CMD-SHELL", "bitcoin-cli -rpcuser=\${BITCOIN_RPC_USER:-stacks} -rpcpassword=\${BITCOIN_RPC_PASSWORD:-changeme} getblockchaininfo | grep -q '\\"blocks\\"' && [ $$(bitcoin-cli -rpcuser=\${BITCOIN_RPC_USER:-stacks} -rpcpassword=\${BITCOIN_RPC_PASSWORD:-changeme} getblockcount) -gt 666050 ]"]
34920
+ interval: 60s
34921
+ timeout: 15s
34922
+ start_period: 120s
34923
+ retries: 3
34924
+ restart: unless-stopped
34925
+ deploy:
34926
+ resources:
34927
+ limits:
34928
+ memory: 32G
34929
+ profiles: ["full-node"]
34930
+
34931
+ stacks-node:
34932
+ # Consensus-critical — digest-pinned to 4.0.1. See docker/oss/docker-compose.yml.
34933
+ image: ghcr.io/stacks-network/stacks-core:4.0.1@sha256:ceb768f881ef52a1d2792a2b4a89d81e092b1df11293b04c31ce36613c3f9711
34934
+ volumes:
34935
+ - \${STACKS_DATA_DIR:-./data/stacks}:/stacks-blockchain/data
34936
+ - ./Config.toml:/stacks-blockchain/Config.toml:ro
34937
+ ports:
34938
+ - "20443:20443"
34939
+ - "20444:20444"
34940
+ restart: unless-stopped
34941
+ command: ["stacks-node", "start", "--config", "/stacks-blockchain/Config.toml"]
34942
+ # full-node only — see docker/oss/docker-compose.yml. No standalone
34943
+ # "stacks-node, public Bitcoin" profile: nothing here wires the node at
34944
+ # a real public Bitcoin RPC, so that combination never actually synced.
34945
+ profiles: ["full-node"]
34946
+
34947
+ volumes:
34948
+ postgres_data:
34949
+ subgraphs_data:
34950
+ `;
34951
+ }
34952
+ function buildOssStacksConfigToml(opts) {
34953
+ const mode = opts.network === "mainnet" ? "mainnet" : "testnet";
34954
+ return `[node]
34955
+ working_dir = "/stacks-blockchain/data"
34956
+ rpc_bind = "0.0.0.0:20443"
34957
+ p2p_bind = "0.0.0.0:20444"
34958
+ seed = ""
34959
+ marf_compress = true
34960
+ bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444"
34961
+
34962
+ # Event observer points directly at the bundled indexer service, same docker
34963
+ # network. Retries off / low timeout: a stuck indexer must never starve a
34964
+ # co-located signer of block_proposal events. See docker/oss/Config.toml.
34965
+ [[events_observer]]
34966
+ endpoint = "secondlayer:3700"
34967
+ events_keys = ["*"]
34968
+ timeout_ms = 500
34969
+ disable_retries = true
34970
+
34971
+ [burnchain]
34972
+ chain = "bitcoin"
34973
+ mode = "${mode}"
34974
+ peer_host = "bitcoind"
34975
+ username = "stacks"
34976
+ password = "${opts.bitcoinRpcPassword}"
34977
+ rpc_port = 8332
34978
+ peer_port = 8333
34979
+ `;
34980
+ }
34981
+ function buildOssBitcoinConf(opts) {
34982
+ return `server=1
34983
+ txindex=1
34984
+ rpcallowip=0.0.0.0/0
34985
+ rpcbind=0.0.0.0
34986
+ rpcuser=stacks
34987
+ rpcpassword=${opts.rpcPassword}
34988
+ # Prune: 0 = archive (required for Stacks); set to 550+ only for pruned chains.
34989
+ prune=0
34990
+ dbcache=4096
34991
+ `;
34992
+ }
34993
+ var init_oss_compose = () => {};
34994
+
34995
+ // src/lib/setup-wizard.ts
34996
+ import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
34997
+ import { randomBytes as randomBytes4 } from "node:crypto";
34998
+ import { existsSync as existsSync11 } from "node:fs";
34999
+ import { writeFile as writeFile7 } from "node:fs/promises";
35000
+ import { join as join19, resolve as resolvePath2 } from "node:path";
35001
+ import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
35002
+ function parseSetupNodeMode(value) {
35003
+ const mode = value.trim().toLowerCase();
35004
+ if (SETUP_NODE_MODES.includes(mode)) {
35005
+ return mode;
35006
+ }
35007
+ throw new Error(`node mode must be external, stacks, or full (got ${value})`);
35008
+ }
35009
+ function resolveNonInteractiveConfig(flags) {
35010
+ if (!flags.network) {
35011
+ throw new MissingSetupFlagError("--network", "Missing --network <mainnet|testnet|devnet>. Non-interactive setup requires it explicitly — there is no safe default.");
35012
+ }
35013
+ if (!flags.nodeMode) {
35014
+ throw new MissingSetupFlagError("--node-mode", "Missing --node-mode <external|stacks|full>. Non-interactive setup requires it explicitly — there is no safe default.");
35015
+ }
35016
+ if (!flags.against && !flags.skipBootstrap) {
35017
+ throw new MissingSetupFlagError("--against", "Missing --against <manifest-url>. Pass an archive manifest to bootstrap from, or --skip-bootstrap to sync from genesis instead.");
35018
+ }
35019
+ const network = parseInstanceNetwork(flags.network);
35020
+ const nodeMode = parseSetupNodeMode(flags.nodeMode);
35021
+ const dir = resolvePath2(flags.dir ?? process.cwd());
35022
+ return {
35023
+ network,
35024
+ nodeMode,
35025
+ apiPort: flags.apiPort ?? "127.0.0.1:3800",
35026
+ indexerPort: "127.0.0.1:3700",
35027
+ dir,
35028
+ against: flags.against,
35029
+ skipBootstrap: !!flags.skipBootstrap,
35030
+ skipVerify: !!flags.skipVerify,
35031
+ yes: !!flags.yes,
35032
+ force: !!flags.force,
35033
+ owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
35034
+ imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
35035
+ };
35036
+ }
35037
+ function guardrailPreview(nodeMode, network) {
35038
+ return {
35039
+ ramFloorMb: nodeMode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb,
35040
+ diskFloorGb: diskFloorGb(nodeMode, network)
35041
+ };
35042
+ }
35043
+ function isBunRuntime() {
35044
+ return typeof process !== "undefined" && typeof process.versions === "object" && process.versions !== null && typeof process.versions.bun === "string";
35045
+ }
35046
+ async function checkDocker() {
35047
+ const probe = spawnSync5("docker", ["info"], { stdio: "ignore" });
35048
+ return !probe.error && probe.status === 0;
35049
+ }
35050
+ function randomHex(bytes) {
35051
+ return randomBytes4(bytes).toString("hex");
35052
+ }
35053
+ function resolveSecrets(config) {
35054
+ const envPath = join19(config.dir, ".env");
35055
+ const existing = config.force || !existsSync11(envPath) ? {} : readEnvFile(config.dir, envPath);
35056
+ const instance = buildInstanceEnv({
35057
+ network: config.network,
35058
+ existing: {
35059
+ INSTANCE_TOKEN: existing.INSTANCE_TOKEN,
35060
+ SECONDLAYER_SECRETS_KEY: existing.SECONDLAYER_SECRETS_KEY,
35061
+ STREAMS_SIGNING_PRIVATE_KEY: existing.STREAMS_SIGNING_PRIVATE_KEY,
35062
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: existing.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY,
35063
+ SL_API_URL: existing.SL_API_URL,
35064
+ SL_API_KEY: existing.SL_API_KEY,
35065
+ ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
35066
+ },
35067
+ apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`
35068
+ });
35069
+ return {
35070
+ instance,
35071
+ postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
35072
+ bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24)
35073
+ };
35074
+ }
35075
+ function normalizeLoopbackHost(apiPort) {
35076
+ return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
35077
+ }
35078
+ function readEnvFile(dir, envPath) {
35079
+ const shared = loadExistingInstanceEnv(dir, ".env");
35080
+ return {
35081
+ ...shared,
35082
+ POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
35083
+ BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD")
35084
+ };
35085
+ }
35086
+ function renderSetupEnv(config, secrets) {
35087
+ const i3 = secrets.instance;
35088
+ const lines2 = [
35089
+ "# Generated by `secondlayer setup`. Do not commit.",
35090
+ `NETWORK=${config.network}`,
35091
+ `STACKS_NETWORK=${config.network}`,
35092
+ `NODE_MODE=${config.nodeMode}`,
35093
+ `API_PORT=${config.apiPort}`,
35094
+ `INDEXER_PORT=${config.indexerPort}`,
35095
+ "POSTGRES_USER=secondlayer",
35096
+ `POSTGRES_PASSWORD=${secrets.postgresPassword}`,
35097
+ "POSTGRES_DB=secondlayer",
35098
+ "POSTGRES_PORT=127.0.0.1:5432",
35099
+ "# The instance credential. The API validates bearer tokens against it,",
35100
+ "# and the CLI/SDK/MCP read it first when authenticating.",
35101
+ `INSTANCE_TOKEN=${i3.INSTANCE_TOKEN}`,
35102
+ `SECONDLAYER_SECRETS_KEY=${i3.SECONDLAYER_SECRETS_KEY}`,
35103
+ `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(i3.STREAMS_SIGNING_PRIVATE_KEY)}`,
35104
+ `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(i3.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
35105
+ `ALLOW_UNSIGNED_WEBHOOKS=${i3.ALLOW_UNSIGNED_WEBHOOKS}`,
35106
+ "# Legacy alias of INSTANCE_TOKEN, same value.",
35107
+ `SL_API_KEY=${i3.SL_API_KEY}`,
35108
+ `SL_API_URL=${i3.SL_API_URL}`
35109
+ ];
35110
+ if (i3.ARCHIVE_SIGNING_PUBLIC_KEY) {
35111
+ lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(i3.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
35112
+ }
35113
+ if (config.nodeMode === "full") {
35114
+ lines2.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
35115
+ }
35116
+ lines2.push("");
35117
+ return lines2.join(`
35118
+ `);
35119
+ }
35120
+ async function writeSetupFiles(config, secrets) {
35121
+ const composePath = join19(config.dir, "docker-compose.yml");
35122
+ const envPath = join19(config.dir, ".env");
35123
+ await writeFile7(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
35124
+ await writeFile7(envPath, renderSetupEnv(config, secrets), { mode: 384 });
35125
+ const result = { composePath, envPath };
35126
+ if (config.nodeMode === "full") {
35127
+ const configTomlPath = join19(config.dir, "Config.toml");
35128
+ await writeFile7(configTomlPath, buildOssStacksConfigToml({
35129
+ network: config.network,
35130
+ bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
35131
+ }));
35132
+ result.configTomlPath = configTomlPath;
35133
+ }
35134
+ if (config.nodeMode === "full") {
35135
+ const bitcoinConfPath = join19(config.dir, "bitcoin.conf");
35136
+ await writeFile7(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
35137
+ result.bitcoinConfPath = bitcoinConfPath;
35138
+ }
35139
+ return result;
35140
+ }
35141
+ function composeProfileArgs(nodeMode) {
35142
+ if (nodeMode === "full")
35143
+ return ["--profile", "full-node"];
35144
+ return [];
35145
+ }
35146
+ function runStreaming(command, args, opts) {
35147
+ return new Promise((resolvePromise, reject) => {
35148
+ const child = spawn4(command, args, {
35149
+ cwd: opts.cwd,
35150
+ stdio: ["ignore", "pipe", "pipe"]
35151
+ });
35152
+ let stdout = "";
35153
+ let stderr = "";
35154
+ const pump = (stream2) => (chunk) => {
35155
+ const text2 = chunk.toString("utf8");
35156
+ if (stream2 === "stdout")
35157
+ stdout += text2;
35158
+ else
35159
+ stderr += text2;
35160
+ if (opts.onLine) {
35161
+ for (const line of text2.split(`
35162
+ `)) {
35163
+ if (line.length > 0)
35164
+ opts.onLine(line, stream2);
35165
+ }
35166
+ }
35167
+ };
35168
+ child.stdout?.on("data", pump("stdout"));
35169
+ child.stderr?.on("data", pump("stderr"));
35170
+ child.once("error", reject);
35171
+ child.once("close", (code) => {
35172
+ resolvePromise({ code: code ?? 1, stdout, stderr });
35173
+ });
35174
+ });
35175
+ }
35176
+ async function dockerComposeUp(config, files, onLine) {
35177
+ const args = [
35178
+ "compose",
35179
+ "-f",
35180
+ files.composePath,
35181
+ "--env-file",
35182
+ files.envPath,
35183
+ ...composeProfileArgs(config.nodeMode),
35184
+ "up",
35185
+ "-d"
35186
+ ];
35187
+ return runStreaming("docker", args, { cwd: config.dir, onLine });
35188
+ }
35189
+ async function pollHealth(config, opts = {}) {
35190
+ const timeoutMs = opts.timeoutMs ?? 120000;
35191
+ const intervalMs = opts.intervalMs ?? 2000;
35192
+ const url = `http://${normalizeLoopbackHost(config.apiPort)}/health`;
35193
+ const deadline = Date.now() + timeoutMs;
35194
+ for (;; ) {
35195
+ try {
35196
+ const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
35197
+ if (res.ok)
35198
+ return true;
35199
+ } catch {}
35200
+ opts.onTick?.();
35201
+ if (Date.now() >= deadline)
35202
+ return false;
35203
+ await new Promise((r2) => setTimeout(r2, intervalMs));
35204
+ }
35205
+ }
35206
+ function getObserverInfo(config) {
35207
+ if (config.nodeMode !== "external") {
35208
+ return { applicable: false };
35209
+ }
35210
+ const endpoint = defaultObserverEndpoint(config.network);
35211
+ const stanza = renderObserverStanza({
35212
+ mode: "indexer",
35213
+ endpoint,
35214
+ network: config.network
35215
+ });
35216
+ return { applicable: true, stanza, endpoint };
35217
+ }
35218
+ function cliEntry() {
35219
+ const entry = process.argv[1];
35220
+ if (!entry) {
35221
+ throw new Error("Could not resolve the secondlayer CLI entry to shell out to.");
35222
+ }
35223
+ return [process.execPath, entry];
35224
+ }
35225
+ async function runBootstrap(config, against, onLine) {
35226
+ const [command, ...prefix] = cliEntry();
35227
+ const args = [...prefix, "bootstrap", "--against", against, "--yes"];
35228
+ return runStreaming(command, args, { cwd: config.dir, onLine });
35229
+ }
35230
+ async function runVerify(config, against, onLine) {
35231
+ const [command, ...prefix] = cliEntry();
35232
+ const args = [...prefix, "verify", "all", "--against", against];
35233
+ return runStreaming(command, args, { cwd: config.dir, onLine });
35234
+ }
35235
+ function buildSuccessSummary(config, secrets) {
35236
+ const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
35237
+ const lines2 = [
35238
+ "Secondlayer is up.",
35239
+ "",
35240
+ ` api ${apiUrl}`,
35241
+ ` network ${config.network}`,
35242
+ ` node mode ${config.nodeMode}`,
35243
+ ` dir ${config.dir}`,
35244
+ "",
35245
+ "Export the instance token for writes (reads on loopback need no credential):",
35246
+ ` export INSTANCE_TOKEN=${secrets.instance.INSTANCE_TOKEN}`,
35247
+ "",
35248
+ "Try it:",
35249
+ " secondlayer subgraphs create my-balances --from-contract SP....my-contract",
35250
+ " secondlayer subgraphs deploy subgraphs/my-balances.ts",
35251
+ ` curl ${apiUrl}/v1/subgraphs/my-balances/balances`,
35252
+ "",
35253
+ "Docs: https://secondlayer.tools/docs/self-host"
35254
+ ];
35255
+ return lines2.join(`
35256
+ `);
35257
+ }
35258
+ async function runSetup(config, emit2) {
35259
+ emit2({ type: "step-start", step: "preflight" });
35260
+ if (!await checkDocker()) {
35261
+ emit2({
35262
+ type: "step-error",
35263
+ step: "preflight",
35264
+ message: "Docker isn't reachable. Start Docker Desktop/OrbStack, or install Docker, then re-run."
35265
+ });
35266
+ return { ok: false, config };
35267
+ }
35268
+ const floors = guardrailPreview(config.nodeMode, config.network);
35269
+ emit2({
35270
+ type: "step-done",
35271
+ step: "preflight",
35272
+ detail: `NODE_MODE=${config.nodeMode} on ${config.network} needs at least ${floors.ramFloorMb}MB RAM and ${floors.diskFloorGb}GB disk.`
35273
+ });
35274
+ emit2({ type: "step-start", step: "secrets" });
35275
+ const secrets = resolveSecrets({
35276
+ dir: config.dir,
35277
+ network: config.network,
35278
+ apiPort: config.apiPort,
35279
+ force: config.force
35280
+ });
35281
+ emit2({ type: "step-done", step: "secrets" });
35282
+ emit2({ type: "step-start", step: "config" });
35283
+ const files = await writeSetupFiles(config, secrets);
35284
+ emit2({
35285
+ type: "step-done",
35286
+ step: "config",
35287
+ detail: `${files.composePath}, ${files.envPath}`
35288
+ });
35289
+ emit2({ type: "step-start", step: "docker-up" });
35290
+ const up = await dockerComposeUp(config, files, (line) => emit2({ type: "step-log", step: "docker-up", line }));
35291
+ if (up.code !== 0) {
35292
+ emit2({
35293
+ type: "step-error",
35294
+ step: "docker-up",
35295
+ message: `docker compose up failed (exit ${up.code}).
35296
+ ${up.stderr || up.stdout}`
35297
+ });
35298
+ return { ok: false, config, secrets, files };
35299
+ }
35300
+ const healthy = await pollHealth(config, {
35301
+ onTick: () => emit2({
35302
+ type: "step-log",
35303
+ step: "docker-up",
35304
+ line: "waiting for /health…"
35305
+ })
35306
+ });
35307
+ if (!healthy) {
35308
+ emit2({
35309
+ type: "step-error",
35310
+ step: "docker-up",
35311
+ message: "Containers started but /health never came up. Check `docker compose logs`."
35312
+ });
35313
+ return { ok: false, config, secrets, files };
35314
+ }
35315
+ emit2({ type: "step-done", step: "docker-up" });
35316
+ emit2({ type: "step-start", step: "observer" });
35317
+ const observer = getObserverInfo(config);
35318
+ if (observer.applicable && observer.stanza) {
35319
+ emit2({
35320
+ type: "step-log",
35321
+ step: "observer",
35322
+ line: "ACTION REQUIRED — paste this into your Stacks node's Config.toml:"
35323
+ });
35324
+ for (const line of observer.stanza.split(`
35325
+ `)) {
35326
+ if (line.length > 0)
35327
+ emit2({ type: "step-log", step: "observer", line });
35328
+ }
35329
+ emit2({
35330
+ type: "step-done",
35331
+ step: "observer",
35332
+ detail: `endpoint ${observer.endpoint} — see the ACTION REQUIRED lines above`
35333
+ });
35334
+ } else {
35335
+ emit2({
35336
+ type: "step-done",
35337
+ step: "observer",
35338
+ detail: "bundled node — the observer is pre-configured, nothing to paste"
35339
+ });
35340
+ }
35341
+ let bootstrapped = false;
35342
+ if (config.skipBootstrap) {
35343
+ emit2({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
35344
+ } else if (config.against) {
35345
+ emit2({ type: "step-start", step: "bootstrap" });
35346
+ const res = await runBootstrap(config, config.against, (line) => emit2({ type: "step-log", step: "bootstrap", line }));
35347
+ if (res.code === 0) {
35348
+ bootstrapped = true;
35349
+ emit2({ type: "step-done", step: "bootstrap" });
35350
+ } else {
35351
+ emit2({
35352
+ type: "step-error",
35353
+ step: "bootstrap",
35354
+ message: `secondlayer bootstrap exited ${res.code}.`
35355
+ });
35356
+ }
35357
+ }
35358
+ if (config.skipVerify) {
35359
+ emit2({ type: "step-skip", step: "verify", reason: "--skip-verify" });
35360
+ } else if (!config.against) {
35361
+ emit2({
35362
+ type: "step-skip",
35363
+ step: "verify",
35364
+ reason: "no manifest to verify against"
35365
+ });
35366
+ } else if (!bootstrapped) {
35367
+ emit2({
35368
+ type: "step-skip",
35369
+ step: "verify",
35370
+ reason: "bootstrap did not run — nothing restored yet to verify"
35371
+ });
35372
+ } else {
35373
+ emit2({ type: "step-start", step: "verify" });
35374
+ const res = await runVerify(config, config.against, (line) => emit2({ type: "step-log", step: "verify", line }));
35375
+ if (res.code === 0) {
35376
+ emit2({ type: "step-done", step: "verify" });
35377
+ } else {
35378
+ emit2({
35379
+ type: "step-error",
35380
+ step: "verify",
35381
+ message: `secondlayer verify exited ${res.code}.`
35382
+ });
35383
+ }
35384
+ }
35385
+ const summary = buildSuccessSummary(config, secrets);
35386
+ emit2({ type: "step-start", step: "summary" });
35387
+ emit2({ type: "step-done", step: "summary", detail: summary });
35388
+ return { ok: true, config, secrets, files, observer, summary };
35389
+ }
35390
+ var SETUP_NODE_MODES, DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json", MissingSetupFlagError;
35391
+ var init_setup_wizard = __esm(() => {
35392
+ init_instance_init();
35393
+ init_observer_stanza();
35394
+ init_oss_compose();
35395
+ SETUP_NODE_MODES = ["external", "stacks", "full"];
35396
+ MissingSetupFlagError = class MissingSetupFlagError extends Error {
35397
+ flag;
35398
+ constructor(flag, message) {
35399
+ super(message);
35400
+ this.name = "MissingSetupFlagError";
35401
+ this.flag = flag;
35402
+ }
35403
+ };
35404
+ });
35405
+
35406
+ // src/lib/setup-tui.tsx
35407
+ var exports_setup_tui = {};
35408
+ __export(exports_setup_tui, {
35409
+ runSetupTui: () => runSetupTui
35410
+ });
35411
+ import {
35412
+ CliRenderEvents,
35413
+ createCliRenderer
35414
+ } from "@opentui/core";
35415
+ import { createRoot, useKeyboard, useRenderer } from "@opentui/react";
35416
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
35417
+ import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
35418
+ function useTerminalThemeMode() {
35419
+ const renderer = useRenderer();
35420
+ const [mode, setMode] = useState(renderer.themeMode ?? "dark");
35421
+ useEffect(() => {
35422
+ let cancelled = false;
35423
+ renderer.waitForThemeMode(500).then((resolved) => {
35424
+ if (!cancelled && resolved)
35425
+ setMode(resolved);
35426
+ });
35427
+ const onThemeMode = (next) => setMode(next);
35428
+ renderer.on(CliRenderEvents.THEME_MODE, onThemeMode);
35429
+ return () => {
35430
+ cancelled = true;
35431
+ renderer.off(CliRenderEvents.THEME_MODE, onThemeMode);
35432
+ };
35433
+ }, [renderer]);
35434
+ return mode;
35435
+ }
35436
+ function formatFloor(f2) {
35437
+ return `needs ≥ ${(f2.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${f2.diskFloorGb}GB disk`;
35438
+ }
35439
+ function useSpinner(active) {
35440
+ const [frame, setFrame] = useState(0);
35441
+ useEffect(() => {
35442
+ if (!active)
35443
+ return;
35444
+ const id = setInterval(() => setFrame((f2) => (f2 + 1) % SPINNER_FRAMES.length), 80);
35445
+ return () => clearInterval(id);
35446
+ }, [active]);
35447
+ return SPINNER_FRAMES[frame];
35448
+ }
35449
+ function App({ flags }) {
35450
+ const renderer = useRenderer();
35451
+ const themeMode = useTerminalThemeMode();
35452
+ const colors2 = themeMode === "light" ? LIGHT_COLORS : DARK_COLORS;
35453
+ const [stage, setStage] = useState("welcome");
35454
+ const [network, setNetwork] = useState(safeParseNetwork(flags.network));
35455
+ const [nodeMode, setNodeMode] = useState(safeParseNodeMode(flags.nodeMode));
35456
+ const [preview, setPreview] = useState(guardrailPreview(NODE_MODE_OPTIONS[0].value, "mainnet"));
35457
+ const [against, setAgainst] = useState(flags.against);
35458
+ const [skipBootstrap, setSkipBootstrap] = useState(!!flags.skipBootstrap);
35459
+ const [events, setEvents] = useState([]);
35460
+ const [log, setLog] = useState([]);
35461
+ const [fatal, setFatal] = useState(null);
35462
+ const [summary, setSummary] = useState(null);
35463
+ const started = useRef(false);
35464
+ function exit(code) {
35465
+ renderer.destroy();
35466
+ process.exit(code);
35467
+ }
35468
+ useKeyboard((key) => {
35469
+ if (key.eventType === "release")
35470
+ return;
35471
+ if ((stage === "done" || stage === "error") && (key.name === "q" || key.name === "return")) {
35472
+ exit(fatal ? 1 : 0);
35473
+ return;
35474
+ }
35475
+ if (stage === "welcome" && (key.name === "return" || key.name === "space")) {
35476
+ advanceFromWelcome();
35477
+ return;
35478
+ }
35479
+ if (stage === "welcome" && key.name === "q") {
35480
+ exit(0);
35481
+ }
35482
+ });
35483
+ function advanceFromWelcome() {
35484
+ if (!network) {
35485
+ setStage("network");
35486
+ } else if (!nodeMode) {
35487
+ setStage("nodeMode");
35488
+ } else if (!against && !skipBootstrap) {
35489
+ setStage("manifestChoice");
35490
+ } else {
35491
+ setStage("confirm");
35492
+ }
35493
+ }
35494
+ function afterNetwork(net3) {
35495
+ setNetwork(net3);
35496
+ if (!nodeMode)
35497
+ setStage("nodeMode");
35498
+ else if (!against && !skipBootstrap)
35499
+ setStage("manifestChoice");
35500
+ else
35501
+ setStage("confirm");
35502
+ }
35503
+ function afterNodeMode(mode) {
35504
+ setNodeMode(mode);
35505
+ if (!against && !skipBootstrap)
35506
+ setStage("manifestChoice");
35507
+ else
35508
+ setStage("confirm");
35509
+ }
35510
+ const config = network && nodeMode ? {
35511
+ network,
35512
+ nodeMode,
35513
+ apiPort: flags.apiPort ?? "127.0.0.1:3800",
35514
+ indexerPort: "127.0.0.1:3700",
35515
+ dir: flags.dir ?? process.cwd(),
35516
+ against: skipBootstrap ? undefined : against,
35517
+ skipBootstrap,
35518
+ skipVerify: !!flags.skipVerify,
35519
+ yes: false,
35520
+ force: !!flags.force,
35521
+ owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
35522
+ imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
35523
+ } : null;
35524
+ useEffect(() => {
35525
+ if (stage !== "running" || started.current || !config)
35526
+ return;
35527
+ started.current = true;
35528
+ let failed = false;
35529
+ runSetup(config, (event) => {
35530
+ setEvents((prev) => [...prev, event]);
35531
+ if (event.type === "step-log") {
35532
+ setLog((prev) => [...prev.slice(-40), event.line]);
35533
+ }
35534
+ if (event.type === "step-error") {
35535
+ failed = true;
35536
+ setLog((prev) => [...prev.slice(-40), `ERROR: ${event.message}`]);
35537
+ }
35538
+ }).then((result) => {
35539
+ if (!result.ok) {
35540
+ setFatal("Setup could not finish — see the log above.");
35541
+ setStage("error");
35542
+ return;
35543
+ }
35544
+ setSummary(result.summary ?? null);
35545
+ setStage(failed ? "error" : "done");
35546
+ if (failed)
35547
+ setFatal("Setup finished but at least one step reported an error.");
35548
+ }).catch((err) => {
35549
+ setFatal(err instanceof Error ? err.message : String(err));
35550
+ setStage("error");
35551
+ });
35552
+ }, [stage, config]);
35553
+ return /* @__PURE__ */ jsxDEV(ColorsContext.Provider, {
35554
+ value: colors2,
35555
+ children: /* @__PURE__ */ jsxDEV("box", {
35556
+ style: {
35557
+ border: true,
35558
+ borderStyle: "single",
35559
+ borderColor: colors2.borderIdle,
35560
+ backgroundColor: colors2.bg,
35561
+ padding: 1
35562
+ },
35563
+ title: "secondlayer setup",
35564
+ children: /* @__PURE__ */ jsxDEV("box", {
35565
+ style: { flexDirection: "column" },
35566
+ children: [
35567
+ stage === "welcome" && /* @__PURE__ */ jsxDEV(Welcome, {}, undefined, false, undefined, this),
35568
+ stage === "network" && /* @__PURE__ */ jsxDEV(Picker, {
35569
+ title: "Network",
35570
+ options: NETWORK_OPTIONS,
35571
+ onSelect: (value) => afterNetwork(parseInstanceNetwork(value))
35572
+ }, undefined, false, undefined, this),
35573
+ stage === "nodeMode" && /* @__PURE__ */ jsxDEV(Picker, {
35574
+ title: "Node mode",
35575
+ options: NODE_MODE_OPTIONS,
35576
+ subtitle: formatFloor(preview),
35577
+ onChange: (value) => setPreview(guardrailPreview(parseSetupNodeMode(value), network ?? "mainnet")),
35578
+ onSelect: (value) => afterNodeMode(parseSetupNodeMode(value))
35579
+ }, undefined, false, undefined, this),
35580
+ stage === "manifestChoice" && /* @__PURE__ */ jsxDEV(Picker, {
35581
+ title: "Bootstrap from",
35582
+ options: [
35583
+ {
35584
+ name: "Hosted archive (recommended)",
35585
+ description: DEFAULT_ARCHIVE_MANIFEST,
35586
+ value: "hosted"
35587
+ },
35588
+ {
35589
+ name: "Custom manifest URL or path",
35590
+ description: "Enter your own",
35591
+ value: "custom"
35592
+ },
35593
+ {
35594
+ name: "Sync from genesis",
35595
+ description: "Skip bootstrap — slower, no download",
35596
+ value: "skip"
35597
+ }
35598
+ ],
35599
+ onSelect: (value) => {
35600
+ if (value === "hosted") {
35601
+ setAgainst(DEFAULT_ARCHIVE_MANIFEST);
35602
+ setStage("confirm");
35603
+ } else if (value === "skip") {
35604
+ setSkipBootstrap(true);
35605
+ setStage("confirm");
35606
+ } else {
35607
+ setStage("manifestInput");
35608
+ }
35609
+ }
35610
+ }, undefined, false, undefined, this),
35611
+ stage === "manifestInput" && /* @__PURE__ */ jsxDEV(ManifestInput, {
35612
+ onSubmit: (value) => {
35613
+ setAgainst(value.trim() || DEFAULT_ARCHIVE_MANIFEST);
35614
+ setStage("confirm");
35615
+ }
35616
+ }, undefined, false, undefined, this),
35617
+ stage === "confirm" && config && /* @__PURE__ */ jsxDEV(Confirm, {
35618
+ config,
35619
+ onConfirm: () => setStage("running")
35620
+ }, undefined, false, undefined, this),
35621
+ stage === "running" && /* @__PURE__ */ jsxDEV(RunningView, {
35622
+ events,
35623
+ log
35624
+ }, undefined, false, undefined, this),
35625
+ stage === "done" && summary && /* @__PURE__ */ jsxDEV(Done, {
35626
+ summary
35627
+ }, undefined, false, undefined, this),
35628
+ stage === "error" && /* @__PURE__ */ jsxDEV(ErrorView, {
35629
+ message: fatal ?? "Setup failed.",
35630
+ log
35631
+ }, undefined, false, undefined, this)
35632
+ ]
35633
+ }, undefined, true, undefined, this)
35634
+ }, undefined, false, undefined, this)
35635
+ }, undefined, false, undefined, this);
35636
+ }
35637
+ function safeParseNetwork(value) {
35638
+ if (!value)
35639
+ return;
35640
+ try {
35641
+ return parseInstanceNetwork(value);
35642
+ } catch {
35643
+ return;
35644
+ }
35645
+ }
35646
+ function safeParseNodeMode(value) {
35647
+ if (!value)
35648
+ return;
35649
+ try {
35650
+ return parseSetupNodeMode(value);
35651
+ } catch {
35652
+ return;
35653
+ }
35654
+ }
35655
+ function Welcome() {
35656
+ const colors2 = useContext(ColorsContext);
35657
+ return /* @__PURE__ */ jsxDEV("box", {
35658
+ style: { flexDirection: "column" },
35659
+ children: [
35660
+ /* @__PURE__ */ jsxDEV("text", {
35661
+ fg: colors2.bright,
35662
+ children: "Guided self-host setup."
35663
+ }, undefined, false, undefined, this),
35664
+ /* @__PURE__ */ jsxDEV("text", {
35665
+ fg: colors2.muted,
35666
+ children: [
35667
+ `
35668
+ `,
35669
+ "This writes secrets, a docker-compose file, and .env into a target directory, brings the stack up, and (optionally) restores verified history from the archive.",
35670
+ `
35671
+
35672
+ `,
35673
+ "Press Enter to begin, or q to quit."
35674
+ ]
35675
+ }, undefined, true, undefined, this)
35676
+ ]
35677
+ }, undefined, true, undefined, this);
35678
+ }
35679
+ function Picker({
35680
+ title,
35681
+ subtitle,
35682
+ options: options3,
35683
+ onChange,
35684
+ onSelect
35685
+ }) {
35686
+ const colors2 = useContext(ColorsContext);
35687
+ return /* @__PURE__ */ jsxDEV("box", {
35688
+ style: { flexDirection: "column" },
35689
+ children: [
35690
+ /* @__PURE__ */ jsxDEV("text", {
35691
+ fg: colors2.bright,
35692
+ children: title
35693
+ }, undefined, false, undefined, this),
35694
+ subtitle && /* @__PURE__ */ jsxDEV("text", {
35695
+ fg: colors2.accent,
35696
+ children: subtitle
35697
+ }, undefined, false, undefined, this),
35698
+ /* @__PURE__ */ jsxDEV("select", {
35699
+ style: { height: options3.length * 2 + 1 },
35700
+ options: options3,
35701
+ focused: true,
35702
+ showDescription: true,
35703
+ backgroundColor: "transparent",
35704
+ textColor: colors2.fg,
35705
+ focusedBackgroundColor: colors2.raise,
35706
+ focusedTextColor: colors2.bright,
35707
+ selectedBackgroundColor: colors2.raise,
35708
+ selectedTextColor: colors2.accent,
35709
+ descriptionColor: colors2.muted,
35710
+ selectedDescriptionColor: colors2.fg,
35711
+ onChange: (_index, option) => {
35712
+ if (option)
35713
+ onChange?.(String(option.value));
35714
+ },
35715
+ onSelect: (_index, option) => {
35716
+ if (option)
35717
+ onSelect(String(option.value));
35718
+ }
35719
+ }, undefined, false, undefined, this),
35720
+ /* @__PURE__ */ jsxDEV("text", {
35721
+ fg: colors2.muted,
35722
+ children: [
35723
+ `
35724
+ `,
35725
+ "↑↓ to move, enter to choose"
35726
+ ]
35727
+ }, undefined, true, undefined, this)
35728
+ ]
35729
+ }, undefined, true, undefined, this);
35730
+ }
35731
+ function ManifestInput({ onSubmit }) {
35732
+ const colors2 = useContext(ColorsContext);
35733
+ return /* @__PURE__ */ jsxDEV("box", {
35734
+ style: { flexDirection: "column" },
35735
+ children: [
35736
+ /* @__PURE__ */ jsxDEV("text", {
35737
+ fg: colors2.bright,
35738
+ children: "Manifest URL or local path"
35739
+ }, undefined, false, undefined, this),
35740
+ /* @__PURE__ */ jsxDEV("text", {
35741
+ fg: colors2.muted,
35742
+ children: [
35743
+ "Suggested: ",
35744
+ DEFAULT_ARCHIVE_MANIFEST
35745
+ ]
35746
+ }, undefined, true, undefined, this),
35747
+ /* @__PURE__ */ jsxDEV("input", {
35748
+ focused: true,
35749
+ placeholder: DEFAULT_ARCHIVE_MANIFEST,
35750
+ textColor: colors2.fg,
35751
+ focusedTextColor: colors2.accent,
35752
+ placeholderColor: colors2.muted,
35753
+ onSubmit: (value) => onSubmit(String(value ?? ""))
35754
+ }, undefined, false, undefined, this),
35755
+ /* @__PURE__ */ jsxDEV("text", {
35756
+ fg: colors2.muted,
35757
+ children: [
35758
+ `
35759
+ `,
35760
+ "enter to accept (blank = suggested default)"
35761
+ ]
35762
+ }, undefined, true, undefined, this)
35763
+ ]
35764
+ }, undefined, true, undefined, this);
35765
+ }
35766
+ function Confirm({
35767
+ config,
35768
+ onConfirm
35769
+ }) {
35770
+ const colors2 = useContext(ColorsContext);
35771
+ useKeyboard((key) => {
35772
+ if (key.eventType === "release")
35773
+ return;
35774
+ if (key.name === "return")
35775
+ onConfirm();
35776
+ });
35777
+ return /* @__PURE__ */ jsxDEV("box", {
35778
+ style: { flexDirection: "column" },
35779
+ children: [
35780
+ /* @__PURE__ */ jsxDEV("text", {
35781
+ fg: colors2.bright,
35782
+ children: "Ready"
35783
+ }, undefined, false, undefined, this),
35784
+ /* @__PURE__ */ jsxDEV("text", {
35785
+ fg: colors2.fg,
35786
+ children: [
35787
+ `
35788
+ `,
35789
+ "network ",
35790
+ config.network,
35791
+ `
35792
+ `,
35793
+ "node mode ",
35794
+ config.nodeMode,
35795
+ `
35796
+ `,
35797
+ "dir ",
35798
+ config.dir,
35799
+ `
35800
+ `,
35801
+ "api port ",
35802
+ config.apiPort,
35803
+ `
35804
+ `,
35805
+ "bootstrap",
35806
+ " ",
35807
+ config.skipBootstrap ? "skip (sync from genesis)" : config.against,
35808
+ `
35809
+ `,
35810
+ "image ghcr.io/",
35811
+ config.owner,
35812
+ "/secondlayer-runtime:",
35813
+ config.imageTag
35814
+ ]
35815
+ }, undefined, true, undefined, this),
35816
+ /* @__PURE__ */ jsxDEV("text", {
35817
+ fg: colors2.accent,
35818
+ children: [
35819
+ `
35820
+ `,
35821
+ "Press Enter to start."
35822
+ ]
35823
+ }, undefined, true, undefined, this)
35824
+ ]
35825
+ }, undefined, true, undefined, this);
35826
+ }
35827
+ function RunningView({ events, log }) {
35828
+ const colors2 = useContext(ColorsContext);
35829
+ const latestByStep = new Map;
35830
+ for (const e2 of events)
35831
+ latestByStep.set(e2.step, e2);
35832
+ const anyActive = STEP_ORDER.some((step) => latestByStep.get(step)?.type === "step-start");
35833
+ const spinner = useSpinner(anyActive);
35834
+ return /* @__PURE__ */ jsxDEV("box", {
35835
+ style: { flexDirection: "column" },
35836
+ children: [
35837
+ STEP_ORDER.map((step) => {
35838
+ const e2 = latestByStep.get(step);
35839
+ const active = e2?.type === "step-start";
35840
+ const symbol = !e2 ? "·" : active ? spinner : e2.type === "step-done" ? "✓" : e2.type === "step-skip" ? "–" : e2.type === "step-error" ? "✗" : " ";
35841
+ const color = active ? colors2.accent : e2?.type === "step-done" ? colors2.success : e2?.type === "step-error" ? colors2.error : e2?.type === "step-skip" ? colors2.muted : colors2.muted;
35842
+ return /* @__PURE__ */ jsxDEV("text", {
35843
+ fg: color,
35844
+ children: [
35845
+ symbol,
35846
+ " ",
35847
+ STEP_LABEL[step]
35848
+ ]
35849
+ }, step, true, undefined, this);
35850
+ }),
35851
+ /* @__PURE__ */ jsxDEV("box", {
35852
+ style: {
35853
+ border: true,
35854
+ borderStyle: "single",
35855
+ borderColor: colors2.borderIdle,
35856
+ marginTop: 1,
35857
+ height: 10
35858
+ },
35859
+ title: "log",
35860
+ children: /* @__PURE__ */ jsxDEV("box", {
35861
+ style: { flexDirection: "column", padding: 1 },
35862
+ children: log.length === 0 ? /* @__PURE__ */ jsxDEV("text", {
35863
+ fg: colors2.muted,
35864
+ children: "waiting…"
35865
+ }, undefined, false, undefined, this) : log.map((line, i3) => /* @__PURE__ */ jsxDEV("text", {
35866
+ fg: line.startsWith("ERROR:") ? colors2.error : colors2.fg,
35867
+ children: line
35868
+ }, `${i3}:${line}`, false, undefined, this))
35869
+ }, undefined, false, undefined, this)
35870
+ }, undefined, false, undefined, this)
35871
+ ]
35872
+ }, undefined, true, undefined, this);
35873
+ }
35874
+ function Done({ summary }) {
35875
+ const colors2 = useContext(ColorsContext);
35876
+ return /* @__PURE__ */ jsxDEV("box", {
35877
+ style: { flexDirection: "column" },
35878
+ children: [
35879
+ /* @__PURE__ */ jsxDEV("box", {
35880
+ style: { backgroundColor: colors2.successBg, padding: 1 },
35881
+ children: /* @__PURE__ */ jsxDEV("text", {
35882
+ fg: colors2.success,
35883
+ children: summary
35884
+ }, undefined, false, undefined, this)
35885
+ }, undefined, false, undefined, this),
35886
+ /* @__PURE__ */ jsxDEV("text", {
35887
+ fg: colors2.muted,
35888
+ children: [
35889
+ `
35890
+ `,
35891
+ "press enter or q to exit"
35892
+ ]
35893
+ }, undefined, true, undefined, this)
35894
+ ]
35895
+ }, undefined, true, undefined, this);
35896
+ }
35897
+ function ErrorView({ message, log }) {
35898
+ const colors2 = useContext(ColorsContext);
35899
+ return /* @__PURE__ */ jsxDEV("box", {
35900
+ style: { flexDirection: "column" },
35901
+ children: [
35902
+ /* @__PURE__ */ jsxDEV("box", {
35903
+ style: { backgroundColor: colors2.errorBg, padding: 1 },
35904
+ children: /* @__PURE__ */ jsxDEV("text", {
35905
+ fg: colors2.error,
35906
+ children: message
35907
+ }, undefined, false, undefined, this)
35908
+ }, undefined, false, undefined, this),
35909
+ log.slice(-10).map((line, i3) => /* @__PURE__ */ jsxDEV("text", {
35910
+ fg: colors2.muted,
35911
+ children: line
35912
+ }, `${i3}:${line}`, false, undefined, this)),
35913
+ /* @__PURE__ */ jsxDEV("text", {
35914
+ fg: colors2.muted,
35915
+ children: [
35916
+ `
35917
+ `,
35918
+ "press enter or q to exit"
35919
+ ]
35920
+ }, undefined, true, undefined, this)
35921
+ ]
35922
+ }, undefined, true, undefined, this);
35923
+ }
35924
+ async function runSetupTui(flags) {
35925
+ const renderer = await createCliRenderer({ exitOnCtrlC: true });
35926
+ createRoot(renderer).render(/* @__PURE__ */ jsxDEV(App, {
35927
+ flags
35928
+ }, undefined, false, undefined, this));
35929
+ return new Promise((resolve16, reject) => {
35930
+ let settled = false;
35931
+ const onDestroy = () => {
35932
+ if (settled)
35933
+ return;
35934
+ settled = true;
35935
+ resolve16();
35936
+ };
35937
+ const onError = (event) => {
35938
+ if (settled)
35939
+ return;
35940
+ settled = true;
35941
+ try {
35942
+ renderer.destroy();
35943
+ } catch {}
35944
+ reject(event.error ?? new Error("OpenTUI renderer error"));
35945
+ };
35946
+ renderer.once(CliRenderEvents.DESTROY, onDestroy);
35947
+ renderer.once(CliRenderEvents.RENDER_ERROR, onError);
35948
+ renderer.once(CliRenderEvents.HANDLER_ERROR, onError);
35949
+ });
35950
+ }
35951
+ var DARK_COLORS, LIGHT_COLORS, ColorsContext, NETWORK_OPTIONS, NODE_MODE_OPTIONS, SPINNER_FRAMES, STEP_LABEL, STEP_ORDER;
35952
+ var init_setup_tui = __esm(() => {
35953
+ init_instance_init();
35954
+ init_oss_compose();
35955
+ init_setup_wizard();
35956
+ DARK_COLORS = {
35957
+ bg: "#151515",
35958
+ raise: "#202020",
35959
+ fg: "#d7d1c9",
35960
+ muted: "#8d867e",
35961
+ bright: "#f2ede5",
35962
+ accent: "#29c6be",
35963
+ success: "#22c55e",
35964
+ warning: "#eab308",
35965
+ error: "#ef4444",
35966
+ borderIdle: "#FFFFFF14",
35967
+ successBg: "#22c55e14",
35968
+ errorBg: "#ef444414"
35969
+ };
35970
+ LIGHT_COLORS = {
35971
+ bg: "#ffffff",
35972
+ raise: "#f8f6f2",
35973
+ fg: "#1f2228",
35974
+ muted: "#7d8288",
35975
+ bright: "#0a0a0a",
35976
+ accent: "#29c6be",
35977
+ success: "#22c55e",
35978
+ warning: "#eab308",
35979
+ error: "#ef4444",
35980
+ borderIdle: "#0A0A0A14",
35981
+ successBg: "#22c55e14",
35982
+ errorBg: "#ef444414"
35983
+ };
35984
+ ColorsContext = createContext(DARK_COLORS);
35985
+ NETWORK_OPTIONS = [
35986
+ {
35987
+ name: "mainnet",
35988
+ description: "Production Stacks chain. Large: plan for hundreds of GB.",
35989
+ value: "mainnet"
35990
+ },
35991
+ {
35992
+ name: "testnet",
35993
+ description: "Stacks testnet. Small history.",
35994
+ value: "testnet"
35995
+ },
35996
+ { name: "devnet", description: "Local Clarinet devnet.", value: "devnet" }
35997
+ ];
35998
+ NODE_MODE_OPTIONS = SETUP_NODE_MODES.map((mode) => ({
35999
+ name: mode,
36000
+ description: mode === "external" ? "You run the Stacks node; paste an observer stanza into its Config.toml." : mode === "stacks" ? "You run the Stacks node yourself, no bundled bitcoind (same as external — no bundled-stacks-only profile exists)." : "Bundled Stacks node + bitcoind.",
36001
+ value: mode
36002
+ }));
36003
+ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
36004
+ STEP_LABEL = {
36005
+ preflight: "Preflight",
36006
+ secrets: "Secrets",
36007
+ config: "Write config",
36008
+ "docker-up": "docker compose up",
36009
+ observer: "Observer stanza",
36010
+ bootstrap: "Bootstrap",
36011
+ verify: "Verify",
36012
+ summary: "Summary"
36013
+ };
36014
+ STEP_ORDER = Object.keys(STEP_LABEL);
36015
+ });
36016
+
36017
+ // ../../node_modules/commander/esm.mjs
36018
+ var import__ = __toESM(require_commander(), 1);
36019
+ var {
36020
+ program,
36021
+ createCommand,
36022
+ createArgument,
36023
+ createOption,
36024
+ CommanderError,
36025
+ InvalidArgumentError,
36026
+ InvalidOptionArgumentError,
36027
+ Command,
36028
+ Argument,
36029
+ Option,
36030
+ Help
36031
+ } = import__.default;
34320
36032
  // package.json
34321
36033
  var package_default = {
34322
36034
  name: "@secondlayer/cli",
34323
- version: "12.1.0",
36035
+ version: "13.0.0",
34324
36036
  description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
34325
36037
  repository: {
34326
36038
  type: "git",
@@ -34361,13 +36073,16 @@ var package_default = {
34361
36073
  "@biomejs/wasm-nodejs": "^1.9.0",
34362
36074
  "@dsnp/parquetjs": "^1.8.8",
34363
36075
  "@inquirer/prompts": "^8.2.0",
36076
+ "@opentui/core": "^0.5.3",
36077
+ "@opentui/react": "^0.5.3",
34364
36078
  "@secondlayer/bundler": "^0.5.1",
34365
36079
  "@secondlayer/scaffold": "^1.5.4",
34366
- "@secondlayer/sdk": "^8.0.1",
34367
- "@secondlayer/shared": "^10.0.0",
36080
+ "@secondlayer/sdk": "^9.0.0",
36081
+ "@secondlayer/shared": "^11.0.0",
34368
36082
  "@secondlayer/stacks": "^4.0.0",
34369
- "@secondlayer/subgraphs": "^4.0.2",
36083
+ "@secondlayer/subgraphs": "^4.0.4",
34370
36084
  esbuild: "^0.27.4",
36085
+ react: "^19.1.0",
34371
36086
  "smol-toml": "^1.6.1",
34372
36087
  zod: "^4.3.6"
34373
36088
  },
@@ -34381,8 +36096,7 @@ var package_default = {
34381
36096
  commander: "^14.0.0",
34382
36097
  execa: "^9.5.3",
34383
36098
  "fast-glob": "^3.3.3",
34384
- got: "^13.0.0",
34385
- react: "^19.1.0"
36099
+ got: "^13.0.0"
34386
36100
  }
34387
36101
  };
34388
36102
 
@@ -34498,10 +36212,30 @@ async function runRefill(opts) {
34498
36212
  }
34499
36213
  });
34500
36214
  }
34501
-
34502
- // src/commands/index.ts
34503
- init_codegen();
34504
-
36215
+ // src/commands/codegen.ts
36216
+ function registerCodegenCommand(program2) {
36217
+ const codegen = program2.command("codegen").description("Generate typed code: contracts, subgraphs, Index, clients");
36218
+ codegen.command("contracts [files...]").description("Generate TypeScript interfaces from Clarity contracts").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output file path").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("-w, --watch", "Watch for changes").action(async (files, options3) => {
36219
+ const { generate: generate2 } = await Promise.resolve().then(() => (init_generate(), exports_generate));
36220
+ await generate2(files, { ...options3, out: options3.output });
36221
+ });
36222
+ codegen.command("subgraph <file>").description("Generate an ORM schema (Kysely, Prisma, or Drizzle) for a subgraph's tables").option("--target <orm>", "ORM target: kysely | prisma | drizzle", "kysely").option("--schema <name>", "Postgres schema name (defaults to subgraph_<name>)").option("--env <var>", "datasource url env var", "DATABASE_URL").option("--models-only", "Emit only Prisma models (compose via prismaSchemaFolder)").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (file, options3, command) => {
36223
+ const { runSubgraphSchemaCodegen: runSubgraphSchemaCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
36224
+ await runSubgraphSchemaCodegen2(file, options3, (key) => command.getOptionValueSource(key), "kysely");
36225
+ });
36226
+ codegen.command("index").description("Generate a typed schema (Kysely, Prisma, Drizzle, or JSON-Schema) for the Index domain tables").option("--target <orm>", "kysely | prisma | drizzle | json-schema", "kysely").option("--schema <name>", "Postgres schema to qualify table names with").option("--tables <list>", "Comma-separated subset of Index tables (default: all)").option("--env <var>", "Prisma datasource url env var", "DATABASE_URL").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (options3) => {
36227
+ const { runIndexCodegen: runIndexCodegen2 } = await Promise.resolve().then(() => (init_index_api(), exports_index_api));
36228
+ await runIndexCodegen2(options3);
36229
+ });
36230
+ codegen.command("client <subgraphName>").description("Generate a typed query client for a deployed subgraph").option("-o, --output <path>", "Output file path (required)").action(async (subgraphName, options3) => {
36231
+ const { runSubgraphClientCodegen: runSubgraphClientCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
36232
+ await runSubgraphClientCodegen2(subgraphName, options3);
36233
+ });
36234
+ codegen.command("prints <file>").description("Emit a .d.ts of print payload types for a subgraph's pinned print_event sources (inferred from observed on-chain events; requires network)").option("-o, --output <path>", "Write to a file (defaults to stdout)").action(async (file, options3) => {
36235
+ const { runPayloadsCodegen: runPayloadsCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
36236
+ await runPayloadsCodegen2(file, options3.output);
36237
+ });
36238
+ }
34505
36239
  // src/commands/config.ts
34506
36240
  init_config2();
34507
36241
 
@@ -34959,22 +36693,138 @@ function registerConsoleCommand(program2) {
34959
36693
  }
34960
36694
  });
34961
36695
  }
36696
+ // src/commands/context.ts
36697
+ init_http();
36698
+ import { SecondLayer as SecondLayer2 } from "@secondlayer/sdk";
36699
+
36700
+ // src/lib/instance-diagnosis.ts
36701
+ var BOOTSTRAP_STEP = "Restore verified history: secondlayer bootstrap --against <manifest>";
36702
+ var OBSERVER_STEP = "Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs";
36703
+ var SELF_HOST_DOCS_STEP = "Docs: https://www.secondlayer.tools/docs/self-host";
36704
+ function hasNoChainData(status) {
36705
+ const integrity = status.chainIntegrity;
36706
+ if (!integrity || integrity.reason === "check_failed")
36707
+ return false;
36708
+ if (integrity.maxHeight !== 0)
36709
+ return false;
36710
+ return (status.chainTip ?? null) === null;
36711
+ }
36712
+ function emptyIndexIssue(status) {
36713
+ const decoders = status.index?.decoders ?? [];
36714
+ const stalled = decoders.filter((d2) => d2.status !== "ok").length;
36715
+ const decoderClause = stalled > 0 ? ` and ${stalled} decoder${stalled === 1 ? "" : "s"} report unavailable because there is nothing to decode` : "";
36716
+ return {
36717
+ title: "No blocks indexed yet — this instance holds 0 blocks.",
36718
+ detail: `Streams has no tip${decoderClause}, which is what makes the overall status DEGRADED. This is the expected state for a fresh instance: nothing is broken, but no read returns data until history lands.`,
36719
+ nextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP]
36720
+ };
36721
+ }
36722
+ var SERVICE_STEPS = [
36723
+ "See the raw per-service report: secondlayer status --json",
36724
+ "Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer"
36725
+ ];
36726
+ function diagnoseInstanceStatus(status) {
36727
+ const overall = String(status.status ?? "unknown");
36728
+ if (hasNoChainData(status)) {
36729
+ return { state: "empty-index", overall, issues: [emptyIndexIssue(status)] };
36730
+ }
36731
+ const issues = [];
36732
+ for (const service of status.services ?? []) {
36733
+ if (service.status === "ok")
36734
+ continue;
36735
+ issues.push({
36736
+ title: `Service ${service.name} reports ${service.status}.`,
36737
+ nextSteps: SERVICE_STEPS
36738
+ });
36739
+ }
36740
+ const integrity = status.chainIntegrity;
36741
+ if (integrity && integrity.ok === false) {
36742
+ issues.push({
36743
+ title: "Chain-data integrity check failed.",
36744
+ detail: integrity.reason ?? "The tip the instance serves is not backed by the history it implies.",
36745
+ nextSteps: [
36746
+ "Verify against the archive: secondlayer verify all --against <manifest>",
36747
+ "Repair what diverged: secondlayer repair --against <manifest> --apply"
36748
+ ]
36749
+ });
36750
+ }
36751
+ if (issues.length === 0 && overall !== "healthy") {
36752
+ issues.push({
36753
+ title: `The API reports status=${overall} but names no failing service.`,
36754
+ detail: "Every service in the report is ok, so the degradation is in a surface this command does not summarize.",
36755
+ nextSteps: SERVICE_STEPS
36756
+ });
36757
+ }
36758
+ return {
36759
+ state: issues.length > 0 ? "degraded" : "healthy",
36760
+ overall,
36761
+ issues
36762
+ };
36763
+ }
36764
+ var CONTEXT_FIELDS = [
36765
+ "account",
36766
+ "streamsTip",
36767
+ "indexTip",
36768
+ "subgraphs",
36769
+ "subscriptions",
36770
+ "activeOperations"
36771
+ ];
36772
+ function explainContextNulls(snapshot, probe) {
36773
+ const nullFields = CONTEXT_FIELDS.filter((field) => (snapshot[field] ?? null) === null);
36774
+ if (nullFields.length === 0) {
36775
+ return { summary: "Every field resolved.", nulls: {} };
36776
+ }
36777
+ if (!probe.status) {
36778
+ const reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? "unreachable"}), so this read never reached an API.`;
36779
+ return {
36780
+ summary: `${reason} Start it from docker/oss with: docker compose up -d — or point SL_API_URL at the instance you meant.`,
36781
+ nulls: Object.fromEntries(nullFields.map((field) => [field, reason]))
36782
+ };
36783
+ }
36784
+ const emptyIndex = hasNoChainData(probe.status);
36785
+ const blocks = probe.status.chainIntegrity?.maxHeight ?? null;
36786
+ const accountReason = probe.selfHosted ? "Expected: a self-hosted instance has no account system. Accounts exist only on the metered archive deployment at api.secondlayer.tools, so null here is normal and not a failure." : "Not authenticated — run `secondlayer login`, or export INSTANCE_TOKEN.";
36787
+ const listReason = unresolvedReadReason(probe, blocks);
36788
+ const chainReason = emptyIndex ? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}` : listReason;
36789
+ const nulls = {};
36790
+ for (const field of nullFields) {
36791
+ if (field === "account")
36792
+ nulls[field] = accountReason;
36793
+ else if (field === "streamsTip" || field === "indexTip")
36794
+ nulls[field] = chainReason;
36795
+ else if (field === "activeOperations")
36796
+ nulls[field] = (snapshot.subgraphs ?? null) === null ? "Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve." : listReason;
36797
+ else
36798
+ nulls[field] = listReason;
36799
+ }
36800
+ const summary = emptyIndex ? `Instance at ${probe.apiUrl} is reachable but holds 0 blocks — the chain fields are null because nothing has been indexed yet. ${BOOTSTRAP_STEP}` : nullFields.length === 1 && nullFields[0] === "account" ? `Instance at ${probe.apiUrl} is reachable and indexed to block ${blocks ?? "unknown"}; only \`account\` is null, which is normal when self-hosting.` : `Instance at ${probe.apiUrl} is reachable, but ${nullFields.length} field${nullFields.length === 1 ? "" : "s"} did not resolve — see \`nulls\` below for each one.`;
36801
+ return { summary, nulls };
36802
+ }
36803
+ function unresolvedReadReason(probe, blocks) {
36804
+ const where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : ""}, but this read returned nothing.`;
36805
+ return probe.hasCredential ? `${where} Check the surface directly: secondlayer status --json` : `${where} No credential resolved: export INSTANCE_TOKEN — an instance published past loopback requires it on every /v1 read.`;
36806
+ }
36807
+
34962
36808
  // src/commands/context.ts
34963
36809
  init_output();
34964
36810
  init_resolve_auth();
34965
- import { SecondLayer as SecondLayer2 } from "@secondlayer/sdk";
34966
36811
  function registerContextCommand(program2) {
34967
36812
  program2.command("context").description("Print an agent orientation snapshot: account, live Streams/Index tips, your subgraphs/subscriptions, and in-flight reindex operations").option("--json", "Print as JSON (default)").action(async (o3) => {
34968
36813
  try {
34969
- const sl = new SecondLayer2({
34970
- baseUrl: resolveApiUrl(),
34971
- apiKey: resolveEnvKey()
34972
- });
36814
+ const apiUrl = resolveApiUrl();
36815
+ const apiKey = resolveEnvKey();
36816
+ const sl = new SecondLayer2({ baseUrl: apiUrl, apiKey });
34973
36817
  const snapshot = await sl.context();
36818
+ const diagnostics = await explain(snapshot, apiUrl, apiKey);
36819
+ const payload = { ...snapshot, diagnostics };
34974
36820
  output({
34975
36821
  json: o3.json,
34976
- data: snapshot,
34977
- human: () => writeData(JSON.stringify(snapshot, null, 2))
36822
+ data: payload,
36823
+ human: () => {
36824
+ writeData(JSON.stringify(payload, null, 2));
36825
+ if (diagnostics)
36826
+ note(diagnostics.summary);
36827
+ }
34978
36828
  });
34979
36829
  } catch (err) {
34980
36830
  error(err instanceof Error ? err.message : String(err));
@@ -34982,6 +36832,25 @@ function registerContextCommand(program2) {
34982
36832
  }
34983
36833
  });
34984
36834
  }
36835
+ async function explain(snapshot, apiUrl, apiKey) {
36836
+ const hasNull = Object.values(snapshot).some((v2) => (v2 ?? null) === null);
36837
+ if (!hasNull)
36838
+ return null;
36839
+ let status = null;
36840
+ let statusError;
36841
+ try {
36842
+ status = await httpPlatformAnon("/public/status");
36843
+ } catch (err) {
36844
+ statusError = err instanceof Error ? err.message : String(err);
36845
+ }
36846
+ return explainContextNulls(snapshot, {
36847
+ apiUrl,
36848
+ selfHosted: isOssMode(),
36849
+ hasCredential: Boolean(apiKey),
36850
+ status,
36851
+ statusError
36852
+ });
36853
+ }
34985
36854
  // src/commands/status.ts
34986
36855
  init_http();
34987
36856
  init_output();
@@ -34990,10 +36859,11 @@ function registerStatusCommand(program2) {
34990
36859
  program2.command("status").description("Show local instance status").option("--json", "Output as JSON").action(async (options3) => {
34991
36860
  try {
34992
36861
  const status = await httpPlatformAnon("/public/status");
36862
+ const diagnosis = diagnoseInstanceStatus(status);
34993
36863
  output({
34994
36864
  json: options3.json,
34995
- data: status,
34996
- human: () => printPublicStatus(status)
36865
+ data: { ...status, diagnosis },
36866
+ human: () => printPublicStatus(status, diagnosis)
34997
36867
  });
34998
36868
  } catch {
34999
36869
  console.log("");
@@ -35007,7 +36877,7 @@ function registerStatusCommand(program2) {
35007
36877
  }
35008
36878
  });
35009
36879
  }
35010
- function printPublicStatus(status) {
36880
+ function printPublicStatus(status, diagnosis) {
35011
36881
  const overall = String(status.status ?? "unknown");
35012
36882
  const color = overall === "healthy" ? green : red;
35013
36883
  console.log("");
@@ -35045,6 +36915,19 @@ function printPublicStatus(status) {
35045
36915
  }
35046
36916
  console.log("");
35047
36917
  }
36918
+ if (diagnosis.issues.length > 0) {
36919
+ console.log(blue("Diagnosis"));
36920
+ for (const issue of diagnosis.issues) {
36921
+ console.log(` ${yellow(issue.title)}`);
36922
+ for (const line of issue.detail ? wrapText(issue.detail, 74) : []) {
36923
+ console.log(dim(` ${line}`));
36924
+ }
36925
+ for (const step of issue.nextSteps) {
36926
+ console.log(dim(` → ${step}`));
36927
+ }
36928
+ }
36929
+ console.log("");
36930
+ }
35048
36931
  if (typeof status.timestamp === "string") {
35049
36932
  console.log(dim(`Last updated: ${status.timestamp}`));
35050
36933
  }
@@ -35061,7 +36944,7 @@ import {
35061
36944
  validateSubscriptionFilterForTable
35062
36945
  } from "@secondlayer/shared/schemas/subscriptions";
35063
36946
  async function validateSubscriptionTargetFromApi(client2, input2) {
35064
- const subgraph = await client2.subgraphs.get(input2.subgraphName);
36947
+ const subgraph = await client2.subgraphs.status(input2.subgraphName);
35065
36948
  const errors2 = validateSubscriptionFilterForTable({
35066
36949
  subgraphName: input2.subgraphName,
35067
36950
  tableName: input2.tableName,
@@ -35876,7 +37759,7 @@ Examples:
35876
37759
  try {
35877
37760
  const client2 = await getSubscriptionClient();
35878
37761
  const { id } = await resolveSubscriptionRef(client2, idOrName);
35879
- const { data } = await client2.subscriptions.recentDeliveries(id);
37762
+ const { data } = await client2.subscriptions.deliveries(id);
35880
37763
  if (options3.json)
35881
37764
  printJson(data);
35882
37765
  else
@@ -35905,7 +37788,7 @@ Examples:
35905
37788
  const ok = await confirmOrExit(`Requeue ${outboxId} for "${detail.name}"?`, options3.yes);
35906
37789
  if (!ok)
35907
37790
  return;
35908
- const res = await client2.subscriptions.requeueDead(id, outboxId);
37791
+ const res = await client2.subscriptions.requeue(id, outboxId);
35909
37792
  if (options3.json)
35910
37793
  printJson(res);
35911
37794
  else
@@ -35950,9 +37833,9 @@ Examples:
35950
37833
  const client2 = await getSubscriptionClient();
35951
37834
  const { id, detail } = await resolveSubscriptionRef(client2, idOrName);
35952
37835
  const [deliveries, dead, subgraph] = await Promise.allSettled([
35953
- client2.subscriptions.recentDeliveries(id),
37836
+ client2.subscriptions.deliveries(id),
35954
37837
  client2.subscriptions.dead(id),
35955
- detail.subgraphName ? client2.subgraphs.get(detail.subgraphName) : Promise.resolve(null)
37838
+ detail.subgraphName ? client2.subgraphs.status(detail.subgraphName) : Promise.resolve(null)
35956
37839
  ]);
35957
37840
  const report = buildDoctorReport({
35958
37841
  subscription: detail,
@@ -35973,7 +37856,7 @@ Examples:
35973
37856
  const signingSecret = resolveSigningSecret(options3);
35974
37857
  const client2 = await getSubscriptionClient();
35975
37858
  const { detail } = await resolveSubscriptionRef(client2, idOrName);
35976
- const subgraph = detail.subgraphName ? await client2.subgraphs.get(detail.subgraphName).catch(() => null) : null;
37859
+ const subgraph = detail.subgraphName ? await client2.subgraphs.status(detail.subgraphName).catch(() => null) : null;
35977
37860
  const row = await representativeRow(client2, detail, subgraph);
35978
37861
  const fixture = buildSubscriptionTestFixture({
35979
37862
  subscription: detail,
@@ -36027,152 +37910,10 @@ POST result (client-side, not logged):`));
36027
37910
  });
36028
37911
  }
36029
37912
  // src/commands/devnet.ts
36030
- import { spawnSync as spawnSync2 } from "node:child_process";
37913
+ import { spawnSync as spawnSync3 } from "node:child_process";
36031
37914
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
36032
37915
  import { dirname as dirname8, join as join9 } from "node:path";
36033
37916
 
36034
- // src/lib/devnet-compose.ts
36035
- var DEFAULT_IMAGE_OWNER = "ryanwaits";
36036
- var DEFAULT_IMAGE_TAG = "latest";
36037
- function buildDevnetCompose(opts = {}) {
36038
- const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
36039
- const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
36040
- const apiPort = opts.apiPort ?? 3800;
36041
- const indexerPort = opts.indexerPort ?? 3700;
36042
- const apiImage = `ghcr.io/${owner}/secondlayer-api:${tag}`;
36043
- const indexerImage = `ghcr.io/${owner}/secondlayer-indexer:${tag}`;
36044
- const dbUrl = "postgres://secondlayer:secondlayer@postgres:5432/secondlayer";
36045
- const DEV_SECRETS_KEY = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
36046
- const DEV_INSTANCE_TOKEN = "dev-instance-token";
36047
- return `# Generated by \`secondlayer devnet connect\` — do not edit by hand.
36048
- # Secondlayer app services for local clarinet devnet development.
36049
- name: secondlayer-devnet
36050
-
36051
- services:
36052
- postgres:
36053
- image: postgres:17-alpine
36054
- environment:
36055
- POSTGRES_USER: secondlayer
36056
- POSTGRES_PASSWORD: secondlayer
36057
- POSTGRES_DB: secondlayer
36058
- volumes:
36059
- - postgres_data:/var/lib/postgresql/data
36060
- healthcheck:
36061
- test: ["CMD-SHELL", "pg_isready -U secondlayer"]
36062
- interval: 5s
36063
- timeout: 5s
36064
- retries: 10
36065
- restart: unless-stopped
36066
-
36067
- migrate:
36068
- image: ${apiImage}
36069
- command: ["bun", "run", "packages/shared/src/db/migrate.ts"]
36070
- environment:
36071
- DATABASE_URL: ${dbUrl}
36072
- depends_on:
36073
- postgres:
36074
- condition: service_healthy
36075
- restart: "no"
36076
-
36077
- api:
36078
- image: ${apiImage}
36079
- environment:
36080
- INSTANCE_MODE: oss
36081
- DATABASE_URL: ${dbUrl}
36082
- PORT: 3800
36083
- LISTEN_HOST: "0.0.0.0"
36084
- INSTANCE_TOKEN: ${DEV_INSTANCE_TOKEN}
36085
- NODE_ENV: production
36086
- DATA_DIR: /data
36087
- # Shared across api + subgraph-processor so the secret the api encrypts
36088
- # (e.g. a subscription's signing secret) can be decrypted by the emitter.
36089
- # OSS auto-generates a per-process key when unset — that splits them.
36090
- # Local-dev fixed value; not a production secret.
36091
- SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
36092
- ports:
36093
- - "${apiPort}:3800"
36094
- volumes:
36095
- - subgraphs_data:/data/subgraphs
36096
- depends_on:
36097
- migrate:
36098
- condition: service_completed_successfully
36099
- postgres:
36100
- condition: service_healthy
36101
- healthcheck:
36102
- test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
36103
- interval: 10s
36104
- timeout: 3s
36105
- start_period: 10s
36106
- retries: 5
36107
- restart: unless-stopped
36108
-
36109
- indexer:
36110
- image: ${indexerImage}
36111
- environment:
36112
- DATABASE_URL: ${dbUrl}
36113
- PORT: 3700
36114
- NODE_ENV: production
36115
- # Keep every network key aligned: the indexer writes index_progress under
36116
- # STACKS_NETWORK, the subgraph processor reads it under NETWORK. If they
36117
- # differ, catch-up finds no progress row and subgraphs never advance.
36118
- NETWORKS: testnet
36119
- STACKS_NETWORK: testnet
36120
- NETWORK: testnet
36121
- # Fed by the devnet node's event observer — never reach out to Hiro mainnet.
36122
- AUTO_BACKFILL_ENABLED: "false"
36123
- TIP_FOLLOWER_ENABLED: "false"
36124
- # Bind on all interfaces so the devnet's stacks-node container can POST to
36125
- # host.docker.internal:3700.
36126
- ports:
36127
- - "${indexerPort}:3700"
36128
- extra_hosts:
36129
- - "host.docker.internal:host-gateway"
36130
- depends_on:
36131
- migrate:
36132
- condition: service_completed_successfully
36133
- postgres:
36134
- condition: service_healthy
36135
- healthcheck:
36136
- test: ["CMD", "curl", "-f", "http://localhost:3700/health"]
36137
- interval: 10s
36138
- timeout: 3s
36139
- start_period: 10s
36140
- retries: 5
36141
- restart: unless-stopped
36142
-
36143
- subgraph-processor:
36144
- image: ${apiImage}
36145
- command: ["bun", "run", "packages/subgraphs/src/service.ts"]
36146
- environment:
36147
- DATABASE_URL: ${dbUrl}
36148
- NODE_ENV: production
36149
- DATA_DIR: /data
36150
- # Must match the indexer's network key (see indexer note) — catch-up reads
36151
- # index_progress under NETWORK.
36152
- NETWORK: testnet
36153
- STACKS_NETWORK: testnet
36154
- # The emitter blocks webhook delivery to private/loopback IPs by default.
36155
- # On a local devnet you deliver to your own localhost receiver, so allow it.
36156
- SECONDLAYER_ALLOW_PRIVATE_EGRESS: "true"
36157
- # Must match the api's key so the emitter can decrypt signing secrets.
36158
- SECONDLAYER_SECRETS_KEY: ${DEV_SECRETS_KEY}
36159
- volumes:
36160
- - subgraphs_data:/data/subgraphs
36161
- depends_on:
36162
- migrate:
36163
- condition: service_completed_successfully
36164
- postgres:
36165
- condition: service_healthy
36166
- healthcheck:
36167
- disable: true
36168
- restart: unless-stopped
36169
-
36170
- volumes:
36171
- postgres_data:
36172
- subgraphs_data:
36173
- `;
36174
- }
36175
-
36176
37917
  // src/lib/devnet-config.ts
36177
37918
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
36178
37919
  import { dirname as dirname7, join as join7, parse as parsePath } from "node:path";
@@ -36229,6 +37970,7 @@ stacks_node_events_observers = [${entry}]
36229
37970
 
36230
37971
  // src/commands/devnet.ts
36231
37972
  init_output();
37973
+ init_resolve_auth();
36232
37974
 
36233
37975
  // src/commands/faucet.ts
36234
37976
  import { randomBytes as randomBytes2 } from "node:crypto";
@@ -36362,7 +38104,7 @@ is what makes minting possible without Bitcoin.`).action(async (opts) => {
36362
38104
  // src/commands/devnet.ts
36363
38105
  var COMPOSE_REL = join9(".secondlayer", "docker-compose.yml");
36364
38106
  function ensureDocker() {
36365
- const probe = spawnSync2("docker", ["info"], { stdio: "ignore" });
38107
+ const probe = spawnSync3("docker", ["info"], { stdio: "ignore" });
36366
38108
  if (probe.error || probe.status !== 0) {
36367
38109
  error(`Docker isn't available.
36368
38110
 
@@ -36373,7 +38115,7 @@ function ensureDocker() {
36373
38115
  }
36374
38116
  }
36375
38117
  function dockerCompose(composePath, args) {
36376
- const res = spawnSync2("docker", ["compose", "-f", composePath, ...args], {
38118
+ const res = spawnSync3("docker", ["compose", "-f", composePath, ...args], {
36377
38119
  stdio: "inherit"
36378
38120
  });
36379
38121
  return res.status ?? 1;
@@ -36455,7 +38197,10 @@ ${bold("Secondlayer stack up")}`);
36455
38197
  console.log(`
36456
38198
  ${bold("Next:")}`);
36457
38199
  console.log(` ${yellow("clarinet devnet start")} ${dim("# auto-deploys + streams to the indexer")}`);
36458
- console.log(` ${yellow("SL_API_URL=http://localhost:3800 SL_API_KEY=dummy secondlayer subgraphs deploy ./subgraph.ts")}`);
38200
+ console.log(` ${yellow(`SL_API_URL=http://localhost:3800 INSTANCE_TOKEN=${DEV_INSTANCE_TOKEN} secondlayer subgraphs deploy ./subgraph.ts`)}`);
38201
+ console.log(dim(`
38202
+ The api is published on 127.0.0.1 only: /v1 reads need no credential.
38203
+ Writes (deploys, subscriptions) send INSTANCE_TOKEN=${DEV_INSTANCE_TOKEN}.`));
36459
38204
  console.log(dim(`
36460
38205
  Stop with: secondlayer devnet down`));
36461
38206
  }
@@ -36495,12 +38240,18 @@ async function logs(service, options3) {
36495
38240
  }
36496
38241
  async function jget(url) {
36497
38242
  try {
36498
- const r2 = await fetch(url, { signal: AbortSignal.timeout(1500) });
38243
+ const r2 = await fetch(url, {
38244
+ signal: AbortSignal.timeout(1500),
38245
+ headers: { authorization: `Bearer ${statusToken()}` }
38246
+ });
36499
38247
  return r2.ok ? await r2.json() : null;
36500
38248
  } catch {
36501
38249
  return null;
36502
38250
  }
36503
38251
  }
38252
+ function statusToken() {
38253
+ return resolveEnvKey() ?? DEV_INSTANCE_TOKEN;
38254
+ }
36504
38255
  var SYS_COLS = new Set([
36505
38256
  "_id",
36506
38257
  "_block_height",
@@ -36752,10 +38503,7 @@ Examples:
36752
38503
  process.exit(1);
36753
38504
  }
36754
38505
  });
36755
- streams.command("pull").description("Download finalized bulk parquet dumps to a local dir (verifies sha256)").requiredOption("--to <dir>", "output directory").option("--dumps-url <url>", "public dumps base URL (or SL_STREAMS_DUMPS_URL)").option("--from-block <n>", "only files covering blocks >= n").option("--to-block <n>", "only files covering blocks <= n").addHelpText("after", `
36756
- Examples:
36757
- $ secondlayer streams pull --to ./dump
36758
- $ secondlayer streams pull --to ./dump --from-block 150000 --to-block 200000`).action(async (options3) => {
38506
+ const runDumps = async (options3) => {
36759
38507
  try {
36760
38508
  const fromBlock = parseHeight(options3.fromBlock, "--from-block");
36761
38509
  const toBlock = parseHeight(options3.toBlock, "--to-block");
@@ -36784,7 +38532,11 @@ Examples:
36784
38532
  error(err instanceof Error ? err.message : String(err));
36785
38533
  process.exit(1);
36786
38534
  }
36787
- });
38535
+ };
38536
+ streams.command("dumps").description("Download finalized bulk parquet dumps to a local dir (verifies sha256)").requiredOption("--to <dir>", "output directory").option("--dumps-url <url>", "public dumps base URL (or SL_STREAMS_DUMPS_URL)").option("--from-block <n>", "only files covering blocks >= n").option("--to-block <n>", "only files covering blocks <= n").addHelpText("after", `
38537
+ Examples:
38538
+ $ secondlayer streams dumps --to ./dump
38539
+ $ secondlayer streams dumps --to ./dump --from-block 150000 --to-block 200000`).action(runDumps);
36788
38540
  streams.command("canonical <height>").description("Canonical block metadata at a given height").option("--json", "Output as JSON (streams always emits JSON)").action(async (heightArg) => {
36789
38541
  try {
36790
38542
  const height = parseHeight(heightArg, "<height>");
@@ -36972,17 +38724,28 @@ function registerDoctorCommand(program2) {
36972
38724
  async function runInstanceDoctor(jsonOutput) {
36973
38725
  const config = await loadConfig2();
36974
38726
  const apiUrl = resolveApiUrl();
36975
- const issues = [];
36976
38727
  const results = { network: config.network, apiUrl };
36977
38728
  let apiHealthy = false;
36978
38729
  let statusData = null;
38730
+ let unreachableError = null;
36979
38731
  try {
36980
38732
  statusData = await httpPlatformAnon("/public/status");
36981
38733
  apiHealthy = true;
36982
- } catch {
36983
- issues.push(`Cannot reach ${apiUrl}/public/status`);
38734
+ } catch (err) {
38735
+ unreachableError = err instanceof Error ? err.message : String(err);
36984
38736
  }
36985
38737
  results.apiHealthy = apiHealthy;
38738
+ const diagnosis = statusData ? diagnoseInstanceStatus(statusData) : null;
38739
+ const issues = diagnosis ? diagnosis.issues : [
38740
+ {
38741
+ title: `Cannot reach ${apiUrl}/public/status.`,
38742
+ detail: unreachableError ?? undefined,
38743
+ nextSteps: [
38744
+ "Start the instance from docker/oss: docker compose up -d",
38745
+ `If it runs elsewhere, point the CLI at it: SL_API_URL=<url> (currently ${apiUrl})`
38746
+ ]
38747
+ }
38748
+ ];
36986
38749
  if (jsonOutput) {
36987
38750
  console.log(JSON.stringify({ ...results, status: statusData, issues }, null, 2));
36988
38751
  return;
@@ -36998,9 +38761,14 @@ async function runInstanceDoctor(jsonOutput) {
36998
38761
  const tip = statusData.chainTip;
36999
38762
  const streams = statusData.streams;
37000
38763
  const index = statusData.index;
38764
+ const blocks = statusData.chainIntegrity?.maxHeight;
37001
38765
  console.log(blue("Instance"));
38766
+ console.log(` status: ${statusData.status ?? "unknown"}`);
37002
38767
  if (typeof tip === "number")
37003
38768
  console.log(` tip: ${tip.toLocaleString()}`);
38769
+ if (typeof blocks === "number") {
38770
+ console.log(` blocks indexed: ${blocks.toLocaleString()}`);
38771
+ }
37004
38772
  if (streams?.status)
37005
38773
  console.log(` streams: ${streams.status}`);
37006
38774
  if (index?.status)
@@ -37012,17 +38780,31 @@ async function runInstanceDoctor(jsonOutput) {
37012
38780
  console.log(` ${green("None")}`);
37013
38781
  } else {
37014
38782
  for (const issue of issues) {
37015
- console.log(` ${red("•")} ${issue}`);
38783
+ console.log(` ${red("•")} ${issue.title}`);
38784
+ for (const line of issue.detail ? wrapText(issue.detail, 72) : []) {
38785
+ console.log(dim(` ${line}`));
38786
+ }
38787
+ for (const step of issue.nextSteps) {
38788
+ console.log(dim(` → ${step}`));
38789
+ }
37016
38790
  }
37017
38791
  }
37018
38792
  console.log("");
37019
38793
  if (issues.length > 0) {
37020
- warn(`${issues.length} issue(s) found`);
38794
+ warn(`${issues.length} issue${issues.length === 1 ? "" : "s"} found${doctorVerdictSuffix(diagnosis)}`);
37021
38795
  } else {
37022
38796
  success("All checks passed");
37023
38797
  }
37024
38798
  console.log("");
37025
38799
  }
38800
+ function doctorVerdictSuffix(diagnosis) {
38801
+ if (!diagnosis)
38802
+ return " — the CLI could not reach this instance.";
38803
+ if (diagnosis.state === "empty-index") {
38804
+ return " — the instance is up but holds no chain data yet.";
38805
+ }
38806
+ return ` — the API reports status=${diagnosis.overall}.`;
38807
+ }
37026
38808
  async function runLocalDoctor(jsonOutput) {
37027
38809
  const config = await loadConfig2();
37028
38810
  const report = await checkHealth();
@@ -38217,9 +39999,22 @@ async function resolveArchivePublicKey(input4) {
38217
39999
  }
38218
40000
  return;
38219
40001
  }
38220
- async function fetchVerifiedPartition(reference, partition) {
40002
+ async function fetchViaGate(gate, partition, retried) {
40003
+ const url = await gate.getUrl(partition.path, retried ? { forceRefresh: true } : undefined);
40004
+ const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
40005
+ if (response2.status === 403 && !retried) {
40006
+ return fetchViaGate(gate, partition, true);
40007
+ }
40008
+ if (!response2.ok) {
40009
+ throw new Error(`could not fetch ${partition.path} (${response2.status} ${response2.statusText})`);
40010
+ }
40011
+ return Buffer.from(await response2.arrayBuffer());
40012
+ }
40013
+ async function fetchVerifiedPartition(reference, partition, gate) {
38221
40014
  let bytes;
38222
- if (reference.isRemote) {
40015
+ if (gate) {
40016
+ bytes = await fetchViaGate(gate, partition, false);
40017
+ } else if (reference.isRemote) {
38223
40018
  const url = `${reference.root.replace(/\/$/, "")}/${partition.path}`;
38224
40019
  const response2 = await fetch(url, { signal: AbortSignal.timeout(300000) });
38225
40020
  if (!response2.ok) {
@@ -38448,11 +40243,123 @@ import { unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
38448
40243
  import { tmpdir as tmpdir2 } from "node:os";
38449
40244
  import { join as join15 } from "node:path";
38450
40245
  import { ParquetReader } from "@dsnp/parquetjs";
40246
+ import { confirm as confirm7 } from "@inquirer/prompts";
38451
40247
  import {
38452
40248
  compareRangeDigests as compareRangeDigests2,
38453
40249
  computeRangeDigest as computeRangeDigest2
38454
40250
  } from "@secondlayer/shared/archive/range-digest";
38455
40251
  import { getDb as getDb3, sql as sql2 } from "@secondlayer/shared/db";
40252
+
40253
+ // src/lib/archive-gate.ts
40254
+ init_http();
40255
+ var OFFICIAL_ARCHIVE_HOST = "archive.secondlayer.tools";
40256
+ function isOfficialArchive(reference) {
40257
+ if (!reference.isRemote)
40258
+ return false;
40259
+ try {
40260
+ return new URL(reference.origin).hostname === OFFICIAL_ARCHIVE_HOST;
40261
+ } catch {
40262
+ return false;
40263
+ }
40264
+ }
40265
+ var defaultDeps = { httpArchiveOps };
40266
+ function quoteFromBody(body) {
40267
+ return {
40268
+ partitions: body.partitions,
40269
+ bundles: body.bundles,
40270
+ usdMicros: body.usd_micros,
40271
+ usd: body.usd,
40272
+ freeAllowanceAppliedMicros: body.free_allowance_applied_micros,
40273
+ allowanceRemainingBundles: body.allowance_remaining_bundles,
40274
+ balanceUsdMicros: body.balance_usd_micros,
40275
+ sufficient: body.sufficient
40276
+ };
40277
+ }
40278
+ async function quoteArchiveFetch(paths, flow, deps = defaultDeps) {
40279
+ try {
40280
+ const body = await deps.httpArchiveOps("/api/archive/quote", { method: "POST", body: { paths, flow } });
40281
+ return { ok: true, quote: quoteFromBody(body) };
40282
+ } catch (err) {
40283
+ if (err instanceof CliHttpError) {
40284
+ if (err.status === 503)
40285
+ return { ok: false, kind: "not_configured" };
40286
+ if (err.status === 401) {
40287
+ return { ok: false, kind: "not_authed", message: err.message };
40288
+ }
40289
+ return { ok: false, kind: "error", message: err.message };
40290
+ }
40291
+ throw err;
40292
+ }
40293
+ }
40294
+ var BUY_CREDITS_COMMAND = "secondlayer credits buy";
40295
+ function formatInsufficientMessage(quote) {
40296
+ const shortfallUsd = ((quote.usdMicros - quote.balanceUsdMicros) / 1e6).toFixed(2);
40297
+ const balanceUsd = (quote.balanceUsdMicros / 1e6).toFixed(2);
40298
+ return `Insufficient archive credits: quote $${quote.usd}, balance $${balanceUsd}, short $${shortfallUsd}. Buy more with \`${BUY_CREDITS_COMMAND}\`.`;
40299
+ }
40300
+ var ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE = "the archive gate is not configured on the server; contact the operator";
40301
+ var ALLOWANCE_MONTHLY_BUNDLES = 6;
40302
+ function formatQuoteValue(quote, flow) {
40303
+ if (flow === "repair" && quote.usdMicros === 0 && quote.freeAllowanceAppliedMicros > 0) {
40304
+ return `free (${quote.allowanceRemainingBundles} of ${ALLOWANCE_MONTHLY_BUNDLES} monthly repair fetches remaining)`;
40305
+ }
40306
+ const balance = `$${(quote.balanceUsdMicros / 1e6).toFixed(2)}`;
40307
+ return `${quote.partitions} partitions ≈ $${quote.usd} · balance ${balance}`;
40308
+ }
40309
+ function shouldPromptForGatedFetch(opts) {
40310
+ return !opts.yes && !opts.json;
40311
+ }
40312
+ var FETCH_BATCH_SIZE = 16;
40313
+ function createGatedFetcher(paths, flow, deps = defaultDeps) {
40314
+ const cache = new Map;
40315
+ const batchOf = new Map;
40316
+ for (const [index, path11] of paths.entries()) {
40317
+ batchOf.set(path11, Math.floor(index / FETCH_BATCH_SIZE));
40318
+ }
40319
+ const fetchedBatches = new Set;
40320
+ const inFlight = new Map;
40321
+ async function fetchAndCache(batchPaths) {
40322
+ const body = await deps.httpArchiveOps("/api/archive/fetch", { method: "POST", body: { paths: batchPaths, flow } });
40323
+ for (const u3 of body.urls)
40324
+ cache.set(u3.path, u3.url);
40325
+ }
40326
+ return {
40327
+ async getUrl(path11, opts) {
40328
+ if (opts?.forceRefresh) {
40329
+ if (process.env.DEBUG) {
40330
+ console.error(`[archive-gate] re-issuing expired URL for ${path11}`);
40331
+ }
40332
+ await fetchAndCache([path11]);
40333
+ const fresh = cache.get(path11);
40334
+ if (!fresh) {
40335
+ throw new Error(`archive gate: no URL returned for ${path11}`);
40336
+ }
40337
+ return fresh;
40338
+ }
40339
+ const batchIndex = batchOf.get(path11);
40340
+ if (batchIndex === undefined) {
40341
+ throw new Error(`archive gate: ${path11} was not in the quoted batch`);
40342
+ }
40343
+ if (!fetchedBatches.has(batchIndex)) {
40344
+ let inflight = inFlight.get(batchIndex);
40345
+ if (!inflight) {
40346
+ const batchPaths = paths.filter((p2) => batchOf.get(p2) === batchIndex);
40347
+ inflight = fetchAndCache(batchPaths).then(() => {
40348
+ fetchedBatches.add(batchIndex);
40349
+ });
40350
+ inFlight.set(batchIndex, inflight);
40351
+ }
40352
+ await inflight;
40353
+ }
40354
+ const url = cache.get(path11);
40355
+ if (!url)
40356
+ throw new Error(`archive gate: no URL returned for ${path11}`);
40357
+ return url;
40358
+ }
40359
+ };
40360
+ }
40361
+
40362
+ // src/commands/repair.ts
38456
40363
  init_output();
38457
40364
  var REPAIR_EXIT = {
38458
40365
  OK: 0,
@@ -38502,8 +40409,8 @@ function parseHeight3(value, flag) {
38502
40409
  }
38503
40410
  return parsed;
38504
40411
  }
38505
- async function planRange(db, reference, partition) {
38506
- const bytes = await fetchVerifiedPartition(reference, partition);
40412
+ async function planRange(db, reference, partition, gate) {
40413
+ const bytes = await fetchVerifiedPartition(reference, partition, gate);
38507
40414
  const archiveBlocks = await readBlocksPartition(bytes, `${partition.from_block}-${partition.to_block}`);
38508
40415
  const localRows = await db.selectFrom("blocks").select(["height", "hash"]).where("canonical", "=", true).where("height", ">=", partition.from_block).where("height", "<=", partition.to_block).execute();
38509
40416
  const localByHeight = new Map(localRows.map((r2) => [Number(r2.height), r2.hash]));
@@ -38530,10 +40437,10 @@ async function planRange(db, reference, partition) {
38530
40437
  const extraHeights = [...localByHeight.keys()].sort((a4, b3) => a4 - b3);
38531
40438
  return { fixes, extraHeights };
38532
40439
  }
38533
- async function applyFixes(db, reference, partition, fixes) {
40440
+ async function applyFixes(db, reference, partition, fixes, gate) {
38534
40441
  if (fixes.length === 0)
38535
40442
  return 0;
38536
- const bytes = await fetchVerifiedPartition(reference, partition);
40443
+ const bytes = await fetchVerifiedPartition(reference, partition, gate);
38537
40444
  const archiveBlocks = await readBlocksPartition(bytes, `apply-${partition.from_block}`);
38538
40445
  const byHeight = new Map(archiveBlocks.map((b3) => [b3.height, b3]));
38539
40446
  const targets = fixes.map((fix) => byHeight.get(fix.height)).filter((b3) => b3 !== undefined);
@@ -38562,7 +40469,7 @@ async function applyFixes(db, reference, partition, fixes) {
38562
40469
  return targets.length;
38563
40470
  }
38564
40471
  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", `
40472
+ 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
40473
  Examples:
38567
40474
  $ secondlayer repair --against ./snapshot.json # plan only
38568
40475
  $ secondlayer repair --against ./snapshot.json --apply # write the fix
@@ -38608,12 +40515,37 @@ Exit codes:
38608
40515
  process.exit(REPAIR_EXIT.OK);
38609
40516
  }
38610
40517
  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));
40518
+ let gate;
40519
+ if (isOfficialArchive(reference)) {
40520
+ const paths = partitions.map((p2) => p2.path);
40521
+ const result = await quoteArchiveFetch(paths, "repair");
40522
+ if (!result.ok) {
40523
+ printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
40524
+ process.exit(REPAIR_EXIT.UNANCHORED);
40525
+ }
40526
+ if (!result.quote.sufficient) {
40527
+ printError(formatInsufficientMessage(result.quote));
40528
+ process.exit(REPAIR_EXIT.UNANCHORED);
40529
+ }
40530
+ note(` metered: ${formatQuoteValue(result.quote, "repair")}`);
40531
+ if (shouldPromptForGatedFetch(opts)) {
40532
+ const proceed = await confirm7({
40533
+ message: `Fetch ${partitions.length} partition(s) from the archive?`,
40534
+ default: true
40535
+ });
40536
+ if (!proceed) {
40537
+ note("Nothing was fetched.");
40538
+ process.exit(REPAIR_EXIT.UNANCHORED);
40539
+ }
40540
+ }
40541
+ gate = createGatedFetcher(paths, "repair");
40542
+ }
38611
40543
  const allFixes = [];
38612
40544
  const allExtra = [];
38613
40545
  const planned = [];
38614
40546
  for (const partition of partitions) {
38615
40547
  note(` reading ${partition.dataset} ${partition.from_block}-${partition.to_block}`);
38616
- const { fixes, extraHeights } = await planRange(db, reference, partition);
40548
+ const { fixes, extraHeights } = await planRange(db, reference, partition, gate);
38617
40549
  planned.push({ partition, fixes });
38618
40550
  allFixes.push(...fixes);
38619
40551
  allExtra.push(...extraHeights);
@@ -38621,7 +40553,7 @@ Exit codes:
38621
40553
  let applied = 0;
38622
40554
  if (opts.apply) {
38623
40555
  for (const { partition, fixes } of planned) {
38624
- applied += await applyFixes(db, reference, partition, fixes);
40556
+ applied += await applyFixes(db, reference, partition, fixes, gate);
38625
40557
  }
38626
40558
  }
38627
40559
  let remaining = divergent.length;
@@ -38698,7 +40630,7 @@ import { unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
38698
40630
  import { tmpdir as tmpdir3 } from "node:os";
38699
40631
  import { join as join16 } from "node:path";
38700
40632
  import { ParquetReader as ParquetReader2 } from "@dsnp/parquetjs";
38701
- import { confirm as confirm7 } from "@inquirer/prompts";
40633
+ import { confirm as confirm8 } from "@inquirer/prompts";
38702
40634
  import {
38703
40635
  copyStatement,
38704
40636
  writeRowsToCopyStream
@@ -38794,8 +40726,8 @@ async function* readPartitionRows(bytes, label) {
38794
40726
  await unlink3(path11).catch(() => {});
38795
40727
  }
38796
40728
  }
38797
- async function loadPartition(reference, partition, rawClient) {
38798
- const bytes = await fetchVerifiedPartition(reference, partition);
40729
+ async function loadPartition(reference, partition, rawClient, gate) {
40730
+ const bytes = await fetchVerifiedPartition(reference, partition, gate);
38799
40731
  const writable2 = await rawClient.unsafe(copyStatement(partition.dataset)).writable();
38800
40732
  const written = await writeRowsToCopyStream({
38801
40733
  writable: writable2,
@@ -38872,6 +40804,23 @@ Exit codes:
38872
40804
  const tipHeight = Math.max(...partitions.map((p2) => p2.to_block));
38873
40805
  const restoreSeconds = totalRows / RESTORE_ROWS_PER_SECOND;
38874
40806
  const genesisSeconds = tipHeight / BLOCKS_PER_DAY_ESTIMATE * 86400;
40807
+ const gated = isOfficialArchive(reference);
40808
+ let quoteLine;
40809
+ let gate;
40810
+ if (gated) {
40811
+ const paths = partitions.map((p2) => p2.path);
40812
+ const result = await quoteArchiveFetch(paths, "bootstrap");
40813
+ if (!result.ok) {
40814
+ printError(result.kind === "not_configured" ? ARCHIVE_GATE_NOT_CONFIGURED_MESSAGE : result.message);
40815
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
40816
+ }
40817
+ if (!result.quote.sufficient) {
40818
+ printError(formatInsufficientMessage(result.quote));
40819
+ process.exit(BOOTSTRAP_EXIT.REFUSED);
40820
+ }
40821
+ quoteLine = formatQuoteValue(result.quote, "bootstrap");
40822
+ gate = createGatedFetcher(paths, "bootstrap");
40823
+ }
38875
40824
  if (!opts.json) {
38876
40825
  console.error("");
38877
40826
  console.error(formatKeyValue([
@@ -38882,15 +40831,16 @@ Exit codes:
38882
40831
  ],
38883
40832
  ["rows", totalRows.toLocaleString()],
38884
40833
  ["download", `${(totalBytes / 1e9).toFixed(1)} GB`],
38885
- ["signature", "verified"]
40834
+ ["signature", "verified"],
40835
+ ...quoteLine ? [["metered", quoteLine]] : []
38886
40836
  ]));
38887
40837
  console.error("");
38888
40838
  console.error(` sync from genesis ${bold(`~${formatDuration2(genesisSeconds)}`)} ${dim("(estimate)")}`);
38889
40839
  console.error(` restore from archive ${bold(`~${formatDuration2(restoreSeconds)}`)}`);
38890
40840
  console.error("");
38891
40841
  }
38892
- if (!opts.yes && !opts.json) {
38893
- const proceed = await confirm7({
40842
+ if (shouldPromptForGatedFetch(opts)) {
40843
+ const proceed = await confirm8({
38894
40844
  message: "Restore from the verified archive?",
38895
40845
  default: true
38896
40846
  });
@@ -38907,7 +40857,7 @@ Exit codes:
38907
40857
  for (const dataset of order) {
38908
40858
  const datasetPartitions = partitions.filter((p2) => p2.dataset === dataset).sort((a4, b3) => a4.from_block - b3.from_block);
38909
40859
  for (const [index, partition] of datasetPartitions.entries()) {
38910
- loaded[dataset] += await loadPartition(reference, partition, rawClient);
40860
+ loaded[dataset] += await loadPartition(reference, partition, rawClient, gate);
38911
40861
  const done = index + 1;
38912
40862
  if (done % 10 === 0 || done === datasetPartitions.length) {
38913
40863
  const elapsed2 = (Date.now() - startedAt) / 1000;
@@ -38964,7 +40914,8 @@ Exit codes:
38964
40914
  elapsed_seconds: Math.round(elapsed),
38965
40915
  resume_from: tipHeight + 1,
38966
40916
  node_tip_at_start: nodeTipAtStart,
38967
- catch_up_blocks: seam?.gap ?? null
40917
+ catch_up_blocks: seam?.gap ?? null,
40918
+ metered: quoteLine ?? null
38968
40919
  };
38969
40920
  output({
38970
40921
  json: opts.json,
@@ -39008,7 +40959,7 @@ function registerBootstrapCommand(program2) {
39008
40959
  }
39009
40960
  // src/commands/uninstall.ts
39010
40961
  init_output();
39011
- import { spawnSync as spawnSync3 } from "node:child_process";
40962
+ import { spawnSync as spawnSync4 } from "node:child_process";
39012
40963
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
39013
40964
  import { join as join17, resolve as resolve13 } from "node:path";
39014
40965
  import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
@@ -39059,7 +41010,7 @@ function registerUninstallCommand(program2) {
39059
41010
  });
39060
41011
  return;
39061
41012
  }
39062
- const res = spawnSync3("docker", cmd, { stdio: "inherit" });
41013
+ const res = spawnSync4("docker", cmd, { stdio: "inherit" });
39063
41014
  if ((res.status ?? 1) !== 0) {
39064
41015
  printError(`docker ${cmd.join(" ")} exited ${res.status}`);
39065
41016
  process.exit(UNINSTALL_EXIT.FAILED);
@@ -39335,110 +41286,15 @@ function registerBackupCommand(program2) {
39335
41286
  function registerRestoreCommand(program2) {
39336
41287
  attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
39337
41288
  }
39338
- // src/lib/instance-init.ts
39339
- import { randomBytes as randomBytes3 } from "node:crypto";
39340
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
39341
- import { resolve as resolve15 } from "node:path";
39342
- import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
39343
- import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
39344
- var INSTANCE_ENV_FILE = ".env.local";
39345
- function parseInstanceNetwork(value) {
39346
- const network = value.trim().toLowerCase();
39347
- if (INSTANCE_NETWORKS.includes(network)) {
39348
- return network;
39349
- }
39350
- throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
39351
- }
39352
- function generateInstanceToken() {
39353
- return randomBytes3(32).toString("hex");
39354
- }
39355
- function generateSecretsKey() {
39356
- return randomBytes3(32).toString("hex");
39357
- }
39358
- function generateSigningPrivateKey() {
39359
- return generateEd25519KeyPair().privateKeyPem;
39360
- }
39361
- function escapeEnvValue(value) {
39362
- if (!/[\s#"']/.test(value) && !value.includes("\\"))
39363
- return value;
39364
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
39365
- }
39366
- function unescapeEnvValue(raw) {
39367
- let value = raw.trim();
39368
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
39369
- value = value.slice(1, -1);
39370
- }
39371
- return value.replace(/\\n/g, `
39372
- `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
39373
- }
39374
- function readEnvValue(envPath, key) {
39375
- if (!existsSync10(envPath))
39376
- return null;
39377
- const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
39378
- return match ? unescapeEnvValue(match[1]) : null;
39379
- }
39380
- function buildInstanceEnv(input4) {
39381
- const token = input4.existing?.INSTANCE_TOKEN || input4.existing?.SL_API_KEY || generateInstanceToken();
39382
- const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
39383
- const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
39384
- const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
39385
- const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
39386
- return {
39387
- INSTANCE_MODE: "oss",
39388
- STACKS_NETWORK: input4.network,
39389
- INSTANCE_TOKEN: token,
39390
- SECONDLAYER_SECRETS_KEY: secrets,
39391
- STREAMS_SIGNING_PRIVATE_KEY: signing,
39392
- SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
39393
- ALLOW_UNSIGNED_WEBHOOKS: "false",
39394
- SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
39395
- SL_API_KEY: token,
39396
- ...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
39397
- };
39398
- }
39399
- function renderInstanceEnv(env2) {
39400
- const lines2 = [
39401
- "# Generated by secondlayer init. Do not commit.",
39402
- `INSTANCE_MODE=${env2.INSTANCE_MODE}`,
39403
- `STACKS_NETWORK=${env2.STACKS_NETWORK}`,
39404
- `INSTANCE_TOKEN=${env2.INSTANCE_TOKEN}`,
39405
- `SECONDLAYER_SECRETS_KEY=${env2.SECONDLAYER_SECRETS_KEY}`,
39406
- `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.STREAMS_SIGNING_PRIVATE_KEY)}`,
39407
- `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
39408
- `ALLOW_UNSIGNED_WEBHOOKS=${env2.ALLOW_UNSIGNED_WEBHOOKS}`,
39409
- `SL_API_URL=${env2.SL_API_URL}`,
39410
- `SL_API_KEY=${env2.SL_API_KEY}`
39411
- ];
39412
- if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
39413
- lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
39414
- }
39415
- lines2.push("");
39416
- return lines2.join(`
39417
- `);
39418
- }
39419
- function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
39420
- const path11 = resolve15(cwd, file);
39421
- writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
39422
- return path11;
39423
- }
39424
- function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
39425
- const path11 = resolve15(cwd, file);
39426
- return {
39427
- INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
39428
- SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
39429
- STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path11, "STREAMS_SIGNING_PRIVATE_KEY"),
39430
- SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path11, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
39431
- SL_API_URL: readEnvValue(path11, "SL_API_URL"),
39432
- SL_API_KEY: readEnvValue(path11, "SL_API_KEY"),
39433
- ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path11, "ARCHIVE_SIGNING_PUBLIC_KEY")
39434
- };
39435
- }
39436
-
39437
41289
  // src/commands/init.ts
41290
+ init_instance_init();
39438
41291
  init_output();
39439
41292
  function registerInitCommand(program2) {
39440
- program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").option("--api-url <url>", "Local API URL", "http://127.0.0.1:3800").option("--force", "Overwrite generated values even if .env.local exists").action((opts) => {
39441
- const network = parseInstanceNetwork(opts.network);
41293
+ program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--api-url <url>", "Local API URL", "http://127.0.0.1:3800").option("--force", "Overwrite generated values even if .env.local exists").addHelpText("after", `
41294
+ --network <mainnet|testnet|devnet> is the top-level global flag (see
41295
+ \`secondlayer --help\`) — default mainnet, same as before.
41296
+ `).action((opts) => {
41297
+ const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
39442
41298
  const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
39443
41299
  const env2 = buildInstanceEnv({
39444
41300
  network,
@@ -39455,89 +41311,251 @@ function registerInitCommand(program2) {
39455
41311
  note("Export these or pass --env-file to compose. Do not commit the file.");
39456
41312
  });
39457
41313
  }
39458
- // src/lib/observer-stanza.ts
39459
- var OBSERVER_MODES = ["indexer", "signer-shared"];
39460
- var RECOVERY_SOURCES = ["journal", "archive"];
39461
-
39462
- class UnsupportedObserverError extends Error {
39463
- name = "UnsupportedObserverError";
41314
+ // src/commands/observer.ts
41315
+ init_instance_init();
41316
+ init_observer_stanza();
41317
+ init_output();
41318
+ function registerObserverCommand(program2) {
41319
+ program2.command("observer").description("Print the Stacks events_observer stanza for this node mode").option("--mode <mode>", "indexer (retry) or signer-shared (no retry)", "indexer").option("--endpoint <host:port>", "Observer callback the node will POST to").option("--recovery <source>", "Required for signer-shared: journal or archive").addHelpText("after", `
41320
+ --network <mainnet|testnet|devnet> is the top-level global flag (see
41321
+ \`secondlayer --help\`) — default mainnet, same as before.
41322
+ `).action((opts) => {
41323
+ const mode = parseObserverMode(opts.mode);
41324
+ const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
41325
+ const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
41326
+ const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
41327
+ if (mode === "signer-shared") {
41328
+ note("Signer-shared nodes skip observer retries. Pair with a journal or archive for completeness.");
41329
+ }
41330
+ writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
41331
+ });
39464
41332
  }
39465
- function parseObserverMode(value) {
39466
- const mode = value.trim().toLowerCase();
39467
- if (OBSERVER_MODES.includes(mode)) {
39468
- return mode;
41333
+ // src/commands/setup.ts
41334
+ init_output();
41335
+
41336
+ // src/lib/setup-prompts.ts
41337
+ init_instance_init();
41338
+ init_oss_compose();
41339
+ init_setup_wizard();
41340
+ import { confirm as confirm9, input as input4, select as select5 } from "@inquirer/prompts";
41341
+
41342
+ class SetupCancelledError extends Error {
41343
+ constructor() {
41344
+ super("Setup cancelled — nothing was changed.");
41345
+ this.name = "SetupCancelledError";
39469
41346
  }
39470
- throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
39471
41347
  }
39472
- function parseRecoverySource(value) {
39473
- const source = value.trim().toLowerCase();
39474
- if (RECOVERY_SOURCES.includes(source)) {
39475
- return source;
41348
+ function safeParse(value, parse2) {
41349
+ if (!value)
41350
+ return;
41351
+ try {
41352
+ return parse2(value);
41353
+ } catch {
41354
+ return;
39476
41355
  }
39477
- throw new UnsupportedObserverError(`recovery source must be journal or archive (got ${value})`);
39478
41356
  }
39479
- function parseObserverEndpoint(value) {
39480
- const trimmed = value.trim();
39481
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) || trimmed.startsWith("unix:")) {
39482
- throw new UnsupportedObserverError(`observer endpoint must be host:port, not a URL (${value})`);
39483
- }
39484
- const match = trimmed.match(/^([a-zA-Z0-9._-]+):(\d+)$/);
39485
- if (!match) {
39486
- throw new UnsupportedObserverError(`observer endpoint must be host:port (got ${value})`);
41357
+ var NODE_MODE_DESCRIPTIONS = {
41358
+ external: "You run the Stacks node; paste an observer stanza into its Config.toml.",
41359
+ stacks: "You run the Stacks node yourself, no bundled bitcoind (same as external).",
41360
+ full: "Bundled Stacks node + bitcoind."
41361
+ };
41362
+ async function promptSetupConfig(flags) {
41363
+ const network = safeParse(flags.network, parseInstanceNetwork) ?? await select5({
41364
+ message: "Network",
41365
+ choices: [
41366
+ {
41367
+ name: "mainnet",
41368
+ value: "mainnet",
41369
+ description: "Production Stacks chain. Large: plan for hundreds of GB."
41370
+ },
41371
+ {
41372
+ name: "testnet",
41373
+ value: "testnet",
41374
+ description: "Stacks testnet. Small history."
41375
+ },
41376
+ {
41377
+ name: "devnet",
41378
+ value: "devnet",
41379
+ description: "Local Clarinet devnet."
41380
+ }
41381
+ ]
41382
+ });
41383
+ const nodeMode = safeParse(flags.nodeMode, parseSetupNodeMode) ?? await select5({
41384
+ message: "Node mode",
41385
+ choices: SETUP_NODE_MODES.map((mode) => {
41386
+ const floor = guardrailPreview(mode, network);
41387
+ return {
41388
+ name: mode,
41389
+ value: mode,
41390
+ description: `${NODE_MODE_DESCRIPTIONS[mode]} needs ≥ ${(floor.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${floor.diskFloorGb}GB disk.`
41391
+ };
41392
+ })
41393
+ });
41394
+ let against = flags.against;
41395
+ let skipBootstrap = !!flags.skipBootstrap;
41396
+ if (!skipBootstrap && !against) {
41397
+ const choice = await select5({
41398
+ message: "Bootstrap from",
41399
+ choices: [
41400
+ {
41401
+ name: "Hosted archive (recommended)",
41402
+ value: "hosted",
41403
+ description: DEFAULT_ARCHIVE_MANIFEST
41404
+ },
41405
+ {
41406
+ name: "Custom manifest URL or path",
41407
+ value: "custom",
41408
+ description: "Enter your own"
41409
+ },
41410
+ {
41411
+ name: "Sync from genesis",
41412
+ value: "skip",
41413
+ description: "Skip bootstrap — slower, no download"
41414
+ }
41415
+ ]
41416
+ });
41417
+ if (choice === "hosted") {
41418
+ against = DEFAULT_ARCHIVE_MANIFEST;
41419
+ } else if (choice === "skip") {
41420
+ skipBootstrap = true;
41421
+ } else {
41422
+ against = (await input4({
41423
+ message: "Manifest URL or local path",
41424
+ default: DEFAULT_ARCHIVE_MANIFEST
41425
+ })).trim();
41426
+ }
39487
41427
  }
39488
- const port = Number(match[2]);
39489
- if (port < 1 || port > 65535) {
39490
- throw new UnsupportedObserverError(`observer port out of range (${port})`);
41428
+ const config = {
41429
+ network,
41430
+ nodeMode,
41431
+ apiPort: flags.apiPort ?? "127.0.0.1:3800",
41432
+ indexerPort: "127.0.0.1:3700",
41433
+ dir: flags.dir ?? process.cwd(),
41434
+ against: skipBootstrap ? undefined : against,
41435
+ skipBootstrap,
41436
+ skipVerify: !!flags.skipVerify,
41437
+ yes: false,
41438
+ force: !!flags.force,
41439
+ owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
41440
+ imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
41441
+ };
41442
+ console.error("");
41443
+ console.error(` network ${config.network}`);
41444
+ console.error(` node mode ${config.nodeMode}`);
41445
+ console.error(` dir ${config.dir}`);
41446
+ console.error(` api port ${config.apiPort}`);
41447
+ console.error(` bootstrap ${config.skipBootstrap ? "skip (sync from genesis)" : config.against}`);
41448
+ console.error("");
41449
+ const proceed = await confirm9({ message: "Start setup?", default: true });
41450
+ if (!proceed)
41451
+ throw new SetupCancelledError;
41452
+ return config;
41453
+ }
41454
+
41455
+ // src/commands/setup.ts
41456
+ init_setup_wizard();
41457
+ function renderProgress(event) {
41458
+ switch (event.type) {
41459
+ case "step-start":
41460
+ note(`→ ${event.step}`);
41461
+ return;
41462
+ case "step-log":
41463
+ note(` ${event.line}`);
41464
+ return;
41465
+ case "step-done":
41466
+ success(event.step + (event.detail ? `: ${event.detail}` : ""));
41467
+ return;
41468
+ case "step-skip":
41469
+ note(` skipped ${event.step} (${event.reason})`);
41470
+ return;
41471
+ case "step-error":
41472
+ printError(`${event.step} failed: ${event.message}`);
41473
+ return;
39491
41474
  }
39492
- return trimmed;
39493
41475
  }
39494
- var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
39495
- function validateObserverStanza(input4) {
39496
- const endpoint = parseObserverEndpoint(input4.endpoint);
39497
- const host = endpoint.slice(0, endpoint.lastIndexOf(":"));
39498
- if (input4.network !== "devnet" && LOOPBACK_HOSTS.has(host.toLowerCase())) {
39499
- throw new UnsupportedObserverError(`loopback observer endpoint is not container-visible on ${input4.network}; use a docker DNS name such as indexer:3700`);
41476
+ async function executeAndRender(config) {
41477
+ console.error(bold("Secondlayer setup"));
41478
+ console.error(dim(` network=${config.network} node-mode=${config.nodeMode} dir=${config.dir}`));
41479
+ console.error("");
41480
+ let failed = false;
41481
+ const result = await runSetup(config, (event) => {
41482
+ if (event.type === "step-error")
41483
+ failed = true;
41484
+ renderProgress(event);
41485
+ });
41486
+ if (!result.ok) {
41487
+ process.exit(1);
39500
41488
  }
39501
- if (input4.mode === "signer-shared" && !input4.recovery) {
39502
- throw new UnsupportedObserverError("signer-shared mode requires --recovery journal or --recovery archive");
41489
+ if (failed) {
41490
+ warn("Setup finished with at least one step reporting an error above.");
39503
41491
  }
39504
- return { endpoint };
41492
+ console.error("");
41493
+ if (result.summary)
41494
+ console.log(result.summary);
41495
+ process.exit(failed ? 1 : 0);
39505
41496
  }
39506
- function renderObserverStanza(input4) {
39507
- const { endpoint } = validateObserverStanza(input4);
39508
- const indexer = input4.mode === "indexer";
39509
- const timeoutMs = indexer ? 2000 : 500;
39510
- const disableRetries = !indexer;
39511
- const comment = indexer ? "# Pure indexer: retry delivery. A slow observer can stall the node." : "# Signer-shared: do not retry. Missed blocks refill from the journal/archive.";
39512
- return [
39513
- comment,
39514
- "[[events_observer]]",
39515
- `endpoint = "${endpoint}"`,
39516
- 'events_keys = ["*"]',
39517
- `timeout_ms = ${timeoutMs}`,
39518
- `disable_retries = ${disableRetries}`,
39519
- ""
39520
- ].join(`
39521
- `);
41497
+ async function runNonInteractive(flags) {
41498
+ let config;
41499
+ try {
41500
+ config = resolveNonInteractiveConfig(flags);
41501
+ } catch (err) {
41502
+ if (err instanceof MissingSetupFlagError) {
41503
+ printError(err.message, { hint: `Pass ${err.flag} and re-run.` });
41504
+ } else {
41505
+ printError(err instanceof Error ? err.message : String(err));
41506
+ }
41507
+ process.exit(1);
41508
+ }
41509
+ await executeAndRender(config);
39522
41510
  }
39523
- function defaultObserverEndpoint(network) {
39524
- if (network === "devnet")
39525
- return "127.0.0.1:3700";
39526
- return "indexer:3700";
41511
+ async function runPromptedInteractive(flags) {
41512
+ let config;
41513
+ try {
41514
+ config = await promptSetupConfig(flags);
41515
+ } catch (err) {
41516
+ if (err instanceof SetupCancelledError) {
41517
+ note(err.message);
41518
+ process.exit(0);
41519
+ }
41520
+ printError(err instanceof Error ? err.message : String(err));
41521
+ process.exit(1);
41522
+ }
41523
+ await executeAndRender(config);
39527
41524
  }
41525
+ function registerSetupCommand(program2) {
41526
+ program2.command("setup").description("Guided self-host onboarding: secrets, config, docker, bootstrap, verify — one command").option("--node-mode <mode>", "external, stacks, or full (required)").option("--api-port <spec>", "API publish spec", "127.0.0.1:3800").option("--dir <path>", "Target directory for compose + .env", process.cwd()).option("--against <manifest>", `Archive manifest to bootstrap from (suggested default: ${DEFAULT_ARCHIVE_MANIFEST})`).option("--skip-bootstrap", "Sync from genesis instead of restoring an archive").option("--skip-verify", "Skip the post-bootstrap verify pass").option("--yes", "Non-interactive: skip the TUI, require flags, never prompt").option("--force", "Regenerate secrets even if a .env already exists in --dir").option("--owner <owner>", "ghcr image owner (namespace) to pull from").option("--image-tag <tag>", "Published image tag to run").addHelpText("after", `
41527
+ --network <mainnet|testnet|devnet> is the top-level global flag (see
41528
+ \`secondlayer --help\`) — required here too, just parsed one level up.
39528
41529
 
39529
- // src/commands/observer.ts
39530
- init_output();
39531
- function registerObserverCommand(program2) {
39532
- program2.command("observer").description("Print the Stacks events_observer stanza for this node mode").option("--mode <mode>", "indexer (retry) or signer-shared (no retry)", "indexer").option("--endpoint <host:port>", "Observer callback the node will POST to").option("--recovery <source>", "Required for signer-shared: journal or archive").option("--network <network>", "mainnet, testnet, or devnet", process.env.STACKS_NETWORK ?? "mainnet").action((opts) => {
39533
- const mode = parseObserverMode(opts.mode);
39534
- const network = parseInstanceNetwork(opts.network);
39535
- const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
39536
- const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
39537
- if (mode === "signer-shared") {
39538
- note("Signer-shared nodes skip observer retries. Pair with a journal or archive for completeness.");
41530
+ Examples:
41531
+ $ secondlayer setup
41532
+ $ secondlayer setup --yes --network mainnet --node-mode external --against ${DEFAULT_ARCHIVE_MANIFEST}
41533
+ $ secondlayer setup --yes --network testnet --node-mode external --skip-bootstrap --skip-verify
41534
+
41535
+ Without a TTY (piped, CI, an agent), or with --yes, the TUI is skipped: every
41536
+ decision with no safe default (--network, --node-mode, and --against unless
41537
+ --skip-bootstrap) must come from a flag or the command fails fast naming it.
41538
+ `).action(async (opts) => {
41539
+ const flags = {
41540
+ ...opts,
41541
+ network: opts.network ?? process.env.STACKS_NETWORK
41542
+ };
41543
+ const interactive = process.stdout.isTTY && !flags.yes;
41544
+ if (!interactive) {
41545
+ await runNonInteractive(flags);
41546
+ return;
41547
+ }
41548
+ if (!isBunRuntime()) {
41549
+ await runPromptedInteractive(flags);
41550
+ return;
41551
+ }
41552
+ try {
41553
+ const { runSetupTui: runSetupTui2 } = await Promise.resolve().then(() => (init_setup_tui(), exports_setup_tui));
41554
+ await runSetupTui2(flags);
41555
+ } catch (err) {
41556
+ warn(`Interactive TUI unavailable (${err instanceof Error ? err.message : String(err)}) — falling back to guided prompts.`);
41557
+ await runPromptedInteractive(flags);
39539
41558
  }
39540
- writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
39541
41559
  });
39542
41560
  }
39543
41561
  // src/commands/start.ts
@@ -39563,7 +41581,7 @@ function registerStartCommand(program2) {
39563
41581
  });
39564
41582
  process.exit(1);
39565
41583
  }
39566
- const extra = parsed.config.NODE_MODE === "full" ? " --profile full-node" : parsed.config.NODE_MODE === "stacks" ? " --profile stacks-node" : "";
41584
+ const extra = parsed.config.NODE_MODE === "full" ? " --profile full-node" : "";
39567
41585
  const cmd = `docker compose -f docker/oss/docker-compose.yml${extra} up -d`;
39568
41586
  if (opts.print) {
39569
41587
  console.log(cmd);
@@ -39576,24 +41594,30 @@ function registerStartCommand(program2) {
39576
41594
  }
39577
41595
  // src/cli.ts
39578
41596
  var { version } = package_default;
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)");
41597
+ 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 INSTANCE_TOKEN)").option("--api-url <url>", "API endpoint (overrides SL_API_URL)").showSuggestionAfterError(true).showHelpAfterError("(run `secondlayer --help` to see available commands)");
39580
41598
  program.hook("preAction", (thisCommand) => {
39581
41599
  const { network, apiKey, apiUrl } = thisCommand.opts();
39582
41600
  if (network)
39583
41601
  process.env.STACKS_NETWORK = network;
39584
- if (apiKey)
41602
+ if (apiKey) {
41603
+ process.env.INSTANCE_TOKEN = apiKey;
39585
41604
  process.env.SL_API_KEY = apiKey;
41605
+ }
39586
41606
  if (apiUrl)
39587
41607
  process.env.SL_API_URL = apiUrl;
39588
41608
  });
39589
41609
  program.addHelpText("after", `
39590
41610
  Quickstart:
39591
- $ secondlayer init --network mainnet
39592
- $ secondlayer bootstrap --against <manifest>
41611
+ $ secondlayer setup
39593
41612
  $ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
39594
41613
  $ secondlayer subgraphs deploy subgraphs/my-watcher.ts
41614
+
41615
+ Manual / scripted setup, one step at a time:
41616
+ $ secondlayer init --network mainnet
41617
+ $ secondlayer bootstrap --against <manifest>
39595
41618
  `);
39596
41619
  program.commandsGroup("Getting started:");
41620
+ registerSetupCommand(program);
39597
41621
  registerInitCommand(program);
39598
41622
  registerBootstrapCommand(program);
39599
41623
  registerObserverCommand(program);
@@ -39609,19 +41633,6 @@ registerStreamsCommand(program);
39609
41633
  registerIndexCommand(program);
39610
41634
  program.commandsGroup("Project & codegen:");
39611
41635
  registerCodegenCommand(program);
39612
- var configureGenerate = (cmd) => cmd.option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output file path (required when using direct files)").option("-k, --api-key <key>", "Stacks node API key for direct RPC URLs").option("-w, --watch", "Watch for changes").action(async (files, options3) => {
39613
- const { deprecatedCodegenNotice: deprecatedCodegenNotice2 } = await Promise.resolve().then(() => (init_codegen(), exports_codegen));
39614
- deprecatedCodegenNotice2("secondlayer contracts generate", "secondlayer codegen contracts");
39615
- options3.out = options3.output;
39616
- const { generate: generate2 } = await Promise.resolve().then(() => (init_generate(), exports_generate));
39617
- await generate2(files, options3);
39618
- });
39619
- var contracts = program.command("contracts").description("Work with Clarity contracts");
39620
- configureGenerate(contracts.command("generate [files...]").aliases(["gen"]).description("Generate TypeScript interfaces from Clarity contracts")).addHelpText("after", `
39621
- Examples:
39622
- $ secondlayer contracts generate
39623
- $ secondlayer contracts generate ./contracts/pool.clar -o ./src/generated.ts
39624
- $ secondlayer contracts generate --config secondlayer.config.ts --watch`);
39625
41636
  program.commandsGroup("Local development:");
39626
41637
  registerLocalCommand(program);
39627
41638
  registerDevnetCommand(program);
@@ -39639,5 +41650,5 @@ program.commandsGroup("Archive:");
39639
41650
  registerCreditsCommand(program);
39640
41651
  program.parse();
39641
41652
 
39642
- //# debugId=135579D3DFB23FDC64756E2164756E21
41653
+ //# debugId=9A62CC1B91BCEC2C64756E2164756E21
39643
41654
  //# sourceMappingURL=cli.js.map