@secondlayer/cli 20.1.1 → 21.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.
package/dist/cli.js CHANGED
@@ -29423,7 +29423,7 @@ var init_git_status = () => {};
29423
29423
 
29424
29424
  // ../../node_modules/typescript/lib/typescript.js
29425
29425
  var require_typescript = __commonJS(function(exports, module) {
29426
- var __dirname = "/Users/ryan/projects/secondlayer/node_modules/typescript/lib", __filename = "/Users/ryan/projects/secondlayer/node_modules/typescript/lib/typescript.js";
29426
+ var __dirname = "/Users/ryan/projects/secondlayer-release/node_modules/typescript/lib", __filename = "/Users/ryan/projects/secondlayer-release/node_modules/typescript/lib/typescript.js";
29427
29427
  /*! *****************************************************************************
29428
29428
  Copyright (c) Microsoft Corporation. All rights reserved.
29429
29429
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@@ -202646,7 +202646,7 @@ var {
202646
202646
  // package.json
202647
202647
  var package_default = {
202648
202648
  name: "@secondlayer/cli",
202649
- version: "20.1.1",
202649
+ version: "21.0.0",
202650
202650
  description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
202651
202651
  repository: {
202652
202652
  type: "git",
@@ -202685,11 +202685,11 @@ var package_default = {
202685
202685
  "@inquirer/prompts": "^8.2.0",
202686
202686
  "@secondlayer/bundler": "^0.7.1",
202687
202687
  "@secondlayer/scaffold": "^1.6.2",
202688
- "@secondlayer/sdk": "^15.0.1",
202689
- "@secondlayer/shared": "^12.1.1",
202690
- "@secondlayer/stacks": "^6.2.1",
202691
- "@secondlayer/subgraphs": "^7.0.2",
202692
- "@secondlayer/verify": "^0.5.3",
202688
+ "@secondlayer/sdk": "^15.0.3",
202689
+ "@secondlayer/shared": "^13.0.0",
202690
+ "@secondlayer/stacks": "^6.2.2",
202691
+ "@secondlayer/subgraphs": "^7.0.4",
202692
+ "@secondlayer/verify": "^0.5.4",
202693
202693
  esbuild: "^0.28.2",
202694
202694
  "smol-toml": "^1.9.1",
202695
202695
  zod: "^4.3.6"
@@ -207809,268 +207809,19 @@ async function runLogout(opts = {}) {
207809
207809
  function registerLogoutCommand(program) {
207810
207810
  program.command("logout").description("Log out and revoke the local session").option("--credits", "Log out of the archive credits account used by bootstrap, repair, and credits").action((opts) => runLogout({ credits: opts.credits }));
207811
207811
  }
207812
- // src/commands/uninstall.ts
207813
- import { spawnSync as spawnSync4 } from "node:child_process";
207814
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
207815
- import { join as join14, resolve as resolve10 } from "node:path";
207816
- import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
207817
-
207818
- // src/lib/instance-init.ts
207819
- init_output();
207820
- import { randomBytes as randomBytes3 } from "node:crypto";
207821
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
207822
- import { resolve as resolve9 } from "node:path";
207823
- import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
207824
- import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
207825
- import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
207826
- var INSTANCE_ENV_FILE = ".env.local";
207827
- function isHostedAccountKey(value) {
207828
- return value.startsWith("sk-sl_") || value.startsWith("ss-sl_");
207829
- }
207830
- function parseInstanceNetwork(value) {
207831
- const network = value.trim().toLowerCase();
207832
- if (INSTANCE_NETWORKS.includes(network)) {
207833
- return network;
207834
- }
207835
- throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
207836
- }
207837
- function instanceNetworkFromEnv(fallback = "mainnet") {
207838
- const raw = process.env.STACKS_NETWORK ?? fallback;
207839
- try {
207840
- return parseInstanceNetwork(raw);
207841
- } catch (err) {
207842
- printError(err instanceof Error ? err.message : String(err), {
207843
- hint: "Pass --network mainnet, --network testnet, or --network devnet."
207844
- });
207845
- process.exit(1);
207846
- }
207847
- }
207848
- function generateInstanceToken() {
207849
- return randomBytes3(32).toString("hex");
207850
- }
207851
- function generateSecretsKey() {
207852
- return randomBytes3(32).toString("hex");
207853
- }
207854
- function generateSigningPrivateKey() {
207855
- return generateEd25519KeyPair().privateKeyPem;
207856
- }
207857
- function escapeEnvValue(value) {
207858
- if (!/[\s#"']/.test(value) && !value.includes("\\"))
207859
- return value;
207860
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
207861
- }
207862
- function unescapeEnvValue(raw) {
207863
- let value = raw.trim();
207864
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
207865
- value = value.slice(1, -1);
207866
- }
207867
- return value.replace(/\\n/g, `
207868
- `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
207869
- }
207870
- function readEnvValue(envPath, key) {
207871
- if (!existsSync8(envPath))
207872
- return null;
207873
- const match = readFileSync7(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
207874
- return match ? unescapeEnvValue(match[1]) : null;
207875
- }
207876
- function buildInstanceEnv(input) {
207877
- const token = input.existing?.INSTANCE_TOKEN || generateInstanceToken();
207878
- const secrets = input.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
207879
- const signing = input.existing?.STREAMS_SIGNING_PRIVATE_KEY || input.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
207880
- const apiUrl = input.existing?.SECONDLAYER_API_URL || input.apiUrl || "http://127.0.0.1:3800";
207881
- const archiveKey = input.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM;
207882
- const hostedCandidate = input.existing?.SECONDLAYER_API_KEY || undefined;
207883
- const hostedKey = hostedCandidate && isHostedAccountKey(hostedCandidate) ? hostedCandidate : undefined;
207884
- return {
207885
- INSTANCE_MODE: "oss",
207886
- STACKS_NETWORK: input.network,
207887
- INSTANCE_TOKEN: token,
207888
- SECONDLAYER_SECRETS_KEY: secrets,
207889
- STREAMS_SIGNING_PRIVATE_KEY: signing,
207890
- SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
207891
- ALLOW_UNSIGNED_WEBHOOKS: "false",
207892
- SECONDLAYER_API_URL: apiUrl,
207893
- ...hostedKey ? { SECONDLAYER_API_KEY: hostedKey } : {},
207894
- ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
207895
- };
207896
- }
207897
- function renderInstanceEnv(env) {
207898
- const lines = [
207899
- "# Generated by secondlayer init. Do not commit.",
207900
- `INSTANCE_MODE=${env.INSTANCE_MODE}`,
207901
- `STACKS_NETWORK=${env.STACKS_NETWORK}`,
207902
- "# The instance credential. The API validates bearer tokens against it,",
207903
- "# and the CLI/SDK/MCP read it first when authenticating.",
207904
- `INSTANCE_TOKEN=${env.INSTANCE_TOKEN}`,
207905
- `SECONDLAYER_SECRETS_KEY=${env.SECONDLAYER_SECRETS_KEY}`,
207906
- `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.STREAMS_SIGNING_PRIVATE_KEY)}`,
207907
- `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
207908
- `ALLOW_UNSIGNED_WEBHOOKS=${env.ALLOW_UNSIGNED_WEBHOOKS}`,
207909
- `SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`
207910
- ];
207911
- if (env.SECONDLAYER_API_KEY) {
207912
- lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
207913
- }
207914
- lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env.ARCHIVE_SIGNING_PUBLIC_KEY)}`, "");
207915
- return lines.join(`
207916
- `);
207917
- }
207918
- function writeInstanceEnv(cwd, env, file = INSTANCE_ENV_FILE) {
207919
- const path = resolve9(cwd, file);
207920
- writeFileSync6(path, renderInstanceEnv(env), { mode: 384 });
207921
- return path;
207922
- }
207923
- function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
207924
- const path = resolve9(cwd, file);
207925
- return {
207926
- INSTANCE_TOKEN: readEnvValue(path, "INSTANCE_TOKEN"),
207927
- SECONDLAYER_SECRETS_KEY: readEnvValue(path, "SECONDLAYER_SECRETS_KEY"),
207928
- STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path, "STREAMS_SIGNING_PRIVATE_KEY"),
207929
- SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
207930
- SECONDLAYER_API_URL: readEnvValue(path, "SECONDLAYER_API_URL"),
207931
- SECONDLAYER_API_KEY: readEnvValue(path, "SECONDLAYER_API_KEY"),
207932
- ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path, "ARCHIVE_SIGNING_PUBLIC_KEY")
207933
- };
207934
- }
207935
-
207936
- // src/commands/uninstall.ts
207937
- init_output();
207938
- init_resolve_auth();
207939
- var UNINSTALL_EXIT = { OK: 0, FAILED: 1, REFUSED: 2 };
207940
- var REPO_COMPOSE = "docker/oss/docker-compose.yml";
207941
- function bundleHasKeys(bundleDir) {
207942
- const manifest = join14(bundleDir, "manifest.json");
207943
- if (!existsSync9(manifest))
207944
- return false;
207945
- try {
207946
- const parsed = JSON.parse(readFileSync8(manifest, "utf8"));
207947
- return parsed.secrets != null;
207948
- } catch {
207949
- return false;
207950
- }
207951
- }
207952
- function resolveUninstallLayout(dir, composeOverride) {
207953
- const setupCompose = resolve10(dir, "docker-compose.yml");
207954
- const setupEnv = resolve10(dir, ".env");
207955
- const initEnv = resolve10(dir, ".env.local");
207956
- const secretsFile = [setupEnv, initEnv].find((file) => readEnvValue(file, "SECONDLAYER_SECRETS_KEY") !== null) ?? [setupEnv, initEnv].find((file) => existsSync9(file));
207957
- if (composeOverride) {
207958
- const composeFile = resolve10(dir, composeOverride);
207959
- const sibling = resolve10(composeFile, "..", ".env");
207960
- return {
207961
- composeFile,
207962
- envFile: existsSync9(sibling) ? sibling : undefined,
207963
- secretsFile
207964
- };
207965
- }
207966
- if (existsSync9(setupCompose)) {
207967
- return {
207968
- composeFile: setupCompose,
207969
- envFile: existsSync9(setupEnv) ? setupEnv : undefined,
207970
- secretsFile
207971
- };
207972
- }
207973
- return { composeFile: resolve10(dir, REPO_COMPOSE), secretsFile };
207974
- }
207975
- function composeDownArgs(plan, layout) {
207976
- const base = uninstallCommand(plan, layout.composeFile);
207977
- if (!layout.envFile)
207978
- return base;
207979
- const fileFlagEnd = base.indexOf("-f") + 2;
207980
- return [
207981
- ...base.slice(0, fileFlagEnd),
207982
- "--env-file",
207983
- layout.envFile,
207984
- ...base.slice(fileFlagEnd)
207985
- ];
207986
- }
207987
- function registerUninstallCommand(program) {
207988
- program.command("uninstall").description("Stop the stack; the index, chainstate, and keys survive").option("--compose <file>", `compose file to tear down (default: ./docker-compose.yml from setup, else ${REPO_COMPOSE})`).option("--purge", "also destroy the volumes (wipes the index)").option("--yes", "confirm a purge").option("--backup <dir>", "a bundle proving the keys exist elsewhere (required to purge)").option("--apply", "actually run it (default is a dry run)").option("--json", "Output as JSON").action(async (opts) => {
207989
- assertInstanceUrl();
207990
- const dataDir = process.env.DATA_DIR ?? "./data";
207991
- const layout = resolveUninstallLayout(process.cwd(), opts.compose);
207992
- const secretsPresent = !!process.env.SECONDLAYER_SECRETS_KEY || !!layout.secretsFile;
207993
- const keysBackedUp = opts.backup ? bundleHasKeys(resolve10(opts.backup)) : false;
207994
- const decision = planUninstall({
207995
- purge: opts.purge === true,
207996
- confirmed: opts.yes === true,
207997
- keysBackedUp,
207998
- secretsPresent,
207999
- dataDir
208000
- });
208001
- if (!decision.ok) {
208002
- printError(decision.reason, {
208003
- hint: layout.secretsFile ? `keys found in ${layout.secretsFile}` : undefined
208004
- });
208005
- process.exit(UNINSTALL_EXIT.REFUSED);
208006
- }
208007
- const cmd = composeDownArgs(decision.plan, layout);
208008
- if (!opts.apply) {
208009
- output({
208010
- json: opts.json,
208011
- data: {
208012
- dryRun: true,
208013
- plan: decision.plan,
208014
- command: cmd,
208015
- composeFile: layout.composeFile,
208016
- envFile: layout.envFile ?? null,
208017
- secretsFile: layout.secretsFile ?? null
208018
- },
208019
- human: () => {
208020
- note("dry run — nothing removed. Pass --apply to proceed.");
208021
- note(`compose: ${layout.composeFile}`);
208022
- if (layout.envFile)
208023
- note(`env: ${layout.envFile}`);
208024
- if (layout.secretsFile)
208025
- note(`keys: ${layout.secretsFile}`);
208026
- note(`would run: docker ${cmd.join(" ")}`);
208027
- for (const p of decision.plan.preserves) {
208028
- note(`preserved · ${p.what}: ${p.detail}`);
208029
- }
208030
- for (const d of decision.plan.destroys) {
208031
- warn(`DESTROYED · volume ${d}`);
208032
- }
208033
- }
208034
- });
208035
- return;
208036
- }
208037
- const res = spawnSync4("docker", cmd, { stdio: "inherit" });
208038
- if ((res.status ?? 1) !== 0) {
208039
- printError(`docker ${cmd.join(" ")} exited ${res.status}`);
208040
- process.exit(UNINSTALL_EXIT.FAILED);
208041
- }
208042
- output({
208043
- json: opts.json,
208044
- data: {
208045
- removed: true,
208046
- plan: decision.plan,
208047
- composeFile: layout.composeFile,
208048
- envFile: layout.envFile ?? null
208049
- },
208050
- human: () => {
208051
- success("stack removed");
208052
- for (const w of decision.warnings)
208053
- warn(w);
208054
- for (const p of decision.plan.preserves) {
208055
- note(`preserved · ${p.what}: ${p.detail}`);
208056
- }
208057
- }
208058
- });
208059
- });
208060
- }
208061
207812
  // src/commands/backup.ts
208062
207813
  init_fs();
208063
207814
  init_output();
208064
207815
  init_resolve_auth();
208065
207816
  import {
208066
207817
  chmodSync,
208067
- existsSync as existsSync10,
207818
+ existsSync as existsSync8,
208068
207819
  mkdirSync as mkdirSync4,
208069
- readFileSync as readFileSync9,
207820
+ readFileSync as readFileSync7,
208070
207821
  statSync as statSync2,
208071
- writeFileSync as writeFileSync7
207822
+ writeFileSync as writeFileSync6
208072
207823
  } from "node:fs";
208073
- import { join as join15, resolve as resolve11 } from "node:path";
207824
+ import { join as join14, resolve as resolve9 } from "node:path";
208074
207825
  import { closeDb, getDb as getDb4, sql as sql2 } from "@secondlayer/shared/db";
208075
207826
  import {
208076
207827
  decryptBundle,
@@ -208219,10 +207970,10 @@ async function runBackup(opts) {
208219
207970
  printError(plan.reason);
208220
207971
  process.exit(BACKUP_EXIT.REFUSED);
208221
207972
  }
208222
- const outDir = resolve11(opts.out);
207973
+ const outDir = resolve9(opts.out);
208223
207974
  mkdirSync4(outDir, { recursive: true });
208224
207975
  console.error("dumping database…");
208225
- const dumpPath = join15(outDir, DB_FILE);
207976
+ const dumpPath = join14(outDir, DB_FILE);
208226
207977
  const dumpCmd = pgDumpInvocation(databaseUrl, dumpPath);
208227
207978
  const dump = await runProcess(dumpCmd.cmd, "pg_dump", dumpCmd.env);
208228
207979
  if (!dump.ok) {
@@ -208242,8 +207993,8 @@ async function runBackup(opts) {
208242
207993
  const lines = SECRET_ENV_KEYS.filter((k) => process.env[k]).map((k) => `${k}=${process.env[k]}`);
208243
207994
  const sealed = encryptBundle(lines.join(`
208244
207995
  `), passphrase);
208245
- const secretsPath = join15(outDir, SECRETS_FILE);
208246
- writeFileSync7(secretsPath, sealed);
207996
+ const secretsPath = join14(outDir, SECRETS_FILE);
207997
+ writeFileSync6(secretsPath, sealed);
208247
207998
  chmodSync(secretsPath, 384);
208248
207999
  manifest.secrets = {
208249
208000
  file: SECRETS_FILE,
@@ -208251,7 +208002,7 @@ async function runBackup(opts) {
208251
208002
  canary: sealKeyCanary(secretsKey)
208252
208003
  };
208253
208004
  }
208254
- writeFileSync7(join15(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
208005
+ writeFileSync6(join14(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
208255
208006
  `);
208256
208007
  output({
208257
208008
  json: opts.json,
@@ -208280,14 +208031,14 @@ function attachRestoreCommand(cmd) {
208280
208031
  }
208281
208032
  async function runRestore(opts) {
208282
208033
  const databaseUrl = requireDatabaseUrl();
208283
- const dir = resolve11(opts.from);
208284
- const manifestPath = join15(dir, MANIFEST_FILE);
208285
- if (!existsSync10(manifestPath)) {
208034
+ const dir = resolve9(opts.from);
208035
+ const manifestPath = join14(dir, MANIFEST_FILE);
208036
+ if (!existsSync8(manifestPath)) {
208286
208037
  printError(`no ${MANIFEST_FILE} in ${dir}`);
208287
208038
  process.exit(BACKUP_EXIT.REFUSED);
208288
208039
  }
208289
- const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
208290
- const dumpPath = join15(dir, manifest.db?.file ?? DB_FILE);
208040
+ const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
208041
+ const dumpPath = join14(dir, manifest.db?.file ?? DB_FILE);
208291
208042
  if (manifest.db && await sha256File(dumpPath) !== manifest.db.sha256) {
208292
208043
  printError("the database dump does not match the digest in the manifest; this bundle is corrupt or truncated");
208293
208044
  process.exit(BACKUP_EXIT.REFUSED);
@@ -208301,7 +208052,7 @@ async function runRestore(opts) {
208301
208052
  process.exit(BACKUP_EXIT.REFUSED);
208302
208053
  }
208303
208054
  try {
208304
- const env = decryptBundle(readFileSync9(join15(dir, manifest.secrets.file)), passphrase);
208055
+ const env = decryptBundle(readFileSync7(join14(dir, manifest.secrets.file)), passphrase);
208305
208056
  bundleKey = env.split(`
208306
208057
  `).find((l) => l.startsWith("SECONDLAYER_SECRETS_KEY="))?.slice("SECONDLAYER_SECRETS_KEY=".length).trim();
208307
208058
  } catch {
@@ -208376,6 +208127,124 @@ function registerBackupCommand(program) {
208376
208127
  function registerRestoreCommand(program) {
208377
208128
  attachRestoreCommand(program.command("restore").description("Restore an instance from a backup bundle").hook("preAction", () => assertInstanceUrl()));
208378
208129
  }
208130
+ // src/lib/instance-init.ts
208131
+ init_output();
208132
+ import { randomBytes as randomBytes3 } from "node:crypto";
208133
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
208134
+ import { resolve as resolve10 } from "node:path";
208135
+ import { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from "@secondlayer/shared/archive/root-key";
208136
+ import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
208137
+ import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
208138
+ var INSTANCE_ENV_FILE = ".env.local";
208139
+ function isHostedAccountKey(value) {
208140
+ return value.startsWith("sk-sl_") || value.startsWith("ss-sl_");
208141
+ }
208142
+ function parseInstanceNetwork(value) {
208143
+ const network = value.trim().toLowerCase();
208144
+ if (INSTANCE_NETWORKS.includes(network)) {
208145
+ return network;
208146
+ }
208147
+ throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
208148
+ }
208149
+ function instanceNetworkFromEnv(fallback = "mainnet") {
208150
+ const raw = process.env.STACKS_NETWORK ?? fallback;
208151
+ try {
208152
+ return parseInstanceNetwork(raw);
208153
+ } catch (err) {
208154
+ printError(err instanceof Error ? err.message : String(err), {
208155
+ hint: "Pass --network mainnet, --network testnet, or --network devnet."
208156
+ });
208157
+ process.exit(1);
208158
+ }
208159
+ }
208160
+ function generateInstanceToken() {
208161
+ return randomBytes3(32).toString("hex");
208162
+ }
208163
+ function generateSecretsKey() {
208164
+ return randomBytes3(32).toString("hex");
208165
+ }
208166
+ function generateSigningPrivateKey() {
208167
+ return generateEd25519KeyPair().privateKeyPem;
208168
+ }
208169
+ function escapeEnvValue(value) {
208170
+ if (!/[\s#"']/.test(value) && !value.includes("\\"))
208171
+ return value;
208172
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
208173
+ }
208174
+ function unescapeEnvValue(raw) {
208175
+ let value = raw.trim();
208176
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
208177
+ value = value.slice(1, -1);
208178
+ }
208179
+ return value.replace(/\\n/g, `
208180
+ `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
208181
+ }
208182
+ function readEnvValue(envPath, key) {
208183
+ if (!existsSync9(envPath))
208184
+ return null;
208185
+ const match = readFileSync8(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
208186
+ return match ? unescapeEnvValue(match[1]) : null;
208187
+ }
208188
+ function buildInstanceEnv(input) {
208189
+ const token = input.existing?.INSTANCE_TOKEN || generateInstanceToken();
208190
+ const secrets = input.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
208191
+ const signing = input.existing?.STREAMS_SIGNING_PRIVATE_KEY || input.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
208192
+ const apiUrl = input.existing?.SECONDLAYER_API_URL || input.apiUrl || "http://127.0.0.1:3800";
208193
+ const archiveKey = input.existing?.ARCHIVE_SIGNING_PUBLIC_KEY || input.archivePublicKeyPem || ARCHIVE_ROOT_PUBLIC_KEY_PEM;
208194
+ const hostedCandidate = input.existing?.SECONDLAYER_API_KEY || undefined;
208195
+ const hostedKey = hostedCandidate && isHostedAccountKey(hostedCandidate) ? hostedCandidate : undefined;
208196
+ return {
208197
+ INSTANCE_MODE: "oss",
208198
+ STACKS_NETWORK: input.network,
208199
+ INSTANCE_TOKEN: token,
208200
+ SECONDLAYER_SECRETS_KEY: secrets,
208201
+ STREAMS_SIGNING_PRIVATE_KEY: signing,
208202
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
208203
+ ALLOW_UNSIGNED_WEBHOOKS: "false",
208204
+ SECONDLAYER_API_URL: apiUrl,
208205
+ ...hostedKey ? { SECONDLAYER_API_KEY: hostedKey } : {},
208206
+ ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey
208207
+ };
208208
+ }
208209
+ function renderInstanceEnv(env) {
208210
+ const lines = [
208211
+ "# Generated by secondlayer init. Do not commit.",
208212
+ `INSTANCE_MODE=${env.INSTANCE_MODE}`,
208213
+ `STACKS_NETWORK=${env.STACKS_NETWORK}`,
208214
+ "# The instance credential. The API validates bearer tokens against it,",
208215
+ "# and the CLI/SDK/MCP read it first when authenticating.",
208216
+ `INSTANCE_TOKEN=${env.INSTANCE_TOKEN}`,
208217
+ `SECONDLAYER_SECRETS_KEY=${env.SECONDLAYER_SECRETS_KEY}`,
208218
+ `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.STREAMS_SIGNING_PRIVATE_KEY)}`,
208219
+ `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
208220
+ `ALLOW_UNSIGNED_WEBHOOKS=${env.ALLOW_UNSIGNED_WEBHOOKS}`,
208221
+ `SECONDLAYER_API_URL=${env.SECONDLAYER_API_URL}`
208222
+ ];
208223
+ if (env.SECONDLAYER_API_KEY) {
208224
+ lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${env.SECONDLAYER_API_KEY}`);
208225
+ }
208226
+ lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env.ARCHIVE_SIGNING_PUBLIC_KEY)}`, "");
208227
+ return lines.join(`
208228
+ `);
208229
+ }
208230
+ function writeInstanceEnv(cwd, env, file = INSTANCE_ENV_FILE) {
208231
+ const path = resolve10(cwd, file);
208232
+ writeFileSync7(path, renderInstanceEnv(env), { mode: 384 });
208233
+ return path;
208234
+ }
208235
+ function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
208236
+ const path = resolve10(cwd, file);
208237
+ return {
208238
+ INSTANCE_TOKEN: readEnvValue(path, "INSTANCE_TOKEN"),
208239
+ SECONDLAYER_SECRETS_KEY: readEnvValue(path, "SECONDLAYER_SECRETS_KEY"),
208240
+ STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path, "STREAMS_SIGNING_PRIVATE_KEY"),
208241
+ SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
208242
+ SECONDLAYER_API_URL: readEnvValue(path, "SECONDLAYER_API_URL"),
208243
+ SECONDLAYER_API_KEY: readEnvValue(path, "SECONDLAYER_API_KEY"),
208244
+ ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path, "ARCHIVE_SIGNING_PUBLIC_KEY")
208245
+ };
208246
+ }
208247
+
208379
208248
  // src/commands/init.ts
208380
208249
  init_output();
208381
208250
  init_resolve_auth();
@@ -208485,968 +208354,6 @@ function registerObserverCommand(program) {
208485
208354
  writeData(renderObserverStanza({ mode, endpoint, network }));
208486
208355
  });
208487
208356
  }
208488
- // src/commands/setup.ts
208489
- init_output();
208490
- init_resolve_auth();
208491
-
208492
- // src/lib/setup-prompts.ts
208493
- import { resolve as resolvePath3 } from "node:path";
208494
- import { confirm as confirm4, input as input2, select as select3 } from "@inquirer/prompts";
208495
-
208496
- // src/lib/oss-compose.ts
208497
- function buildOssCompose(opts = {}) {
208498
- const owner = opts.owner ?? DEFAULT_IMAGE_OWNER;
208499
- const tag = opts.imageTag ?? DEFAULT_IMAGE_TAG;
208500
- const runtimeImage = `ghcr.io/${owner}/secondlayer-runtime:${tag}`;
208501
- return `# Generated by \`secondlayer setup\` — do not edit by hand.
208502
- # Mirrors docker/oss/docker-compose.yml; see that file for the source of truth
208503
- # on env vars, profiles, and third-party digest pins. \`secondlayer\` is
208504
- # swapped from a repo build to the published OSS image (no checkout here).
208505
- name: secondlayer
208506
-
208507
- services:
208508
- postgres:
208509
- image: postgres:17-alpine@sha256:d4bb0a8c1b7bb2e29f976d099e7bfb9a5d8858cffe9e46b35cd302cd1f1f8168
208510
- environment:
208511
- POSTGRES_USER: \${POSTGRES_USER:-secondlayer}
208512
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-secondlayer}
208513
- POSTGRES_DB: \${POSTGRES_DB:-secondlayer}
208514
- # Postgres ships with a 128 MB cache, too small for a full-chain sync:
208515
- # inserts stall on index reads from disk. ~25% of RAM is the usual size.
208516
- command: ["postgres", "-c", "shared_buffers=\${POSTGRES_SHARED_BUFFERS:-1GB}"]
208517
- volumes:
208518
- - postgres_data:/var/lib/postgresql/data
208519
- ports:
208520
- - "\${POSTGRES_PORT:-127.0.0.1:5432}:5432"
208521
- healthcheck:
208522
- test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-secondlayer}"]
208523
- interval: 10s
208524
- timeout: 5s
208525
- retries: 5
208526
- restart: unless-stopped
208527
-
208528
- secondlayer:
208529
- image: ${runtimeImage}
208530
- environment:
208531
- INSTANCE_MODE: oss
208532
- NETWORK: \${NETWORK:-\${STACKS_NETWORK:-mainnet}}
208533
- STACKS_NETWORK: \${STACKS_NETWORK:-mainnet}
208534
- NODE_MODE: \${NODE_MODE:-external}
208535
- SECONDLAYER_ALLOW_UNDERSIZED: \${SECONDLAYER_ALLOW_UNDERSIZED:-false}
208536
- DATABASE_URL: postgres://\${POSTGRES_USER:-secondlayer}:\${POSTGRES_PASSWORD:-secondlayer}@postgres:5432/\${POSTGRES_DB:-secondlayer}
208537
- DATA_DIR: /data
208538
- API_PORT: "3800"
208539
- INDEXER_PORT: "3700"
208540
- # See docker/oss/docker-compose.yml: API_PORT above is the *container*
208541
- # port; \${API_PORT} in ports:/here is the *publish spec* from .env.
208542
- LISTEN_HOST: "0.0.0.0"
208543
- API_PUBLISH_ADDR: \${API_PORT:-127.0.0.1:3800}
208544
- INSTANCE_TOKEN: \${INSTANCE_TOKEN:-}
208545
- SECONDLAYER_SECRETS_KEY: \${SECONDLAYER_SECRETS_KEY:-}
208546
- STREAMS_SIGNING_PRIVATE_KEY: \${STREAMS_SIGNING_PRIVATE_KEY:-}
208547
- SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: \${SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY:-}
208548
- ALLOW_UNSIGNED_WEBHOOKS: \${ALLOW_UNSIGNED_WEBHOOKS:-false}
208549
- LOG_LEVEL: \${LOG_LEVEL:-info}
208550
- NODE_ENV: production
208551
- STACKS_NODE_RPC_URL: \${STACKS_NODE_RPC_URL:-}
208552
- OBSERVER_JOURNAL_ENABLED: \${OBSERVER_JOURNAL_ENABLED:-true}
208553
- STREAMS_API_URL: http://127.0.0.1:3800
208554
- STREAMS_INTERNAL_API_KEY: \${STREAMS_INTERNAL_API_KEY:-}
208555
- SBTC_DECODER_ENABLED: \${SBTC_DECODER_ENABLED:-true}
208556
- POX4_DECODER_ENABLED: \${POX4_DECODER_ENABLED:-true}
208557
- BNS_DECODER_ENABLED: \${BNS_DECODER_ENABLED:-false}
208558
- SUBGRAPH_SOURCE: \${SUBGRAPH_SOURCE:-streams-index}
208559
- SUBGRAPH_INDEX_API_URL: http://127.0.0.1:3800
208560
- BITCOIN_RPC_PASSWORD: \${BITCOIN_RPC_PASSWORD:-}
208561
- ports:
208562
- - "\${API_PORT:-127.0.0.1:3800}:3800"
208563
- - "\${INDEXER_PORT:-127.0.0.1:3700}:3700"
208564
- # \`secondlayer observer\`'s default endpoint is \`indexer:3700\` — this
208565
- # alias is what makes that default actually resolve on this network,
208566
- # without renaming the service. Mirrors docker/oss/docker-compose.yml.
208567
- networks:
208568
- default:
208569
- aliases:
208570
- - indexer
208571
- volumes:
208572
- - subgraphs_data:/data/subgraphs
208573
- depends_on:
208574
- postgres:
208575
- condition: service_healthy
208576
- healthcheck:
208577
- test: ["CMD", "curl", "-f", "http://localhost:3800/health"]
208578
- interval: 30s
208579
- timeout: 5s
208580
- start_period: 40s
208581
- retries: 3
208582
- restart: unless-stopped
208583
-
208584
- bitcoind:
208585
- # Digest = kylemanna/bitcoind:latest as published 2026-07-20 — see
208586
- # docker/oss/docker-compose.yml for why the digest is the real pin here.
208587
- image: kylemanna/bitcoind:latest@sha256:86bbcaa99bf3bf3d5df7fd6d9217b3a41194db4c4e34385064f2cfc9fa7bdf91
208588
- volumes:
208589
- - \${BITCOIN_DATA_DIR:-./data/bitcoin}:/bitcoin/.bitcoin
208590
- ports:
208591
- - "127.0.0.1:8332:8332"
208592
- - "8333:8333"
208593
- healthcheck:
208594
- 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 ]"]
208595
- interval: 60s
208596
- timeout: 15s
208597
- start_period: 120s
208598
- retries: 3
208599
- restart: unless-stopped
208600
- deploy:
208601
- resources:
208602
- limits:
208603
- memory: 32G
208604
- profiles: ["full-node"]
208605
-
208606
- stacks-node:
208607
- # Consensus-critical — digest-pinned to 4.0.5. See docker/oss/docker-compose.yml.
208608
- image: ghcr.io/stacks-network/stacks-core:4.0.5@sha256:51aa7bf6f990b99fa010ebb23abe78653cf1f1d129ab8e7d88b1fe11a403c97d
208609
- volumes:
208610
- - \${STACKS_DATA_DIR:-./data/stacks}:/stacks-blockchain/data
208611
- - ./Config.toml:/stacks-blockchain/Config.toml:ro
208612
- ports:
208613
- - "20443:20443"
208614
- - "20444:20444"
208615
- restart: unless-stopped
208616
- command: ["stacks-node", "start", "--config", "/stacks-blockchain/Config.toml"]
208617
- # full-node only — see docker/oss/docker-compose.yml. No standalone
208618
- # "stacks-node, public Bitcoin" profile: nothing here wires the node at
208619
- # a real public Bitcoin RPC, so that combination never actually synced.
208620
- profiles: ["full-node"]
208621
-
208622
- volumes:
208623
- postgres_data:
208624
- subgraphs_data:
208625
- `;
208626
- }
208627
- function buildOssStacksConfigToml(opts) {
208628
- const mode = opts.network === "mainnet" ? "mainnet" : "testnet";
208629
- return `[node]
208630
- working_dir = "/stacks-blockchain/data"
208631
- rpc_bind = "0.0.0.0:20443"
208632
- p2p_bind = "0.0.0.0:20444"
208633
- seed = ""
208634
- marf_compress = true
208635
- bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444"
208636
-
208637
- # Event observer points directly at the bundled indexer service, same docker
208638
- # network. Retries off / low timeout: a stuck indexer must never starve a
208639
- # co-located signer of block_proposal events. See docker/oss/Config.toml.
208640
- [[events_observer]]
208641
- endpoint = "secondlayer:3700"
208642
- events_keys = ["*"]
208643
- timeout_ms = 500
208644
- disable_retries = true
208645
-
208646
- [burnchain]
208647
- chain = "bitcoin"
208648
- mode = "${mode}"
208649
- peer_host = "bitcoind"
208650
- username = "stacks"
208651
- password = "${opts.bitcoinRpcPassword}"
208652
- rpc_port = 8332
208653
- peer_port = 8333
208654
- `;
208655
- }
208656
- function buildOssBitcoinConf(opts) {
208657
- return `server=1
208658
- txindex=1
208659
- rpcallowip=0.0.0.0/0
208660
- rpcbind=0.0.0.0
208661
- rpcuser=stacks
208662
- rpcpassword=${opts.rpcPassword}
208663
- # Prune: 0 = archive (required for Stacks); set to 550+ only for pruned chains.
208664
- prune=0
208665
- dbcache=4096
208666
- `;
208667
- }
208668
-
208669
- // src/lib/setup-wizard.ts
208670
- import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
208671
- import { randomBytes as randomBytes4 } from "node:crypto";
208672
- import { existsSync as existsSync11 } from "node:fs";
208673
- import { writeFile as writeFile6 } from "node:fs/promises";
208674
- import { createServer } from "node:net";
208675
- import { join as join16, resolve as resolvePath2 } from "node:path";
208676
- import { closeDb as closeDb2, getRawClientFor } from "@secondlayer/shared/db";
208677
- import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
208678
- init_resolve_auth();
208679
- var SETUP_NODE_MODES = ["external", "stacks", "full"];
208680
- var DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json";
208681
- class MissingSetupFlagError extends Error {
208682
- flag;
208683
- constructor(flag, message) {
208684
- super(message);
208685
- this.name = "MissingSetupFlagError";
208686
- this.flag = flag;
208687
- }
208688
- }
208689
- function parseSetupNodeMode(value) {
208690
- const mode = value.trim().toLowerCase();
208691
- if (SETUP_NODE_MODES.includes(mode)) {
208692
- return mode;
208693
- }
208694
- throw new Error(`node mode must be external, stacks, or full (got ${value})`);
208695
- }
208696
- function resolveNonInteractiveConfig(flags) {
208697
- if (!flags.network) {
208698
- throw new MissingSetupFlagError("--network", "Missing --network <mainnet|testnet|devnet>. Non-interactive setup requires it explicitly — there is no safe default.");
208699
- }
208700
- if (!flags.nodeMode) {
208701
- throw new MissingSetupFlagError("--node-mode", "Missing --node-mode <external|stacks|full>. Non-interactive setup requires it explicitly — there is no safe default.");
208702
- }
208703
- const network = parseInstanceNetwork(flags.network);
208704
- const nodeMode = parseSetupNodeMode(flags.nodeMode);
208705
- const dir = resolvePath2(flags.dir ?? process.cwd());
208706
- const skipBootstrap = !!flags.skipBootstrap;
208707
- return {
208708
- network,
208709
- nodeMode,
208710
- apiPort: flags.apiPort ?? "127.0.0.1:3800",
208711
- indexerPort: "127.0.0.1:3700",
208712
- postgresPort: "127.0.0.1:5432",
208713
- dir,
208714
- against: skipBootstrap ? undefined : flags.against ?? DEFAULT_ARCHIVE_MANIFEST,
208715
- skipBootstrap,
208716
- skipVerify: !!flags.skipVerify,
208717
- yes: !!flags.yes,
208718
- force: !!flags.force,
208719
- owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
208720
- imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
208721
- };
208722
- }
208723
- function guardrailPreview(nodeMode, network) {
208724
- return {
208725
- ramFloorMb: nodeMode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb,
208726
- diskFloorGb: diskFloorGb(nodeMode, network)
208727
- };
208728
- }
208729
- async function checkDocker() {
208730
- const probe = spawnSync5("docker", ["info"], { stdio: "ignore" });
208731
- return !probe.error && probe.status === 0;
208732
- }
208733
- function isPortFree(host, port) {
208734
- return new Promise((resolvePromise) => {
208735
- const srv = createServer();
208736
- srv.once("error", () => resolvePromise(false));
208737
- srv.once("listening", () => srv.close(() => resolvePromise(true)));
208738
- srv.listen(port, host);
208739
- });
208740
- }
208741
- function parsePublishSpec(spec) {
208742
- const idx = spec.lastIndexOf(":");
208743
- if (idx === -1)
208744
- return { host: "127.0.0.1", port: Number(spec) };
208745
- return { host: spec.slice(0, idx), port: Number(spec.slice(idx + 1)) };
208746
- }
208747
- function dockerPublishedHostPorts() {
208748
- const probe = spawnSync5("docker", ["ps", "--format", "{{.Ports}}"], {
208749
- encoding: "utf8"
208750
- });
208751
- const ports = new Set;
208752
- if (probe.error || probe.status !== 0 || !probe.stdout)
208753
- return ports;
208754
- for (const match of probe.stdout.matchAll(/:(\d+)->/g)) {
208755
- ports.add(Number(match[1]));
208756
- }
208757
- return ports;
208758
- }
208759
- async function resolveAvailablePublishSpec(spec, claimedByDocker = new Set, maxAttempts = 20) {
208760
- const { host, port } = parsePublishSpec(spec);
208761
- for (let attempt = 0;attempt < maxAttempts; attempt++) {
208762
- const candidate = port + attempt;
208763
- if (claimedByDocker.has(candidate))
208764
- continue;
208765
- if (await isPortFree(host, candidate)) {
208766
- return { spec: `${host}:${candidate}`, remapped: attempt > 0 };
208767
- }
208768
- }
208769
- return { spec, remapped: false };
208770
- }
208771
- function randomHex(bytes) {
208772
- return randomBytes4(bytes).toString("hex");
208773
- }
208774
- function resolveSecrets(config) {
208775
- const envPath = join16(config.dir, ".env");
208776
- const existing = config.force || !existsSync11(envPath) ? {} : readEnvFile(config.dir, envPath);
208777
- const instance = buildInstanceEnv({
208778
- network: config.network,
208779
- existing: {
208780
- INSTANCE_TOKEN: existing.INSTANCE_TOKEN,
208781
- SECONDLAYER_SECRETS_KEY: existing.SECONDLAYER_SECRETS_KEY,
208782
- STREAMS_SIGNING_PRIVATE_KEY: existing.STREAMS_SIGNING_PRIVATE_KEY,
208783
- SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: existing.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY,
208784
- SECONDLAYER_API_URL: existing.SECONDLAYER_API_URL,
208785
- SECONDLAYER_API_KEY: existing.SECONDLAYER_API_KEY,
208786
- ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
208787
- },
208788
- apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`,
208789
- archivePublicKeyPem: config.archivePublicKeyPem
208790
- });
208791
- return {
208792
- instance,
208793
- postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
208794
- bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24),
208795
- databaseUrl: operatorEditedDatabaseUrl(existing)
208796
- };
208797
- }
208798
- function operatorEditedDatabaseUrl(existing) {
208799
- const url = existing.DATABASE_URL;
208800
- if (!url)
208801
- return;
208802
- if (existing.POSTGRES_PORT && existing.POSTGRES_PASSWORD) {
208803
- const generated = setupDatabaseUrl({ postgresPort: existing.POSTGRES_PORT }, { postgresPassword: existing.POSTGRES_PASSWORD });
208804
- if (url === generated)
208805
- return;
208806
- }
208807
- return url;
208808
- }
208809
- var SETUP_POSTGRES_USER = "secondlayer";
208810
- var SETUP_POSTGRES_DB = "secondlayer";
208811
- function setupDatabaseUrl(config, secrets) {
208812
- if (secrets.databaseUrl)
208813
- return secrets.databaseUrl;
208814
- const password = encodeURIComponent(secrets.postgresPassword);
208815
- return `postgres://${SETUP_POSTGRES_USER}:${password}@${normalizeLoopbackHost(config.postgresPort)}/${SETUP_POSTGRES_DB}`;
208816
- }
208817
- function normalizeLoopbackHost(apiPort) {
208818
- return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
208819
- }
208820
- function readEnvFile(dir, envPath) {
208821
- const shared = loadExistingInstanceEnv(dir, ".env");
208822
- return {
208823
- ...shared,
208824
- POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
208825
- POSTGRES_PORT: readEnvValue(envPath, "POSTGRES_PORT"),
208826
- BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD"),
208827
- DATABASE_URL: readEnvValue(envPath, "DATABASE_URL")
208828
- };
208829
- }
208830
- function renderSetupEnv(config, secrets) {
208831
- const i = secrets.instance;
208832
- const lines = [
208833
- "# Generated by `secondlayer setup`. Do not commit.",
208834
- `NETWORK=${config.network}`,
208835
- `STACKS_NETWORK=${config.network}`,
208836
- `NODE_MODE=${config.nodeMode}`,
208837
- `API_PORT=${config.apiPort}`,
208838
- `INDEXER_PORT=${config.indexerPort}`,
208839
- `POSTGRES_USER=${SETUP_POSTGRES_USER}`,
208840
- `POSTGRES_PASSWORD=${secrets.postgresPassword}`,
208841
- `POSTGRES_DB=${SETUP_POSTGRES_DB}`,
208842
- `POSTGRES_PORT=${config.postgresPort}`,
208843
- "# Where bootstrap, verify, repair, and backup connect from this directory:",
208844
- "# the compose Postgres above, published on the host. Point it elsewhere",
208845
- "# only if you moved the index.",
208846
- `DATABASE_URL=${escapeEnvValue(setupDatabaseUrl(config, secrets))}`,
208847
- "# The instance credential. The API validates bearer tokens against it,",
208848
- "# and the CLI/SDK/MCP read it first when authenticating.",
208849
- `INSTANCE_TOKEN=${i.INSTANCE_TOKEN}`,
208850
- `SECONDLAYER_SECRETS_KEY=${i.SECONDLAYER_SECRETS_KEY}`,
208851
- `STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.STREAMS_SIGNING_PRIVATE_KEY)}`,
208852
- `SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(i.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
208853
- `ALLOW_UNSIGNED_WEBHOOKS=${i.ALLOW_UNSIGNED_WEBHOOKS}`,
208854
- `SECONDLAYER_API_URL=${i.SECONDLAYER_API_URL}`
208855
- ];
208856
- if (i.SECONDLAYER_API_KEY) {
208857
- lines.push("# Hosted account key for archive credits (sk-sl_*).", `SECONDLAYER_API_KEY=${i.SECONDLAYER_API_KEY}`);
208858
- }
208859
- lines.push("# Archive trust root: bootstrap, verify, and repair check manifests", "# against this key. Replace it to pin a different archive.", `ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(i.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
208860
- if (config.nodeMode === "full") {
208861
- lines.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
208862
- }
208863
- lines.push("");
208864
- return lines.join(`
208865
- `);
208866
- }
208867
- async function writeSetupFiles(config, secrets) {
208868
- const composePath = join16(config.dir, "docker-compose.yml");
208869
- const envPath = join16(config.dir, ".env");
208870
- await writeFile6(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
208871
- await writeFile6(envPath, renderSetupEnv(config, secrets), { mode: 384 });
208872
- const result = { composePath, envPath };
208873
- if (config.nodeMode === "full") {
208874
- const configTomlPath = join16(config.dir, "Config.toml");
208875
- await writeFile6(configTomlPath, buildOssStacksConfigToml({
208876
- network: config.network,
208877
- bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
208878
- }));
208879
- result.configTomlPath = configTomlPath;
208880
- }
208881
- if (config.nodeMode === "full") {
208882
- const bitcoinConfPath = join16(config.dir, "bitcoin.conf");
208883
- await writeFile6(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
208884
- result.bitcoinConfPath = bitcoinConfPath;
208885
- }
208886
- return result;
208887
- }
208888
- function composeProfileArgs(nodeMode) {
208889
- if (nodeMode === "full")
208890
- return ["--profile", "full-node"];
208891
- return [];
208892
- }
208893
- function runStreaming(command, args, opts) {
208894
- return new Promise((resolvePromise, reject) => {
208895
- const child = spawn3(command, args, {
208896
- cwd: opts.cwd,
208897
- env: opts.env ?? process.env,
208898
- stdio: ["ignore", "pipe", "pipe"]
208899
- });
208900
- let stdout = "";
208901
- let stderr = "";
208902
- const pump = (stream) => (chunk) => {
208903
- const text = chunk.toString("utf8");
208904
- if (stream === "stdout")
208905
- stdout += text;
208906
- else
208907
- stderr += text;
208908
- if (opts.onLine) {
208909
- for (const line of text.split(`
208910
- `)) {
208911
- if (line.length > 0)
208912
- opts.onLine(line, stream);
208913
- }
208914
- }
208915
- };
208916
- child.stdout?.on("data", pump("stdout"));
208917
- child.stderr?.on("data", pump("stderr"));
208918
- child.once("error", reject);
208919
- child.once("close", (code) => {
208920
- resolvePromise({ code: code ?? 1, stdout, stderr });
208921
- });
208922
- });
208923
- }
208924
- async function dockerComposeUp(config, files, onLine) {
208925
- const args = [
208926
- "compose",
208927
- "-f",
208928
- files.composePath,
208929
- "--env-file",
208930
- files.envPath,
208931
- ...composeProfileArgs(config.nodeMode),
208932
- "up",
208933
- "-d"
208934
- ];
208935
- return runStreaming("docker", args, { cwd: config.dir, onLine });
208936
- }
208937
- async function pollHealth(config, opts = {}) {
208938
- const timeoutMs = opts.timeoutMs ?? 120000;
208939
- const intervalMs = opts.intervalMs ?? 2000;
208940
- const url = `http://${normalizeLoopbackHost(config.apiPort)}/health`;
208941
- const deadline = Date.now() + timeoutMs;
208942
- for (;; ) {
208943
- try {
208944
- const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
208945
- if (res.ok)
208946
- return true;
208947
- } catch {}
208948
- opts.onTick?.();
208949
- if (Date.now() >= deadline)
208950
- return false;
208951
- await new Promise((r) => setTimeout(r, intervalMs));
208952
- }
208953
- }
208954
- function getObserverInfo(config) {
208955
- if (config.nodeMode !== "external") {
208956
- return { applicable: false };
208957
- }
208958
- const endpoint = defaultObserverEndpoint(config.network);
208959
- const stanza = renderObserverStanza({
208960
- mode: "indexer",
208961
- endpoint,
208962
- network: config.network
208963
- });
208964
- return { applicable: true, stanza, endpoint };
208965
- }
208966
- function cliEntry() {
208967
- const entry = process.argv[1];
208968
- if (!entry) {
208969
- throw new Error("Could not resolve the secondlayer CLI entry to shell out to.");
208970
- }
208971
- return [process.execPath, entry];
208972
- }
208973
- var CHILD_ENV_KEYS = [
208974
- "DATABASE_URL",
208975
- "ARCHIVE_SIGNING_PUBLIC_KEY",
208976
- "SECONDLAYER_API_URL",
208977
- "INSTANCE_TOKEN"
208978
- ];
208979
- function buildChildEnv(envPath, base = process.env) {
208980
- const env = { ...base };
208981
- for (const key of CHILD_ENV_KEYS) {
208982
- const value = readEnvValue(envPath, key);
208983
- if (value)
208984
- env[key] = value;
208985
- }
208986
- return env;
208987
- }
208988
- async function checkDatabaseReachable(databaseUrl) {
208989
- try {
208990
- const client = getRawClientFor(databaseUrl);
208991
- await client`SELECT 1`;
208992
- return { ok: true };
208993
- } catch (err) {
208994
- return {
208995
- ok: false,
208996
- error: err instanceof Error ? err.message : String(err)
208997
- };
208998
- } finally {
208999
- await closeDb2().catch(() => {});
209000
- }
209001
- }
209002
- async function preflightBootstrapDatabase(config, secrets, emit) {
209003
- const databaseUrl = setupDatabaseUrl(config, secrets);
209004
- const reachable = await checkDatabaseReachable(databaseUrl);
209005
- if (!reachable.ok) {
209006
- emit({
209007
- type: "step-error",
209008
- step: "bootstrap",
209009
- message: `Postgres at ${redactUrl(databaseUrl)} did not answer SELECT 1: ${reachable.error}. Check \`docker compose logs postgres\`, then re-run setup to resume.`
209010
- });
209011
- return null;
209012
- }
209013
- emit({
209014
- type: "step-log",
209015
- step: "bootstrap",
209016
- line: `database ${redactUrl(databaseUrl)}`
209017
- });
209018
- return databaseUrl;
209019
- }
209020
- async function runBootstrap(config, against, files, onLine) {
209021
- const [command, ...prefix] = cliEntry();
209022
- const args = [...prefix, "bootstrap", "--against", against, "--yes"];
209023
- return runStreaming(command, args, {
209024
- cwd: config.dir,
209025
- env: buildChildEnv(files.envPath),
209026
- onLine
209027
- });
209028
- }
209029
- async function runVerify(config, against, files, onLine) {
209030
- const [command, ...prefix] = cliEntry();
209031
- const args = [...prefix, "verify", "all", "--against", against];
209032
- return runStreaming(command, args, {
209033
- cwd: config.dir,
209034
- env: buildChildEnv(files.envPath),
209035
- onLine
209036
- });
209037
- }
209038
- function redactUrl(url) {
209039
- try {
209040
- const u = new URL(url);
209041
- if (u.password)
209042
- u.password = "***";
209043
- return u.toString();
209044
- } catch {
209045
- return url;
209046
- }
209047
- }
209048
- function buildSuccessSummary(config, secrets) {
209049
- const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
209050
- const lines = [
209051
- "Secondlayer is up.",
209052
- "",
209053
- ` api ${apiUrl}`,
209054
- ` network ${config.network}`,
209055
- ` node mode ${config.nodeMode}`,
209056
- ` dir ${config.dir}`,
209057
- "",
209058
- "Export the instance token for writes (reads on loopback need no credential):",
209059
- ` export INSTANCE_TOKEN=${secrets.instance.INSTANCE_TOKEN}`,
209060
- "",
209061
- "Try it:",
209062
- " secondlayer subgraphs create my-balances --from-contract SP....my-contract",
209063
- " secondlayer subgraphs deploy subgraphs/my-balances.ts",
209064
- ` curl ${apiUrl}/v1/subgraphs/my-balances/balances`,
209065
- "",
209066
- "Docs: https://secondlayer.tools/docs/self-host"
209067
- ];
209068
- return lines.join(`
209069
- `);
209070
- }
209071
- async function runSetup(initialConfig, emit) {
209072
- let config = initialConfig;
209073
- emit({ type: "step-start", step: "preflight" });
209074
- if (!await checkDocker()) {
209075
- emit({
209076
- type: "step-error",
209077
- step: "preflight",
209078
- message: "Docker isn't reachable. Start Docker Desktop/OrbStack, or install Docker, then re-run."
209079
- });
209080
- return { ok: false, config };
209081
- }
209082
- const claimedByDocker = dockerPublishedHostPorts();
209083
- const ports = {
209084
- api: await resolveAvailablePublishSpec(config.apiPort, claimedByDocker),
209085
- indexer: await resolveAvailablePublishSpec(config.indexerPort, claimedByDocker),
209086
- postgres: await resolveAvailablePublishSpec(config.postgresPort, claimedByDocker)
209087
- };
209088
- config = {
209089
- ...config,
209090
- apiPort: ports.api.spec,
209091
- indexerPort: ports.indexer.spec,
209092
- postgresPort: ports.postgres.spec
209093
- };
209094
- for (const [label, result] of [
209095
- ["api", ports.api],
209096
- ["indexer", ports.indexer],
209097
- ["postgres", ports.postgres]
209098
- ]) {
209099
- if (result.remapped) {
209100
- emit({
209101
- type: "step-log",
209102
- step: "preflight",
209103
- line: `${label} port was already in use locally, using ${result.spec} instead`
209104
- });
209105
- }
209106
- }
209107
- const floors = guardrailPreview(config.nodeMode, config.network);
209108
- emit({
209109
- type: "step-done",
209110
- step: "preflight",
209111
- detail: `NODE_MODE=${config.nodeMode} on ${config.network} needs at least ${floors.ramFloorMb}MB RAM and ${floors.diskFloorGb}GB disk.`
209112
- });
209113
- emit({ type: "step-start", step: "secrets" });
209114
- const secrets = resolveSecrets({
209115
- dir: config.dir,
209116
- network: config.network,
209117
- apiPort: config.apiPort,
209118
- force: config.force,
209119
- archivePublicKeyPem: await resolveArchivePublicKey({
209120
- envPem: process.env.ARCHIVE_SIGNING_PUBLIC_KEY ?? process.env.STREAMS_SIGNING_PUBLIC_KEY,
209121
- allowHostedApi: !isOssMode()
209122
- })
209123
- });
209124
- emit({ type: "step-done", step: "secrets" });
209125
- emit({ type: "step-start", step: "config" });
209126
- const files = await writeSetupFiles(config, secrets);
209127
- emit({
209128
- type: "step-done",
209129
- step: "config",
209130
- detail: `${files.composePath}, ${files.envPath}`
209131
- });
209132
- emit({ type: "step-start", step: "docker-up" });
209133
- const up = await dockerComposeUp(config, files, (line) => emit({ type: "step-log", step: "docker-up", line }));
209134
- if (up.code !== 0) {
209135
- emit({
209136
- type: "step-error",
209137
- step: "docker-up",
209138
- message: `docker compose up failed (exit ${up.code}).
209139
- ${up.stderr || up.stdout}`
209140
- });
209141
- return { ok: false, config, secrets, files };
209142
- }
209143
- const healthy = await pollHealth(config, {
209144
- onTick: () => emit({
209145
- type: "step-log",
209146
- step: "docker-up",
209147
- line: "waiting for /health…"
209148
- })
209149
- });
209150
- if (!healthy) {
209151
- emit({
209152
- type: "step-error",
209153
- step: "docker-up",
209154
- message: "Containers started but /health never came up. Check `docker compose logs`."
209155
- });
209156
- return { ok: false, config, secrets, files };
209157
- }
209158
- emit({ type: "step-done", step: "docker-up" });
209159
- emit({ type: "step-start", step: "observer" });
209160
- const observer = getObserverInfo(config);
209161
- if (observer.applicable && observer.stanza) {
209162
- emit({
209163
- type: "step-log",
209164
- step: "observer",
209165
- line: "ACTION REQUIRED — paste this into your Stacks node's Config.toml:"
209166
- });
209167
- for (const line of observer.stanza.split(`
209168
- `)) {
209169
- if (line.length > 0)
209170
- emit({ type: "step-log", step: "observer", line });
209171
- }
209172
- emit({
209173
- type: "step-done",
209174
- step: "observer",
209175
- detail: `endpoint ${observer.endpoint} — see the ACTION REQUIRED lines above`
209176
- });
209177
- } else {
209178
- emit({
209179
- type: "step-done",
209180
- step: "observer",
209181
- detail: "bundled node — the observer is pre-configured, nothing to paste"
209182
- });
209183
- }
209184
- let bootstrapped = false;
209185
- if (config.skipBootstrap) {
209186
- emit({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
209187
- } else if (config.against) {
209188
- emit({ type: "step-start", step: "bootstrap" });
209189
- const databaseUrl = await preflightBootstrapDatabase(config, secrets, emit);
209190
- if (databaseUrl === null) {
209191
- return { ok: false, config, secrets, files, observer };
209192
- }
209193
- const res = await runBootstrap(config, config.against, files, (line) => emit({ type: "step-log", step: "bootstrap", line }));
209194
- if (res.code === 0) {
209195
- bootstrapped = true;
209196
- emit({ type: "step-done", step: "bootstrap" });
209197
- } else {
209198
- emit({
209199
- type: "step-error",
209200
- step: "bootstrap",
209201
- message: `secondlayer bootstrap exited ${res.code}.`
209202
- });
209203
- }
209204
- }
209205
- if (config.skipVerify) {
209206
- emit({ type: "step-skip", step: "verify", reason: "--skip-verify" });
209207
- } else if (!config.against) {
209208
- emit({
209209
- type: "step-skip",
209210
- step: "verify",
209211
- reason: "no manifest to verify against"
209212
- });
209213
- } else if (!bootstrapped) {
209214
- emit({
209215
- type: "step-skip",
209216
- step: "verify",
209217
- reason: "bootstrap did not run — nothing restored yet to verify"
209218
- });
209219
- } else {
209220
- emit({ type: "step-start", step: "verify" });
209221
- const res = await runVerify(config, config.against, files, (line) => emit({ type: "step-log", step: "verify", line }));
209222
- if (res.code === 0) {
209223
- emit({ type: "step-done", step: "verify" });
209224
- } else {
209225
- emit({
209226
- type: "step-error",
209227
- step: "verify",
209228
- message: `secondlayer verify exited ${res.code}.`
209229
- });
209230
- }
209231
- }
209232
- const summary = buildSuccessSummary(config, secrets);
209233
- emit({ type: "step-start", step: "summary" });
209234
- emit({ type: "step-done", step: "summary", detail: summary });
209235
- return { ok: true, config, secrets, files, observer, summary };
209236
- }
209237
-
209238
- // src/lib/setup-prompts.ts
209239
- class SetupCancelledError extends Error {
209240
- constructor() {
209241
- super("Setup cancelled — nothing was changed.");
209242
- this.name = "SetupCancelledError";
209243
- }
209244
- }
209245
- function safeParse(value, parse) {
209246
- if (!value)
209247
- return;
209248
- try {
209249
- return parse(value);
209250
- } catch {
209251
- return;
209252
- }
209253
- }
209254
- var NODE_MODE_DESCRIPTIONS = {
209255
- external: "You run the Stacks node; paste an observer stanza into its Config.toml.",
209256
- stacks: "You run the Stacks node yourself, no bundled bitcoind (same as external).",
209257
- full: "Bundled Stacks node + bitcoind."
209258
- };
209259
- async function promptSetupConfig(flags) {
209260
- const network = safeParse(flags.network, parseInstanceNetwork) ?? await select3({
209261
- message: "Network",
209262
- choices: [
209263
- {
209264
- name: "mainnet",
209265
- value: "mainnet",
209266
- description: "Production Stacks chain. Large: plan for hundreds of GB."
209267
- },
209268
- {
209269
- name: "testnet",
209270
- value: "testnet",
209271
- description: "Stacks testnet. Small history."
209272
- },
209273
- {
209274
- name: "devnet",
209275
- value: "devnet",
209276
- description: "Local Clarinet devnet."
209277
- }
209278
- ]
209279
- });
209280
- const nodeMode = safeParse(flags.nodeMode, parseSetupNodeMode) ?? await select3({
209281
- message: "Node mode",
209282
- choices: SETUP_NODE_MODES.map((mode) => {
209283
- const floor = guardrailPreview(mode, network);
209284
- return {
209285
- name: mode,
209286
- value: mode,
209287
- description: `${NODE_MODE_DESCRIPTIONS[mode]} needs ≥ ${(floor.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${floor.diskFloorGb}GB disk.`
209288
- };
209289
- })
209290
- });
209291
- let against = flags.against;
209292
- let skipBootstrap = !!flags.skipBootstrap;
209293
- if (!skipBootstrap && !against) {
209294
- const choice = await select3({
209295
- message: "Bootstrap from",
209296
- choices: [
209297
- {
209298
- name: "Hosted archive (recommended)",
209299
- value: "hosted",
209300
- description: DEFAULT_ARCHIVE_MANIFEST
209301
- },
209302
- {
209303
- name: "Custom manifest URL or path",
209304
- value: "custom",
209305
- description: "Enter your own"
209306
- },
209307
- {
209308
- name: "Skip the archive",
209309
- value: "skip",
209310
- description: "Index only what your node sends from now on"
209311
- }
209312
- ]
209313
- });
209314
- if (choice === "hosted") {
209315
- against = DEFAULT_ARCHIVE_MANIFEST;
209316
- } else if (choice === "skip") {
209317
- skipBootstrap = true;
209318
- } else {
209319
- against = (await input2({
209320
- message: "Manifest URL or local path",
209321
- default: DEFAULT_ARCHIVE_MANIFEST
209322
- })).trim();
209323
- }
209324
- }
209325
- const config = {
209326
- network,
209327
- nodeMode,
209328
- apiPort: flags.apiPort ?? "127.0.0.1:3800",
209329
- indexerPort: "127.0.0.1:3700",
209330
- postgresPort: "127.0.0.1:5432",
209331
- dir: resolvePath3(flags.dir ?? process.cwd()),
209332
- against: skipBootstrap ? undefined : against,
209333
- skipBootstrap,
209334
- skipVerify: !!flags.skipVerify,
209335
- yes: false,
209336
- force: !!flags.force,
209337
- owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
209338
- imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
209339
- };
209340
- console.error("");
209341
- console.error(` network ${config.network}`);
209342
- console.error(` node mode ${config.nodeMode}`);
209343
- console.error(` dir ${config.dir}`);
209344
- console.error(` api port ${config.apiPort}`);
209345
- console.error(` bootstrap ${config.skipBootstrap ? "skip (from the node's tip)" : config.against}`);
209346
- console.error("");
209347
- const proceed = await confirm4({ message: "Start setup?", default: true });
209348
- if (!proceed)
209349
- throw new SetupCancelledError;
209350
- return config;
209351
- }
209352
-
209353
- // src/commands/setup.ts
209354
- function renderProgress(event) {
209355
- switch (event.type) {
209356
- case "step-start":
209357
- note(`→ ${event.step}`);
209358
- return;
209359
- case "step-log":
209360
- note(` ${event.line}`);
209361
- return;
209362
- case "step-done":
209363
- success(event.step + (event.detail ? `: ${event.detail}` : ""));
209364
- return;
209365
- case "step-skip":
209366
- note(` skipped ${event.step} (${event.reason})`);
209367
- return;
209368
- case "step-error":
209369
- printError(`${event.step} failed: ${event.message}`);
209370
- return;
209371
- }
209372
- }
209373
- async function executeAndRender(config) {
209374
- console.error(bold("Secondlayer setup"));
209375
- console.error(dim(` network=${config.network} node-mode=${config.nodeMode} dir=${config.dir}`));
209376
- console.error("");
209377
- let failed = false;
209378
- const result = await runSetup(config, (event) => {
209379
- if (event.type === "step-error")
209380
- failed = true;
209381
- renderProgress(event);
209382
- });
209383
- if (!result.ok) {
209384
- process.exit(1);
209385
- }
209386
- if (failed) {
209387
- warn("Setup finished with at least one step reporting an error above.");
209388
- }
209389
- console.error("");
209390
- if (result.summary)
209391
- console.log(result.summary);
209392
- process.exit(failed ? 1 : 0);
209393
- }
209394
- async function runNonInteractive(flags) {
209395
- let config;
209396
- try {
209397
- config = resolveNonInteractiveConfig(flags);
209398
- } catch (err) {
209399
- if (err instanceof MissingSetupFlagError) {
209400
- printError(err.message, { hint: `Pass ${err.flag} and re-run.` });
209401
- } else {
209402
- printError(err instanceof Error ? err.message : String(err));
209403
- }
209404
- process.exit(1);
209405
- }
209406
- await executeAndRender(config);
209407
- }
209408
- async function runPromptedInteractive(flags) {
209409
- let config;
209410
- try {
209411
- config = await promptSetupConfig(flags);
209412
- } catch (err) {
209413
- if (err instanceof SetupCancelledError) {
209414
- note(err.message);
209415
- process.exit(0);
209416
- }
209417
- printError(err instanceof Error ? err.message : String(err));
209418
- process.exit(1);
209419
- }
209420
- await executeAndRender(config);
209421
- }
209422
- function registerSetupCommand(program) {
209423
- program.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", ".").option("--against <manifest>", `Archive manifest to bootstrap from (default: ${DEFAULT_ARCHIVE_MANIFEST})`).option("--skip-bootstrap", "Skip the archive restore; index only what your node sends from now on").option("--skip-verify", "Skip the post-bootstrap verify pass").option("--yes", "Non-interactive: skip the prompts, require flags, never ask").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", `
209424
- --network <mainnet|testnet|devnet> is the top-level global flag (see
209425
- \`secondlayer --help\`) — required here too, just parsed one level up.
209426
-
209427
- Examples:
209428
- $ secondlayer setup
209429
- $ secondlayer setup --yes --network mainnet --node-mode external
209430
- $ secondlayer setup --yes --network testnet --node-mode external --skip-bootstrap --skip-verify
209431
-
209432
- Without a TTY (piped, CI, an agent), or with --yes, the prompts are skipped:
209433
- every decision with no safe default (--network, --node-mode) must come from a
209434
- flag or the command fails fast naming it. --against defaults to the official
209435
- archive (${DEFAULT_ARCHIVE_MANIFEST}) unless --skip-bootstrap is set.
209436
- `).action(async (opts) => {
209437
- assertInstanceUrl();
209438
- const flags = {
209439
- ...opts,
209440
- network: opts.network ?? process.env.STACKS_NETWORK
209441
- };
209442
- const interactive = process.stdout.isTTY && !flags.yes;
209443
- if (!interactive) {
209444
- await runNonInteractive(flags);
209445
- return;
209446
- }
209447
- await runPromptedInteractive(flags);
209448
- });
209449
- }
209450
208357
  // src/cli.ts
209451
208358
  init_resolve_auth();
209452
208359
  var { version } = package_default;
@@ -209463,17 +208370,16 @@ program.hook("preAction", (thisCommand) => {
209463
208370
  });
209464
208371
  program.addHelpText("after", `
209465
208372
  Quickstart:
209466
- $ secondlayer setup
209467
208373
  $ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
209468
208374
  $ git add subgraphs/my-watcher.ts
209469
208375
  $ secondlayer subgraphs deploy subgraphs/my-watcher.ts # or --allow-uncommitted
209470
208376
 
209471
- Manual / scripted setup, one step at a time:
208377
+ Self-host, from docker/oss in a checkout:
209472
208378
  $ secondlayer init --network mainnet
208379
+ $ docker compose up -d
209473
208380
  $ secondlayer bootstrap --against <manifest>
209474
208381
  `);
209475
208382
  program.commandsGroup("Getting started:");
209476
- registerSetupCommand(program);
209477
208383
  registerInitCommand(program);
209478
208384
  registerBootstrapCommand(program);
209479
208385
  registerObserverCommand(program);
@@ -209498,11 +208404,10 @@ registerVerifyCommand(program);
209498
208404
  registerRepairCommand(program);
209499
208405
  registerBackupCommand(program);
209500
208406
  registerRestoreCommand(program);
209501
- registerUninstallCommand(program);
209502
208407
  program.commandsGroup("Archive:");
209503
208408
  registerCreditsCommand(program);
209504
208409
  registerArchiveCommand(program);
209505
208410
  program.parse();
209506
208411
 
209507
- //# debugId=E9EB99B555C4CFDB64756E2164756E21
208412
+ //# debugId=C5F366E55CEA893F64756E2164756E21
209508
208413
  //# sourceMappingURL=cli.js.map