@secondlayer/cli 12.2.0 → 13.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/dist/cli.js +2402 -492
- package/dist/cli.js.map +36 -30
- 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
|
|
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
|
|
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
|
|
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.
|
|
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 "$
|
|
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
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29077
|
-
|
|
29078
|
-
|
|
29079
|
-
|
|
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("
|
|
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)").
|
|
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
|
-
|
|
29842
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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("
|
|
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("
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,1484 @@ var init_node_impl = __esm(() => {
|
|
|
34315
34622
|
init_output();
|
|
34316
34623
|
});
|
|
34317
34624
|
|
|
34318
|
-
// src/
|
|
34319
|
-
|
|
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 { createServer } from "node:net";
|
|
35001
|
+
import { join as join19, resolve as resolvePath2 } from "node:path";
|
|
35002
|
+
import { FLOORS, diskFloorGb } from "@secondlayer/shared/runtime";
|
|
35003
|
+
function parseSetupNodeMode(value) {
|
|
35004
|
+
const mode = value.trim().toLowerCase();
|
|
35005
|
+
if (SETUP_NODE_MODES.includes(mode)) {
|
|
35006
|
+
return mode;
|
|
35007
|
+
}
|
|
35008
|
+
throw new Error(`node mode must be external, stacks, or full (got ${value})`);
|
|
35009
|
+
}
|
|
35010
|
+
function resolveNonInteractiveConfig(flags) {
|
|
35011
|
+
if (!flags.network) {
|
|
35012
|
+
throw new MissingSetupFlagError("--network", "Missing --network <mainnet|testnet|devnet>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
35013
|
+
}
|
|
35014
|
+
if (!flags.nodeMode) {
|
|
35015
|
+
throw new MissingSetupFlagError("--node-mode", "Missing --node-mode <external|stacks|full>. Non-interactive setup requires it explicitly — there is no safe default.");
|
|
35016
|
+
}
|
|
35017
|
+
if (!flags.against && !flags.skipBootstrap) {
|
|
35018
|
+
throw new MissingSetupFlagError("--against", "Missing --against <manifest-url>. Pass an archive manifest to bootstrap from, or --skip-bootstrap to sync from genesis instead.");
|
|
35019
|
+
}
|
|
35020
|
+
const network = parseInstanceNetwork(flags.network);
|
|
35021
|
+
const nodeMode = parseSetupNodeMode(flags.nodeMode);
|
|
35022
|
+
const dir = resolvePath2(flags.dir ?? process.cwd());
|
|
35023
|
+
return {
|
|
35024
|
+
network,
|
|
35025
|
+
nodeMode,
|
|
35026
|
+
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
35027
|
+
indexerPort: "127.0.0.1:3700",
|
|
35028
|
+
postgresPort: "127.0.0.1:5432",
|
|
35029
|
+
dir,
|
|
35030
|
+
against: flags.against,
|
|
35031
|
+
skipBootstrap: !!flags.skipBootstrap,
|
|
35032
|
+
skipVerify: !!flags.skipVerify,
|
|
35033
|
+
yes: !!flags.yes,
|
|
35034
|
+
force: !!flags.force,
|
|
35035
|
+
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
35036
|
+
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
35037
|
+
};
|
|
35038
|
+
}
|
|
35039
|
+
function guardrailPreview(nodeMode, network) {
|
|
35040
|
+
return {
|
|
35041
|
+
ramFloorMb: nodeMode === "full" ? FLOORS.fullRamMb : FLOORS.appRamMb,
|
|
35042
|
+
diskFloorGb: diskFloorGb(nodeMode, network)
|
|
35043
|
+
};
|
|
35044
|
+
}
|
|
35045
|
+
function isBunRuntime() {
|
|
35046
|
+
return typeof process !== "undefined" && typeof process.versions === "object" && process.versions !== null && typeof process.versions.bun === "string";
|
|
35047
|
+
}
|
|
35048
|
+
async function checkDocker() {
|
|
35049
|
+
const probe = spawnSync5("docker", ["info"], { stdio: "ignore" });
|
|
35050
|
+
return !probe.error && probe.status === 0;
|
|
35051
|
+
}
|
|
35052
|
+
function isPortFree(host, port) {
|
|
35053
|
+
return new Promise((resolvePromise) => {
|
|
35054
|
+
const srv = createServer();
|
|
35055
|
+
srv.once("error", () => resolvePromise(false));
|
|
35056
|
+
srv.once("listening", () => srv.close(() => resolvePromise(true)));
|
|
35057
|
+
srv.listen(port, host);
|
|
35058
|
+
});
|
|
35059
|
+
}
|
|
35060
|
+
function parsePublishSpec(spec) {
|
|
35061
|
+
const idx = spec.lastIndexOf(":");
|
|
35062
|
+
if (idx === -1)
|
|
35063
|
+
return { host: "127.0.0.1", port: Number(spec) };
|
|
35064
|
+
return { host: spec.slice(0, idx), port: Number(spec.slice(idx + 1)) };
|
|
35065
|
+
}
|
|
35066
|
+
function dockerPublishedHostPorts() {
|
|
35067
|
+
const probe = spawnSync5("docker", ["ps", "--format", "{{.Ports}}"], {
|
|
35068
|
+
encoding: "utf8"
|
|
35069
|
+
});
|
|
35070
|
+
const ports = new Set;
|
|
35071
|
+
if (probe.error || probe.status !== 0 || !probe.stdout)
|
|
35072
|
+
return ports;
|
|
35073
|
+
for (const match of probe.stdout.matchAll(/:(\d+)->/g)) {
|
|
35074
|
+
ports.add(Number(match[1]));
|
|
35075
|
+
}
|
|
35076
|
+
return ports;
|
|
35077
|
+
}
|
|
35078
|
+
async function resolveAvailablePublishSpec(spec, claimedByDocker = new Set, maxAttempts = 20) {
|
|
35079
|
+
const { host, port } = parsePublishSpec(spec);
|
|
35080
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
35081
|
+
const candidate = port + attempt;
|
|
35082
|
+
if (claimedByDocker.has(candidate))
|
|
35083
|
+
continue;
|
|
35084
|
+
if (await isPortFree(host, candidate)) {
|
|
35085
|
+
return { spec: `${host}:${candidate}`, remapped: attempt > 0 };
|
|
35086
|
+
}
|
|
35087
|
+
}
|
|
35088
|
+
return { spec, remapped: false };
|
|
35089
|
+
}
|
|
35090
|
+
function randomHex(bytes) {
|
|
35091
|
+
return randomBytes4(bytes).toString("hex");
|
|
35092
|
+
}
|
|
35093
|
+
function resolveSecrets(config) {
|
|
35094
|
+
const envPath = join19(config.dir, ".env");
|
|
35095
|
+
const existing = config.force || !existsSync11(envPath) ? {} : readEnvFile(config.dir, envPath);
|
|
35096
|
+
const instance = buildInstanceEnv({
|
|
35097
|
+
network: config.network,
|
|
35098
|
+
existing: {
|
|
35099
|
+
INSTANCE_TOKEN: existing.INSTANCE_TOKEN,
|
|
35100
|
+
SECONDLAYER_SECRETS_KEY: existing.SECONDLAYER_SECRETS_KEY,
|
|
35101
|
+
STREAMS_SIGNING_PRIVATE_KEY: existing.STREAMS_SIGNING_PRIVATE_KEY,
|
|
35102
|
+
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: existing.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY,
|
|
35103
|
+
SL_API_URL: existing.SL_API_URL,
|
|
35104
|
+
SL_API_KEY: existing.SL_API_KEY,
|
|
35105
|
+
ARCHIVE_SIGNING_PUBLIC_KEY: existing.ARCHIVE_SIGNING_PUBLIC_KEY
|
|
35106
|
+
},
|
|
35107
|
+
apiUrl: `http://${normalizeLoopbackHost(config.apiPort)}`
|
|
35108
|
+
});
|
|
35109
|
+
return {
|
|
35110
|
+
instance,
|
|
35111
|
+
postgresPassword: existing.POSTGRES_PASSWORD || randomHex(24),
|
|
35112
|
+
bitcoinRpcPassword: existing.BITCOIN_RPC_PASSWORD || randomHex(24)
|
|
35113
|
+
};
|
|
35114
|
+
}
|
|
35115
|
+
function normalizeLoopbackHost(apiPort) {
|
|
35116
|
+
return apiPort.includes(":") ? apiPort : `127.0.0.1:${apiPort}`;
|
|
35117
|
+
}
|
|
35118
|
+
function readEnvFile(dir, envPath) {
|
|
35119
|
+
const shared = loadExistingInstanceEnv(dir, ".env");
|
|
35120
|
+
return {
|
|
35121
|
+
...shared,
|
|
35122
|
+
POSTGRES_PASSWORD: readEnvValue(envPath, "POSTGRES_PASSWORD"),
|
|
35123
|
+
BITCOIN_RPC_PASSWORD: readEnvValue(envPath, "BITCOIN_RPC_PASSWORD")
|
|
35124
|
+
};
|
|
35125
|
+
}
|
|
35126
|
+
function renderSetupEnv(config, secrets) {
|
|
35127
|
+
const i3 = secrets.instance;
|
|
35128
|
+
const lines2 = [
|
|
35129
|
+
"# Generated by `secondlayer setup`. Do not commit.",
|
|
35130
|
+
`NETWORK=${config.network}`,
|
|
35131
|
+
`STACKS_NETWORK=${config.network}`,
|
|
35132
|
+
`NODE_MODE=${config.nodeMode}`,
|
|
35133
|
+
`API_PORT=${config.apiPort}`,
|
|
35134
|
+
`INDEXER_PORT=${config.indexerPort}`,
|
|
35135
|
+
"POSTGRES_USER=secondlayer",
|
|
35136
|
+
`POSTGRES_PASSWORD=${secrets.postgresPassword}`,
|
|
35137
|
+
"POSTGRES_DB=secondlayer",
|
|
35138
|
+
`POSTGRES_PORT=${config.postgresPort}`,
|
|
35139
|
+
"# The instance credential. The API validates bearer tokens against it,",
|
|
35140
|
+
"# and the CLI/SDK/MCP read it first when authenticating.",
|
|
35141
|
+
`INSTANCE_TOKEN=${i3.INSTANCE_TOKEN}`,
|
|
35142
|
+
`SECONDLAYER_SECRETS_KEY=${i3.SECONDLAYER_SECRETS_KEY}`,
|
|
35143
|
+
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(i3.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
35144
|
+
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(i3.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
35145
|
+
`ALLOW_UNSIGNED_WEBHOOKS=${i3.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
35146
|
+
"# Legacy alias of INSTANCE_TOKEN, same value.",
|
|
35147
|
+
`SL_API_KEY=${i3.SL_API_KEY}`,
|
|
35148
|
+
`SL_API_URL=${i3.SL_API_URL}`
|
|
35149
|
+
];
|
|
35150
|
+
if (i3.ARCHIVE_SIGNING_PUBLIC_KEY) {
|
|
35151
|
+
lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(i3.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
|
|
35152
|
+
}
|
|
35153
|
+
if (config.nodeMode === "full") {
|
|
35154
|
+
lines2.push("BITCOIN_RPC_USER=stacks", `BITCOIN_RPC_PASSWORD=${secrets.bitcoinRpcPassword}`);
|
|
35155
|
+
}
|
|
35156
|
+
lines2.push("");
|
|
35157
|
+
return lines2.join(`
|
|
35158
|
+
`);
|
|
35159
|
+
}
|
|
35160
|
+
async function writeSetupFiles(config, secrets) {
|
|
35161
|
+
const composePath = join19(config.dir, "docker-compose.yml");
|
|
35162
|
+
const envPath = join19(config.dir, ".env");
|
|
35163
|
+
await writeFile7(composePath, buildOssCompose({ owner: config.owner, imageTag: config.imageTag }));
|
|
35164
|
+
await writeFile7(envPath, renderSetupEnv(config, secrets), { mode: 384 });
|
|
35165
|
+
const result = { composePath, envPath };
|
|
35166
|
+
if (config.nodeMode === "full") {
|
|
35167
|
+
const configTomlPath = join19(config.dir, "Config.toml");
|
|
35168
|
+
await writeFile7(configTomlPath, buildOssStacksConfigToml({
|
|
35169
|
+
network: config.network,
|
|
35170
|
+
bitcoinRpcPassword: secrets.bitcoinRpcPassword ?? ""
|
|
35171
|
+
}));
|
|
35172
|
+
result.configTomlPath = configTomlPath;
|
|
35173
|
+
}
|
|
35174
|
+
if (config.nodeMode === "full") {
|
|
35175
|
+
const bitcoinConfPath = join19(config.dir, "bitcoin.conf");
|
|
35176
|
+
await writeFile7(bitcoinConfPath, buildOssBitcoinConf({ rpcPassword: secrets.bitcoinRpcPassword ?? "" }));
|
|
35177
|
+
result.bitcoinConfPath = bitcoinConfPath;
|
|
35178
|
+
}
|
|
35179
|
+
return result;
|
|
35180
|
+
}
|
|
35181
|
+
function composeProfileArgs(nodeMode) {
|
|
35182
|
+
if (nodeMode === "full")
|
|
35183
|
+
return ["--profile", "full-node"];
|
|
35184
|
+
return [];
|
|
35185
|
+
}
|
|
35186
|
+
function runStreaming(command, args, opts) {
|
|
35187
|
+
return new Promise((resolvePromise, reject) => {
|
|
35188
|
+
const child = spawn4(command, args, {
|
|
35189
|
+
cwd: opts.cwd,
|
|
35190
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
35191
|
+
});
|
|
35192
|
+
let stdout = "";
|
|
35193
|
+
let stderr = "";
|
|
35194
|
+
const pump = (stream2) => (chunk) => {
|
|
35195
|
+
const text2 = chunk.toString("utf8");
|
|
35196
|
+
if (stream2 === "stdout")
|
|
35197
|
+
stdout += text2;
|
|
35198
|
+
else
|
|
35199
|
+
stderr += text2;
|
|
35200
|
+
if (opts.onLine) {
|
|
35201
|
+
for (const line of text2.split(`
|
|
35202
|
+
`)) {
|
|
35203
|
+
if (line.length > 0)
|
|
35204
|
+
opts.onLine(line, stream2);
|
|
35205
|
+
}
|
|
35206
|
+
}
|
|
35207
|
+
};
|
|
35208
|
+
child.stdout?.on("data", pump("stdout"));
|
|
35209
|
+
child.stderr?.on("data", pump("stderr"));
|
|
35210
|
+
child.once("error", reject);
|
|
35211
|
+
child.once("close", (code) => {
|
|
35212
|
+
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
35213
|
+
});
|
|
35214
|
+
});
|
|
35215
|
+
}
|
|
35216
|
+
async function dockerComposeUp(config, files, onLine) {
|
|
35217
|
+
const args = [
|
|
35218
|
+
"compose",
|
|
35219
|
+
"-f",
|
|
35220
|
+
files.composePath,
|
|
35221
|
+
"--env-file",
|
|
35222
|
+
files.envPath,
|
|
35223
|
+
...composeProfileArgs(config.nodeMode),
|
|
35224
|
+
"up",
|
|
35225
|
+
"-d"
|
|
35226
|
+
];
|
|
35227
|
+
return runStreaming("docker", args, { cwd: config.dir, onLine });
|
|
35228
|
+
}
|
|
35229
|
+
async function pollHealth(config, opts = {}) {
|
|
35230
|
+
const timeoutMs = opts.timeoutMs ?? 120000;
|
|
35231
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
35232
|
+
const url = `http://${normalizeLoopbackHost(config.apiPort)}/health`;
|
|
35233
|
+
const deadline = Date.now() + timeoutMs;
|
|
35234
|
+
for (;; ) {
|
|
35235
|
+
try {
|
|
35236
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
35237
|
+
if (res.ok)
|
|
35238
|
+
return true;
|
|
35239
|
+
} catch {}
|
|
35240
|
+
opts.onTick?.();
|
|
35241
|
+
if (Date.now() >= deadline)
|
|
35242
|
+
return false;
|
|
35243
|
+
await new Promise((r2) => setTimeout(r2, intervalMs));
|
|
35244
|
+
}
|
|
35245
|
+
}
|
|
35246
|
+
function getObserverInfo(config) {
|
|
35247
|
+
if (config.nodeMode !== "external") {
|
|
35248
|
+
return { applicable: false };
|
|
35249
|
+
}
|
|
35250
|
+
const endpoint = defaultObserverEndpoint(config.network);
|
|
35251
|
+
const stanza = renderObserverStanza({
|
|
35252
|
+
mode: "indexer",
|
|
35253
|
+
endpoint,
|
|
35254
|
+
network: config.network
|
|
35255
|
+
});
|
|
35256
|
+
return { applicable: true, stanza, endpoint };
|
|
35257
|
+
}
|
|
35258
|
+
function cliEntry() {
|
|
35259
|
+
const entry = process.argv[1];
|
|
35260
|
+
if (!entry) {
|
|
35261
|
+
throw new Error("Could not resolve the secondlayer CLI entry to shell out to.");
|
|
35262
|
+
}
|
|
35263
|
+
return [process.execPath, entry];
|
|
35264
|
+
}
|
|
35265
|
+
async function runBootstrap(config, against, onLine) {
|
|
35266
|
+
const [command, ...prefix] = cliEntry();
|
|
35267
|
+
const args = [...prefix, "bootstrap", "--against", against, "--yes"];
|
|
35268
|
+
return runStreaming(command, args, { cwd: config.dir, onLine });
|
|
35269
|
+
}
|
|
35270
|
+
async function runVerify(config, against, onLine) {
|
|
35271
|
+
const [command, ...prefix] = cliEntry();
|
|
35272
|
+
const args = [...prefix, "verify", "all", "--against", against];
|
|
35273
|
+
return runStreaming(command, args, { cwd: config.dir, onLine });
|
|
35274
|
+
}
|
|
35275
|
+
function buildSuccessSummary(config, secrets) {
|
|
35276
|
+
const apiUrl = `http://${normalizeLoopbackHost(config.apiPort)}`;
|
|
35277
|
+
const lines2 = [
|
|
35278
|
+
"Secondlayer is up.",
|
|
35279
|
+
"",
|
|
35280
|
+
` api ${apiUrl}`,
|
|
35281
|
+
` network ${config.network}`,
|
|
35282
|
+
` node mode ${config.nodeMode}`,
|
|
35283
|
+
` dir ${config.dir}`,
|
|
35284
|
+
"",
|
|
35285
|
+
"Export the instance token for writes (reads on loopback need no credential):",
|
|
35286
|
+
` export INSTANCE_TOKEN=${secrets.instance.INSTANCE_TOKEN}`,
|
|
35287
|
+
"",
|
|
35288
|
+
"Try it:",
|
|
35289
|
+
" secondlayer subgraphs create my-balances --from-contract SP....my-contract",
|
|
35290
|
+
" secondlayer subgraphs deploy subgraphs/my-balances.ts",
|
|
35291
|
+
` curl ${apiUrl}/v1/subgraphs/my-balances/balances`,
|
|
35292
|
+
"",
|
|
35293
|
+
"Docs: https://secondlayer.tools/docs/self-host"
|
|
35294
|
+
];
|
|
35295
|
+
return lines2.join(`
|
|
35296
|
+
`);
|
|
35297
|
+
}
|
|
35298
|
+
async function runSetup(initialConfig, emit2) {
|
|
35299
|
+
let config = initialConfig;
|
|
35300
|
+
emit2({ type: "step-start", step: "preflight" });
|
|
35301
|
+
if (!await checkDocker()) {
|
|
35302
|
+
emit2({
|
|
35303
|
+
type: "step-error",
|
|
35304
|
+
step: "preflight",
|
|
35305
|
+
message: "Docker isn't reachable. Start Docker Desktop/OrbStack, or install Docker, then re-run."
|
|
35306
|
+
});
|
|
35307
|
+
return { ok: false, config };
|
|
35308
|
+
}
|
|
35309
|
+
const claimedByDocker = dockerPublishedHostPorts();
|
|
35310
|
+
const ports = {
|
|
35311
|
+
api: await resolveAvailablePublishSpec(config.apiPort, claimedByDocker),
|
|
35312
|
+
indexer: await resolveAvailablePublishSpec(config.indexerPort, claimedByDocker),
|
|
35313
|
+
postgres: await resolveAvailablePublishSpec(config.postgresPort, claimedByDocker)
|
|
35314
|
+
};
|
|
35315
|
+
config = {
|
|
35316
|
+
...config,
|
|
35317
|
+
apiPort: ports.api.spec,
|
|
35318
|
+
indexerPort: ports.indexer.spec,
|
|
35319
|
+
postgresPort: ports.postgres.spec
|
|
35320
|
+
};
|
|
35321
|
+
for (const [label, result] of [
|
|
35322
|
+
["api", ports.api],
|
|
35323
|
+
["indexer", ports.indexer],
|
|
35324
|
+
["postgres", ports.postgres]
|
|
35325
|
+
]) {
|
|
35326
|
+
if (result.remapped) {
|
|
35327
|
+
emit2({
|
|
35328
|
+
type: "step-log",
|
|
35329
|
+
step: "preflight",
|
|
35330
|
+
line: `${label} port was already in use locally, using ${result.spec} instead`
|
|
35331
|
+
});
|
|
35332
|
+
}
|
|
35333
|
+
}
|
|
35334
|
+
const floors = guardrailPreview(config.nodeMode, config.network);
|
|
35335
|
+
emit2({
|
|
35336
|
+
type: "step-done",
|
|
35337
|
+
step: "preflight",
|
|
35338
|
+
detail: `NODE_MODE=${config.nodeMode} on ${config.network} needs at least ${floors.ramFloorMb}MB RAM and ${floors.diskFloorGb}GB disk.`
|
|
35339
|
+
});
|
|
35340
|
+
emit2({ type: "step-start", step: "secrets" });
|
|
35341
|
+
const secrets = resolveSecrets({
|
|
35342
|
+
dir: config.dir,
|
|
35343
|
+
network: config.network,
|
|
35344
|
+
apiPort: config.apiPort,
|
|
35345
|
+
force: config.force
|
|
35346
|
+
});
|
|
35347
|
+
emit2({ type: "step-done", step: "secrets" });
|
|
35348
|
+
emit2({ type: "step-start", step: "config" });
|
|
35349
|
+
const files = await writeSetupFiles(config, secrets);
|
|
35350
|
+
emit2({
|
|
35351
|
+
type: "step-done",
|
|
35352
|
+
step: "config",
|
|
35353
|
+
detail: `${files.composePath}, ${files.envPath}`
|
|
35354
|
+
});
|
|
35355
|
+
emit2({ type: "step-start", step: "docker-up" });
|
|
35356
|
+
const up = await dockerComposeUp(config, files, (line) => emit2({ type: "step-log", step: "docker-up", line }));
|
|
35357
|
+
if (up.code !== 0) {
|
|
35358
|
+
emit2({
|
|
35359
|
+
type: "step-error",
|
|
35360
|
+
step: "docker-up",
|
|
35361
|
+
message: `docker compose up failed (exit ${up.code}).
|
|
35362
|
+
${up.stderr || up.stdout}`
|
|
35363
|
+
});
|
|
35364
|
+
return { ok: false, config, secrets, files };
|
|
35365
|
+
}
|
|
35366
|
+
const healthy = await pollHealth(config, {
|
|
35367
|
+
onTick: () => emit2({
|
|
35368
|
+
type: "step-log",
|
|
35369
|
+
step: "docker-up",
|
|
35370
|
+
line: "waiting for /health…"
|
|
35371
|
+
})
|
|
35372
|
+
});
|
|
35373
|
+
if (!healthy) {
|
|
35374
|
+
emit2({
|
|
35375
|
+
type: "step-error",
|
|
35376
|
+
step: "docker-up",
|
|
35377
|
+
message: "Containers started but /health never came up. Check `docker compose logs`."
|
|
35378
|
+
});
|
|
35379
|
+
return { ok: false, config, secrets, files };
|
|
35380
|
+
}
|
|
35381
|
+
emit2({ type: "step-done", step: "docker-up" });
|
|
35382
|
+
emit2({ type: "step-start", step: "observer" });
|
|
35383
|
+
const observer = getObserverInfo(config);
|
|
35384
|
+
if (observer.applicable && observer.stanza) {
|
|
35385
|
+
emit2({
|
|
35386
|
+
type: "step-log",
|
|
35387
|
+
step: "observer",
|
|
35388
|
+
line: "ACTION REQUIRED — paste this into your Stacks node's Config.toml:"
|
|
35389
|
+
});
|
|
35390
|
+
for (const line of observer.stanza.split(`
|
|
35391
|
+
`)) {
|
|
35392
|
+
if (line.length > 0)
|
|
35393
|
+
emit2({ type: "step-log", step: "observer", line });
|
|
35394
|
+
}
|
|
35395
|
+
emit2({
|
|
35396
|
+
type: "step-done",
|
|
35397
|
+
step: "observer",
|
|
35398
|
+
detail: `endpoint ${observer.endpoint} — see the ACTION REQUIRED lines above`
|
|
35399
|
+
});
|
|
35400
|
+
} else {
|
|
35401
|
+
emit2({
|
|
35402
|
+
type: "step-done",
|
|
35403
|
+
step: "observer",
|
|
35404
|
+
detail: "bundled node — the observer is pre-configured, nothing to paste"
|
|
35405
|
+
});
|
|
35406
|
+
}
|
|
35407
|
+
let bootstrapped = false;
|
|
35408
|
+
if (config.skipBootstrap) {
|
|
35409
|
+
emit2({ type: "step-skip", step: "bootstrap", reason: "--skip-bootstrap" });
|
|
35410
|
+
} else if (config.against) {
|
|
35411
|
+
emit2({ type: "step-start", step: "bootstrap" });
|
|
35412
|
+
const res = await runBootstrap(config, config.against, (line) => emit2({ type: "step-log", step: "bootstrap", line }));
|
|
35413
|
+
if (res.code === 0) {
|
|
35414
|
+
bootstrapped = true;
|
|
35415
|
+
emit2({ type: "step-done", step: "bootstrap" });
|
|
35416
|
+
} else {
|
|
35417
|
+
emit2({
|
|
35418
|
+
type: "step-error",
|
|
35419
|
+
step: "bootstrap",
|
|
35420
|
+
message: `secondlayer bootstrap exited ${res.code}.`
|
|
35421
|
+
});
|
|
35422
|
+
}
|
|
35423
|
+
}
|
|
35424
|
+
if (config.skipVerify) {
|
|
35425
|
+
emit2({ type: "step-skip", step: "verify", reason: "--skip-verify" });
|
|
35426
|
+
} else if (!config.against) {
|
|
35427
|
+
emit2({
|
|
35428
|
+
type: "step-skip",
|
|
35429
|
+
step: "verify",
|
|
35430
|
+
reason: "no manifest to verify against"
|
|
35431
|
+
});
|
|
35432
|
+
} else if (!bootstrapped) {
|
|
35433
|
+
emit2({
|
|
35434
|
+
type: "step-skip",
|
|
35435
|
+
step: "verify",
|
|
35436
|
+
reason: "bootstrap did not run — nothing restored yet to verify"
|
|
35437
|
+
});
|
|
35438
|
+
} else {
|
|
35439
|
+
emit2({ type: "step-start", step: "verify" });
|
|
35440
|
+
const res = await runVerify(config, config.against, (line) => emit2({ type: "step-log", step: "verify", line }));
|
|
35441
|
+
if (res.code === 0) {
|
|
35442
|
+
emit2({ type: "step-done", step: "verify" });
|
|
35443
|
+
} else {
|
|
35444
|
+
emit2({
|
|
35445
|
+
type: "step-error",
|
|
35446
|
+
step: "verify",
|
|
35447
|
+
message: `secondlayer verify exited ${res.code}.`
|
|
35448
|
+
});
|
|
35449
|
+
}
|
|
35450
|
+
}
|
|
35451
|
+
const summary = buildSuccessSummary(config, secrets);
|
|
35452
|
+
emit2({ type: "step-start", step: "summary" });
|
|
35453
|
+
emit2({ type: "step-done", step: "summary", detail: summary });
|
|
35454
|
+
return { ok: true, config, secrets, files, observer, summary };
|
|
35455
|
+
}
|
|
35456
|
+
var SETUP_NODE_MODES, DEFAULT_ARCHIVE_MANIFEST = "https://archive.secondlayer.tools/latest.json", MissingSetupFlagError;
|
|
35457
|
+
var init_setup_wizard = __esm(() => {
|
|
35458
|
+
init_instance_init();
|
|
35459
|
+
init_observer_stanza();
|
|
35460
|
+
init_oss_compose();
|
|
35461
|
+
SETUP_NODE_MODES = ["external", "stacks", "full"];
|
|
35462
|
+
MissingSetupFlagError = class MissingSetupFlagError extends Error {
|
|
35463
|
+
flag;
|
|
35464
|
+
constructor(flag, message) {
|
|
35465
|
+
super(message);
|
|
35466
|
+
this.name = "MissingSetupFlagError";
|
|
35467
|
+
this.flag = flag;
|
|
35468
|
+
}
|
|
35469
|
+
};
|
|
35470
|
+
});
|
|
35471
|
+
|
|
35472
|
+
// src/lib/setup-tui.tsx
|
|
35473
|
+
var exports_setup_tui = {};
|
|
35474
|
+
__export(exports_setup_tui, {
|
|
35475
|
+
runSetupTui: () => runSetupTui
|
|
35476
|
+
});
|
|
35477
|
+
import {
|
|
35478
|
+
CliRenderEvents,
|
|
35479
|
+
createCliRenderer
|
|
35480
|
+
} from "@opentui/core";
|
|
35481
|
+
import { createRoot, useKeyboard, useRenderer } from "@opentui/react";
|
|
35482
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
35483
|
+
import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
|
|
35484
|
+
function useTerminalThemeMode() {
|
|
35485
|
+
const renderer = useRenderer();
|
|
35486
|
+
const [mode, setMode] = useState(renderer.themeMode ?? "dark");
|
|
35487
|
+
useEffect(() => {
|
|
35488
|
+
let cancelled = false;
|
|
35489
|
+
renderer.waitForThemeMode(500).then((resolved) => {
|
|
35490
|
+
if (!cancelled && resolved)
|
|
35491
|
+
setMode(resolved);
|
|
35492
|
+
});
|
|
35493
|
+
const onThemeMode = (next) => setMode(next);
|
|
35494
|
+
renderer.on(CliRenderEvents.THEME_MODE, onThemeMode);
|
|
35495
|
+
return () => {
|
|
35496
|
+
cancelled = true;
|
|
35497
|
+
renderer.off(CliRenderEvents.THEME_MODE, onThemeMode);
|
|
35498
|
+
};
|
|
35499
|
+
}, [renderer]);
|
|
35500
|
+
return mode;
|
|
35501
|
+
}
|
|
35502
|
+
function formatFloor(f2) {
|
|
35503
|
+
return `needs ≥ ${(f2.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${f2.diskFloorGb}GB disk`;
|
|
35504
|
+
}
|
|
35505
|
+
function useSpinner(active) {
|
|
35506
|
+
const [frame, setFrame] = useState(0);
|
|
35507
|
+
useEffect(() => {
|
|
35508
|
+
if (!active)
|
|
35509
|
+
return;
|
|
35510
|
+
const id = setInterval(() => setFrame((f2) => (f2 + 1) % SPINNER_FRAMES.length), 80);
|
|
35511
|
+
return () => clearInterval(id);
|
|
35512
|
+
}, [active]);
|
|
35513
|
+
return SPINNER_FRAMES[frame];
|
|
35514
|
+
}
|
|
35515
|
+
function App({ flags }) {
|
|
35516
|
+
const renderer = useRenderer();
|
|
35517
|
+
const themeMode = useTerminalThemeMode();
|
|
35518
|
+
const colors2 = themeMode === "light" ? LIGHT_COLORS : DARK_COLORS;
|
|
35519
|
+
const [stage, setStage] = useState("welcome");
|
|
35520
|
+
const [network, setNetwork] = useState(safeParseNetwork(flags.network));
|
|
35521
|
+
const [nodeMode, setNodeMode] = useState(safeParseNodeMode(flags.nodeMode));
|
|
35522
|
+
const [preview, setPreview] = useState(guardrailPreview(NODE_MODE_OPTIONS[0].value, "mainnet"));
|
|
35523
|
+
const [against, setAgainst] = useState(flags.against);
|
|
35524
|
+
const [skipBootstrap, setSkipBootstrap] = useState(!!flags.skipBootstrap);
|
|
35525
|
+
const [events, setEvents] = useState([]);
|
|
35526
|
+
const [log, setLog] = useState([]);
|
|
35527
|
+
const [fatal, setFatal] = useState(null);
|
|
35528
|
+
const [summary, setSummary] = useState(null);
|
|
35529
|
+
const started = useRef(false);
|
|
35530
|
+
function exit(code) {
|
|
35531
|
+
renderer.destroy();
|
|
35532
|
+
process.exit(code);
|
|
35533
|
+
}
|
|
35534
|
+
useKeyboard((key) => {
|
|
35535
|
+
if (key.eventType === "release")
|
|
35536
|
+
return;
|
|
35537
|
+
if ((stage === "done" || stage === "error") && (key.name === "q" || key.name === "return")) {
|
|
35538
|
+
exit(fatal ? 1 : 0);
|
|
35539
|
+
return;
|
|
35540
|
+
}
|
|
35541
|
+
if (stage === "welcome" && (key.name === "return" || key.name === "space")) {
|
|
35542
|
+
advanceFromWelcome();
|
|
35543
|
+
return;
|
|
35544
|
+
}
|
|
35545
|
+
if (stage === "welcome" && key.name === "q") {
|
|
35546
|
+
exit(0);
|
|
35547
|
+
}
|
|
35548
|
+
});
|
|
35549
|
+
function advanceFromWelcome() {
|
|
35550
|
+
if (!network) {
|
|
35551
|
+
setStage("network");
|
|
35552
|
+
} else if (!nodeMode) {
|
|
35553
|
+
setStage("nodeMode");
|
|
35554
|
+
} else if (!against && !skipBootstrap) {
|
|
35555
|
+
setStage("manifestChoice");
|
|
35556
|
+
} else {
|
|
35557
|
+
setStage("confirm");
|
|
35558
|
+
}
|
|
35559
|
+
}
|
|
35560
|
+
function afterNetwork(net3) {
|
|
35561
|
+
setNetwork(net3);
|
|
35562
|
+
if (!nodeMode)
|
|
35563
|
+
setStage("nodeMode");
|
|
35564
|
+
else if (!against && !skipBootstrap)
|
|
35565
|
+
setStage("manifestChoice");
|
|
35566
|
+
else
|
|
35567
|
+
setStage("confirm");
|
|
35568
|
+
}
|
|
35569
|
+
function afterNodeMode(mode) {
|
|
35570
|
+
setNodeMode(mode);
|
|
35571
|
+
if (!against && !skipBootstrap)
|
|
35572
|
+
setStage("manifestChoice");
|
|
35573
|
+
else
|
|
35574
|
+
setStage("confirm");
|
|
35575
|
+
}
|
|
35576
|
+
const config = network && nodeMode ? {
|
|
35577
|
+
network,
|
|
35578
|
+
nodeMode,
|
|
35579
|
+
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
35580
|
+
indexerPort: "127.0.0.1:3700",
|
|
35581
|
+
postgresPort: "127.0.0.1:5432",
|
|
35582
|
+
dir: flags.dir ?? process.cwd(),
|
|
35583
|
+
against: skipBootstrap ? undefined : against,
|
|
35584
|
+
skipBootstrap,
|
|
35585
|
+
skipVerify: !!flags.skipVerify,
|
|
35586
|
+
yes: false,
|
|
35587
|
+
force: !!flags.force,
|
|
35588
|
+
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
35589
|
+
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
35590
|
+
} : null;
|
|
35591
|
+
useEffect(() => {
|
|
35592
|
+
if (stage !== "running" || started.current || !config)
|
|
35593
|
+
return;
|
|
35594
|
+
started.current = true;
|
|
35595
|
+
let failed = false;
|
|
35596
|
+
runSetup(config, (event) => {
|
|
35597
|
+
setEvents((prev) => [...prev, event]);
|
|
35598
|
+
if (event.type === "step-log") {
|
|
35599
|
+
setLog((prev) => [...prev.slice(-40), event.line]);
|
|
35600
|
+
}
|
|
35601
|
+
if (event.type === "step-error") {
|
|
35602
|
+
failed = true;
|
|
35603
|
+
setLog((prev) => [...prev.slice(-40), `ERROR: ${event.message}`]);
|
|
35604
|
+
}
|
|
35605
|
+
}).then((result) => {
|
|
35606
|
+
if (!result.ok) {
|
|
35607
|
+
setFatal("Setup could not finish — see the log above.");
|
|
35608
|
+
setStage("error");
|
|
35609
|
+
return;
|
|
35610
|
+
}
|
|
35611
|
+
setSummary(result.summary ?? null);
|
|
35612
|
+
setStage(failed ? "error" : "done");
|
|
35613
|
+
if (failed)
|
|
35614
|
+
setFatal("Setup finished but at least one step reported an error.");
|
|
35615
|
+
}).catch((err) => {
|
|
35616
|
+
setFatal(err instanceof Error ? err.message : String(err));
|
|
35617
|
+
setStage("error");
|
|
35618
|
+
});
|
|
35619
|
+
}, [stage, config]);
|
|
35620
|
+
return /* @__PURE__ */ jsxDEV(ColorsContext.Provider, {
|
|
35621
|
+
value: colors2,
|
|
35622
|
+
children: /* @__PURE__ */ jsxDEV("box", {
|
|
35623
|
+
style: {
|
|
35624
|
+
border: true,
|
|
35625
|
+
borderStyle: "single",
|
|
35626
|
+
borderColor: colors2.borderIdle,
|
|
35627
|
+
backgroundColor: colors2.bg,
|
|
35628
|
+
padding: 1
|
|
35629
|
+
},
|
|
35630
|
+
title: "secondlayer setup",
|
|
35631
|
+
children: /* @__PURE__ */ jsxDEV("box", {
|
|
35632
|
+
style: { flexDirection: "column" },
|
|
35633
|
+
children: [
|
|
35634
|
+
stage === "welcome" && /* @__PURE__ */ jsxDEV(Welcome, {}, undefined, false, undefined, this),
|
|
35635
|
+
stage === "network" && /* @__PURE__ */ jsxDEV(Picker, {
|
|
35636
|
+
title: "Network",
|
|
35637
|
+
options: NETWORK_OPTIONS,
|
|
35638
|
+
onSelect: (value) => afterNetwork(parseInstanceNetwork(value))
|
|
35639
|
+
}, undefined, false, undefined, this),
|
|
35640
|
+
stage === "nodeMode" && /* @__PURE__ */ jsxDEV(Picker, {
|
|
35641
|
+
title: "Node mode",
|
|
35642
|
+
options: NODE_MODE_OPTIONS,
|
|
35643
|
+
subtitle: formatFloor(preview),
|
|
35644
|
+
onChange: (value) => setPreview(guardrailPreview(parseSetupNodeMode(value), network ?? "mainnet")),
|
|
35645
|
+
onSelect: (value) => afterNodeMode(parseSetupNodeMode(value))
|
|
35646
|
+
}, undefined, false, undefined, this),
|
|
35647
|
+
stage === "manifestChoice" && /* @__PURE__ */ jsxDEV(Picker, {
|
|
35648
|
+
title: "Bootstrap from",
|
|
35649
|
+
options: [
|
|
35650
|
+
{
|
|
35651
|
+
name: "Hosted archive (recommended)",
|
|
35652
|
+
description: DEFAULT_ARCHIVE_MANIFEST,
|
|
35653
|
+
value: "hosted"
|
|
35654
|
+
},
|
|
35655
|
+
{
|
|
35656
|
+
name: "Custom manifest URL or path",
|
|
35657
|
+
description: "Enter your own",
|
|
35658
|
+
value: "custom"
|
|
35659
|
+
},
|
|
35660
|
+
{
|
|
35661
|
+
name: "Sync from genesis",
|
|
35662
|
+
description: "Skip bootstrap — slower, no download",
|
|
35663
|
+
value: "skip"
|
|
35664
|
+
}
|
|
35665
|
+
],
|
|
35666
|
+
onSelect: (value) => {
|
|
35667
|
+
if (value === "hosted") {
|
|
35668
|
+
setAgainst(DEFAULT_ARCHIVE_MANIFEST);
|
|
35669
|
+
setStage("confirm");
|
|
35670
|
+
} else if (value === "skip") {
|
|
35671
|
+
setSkipBootstrap(true);
|
|
35672
|
+
setStage("confirm");
|
|
35673
|
+
} else {
|
|
35674
|
+
setStage("manifestInput");
|
|
35675
|
+
}
|
|
35676
|
+
}
|
|
35677
|
+
}, undefined, false, undefined, this),
|
|
35678
|
+
stage === "manifestInput" && /* @__PURE__ */ jsxDEV(ManifestInput, {
|
|
35679
|
+
onSubmit: (value) => {
|
|
35680
|
+
setAgainst(value.trim() || DEFAULT_ARCHIVE_MANIFEST);
|
|
35681
|
+
setStage("confirm");
|
|
35682
|
+
}
|
|
35683
|
+
}, undefined, false, undefined, this),
|
|
35684
|
+
stage === "confirm" && config && /* @__PURE__ */ jsxDEV(Confirm, {
|
|
35685
|
+
config,
|
|
35686
|
+
onConfirm: () => setStage("running")
|
|
35687
|
+
}, undefined, false, undefined, this),
|
|
35688
|
+
stage === "running" && /* @__PURE__ */ jsxDEV(RunningView, {
|
|
35689
|
+
events,
|
|
35690
|
+
log
|
|
35691
|
+
}, undefined, false, undefined, this),
|
|
35692
|
+
stage === "done" && summary && /* @__PURE__ */ jsxDEV(Done, {
|
|
35693
|
+
summary
|
|
35694
|
+
}, undefined, false, undefined, this),
|
|
35695
|
+
stage === "error" && /* @__PURE__ */ jsxDEV(ErrorView, {
|
|
35696
|
+
message: fatal ?? "Setup failed.",
|
|
35697
|
+
log
|
|
35698
|
+
}, undefined, false, undefined, this)
|
|
35699
|
+
]
|
|
35700
|
+
}, undefined, true, undefined, this)
|
|
35701
|
+
}, undefined, false, undefined, this)
|
|
35702
|
+
}, undefined, false, undefined, this);
|
|
35703
|
+
}
|
|
35704
|
+
function safeParseNetwork(value) {
|
|
35705
|
+
if (!value)
|
|
35706
|
+
return;
|
|
35707
|
+
try {
|
|
35708
|
+
return parseInstanceNetwork(value);
|
|
35709
|
+
} catch {
|
|
35710
|
+
return;
|
|
35711
|
+
}
|
|
35712
|
+
}
|
|
35713
|
+
function safeParseNodeMode(value) {
|
|
35714
|
+
if (!value)
|
|
35715
|
+
return;
|
|
35716
|
+
try {
|
|
35717
|
+
return parseSetupNodeMode(value);
|
|
35718
|
+
} catch {
|
|
35719
|
+
return;
|
|
35720
|
+
}
|
|
35721
|
+
}
|
|
35722
|
+
function Welcome() {
|
|
35723
|
+
const colors2 = useContext(ColorsContext);
|
|
35724
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35725
|
+
style: { flexDirection: "column" },
|
|
35726
|
+
children: [
|
|
35727
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35728
|
+
fg: colors2.bright,
|
|
35729
|
+
children: "Guided self-host setup."
|
|
35730
|
+
}, undefined, false, undefined, this),
|
|
35731
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35732
|
+
fg: colors2.muted,
|
|
35733
|
+
children: [
|
|
35734
|
+
`
|
|
35735
|
+
`,
|
|
35736
|
+
"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.",
|
|
35737
|
+
`
|
|
35738
|
+
|
|
35739
|
+
`,
|
|
35740
|
+
"Press Enter to begin, or q to quit."
|
|
35741
|
+
]
|
|
35742
|
+
}, undefined, true, undefined, this)
|
|
35743
|
+
]
|
|
35744
|
+
}, undefined, true, undefined, this);
|
|
35745
|
+
}
|
|
35746
|
+
function Picker({
|
|
35747
|
+
title,
|
|
35748
|
+
subtitle,
|
|
35749
|
+
options: options3,
|
|
35750
|
+
onChange,
|
|
35751
|
+
onSelect
|
|
35752
|
+
}) {
|
|
35753
|
+
const colors2 = useContext(ColorsContext);
|
|
35754
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35755
|
+
style: { flexDirection: "column" },
|
|
35756
|
+
children: [
|
|
35757
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35758
|
+
fg: colors2.bright,
|
|
35759
|
+
children: title
|
|
35760
|
+
}, undefined, false, undefined, this),
|
|
35761
|
+
subtitle && /* @__PURE__ */ jsxDEV("text", {
|
|
35762
|
+
fg: colors2.accent,
|
|
35763
|
+
children: subtitle
|
|
35764
|
+
}, undefined, false, undefined, this),
|
|
35765
|
+
/* @__PURE__ */ jsxDEV("select", {
|
|
35766
|
+
style: { height: options3.length * 2 + 1 },
|
|
35767
|
+
options: options3,
|
|
35768
|
+
focused: true,
|
|
35769
|
+
showDescription: true,
|
|
35770
|
+
backgroundColor: "transparent",
|
|
35771
|
+
textColor: colors2.fg,
|
|
35772
|
+
focusedBackgroundColor: colors2.raise,
|
|
35773
|
+
focusedTextColor: colors2.bright,
|
|
35774
|
+
selectedBackgroundColor: colors2.raise,
|
|
35775
|
+
selectedTextColor: colors2.accent,
|
|
35776
|
+
descriptionColor: colors2.muted,
|
|
35777
|
+
selectedDescriptionColor: colors2.fg,
|
|
35778
|
+
onChange: (_index, option) => {
|
|
35779
|
+
if (option)
|
|
35780
|
+
onChange?.(String(option.value));
|
|
35781
|
+
},
|
|
35782
|
+
onSelect: (_index, option) => {
|
|
35783
|
+
if (option)
|
|
35784
|
+
onSelect(String(option.value));
|
|
35785
|
+
}
|
|
35786
|
+
}, undefined, false, undefined, this),
|
|
35787
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35788
|
+
fg: colors2.muted,
|
|
35789
|
+
children: [
|
|
35790
|
+
`
|
|
35791
|
+
`,
|
|
35792
|
+
"↑↓ to move, enter to choose"
|
|
35793
|
+
]
|
|
35794
|
+
}, undefined, true, undefined, this)
|
|
35795
|
+
]
|
|
35796
|
+
}, undefined, true, undefined, this);
|
|
35797
|
+
}
|
|
35798
|
+
function ManifestInput({ onSubmit }) {
|
|
35799
|
+
const colors2 = useContext(ColorsContext);
|
|
35800
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35801
|
+
style: { flexDirection: "column" },
|
|
35802
|
+
children: [
|
|
35803
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35804
|
+
fg: colors2.bright,
|
|
35805
|
+
children: "Manifest URL or local path"
|
|
35806
|
+
}, undefined, false, undefined, this),
|
|
35807
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35808
|
+
fg: colors2.muted,
|
|
35809
|
+
children: [
|
|
35810
|
+
"Suggested: ",
|
|
35811
|
+
DEFAULT_ARCHIVE_MANIFEST
|
|
35812
|
+
]
|
|
35813
|
+
}, undefined, true, undefined, this),
|
|
35814
|
+
/* @__PURE__ */ jsxDEV("input", {
|
|
35815
|
+
focused: true,
|
|
35816
|
+
placeholder: DEFAULT_ARCHIVE_MANIFEST,
|
|
35817
|
+
textColor: colors2.fg,
|
|
35818
|
+
focusedTextColor: colors2.accent,
|
|
35819
|
+
placeholderColor: colors2.muted,
|
|
35820
|
+
onSubmit: (value) => onSubmit(String(value ?? ""))
|
|
35821
|
+
}, undefined, false, undefined, this),
|
|
35822
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35823
|
+
fg: colors2.muted,
|
|
35824
|
+
children: [
|
|
35825
|
+
`
|
|
35826
|
+
`,
|
|
35827
|
+
"enter to accept (blank = suggested default)"
|
|
35828
|
+
]
|
|
35829
|
+
}, undefined, true, undefined, this)
|
|
35830
|
+
]
|
|
35831
|
+
}, undefined, true, undefined, this);
|
|
35832
|
+
}
|
|
35833
|
+
function Confirm({
|
|
35834
|
+
config,
|
|
35835
|
+
onConfirm
|
|
35836
|
+
}) {
|
|
35837
|
+
const colors2 = useContext(ColorsContext);
|
|
35838
|
+
useKeyboard((key) => {
|
|
35839
|
+
if (key.eventType === "release")
|
|
35840
|
+
return;
|
|
35841
|
+
if (key.name === "return")
|
|
35842
|
+
onConfirm();
|
|
35843
|
+
});
|
|
35844
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35845
|
+
style: { flexDirection: "column" },
|
|
35846
|
+
children: [
|
|
35847
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35848
|
+
fg: colors2.bright,
|
|
35849
|
+
children: "Ready"
|
|
35850
|
+
}, undefined, false, undefined, this),
|
|
35851
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35852
|
+
fg: colors2.fg,
|
|
35853
|
+
children: [
|
|
35854
|
+
`
|
|
35855
|
+
`,
|
|
35856
|
+
"network ",
|
|
35857
|
+
config.network,
|
|
35858
|
+
`
|
|
35859
|
+
`,
|
|
35860
|
+
"node mode ",
|
|
35861
|
+
config.nodeMode,
|
|
35862
|
+
`
|
|
35863
|
+
`,
|
|
35864
|
+
"dir ",
|
|
35865
|
+
config.dir,
|
|
35866
|
+
`
|
|
35867
|
+
`,
|
|
35868
|
+
"api port ",
|
|
35869
|
+
config.apiPort,
|
|
35870
|
+
`
|
|
35871
|
+
`,
|
|
35872
|
+
"bootstrap",
|
|
35873
|
+
" ",
|
|
35874
|
+
config.skipBootstrap ? "skip (sync from genesis)" : config.against,
|
|
35875
|
+
`
|
|
35876
|
+
`,
|
|
35877
|
+
"image ghcr.io/",
|
|
35878
|
+
config.owner,
|
|
35879
|
+
"/secondlayer-runtime:",
|
|
35880
|
+
config.imageTag
|
|
35881
|
+
]
|
|
35882
|
+
}, undefined, true, undefined, this),
|
|
35883
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35884
|
+
fg: colors2.accent,
|
|
35885
|
+
children: [
|
|
35886
|
+
`
|
|
35887
|
+
`,
|
|
35888
|
+
"Press Enter to start."
|
|
35889
|
+
]
|
|
35890
|
+
}, undefined, true, undefined, this)
|
|
35891
|
+
]
|
|
35892
|
+
}, undefined, true, undefined, this);
|
|
35893
|
+
}
|
|
35894
|
+
function RunningView({ events, log }) {
|
|
35895
|
+
const colors2 = useContext(ColorsContext);
|
|
35896
|
+
const latestByStep = new Map;
|
|
35897
|
+
for (const e2 of events)
|
|
35898
|
+
latestByStep.set(e2.step, e2);
|
|
35899
|
+
const anyActive = STEP_ORDER.some((step) => latestByStep.get(step)?.type === "step-start");
|
|
35900
|
+
const spinner = useSpinner(anyActive);
|
|
35901
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35902
|
+
style: { flexDirection: "column" },
|
|
35903
|
+
children: [
|
|
35904
|
+
STEP_ORDER.map((step) => {
|
|
35905
|
+
const e2 = latestByStep.get(step);
|
|
35906
|
+
const active = e2?.type === "step-start";
|
|
35907
|
+
const symbol = !e2 ? "·" : active ? spinner : e2.type === "step-done" ? "✓" : e2.type === "step-skip" ? "–" : e2.type === "step-error" ? "✗" : " ";
|
|
35908
|
+
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;
|
|
35909
|
+
return /* @__PURE__ */ jsxDEV("text", {
|
|
35910
|
+
fg: color,
|
|
35911
|
+
children: [
|
|
35912
|
+
symbol,
|
|
35913
|
+
" ",
|
|
35914
|
+
STEP_LABEL[step]
|
|
35915
|
+
]
|
|
35916
|
+
}, step, true, undefined, this);
|
|
35917
|
+
}),
|
|
35918
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
35919
|
+
style: {
|
|
35920
|
+
border: true,
|
|
35921
|
+
borderStyle: "single",
|
|
35922
|
+
borderColor: colors2.borderIdle,
|
|
35923
|
+
marginTop: 1,
|
|
35924
|
+
height: 10
|
|
35925
|
+
},
|
|
35926
|
+
title: "log",
|
|
35927
|
+
children: /* @__PURE__ */ jsxDEV("box", {
|
|
35928
|
+
style: { flexDirection: "column", padding: 1 },
|
|
35929
|
+
children: log.length === 0 ? /* @__PURE__ */ jsxDEV("text", {
|
|
35930
|
+
fg: colors2.muted,
|
|
35931
|
+
children: "waiting…"
|
|
35932
|
+
}, undefined, false, undefined, this) : log.map((line, i3) => /* @__PURE__ */ jsxDEV("text", {
|
|
35933
|
+
fg: line.startsWith("ERROR:") ? colors2.error : colors2.fg,
|
|
35934
|
+
children: line
|
|
35935
|
+
}, `${i3}:${line}`, false, undefined, this))
|
|
35936
|
+
}, undefined, false, undefined, this)
|
|
35937
|
+
}, undefined, false, undefined, this)
|
|
35938
|
+
]
|
|
35939
|
+
}, undefined, true, undefined, this);
|
|
35940
|
+
}
|
|
35941
|
+
function Done({ summary }) {
|
|
35942
|
+
const colors2 = useContext(ColorsContext);
|
|
35943
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35944
|
+
style: { flexDirection: "column" },
|
|
35945
|
+
children: [
|
|
35946
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
35947
|
+
style: { backgroundColor: colors2.successBg, padding: 1 },
|
|
35948
|
+
children: /* @__PURE__ */ jsxDEV("text", {
|
|
35949
|
+
fg: colors2.success,
|
|
35950
|
+
children: summary
|
|
35951
|
+
}, undefined, false, undefined, this)
|
|
35952
|
+
}, undefined, false, undefined, this),
|
|
35953
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35954
|
+
fg: colors2.muted,
|
|
35955
|
+
children: [
|
|
35956
|
+
`
|
|
35957
|
+
`,
|
|
35958
|
+
"press enter or q to exit"
|
|
35959
|
+
]
|
|
35960
|
+
}, undefined, true, undefined, this)
|
|
35961
|
+
]
|
|
35962
|
+
}, undefined, true, undefined, this);
|
|
35963
|
+
}
|
|
35964
|
+
function ErrorView({ message, log }) {
|
|
35965
|
+
const colors2 = useContext(ColorsContext);
|
|
35966
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
35967
|
+
style: { flexDirection: "column" },
|
|
35968
|
+
children: [
|
|
35969
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
35970
|
+
style: { backgroundColor: colors2.errorBg, padding: 1 },
|
|
35971
|
+
children: /* @__PURE__ */ jsxDEV("text", {
|
|
35972
|
+
fg: colors2.error,
|
|
35973
|
+
children: message
|
|
35974
|
+
}, undefined, false, undefined, this)
|
|
35975
|
+
}, undefined, false, undefined, this),
|
|
35976
|
+
log.slice(-10).map((line, i3) => /* @__PURE__ */ jsxDEV("text", {
|
|
35977
|
+
fg: colors2.muted,
|
|
35978
|
+
children: line
|
|
35979
|
+
}, `${i3}:${line}`, false, undefined, this)),
|
|
35980
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
35981
|
+
fg: colors2.muted,
|
|
35982
|
+
children: [
|
|
35983
|
+
`
|
|
35984
|
+
`,
|
|
35985
|
+
"press enter or q to exit"
|
|
35986
|
+
]
|
|
35987
|
+
}, undefined, true, undefined, this)
|
|
35988
|
+
]
|
|
35989
|
+
}, undefined, true, undefined, this);
|
|
35990
|
+
}
|
|
35991
|
+
async function runSetupTui(flags) {
|
|
35992
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
35993
|
+
createRoot(renderer).render(/* @__PURE__ */ jsxDEV(App, {
|
|
35994
|
+
flags
|
|
35995
|
+
}, undefined, false, undefined, this));
|
|
35996
|
+
return new Promise((resolve16, reject) => {
|
|
35997
|
+
let settled = false;
|
|
35998
|
+
const onDestroy = () => {
|
|
35999
|
+
if (settled)
|
|
36000
|
+
return;
|
|
36001
|
+
settled = true;
|
|
36002
|
+
resolve16();
|
|
36003
|
+
};
|
|
36004
|
+
const onError = (event) => {
|
|
36005
|
+
if (settled)
|
|
36006
|
+
return;
|
|
36007
|
+
settled = true;
|
|
36008
|
+
try {
|
|
36009
|
+
renderer.destroy();
|
|
36010
|
+
} catch {}
|
|
36011
|
+
reject(event.error ?? new Error("OpenTUI renderer error"));
|
|
36012
|
+
};
|
|
36013
|
+
renderer.once(CliRenderEvents.DESTROY, onDestroy);
|
|
36014
|
+
renderer.once(CliRenderEvents.RENDER_ERROR, onError);
|
|
36015
|
+
renderer.once(CliRenderEvents.HANDLER_ERROR, onError);
|
|
36016
|
+
});
|
|
36017
|
+
}
|
|
36018
|
+
var DARK_COLORS, LIGHT_COLORS, ColorsContext, NETWORK_OPTIONS, NODE_MODE_OPTIONS, SPINNER_FRAMES, STEP_LABEL, STEP_ORDER;
|
|
36019
|
+
var init_setup_tui = __esm(() => {
|
|
36020
|
+
init_instance_init();
|
|
36021
|
+
init_oss_compose();
|
|
36022
|
+
init_setup_wizard();
|
|
36023
|
+
DARK_COLORS = {
|
|
36024
|
+
bg: "#151515",
|
|
36025
|
+
raise: "#202020",
|
|
36026
|
+
fg: "#d7d1c9",
|
|
36027
|
+
muted: "#8d867e",
|
|
36028
|
+
bright: "#f2ede5",
|
|
36029
|
+
accent: "#29c6be",
|
|
36030
|
+
success: "#22c55e",
|
|
36031
|
+
warning: "#eab308",
|
|
36032
|
+
error: "#ef4444",
|
|
36033
|
+
borderIdle: "#FFFFFF14",
|
|
36034
|
+
successBg: "#22c55e14",
|
|
36035
|
+
errorBg: "#ef444414"
|
|
36036
|
+
};
|
|
36037
|
+
LIGHT_COLORS = {
|
|
36038
|
+
bg: "#ffffff",
|
|
36039
|
+
raise: "#f8f6f2",
|
|
36040
|
+
fg: "#1f2228",
|
|
36041
|
+
muted: "#7d8288",
|
|
36042
|
+
bright: "#0a0a0a",
|
|
36043
|
+
accent: "#29c6be",
|
|
36044
|
+
success: "#22c55e",
|
|
36045
|
+
warning: "#eab308",
|
|
36046
|
+
error: "#ef4444",
|
|
36047
|
+
borderIdle: "#0A0A0A14",
|
|
36048
|
+
successBg: "#22c55e14",
|
|
36049
|
+
errorBg: "#ef444414"
|
|
36050
|
+
};
|
|
36051
|
+
ColorsContext = createContext(DARK_COLORS);
|
|
36052
|
+
NETWORK_OPTIONS = [
|
|
36053
|
+
{
|
|
36054
|
+
name: "mainnet",
|
|
36055
|
+
description: "Production Stacks chain. Large: plan for hundreds of GB.",
|
|
36056
|
+
value: "mainnet"
|
|
36057
|
+
},
|
|
36058
|
+
{
|
|
36059
|
+
name: "testnet",
|
|
36060
|
+
description: "Stacks testnet. Small history.",
|
|
36061
|
+
value: "testnet"
|
|
36062
|
+
},
|
|
36063
|
+
{ name: "devnet", description: "Local Clarinet devnet.", value: "devnet" }
|
|
36064
|
+
];
|
|
36065
|
+
NODE_MODE_OPTIONS = SETUP_NODE_MODES.map((mode) => ({
|
|
36066
|
+
name: mode,
|
|
36067
|
+
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.",
|
|
36068
|
+
value: mode
|
|
36069
|
+
}));
|
|
36070
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
36071
|
+
STEP_LABEL = {
|
|
36072
|
+
preflight: "Preflight",
|
|
36073
|
+
secrets: "Secrets",
|
|
36074
|
+
config: "Write config",
|
|
36075
|
+
"docker-up": "docker compose up",
|
|
36076
|
+
observer: "Observer stanza",
|
|
36077
|
+
bootstrap: "Bootstrap",
|
|
36078
|
+
verify: "Verify",
|
|
36079
|
+
summary: "Summary"
|
|
36080
|
+
};
|
|
36081
|
+
STEP_ORDER = Object.keys(STEP_LABEL);
|
|
36082
|
+
});
|
|
36083
|
+
|
|
36084
|
+
// ../../node_modules/commander/esm.mjs
|
|
36085
|
+
var import__ = __toESM(require_commander(), 1);
|
|
36086
|
+
var {
|
|
36087
|
+
program,
|
|
36088
|
+
createCommand,
|
|
36089
|
+
createArgument,
|
|
36090
|
+
createOption,
|
|
36091
|
+
CommanderError,
|
|
36092
|
+
InvalidArgumentError,
|
|
36093
|
+
InvalidOptionArgumentError,
|
|
36094
|
+
Command,
|
|
36095
|
+
Argument,
|
|
36096
|
+
Option,
|
|
36097
|
+
Help
|
|
36098
|
+
} = import__.default;
|
|
34320
36099
|
// package.json
|
|
34321
36100
|
var package_default = {
|
|
34322
36101
|
name: "@secondlayer/cli",
|
|
34323
|
-
version: "
|
|
36102
|
+
version: "13.0.1",
|
|
34324
36103
|
description: "Run a Stacks index on your own hardware — set it up, restore verified history, and keep it current.",
|
|
34325
36104
|
repository: {
|
|
34326
36105
|
type: "git",
|
|
@@ -34361,13 +36140,16 @@ var package_default = {
|
|
|
34361
36140
|
"@biomejs/wasm-nodejs": "^1.9.0",
|
|
34362
36141
|
"@dsnp/parquetjs": "^1.8.8",
|
|
34363
36142
|
"@inquirer/prompts": "^8.2.0",
|
|
36143
|
+
"@opentui/core": "^0.5.3",
|
|
36144
|
+
"@opentui/react": "^0.5.3",
|
|
34364
36145
|
"@secondlayer/bundler": "^0.5.1",
|
|
34365
36146
|
"@secondlayer/scaffold": "^1.5.4",
|
|
34366
|
-
"@secondlayer/sdk": "^
|
|
34367
|
-
"@secondlayer/shared": "^
|
|
36147
|
+
"@secondlayer/sdk": "^9.0.0",
|
|
36148
|
+
"@secondlayer/shared": "^11.0.0",
|
|
34368
36149
|
"@secondlayer/stacks": "^4.0.0",
|
|
34369
|
-
"@secondlayer/subgraphs": "^4.0.
|
|
36150
|
+
"@secondlayer/subgraphs": "^4.0.4",
|
|
34370
36151
|
esbuild: "^0.27.4",
|
|
36152
|
+
react: "^19.1.0",
|
|
34371
36153
|
"smol-toml": "^1.6.1",
|
|
34372
36154
|
zod: "^4.3.6"
|
|
34373
36155
|
},
|
|
@@ -34381,8 +36163,7 @@ var package_default = {
|
|
|
34381
36163
|
commander: "^14.0.0",
|
|
34382
36164
|
execa: "^9.5.3",
|
|
34383
36165
|
"fast-glob": "^3.3.3",
|
|
34384
|
-
got: "^13.0.0"
|
|
34385
|
-
react: "^19.1.0"
|
|
36166
|
+
got: "^13.0.0"
|
|
34386
36167
|
}
|
|
34387
36168
|
};
|
|
34388
36169
|
|
|
@@ -34498,10 +36279,30 @@ async function runRefill(opts) {
|
|
|
34498
36279
|
}
|
|
34499
36280
|
});
|
|
34500
36281
|
}
|
|
34501
|
-
|
|
34502
|
-
|
|
34503
|
-
|
|
34504
|
-
|
|
36282
|
+
// src/commands/codegen.ts
|
|
36283
|
+
function registerCodegenCommand(program2) {
|
|
36284
|
+
const codegen = program2.command("codegen").description("Generate typed code: contracts, subgraphs, Index, clients");
|
|
36285
|
+
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) => {
|
|
36286
|
+
const { generate: generate2 } = await Promise.resolve().then(() => (init_generate(), exports_generate));
|
|
36287
|
+
await generate2(files, { ...options3, out: options3.output });
|
|
36288
|
+
});
|
|
36289
|
+
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) => {
|
|
36290
|
+
const { runSubgraphSchemaCodegen: runSubgraphSchemaCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
|
|
36291
|
+
await runSubgraphSchemaCodegen2(file, options3, (key) => command.getOptionValueSource(key), "kysely");
|
|
36292
|
+
});
|
|
36293
|
+
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) => {
|
|
36294
|
+
const { runIndexCodegen: runIndexCodegen2 } = await Promise.resolve().then(() => (init_index_api(), exports_index_api));
|
|
36295
|
+
await runIndexCodegen2(options3);
|
|
36296
|
+
});
|
|
36297
|
+
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) => {
|
|
36298
|
+
const { runSubgraphClientCodegen: runSubgraphClientCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
|
|
36299
|
+
await runSubgraphClientCodegen2(subgraphName, options3);
|
|
36300
|
+
});
|
|
36301
|
+
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) => {
|
|
36302
|
+
const { runPayloadsCodegen: runPayloadsCodegen2 } = await Promise.resolve().then(() => (init_subgraphs2(), exports_subgraphs));
|
|
36303
|
+
await runPayloadsCodegen2(file, options3.output);
|
|
36304
|
+
});
|
|
36305
|
+
}
|
|
34505
36306
|
// src/commands/config.ts
|
|
34506
36307
|
init_config2();
|
|
34507
36308
|
|
|
@@ -34959,22 +36760,138 @@ function registerConsoleCommand(program2) {
|
|
|
34959
36760
|
}
|
|
34960
36761
|
});
|
|
34961
36762
|
}
|
|
36763
|
+
// src/commands/context.ts
|
|
36764
|
+
init_http();
|
|
36765
|
+
import { SecondLayer as SecondLayer2 } from "@secondlayer/sdk";
|
|
36766
|
+
|
|
36767
|
+
// src/lib/instance-diagnosis.ts
|
|
36768
|
+
var BOOTSTRAP_STEP = "Restore verified history: secondlayer bootstrap --against <manifest>";
|
|
36769
|
+
var OBSERVER_STEP = "Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs";
|
|
36770
|
+
var SELF_HOST_DOCS_STEP = "Docs: https://www.secondlayer.tools/docs/self-host";
|
|
36771
|
+
function hasNoChainData(status) {
|
|
36772
|
+
const integrity = status.chainIntegrity;
|
|
36773
|
+
if (!integrity || integrity.reason === "check_failed")
|
|
36774
|
+
return false;
|
|
36775
|
+
if (integrity.maxHeight !== 0)
|
|
36776
|
+
return false;
|
|
36777
|
+
return (status.chainTip ?? null) === null;
|
|
36778
|
+
}
|
|
36779
|
+
function emptyIndexIssue(status) {
|
|
36780
|
+
const decoders = status.index?.decoders ?? [];
|
|
36781
|
+
const stalled = decoders.filter((d2) => d2.status !== "ok").length;
|
|
36782
|
+
const decoderClause = stalled > 0 ? ` and ${stalled} decoder${stalled === 1 ? "" : "s"} report unavailable because there is nothing to decode` : "";
|
|
36783
|
+
return {
|
|
36784
|
+
title: "No blocks indexed yet — this instance holds 0 blocks.",
|
|
36785
|
+
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.`,
|
|
36786
|
+
nextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP]
|
|
36787
|
+
};
|
|
36788
|
+
}
|
|
36789
|
+
var SERVICE_STEPS = [
|
|
36790
|
+
"See the raw per-service report: secondlayer status --json",
|
|
36791
|
+
"Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer"
|
|
36792
|
+
];
|
|
36793
|
+
function diagnoseInstanceStatus(status) {
|
|
36794
|
+
const overall = String(status.status ?? "unknown");
|
|
36795
|
+
if (hasNoChainData(status)) {
|
|
36796
|
+
return { state: "empty-index", overall, issues: [emptyIndexIssue(status)] };
|
|
36797
|
+
}
|
|
36798
|
+
const issues = [];
|
|
36799
|
+
for (const service of status.services ?? []) {
|
|
36800
|
+
if (service.status === "ok")
|
|
36801
|
+
continue;
|
|
36802
|
+
issues.push({
|
|
36803
|
+
title: `Service ${service.name} reports ${service.status}.`,
|
|
36804
|
+
nextSteps: SERVICE_STEPS
|
|
36805
|
+
});
|
|
36806
|
+
}
|
|
36807
|
+
const integrity = status.chainIntegrity;
|
|
36808
|
+
if (integrity && integrity.ok === false) {
|
|
36809
|
+
issues.push({
|
|
36810
|
+
title: "Chain-data integrity check failed.",
|
|
36811
|
+
detail: integrity.reason ?? "The tip the instance serves is not backed by the history it implies.",
|
|
36812
|
+
nextSteps: [
|
|
36813
|
+
"Verify against the archive: secondlayer verify all --against <manifest>",
|
|
36814
|
+
"Repair what diverged: secondlayer repair --against <manifest> --apply"
|
|
36815
|
+
]
|
|
36816
|
+
});
|
|
36817
|
+
}
|
|
36818
|
+
if (issues.length === 0 && overall !== "healthy") {
|
|
36819
|
+
issues.push({
|
|
36820
|
+
title: `The API reports status=${overall} but names no failing service.`,
|
|
36821
|
+
detail: "Every service in the report is ok, so the degradation is in a surface this command does not summarize.",
|
|
36822
|
+
nextSteps: SERVICE_STEPS
|
|
36823
|
+
});
|
|
36824
|
+
}
|
|
36825
|
+
return {
|
|
36826
|
+
state: issues.length > 0 ? "degraded" : "healthy",
|
|
36827
|
+
overall,
|
|
36828
|
+
issues
|
|
36829
|
+
};
|
|
36830
|
+
}
|
|
36831
|
+
var CONTEXT_FIELDS = [
|
|
36832
|
+
"account",
|
|
36833
|
+
"streamsTip",
|
|
36834
|
+
"indexTip",
|
|
36835
|
+
"subgraphs",
|
|
36836
|
+
"subscriptions",
|
|
36837
|
+
"activeOperations"
|
|
36838
|
+
];
|
|
36839
|
+
function explainContextNulls(snapshot, probe) {
|
|
36840
|
+
const nullFields = CONTEXT_FIELDS.filter((field) => (snapshot[field] ?? null) === null);
|
|
36841
|
+
if (nullFields.length === 0) {
|
|
36842
|
+
return { summary: "Every field resolved.", nulls: {} };
|
|
36843
|
+
}
|
|
36844
|
+
if (!probe.status) {
|
|
36845
|
+
const reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? "unreachable"}), so this read never reached an API.`;
|
|
36846
|
+
return {
|
|
36847
|
+
summary: `${reason} Start it from docker/oss with: docker compose up -d — or point SL_API_URL at the instance you meant.`,
|
|
36848
|
+
nulls: Object.fromEntries(nullFields.map((field) => [field, reason]))
|
|
36849
|
+
};
|
|
36850
|
+
}
|
|
36851
|
+
const emptyIndex = hasNoChainData(probe.status);
|
|
36852
|
+
const blocks = probe.status.chainIntegrity?.maxHeight ?? null;
|
|
36853
|
+
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.";
|
|
36854
|
+
const listReason = unresolvedReadReason(probe, blocks);
|
|
36855
|
+
const chainReason = emptyIndex ? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}` : listReason;
|
|
36856
|
+
const nulls = {};
|
|
36857
|
+
for (const field of nullFields) {
|
|
36858
|
+
if (field === "account")
|
|
36859
|
+
nulls[field] = accountReason;
|
|
36860
|
+
else if (field === "streamsTip" || field === "indexTip")
|
|
36861
|
+
nulls[field] = chainReason;
|
|
36862
|
+
else if (field === "activeOperations")
|
|
36863
|
+
nulls[field] = (snapshot.subgraphs ?? null) === null ? "Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve." : listReason;
|
|
36864
|
+
else
|
|
36865
|
+
nulls[field] = listReason;
|
|
36866
|
+
}
|
|
36867
|
+
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.`;
|
|
36868
|
+
return { summary, nulls };
|
|
36869
|
+
}
|
|
36870
|
+
function unresolvedReadReason(probe, blocks) {
|
|
36871
|
+
const where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : ""}, but this read returned nothing.`;
|
|
36872
|
+
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.`;
|
|
36873
|
+
}
|
|
36874
|
+
|
|
34962
36875
|
// src/commands/context.ts
|
|
34963
36876
|
init_output();
|
|
34964
36877
|
init_resolve_auth();
|
|
34965
|
-
import { SecondLayer as SecondLayer2 } from "@secondlayer/sdk";
|
|
34966
36878
|
function registerContextCommand(program2) {
|
|
34967
36879
|
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
36880
|
try {
|
|
34969
|
-
const
|
|
34970
|
-
|
|
34971
|
-
|
|
34972
|
-
});
|
|
36881
|
+
const apiUrl = resolveApiUrl();
|
|
36882
|
+
const apiKey = resolveEnvKey();
|
|
36883
|
+
const sl = new SecondLayer2({ baseUrl: apiUrl, apiKey });
|
|
34973
36884
|
const snapshot = await sl.context();
|
|
36885
|
+
const diagnostics = await explain(snapshot, apiUrl, apiKey);
|
|
36886
|
+
const payload = { ...snapshot, diagnostics };
|
|
34974
36887
|
output({
|
|
34975
36888
|
json: o3.json,
|
|
34976
|
-
data:
|
|
34977
|
-
human: () =>
|
|
36889
|
+
data: payload,
|
|
36890
|
+
human: () => {
|
|
36891
|
+
writeData(JSON.stringify(payload, null, 2));
|
|
36892
|
+
if (diagnostics)
|
|
36893
|
+
note(diagnostics.summary);
|
|
36894
|
+
}
|
|
34978
36895
|
});
|
|
34979
36896
|
} catch (err) {
|
|
34980
36897
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -34982,6 +36899,25 @@ function registerContextCommand(program2) {
|
|
|
34982
36899
|
}
|
|
34983
36900
|
});
|
|
34984
36901
|
}
|
|
36902
|
+
async function explain(snapshot, apiUrl, apiKey) {
|
|
36903
|
+
const hasNull = Object.values(snapshot).some((v2) => (v2 ?? null) === null);
|
|
36904
|
+
if (!hasNull)
|
|
36905
|
+
return null;
|
|
36906
|
+
let status = null;
|
|
36907
|
+
let statusError;
|
|
36908
|
+
try {
|
|
36909
|
+
status = await httpPlatformAnon("/public/status");
|
|
36910
|
+
} catch (err) {
|
|
36911
|
+
statusError = err instanceof Error ? err.message : String(err);
|
|
36912
|
+
}
|
|
36913
|
+
return explainContextNulls(snapshot, {
|
|
36914
|
+
apiUrl,
|
|
36915
|
+
selfHosted: isOssMode(),
|
|
36916
|
+
hasCredential: Boolean(apiKey),
|
|
36917
|
+
status,
|
|
36918
|
+
statusError
|
|
36919
|
+
});
|
|
36920
|
+
}
|
|
34985
36921
|
// src/commands/status.ts
|
|
34986
36922
|
init_http();
|
|
34987
36923
|
init_output();
|
|
@@ -34990,10 +36926,11 @@ function registerStatusCommand(program2) {
|
|
|
34990
36926
|
program2.command("status").description("Show local instance status").option("--json", "Output as JSON").action(async (options3) => {
|
|
34991
36927
|
try {
|
|
34992
36928
|
const status = await httpPlatformAnon("/public/status");
|
|
36929
|
+
const diagnosis = diagnoseInstanceStatus(status);
|
|
34993
36930
|
output({
|
|
34994
36931
|
json: options3.json,
|
|
34995
|
-
data: status,
|
|
34996
|
-
human: () => printPublicStatus(status)
|
|
36932
|
+
data: { ...status, diagnosis },
|
|
36933
|
+
human: () => printPublicStatus(status, diagnosis)
|
|
34997
36934
|
});
|
|
34998
36935
|
} catch {
|
|
34999
36936
|
console.log("");
|
|
@@ -35007,7 +36944,7 @@ function registerStatusCommand(program2) {
|
|
|
35007
36944
|
}
|
|
35008
36945
|
});
|
|
35009
36946
|
}
|
|
35010
|
-
function printPublicStatus(status) {
|
|
36947
|
+
function printPublicStatus(status, diagnosis) {
|
|
35011
36948
|
const overall = String(status.status ?? "unknown");
|
|
35012
36949
|
const color = overall === "healthy" ? green : red;
|
|
35013
36950
|
console.log("");
|
|
@@ -35045,6 +36982,19 @@ function printPublicStatus(status) {
|
|
|
35045
36982
|
}
|
|
35046
36983
|
console.log("");
|
|
35047
36984
|
}
|
|
36985
|
+
if (diagnosis.issues.length > 0) {
|
|
36986
|
+
console.log(blue("Diagnosis"));
|
|
36987
|
+
for (const issue of diagnosis.issues) {
|
|
36988
|
+
console.log(` ${yellow(issue.title)}`);
|
|
36989
|
+
for (const line of issue.detail ? wrapText(issue.detail, 74) : []) {
|
|
36990
|
+
console.log(dim(` ${line}`));
|
|
36991
|
+
}
|
|
36992
|
+
for (const step of issue.nextSteps) {
|
|
36993
|
+
console.log(dim(` → ${step}`));
|
|
36994
|
+
}
|
|
36995
|
+
}
|
|
36996
|
+
console.log("");
|
|
36997
|
+
}
|
|
35048
36998
|
if (typeof status.timestamp === "string") {
|
|
35049
36999
|
console.log(dim(`Last updated: ${status.timestamp}`));
|
|
35050
37000
|
}
|
|
@@ -35061,7 +37011,7 @@ import {
|
|
|
35061
37011
|
validateSubscriptionFilterForTable
|
|
35062
37012
|
} from "@secondlayer/shared/schemas/subscriptions";
|
|
35063
37013
|
async function validateSubscriptionTargetFromApi(client2, input2) {
|
|
35064
|
-
const subgraph = await client2.subgraphs.
|
|
37014
|
+
const subgraph = await client2.subgraphs.status(input2.subgraphName);
|
|
35065
37015
|
const errors2 = validateSubscriptionFilterForTable({
|
|
35066
37016
|
subgraphName: input2.subgraphName,
|
|
35067
37017
|
tableName: input2.tableName,
|
|
@@ -35876,7 +37826,7 @@ Examples:
|
|
|
35876
37826
|
try {
|
|
35877
37827
|
const client2 = await getSubscriptionClient();
|
|
35878
37828
|
const { id } = await resolveSubscriptionRef(client2, idOrName);
|
|
35879
|
-
const { data } = await client2.subscriptions.
|
|
37829
|
+
const { data } = await client2.subscriptions.deliveries(id);
|
|
35880
37830
|
if (options3.json)
|
|
35881
37831
|
printJson(data);
|
|
35882
37832
|
else
|
|
@@ -35905,7 +37855,7 @@ Examples:
|
|
|
35905
37855
|
const ok = await confirmOrExit(`Requeue ${outboxId} for "${detail.name}"?`, options3.yes);
|
|
35906
37856
|
if (!ok)
|
|
35907
37857
|
return;
|
|
35908
|
-
const res = await client2.subscriptions.
|
|
37858
|
+
const res = await client2.subscriptions.requeue(id, outboxId);
|
|
35909
37859
|
if (options3.json)
|
|
35910
37860
|
printJson(res);
|
|
35911
37861
|
else
|
|
@@ -35950,9 +37900,9 @@ Examples:
|
|
|
35950
37900
|
const client2 = await getSubscriptionClient();
|
|
35951
37901
|
const { id, detail } = await resolveSubscriptionRef(client2, idOrName);
|
|
35952
37902
|
const [deliveries, dead, subgraph] = await Promise.allSettled([
|
|
35953
|
-
client2.subscriptions.
|
|
37903
|
+
client2.subscriptions.deliveries(id),
|
|
35954
37904
|
client2.subscriptions.dead(id),
|
|
35955
|
-
detail.subgraphName ? client2.subgraphs.
|
|
37905
|
+
detail.subgraphName ? client2.subgraphs.status(detail.subgraphName) : Promise.resolve(null)
|
|
35956
37906
|
]);
|
|
35957
37907
|
const report = buildDoctorReport({
|
|
35958
37908
|
subscription: detail,
|
|
@@ -35973,7 +37923,7 @@ Examples:
|
|
|
35973
37923
|
const signingSecret = resolveSigningSecret(options3);
|
|
35974
37924
|
const client2 = await getSubscriptionClient();
|
|
35975
37925
|
const { detail } = await resolveSubscriptionRef(client2, idOrName);
|
|
35976
|
-
const subgraph = detail.subgraphName ? await client2.subgraphs.
|
|
37926
|
+
const subgraph = detail.subgraphName ? await client2.subgraphs.status(detail.subgraphName).catch(() => null) : null;
|
|
35977
37927
|
const row = await representativeRow(client2, detail, subgraph);
|
|
35978
37928
|
const fixture = buildSubscriptionTestFixture({
|
|
35979
37929
|
subscription: detail,
|
|
@@ -36027,152 +37977,10 @@ POST result (client-side, not logged):`));
|
|
|
36027
37977
|
});
|
|
36028
37978
|
}
|
|
36029
37979
|
// src/commands/devnet.ts
|
|
36030
|
-
import { spawnSync as
|
|
37980
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
36031
37981
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
36032
37982
|
import { dirname as dirname8, join as join9 } from "node:path";
|
|
36033
37983
|
|
|
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
37984
|
// src/lib/devnet-config.ts
|
|
36177
37985
|
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
36178
37986
|
import { dirname as dirname7, join as join7, parse as parsePath } from "node:path";
|
|
@@ -36229,6 +38037,7 @@ stacks_node_events_observers = [${entry}]
|
|
|
36229
38037
|
|
|
36230
38038
|
// src/commands/devnet.ts
|
|
36231
38039
|
init_output();
|
|
38040
|
+
init_resolve_auth();
|
|
36232
38041
|
|
|
36233
38042
|
// src/commands/faucet.ts
|
|
36234
38043
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -36362,7 +38171,7 @@ is what makes minting possible without Bitcoin.`).action(async (opts) => {
|
|
|
36362
38171
|
// src/commands/devnet.ts
|
|
36363
38172
|
var COMPOSE_REL = join9(".secondlayer", "docker-compose.yml");
|
|
36364
38173
|
function ensureDocker() {
|
|
36365
|
-
const probe =
|
|
38174
|
+
const probe = spawnSync3("docker", ["info"], { stdio: "ignore" });
|
|
36366
38175
|
if (probe.error || probe.status !== 0) {
|
|
36367
38176
|
error(`Docker isn't available.
|
|
36368
38177
|
|
|
@@ -36373,7 +38182,7 @@ function ensureDocker() {
|
|
|
36373
38182
|
}
|
|
36374
38183
|
}
|
|
36375
38184
|
function dockerCompose(composePath, args) {
|
|
36376
|
-
const res =
|
|
38185
|
+
const res = spawnSync3("docker", ["compose", "-f", composePath, ...args], {
|
|
36377
38186
|
stdio: "inherit"
|
|
36378
38187
|
});
|
|
36379
38188
|
return res.status ?? 1;
|
|
@@ -36455,7 +38264,10 @@ ${bold("Secondlayer stack up")}`);
|
|
|
36455
38264
|
console.log(`
|
|
36456
38265
|
${bold("Next:")}`);
|
|
36457
38266
|
console.log(` ${yellow("clarinet devnet start")} ${dim("# auto-deploys + streams to the indexer")}`);
|
|
36458
|
-
console.log(` ${yellow(
|
|
38267
|
+
console.log(` ${yellow(`SL_API_URL=http://localhost:3800 INSTANCE_TOKEN=${DEV_INSTANCE_TOKEN} secondlayer subgraphs deploy ./subgraph.ts`)}`);
|
|
38268
|
+
console.log(dim(`
|
|
38269
|
+
The api is published on 127.0.0.1 only: /v1 reads need no credential.
|
|
38270
|
+
Writes (deploys, subscriptions) send INSTANCE_TOKEN=${DEV_INSTANCE_TOKEN}.`));
|
|
36459
38271
|
console.log(dim(`
|
|
36460
38272
|
Stop with: secondlayer devnet down`));
|
|
36461
38273
|
}
|
|
@@ -36495,12 +38307,18 @@ async function logs(service, options3) {
|
|
|
36495
38307
|
}
|
|
36496
38308
|
async function jget(url) {
|
|
36497
38309
|
try {
|
|
36498
|
-
const r2 = await fetch(url, {
|
|
38310
|
+
const r2 = await fetch(url, {
|
|
38311
|
+
signal: AbortSignal.timeout(1500),
|
|
38312
|
+
headers: { authorization: `Bearer ${statusToken()}` }
|
|
38313
|
+
});
|
|
36499
38314
|
return r2.ok ? await r2.json() : null;
|
|
36500
38315
|
} catch {
|
|
36501
38316
|
return null;
|
|
36502
38317
|
}
|
|
36503
38318
|
}
|
|
38319
|
+
function statusToken() {
|
|
38320
|
+
return resolveEnvKey() ?? DEV_INSTANCE_TOKEN;
|
|
38321
|
+
}
|
|
36504
38322
|
var SYS_COLS = new Set([
|
|
36505
38323
|
"_id",
|
|
36506
38324
|
"_block_height",
|
|
@@ -36752,10 +38570,7 @@ Examples:
|
|
|
36752
38570
|
process.exit(1);
|
|
36753
38571
|
}
|
|
36754
38572
|
});
|
|
36755
|
-
|
|
36756
|
-
Examples:
|
|
36757
|
-
$ secondlayer streams pull --to ./dump
|
|
36758
|
-
$ secondlayer streams pull --to ./dump --from-block 150000 --to-block 200000`).action(async (options3) => {
|
|
38573
|
+
const runDumps = async (options3) => {
|
|
36759
38574
|
try {
|
|
36760
38575
|
const fromBlock = parseHeight(options3.fromBlock, "--from-block");
|
|
36761
38576
|
const toBlock = parseHeight(options3.toBlock, "--to-block");
|
|
@@ -36784,7 +38599,11 @@ Examples:
|
|
|
36784
38599
|
error(err instanceof Error ? err.message : String(err));
|
|
36785
38600
|
process.exit(1);
|
|
36786
38601
|
}
|
|
36787
|
-
}
|
|
38602
|
+
};
|
|
38603
|
+
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", `
|
|
38604
|
+
Examples:
|
|
38605
|
+
$ secondlayer streams dumps --to ./dump
|
|
38606
|
+
$ secondlayer streams dumps --to ./dump --from-block 150000 --to-block 200000`).action(runDumps);
|
|
36788
38607
|
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
38608
|
try {
|
|
36790
38609
|
const height = parseHeight(heightArg, "<height>");
|
|
@@ -36972,17 +38791,28 @@ function registerDoctorCommand(program2) {
|
|
|
36972
38791
|
async function runInstanceDoctor(jsonOutput) {
|
|
36973
38792
|
const config = await loadConfig2();
|
|
36974
38793
|
const apiUrl = resolveApiUrl();
|
|
36975
|
-
const issues = [];
|
|
36976
38794
|
const results = { network: config.network, apiUrl };
|
|
36977
38795
|
let apiHealthy = false;
|
|
36978
38796
|
let statusData = null;
|
|
38797
|
+
let unreachableError = null;
|
|
36979
38798
|
try {
|
|
36980
38799
|
statusData = await httpPlatformAnon("/public/status");
|
|
36981
38800
|
apiHealthy = true;
|
|
36982
|
-
} catch {
|
|
36983
|
-
|
|
38801
|
+
} catch (err) {
|
|
38802
|
+
unreachableError = err instanceof Error ? err.message : String(err);
|
|
36984
38803
|
}
|
|
36985
38804
|
results.apiHealthy = apiHealthy;
|
|
38805
|
+
const diagnosis = statusData ? diagnoseInstanceStatus(statusData) : null;
|
|
38806
|
+
const issues = diagnosis ? diagnosis.issues : [
|
|
38807
|
+
{
|
|
38808
|
+
title: `Cannot reach ${apiUrl}/public/status.`,
|
|
38809
|
+
detail: unreachableError ?? undefined,
|
|
38810
|
+
nextSteps: [
|
|
38811
|
+
"Start the instance from docker/oss: docker compose up -d",
|
|
38812
|
+
`If it runs elsewhere, point the CLI at it: SL_API_URL=<url> (currently ${apiUrl})`
|
|
38813
|
+
]
|
|
38814
|
+
}
|
|
38815
|
+
];
|
|
36986
38816
|
if (jsonOutput) {
|
|
36987
38817
|
console.log(JSON.stringify({ ...results, status: statusData, issues }, null, 2));
|
|
36988
38818
|
return;
|
|
@@ -36998,9 +38828,14 @@ async function runInstanceDoctor(jsonOutput) {
|
|
|
36998
38828
|
const tip = statusData.chainTip;
|
|
36999
38829
|
const streams = statusData.streams;
|
|
37000
38830
|
const index = statusData.index;
|
|
38831
|
+
const blocks = statusData.chainIntegrity?.maxHeight;
|
|
37001
38832
|
console.log(blue("Instance"));
|
|
38833
|
+
console.log(` status: ${statusData.status ?? "unknown"}`);
|
|
37002
38834
|
if (typeof tip === "number")
|
|
37003
38835
|
console.log(` tip: ${tip.toLocaleString()}`);
|
|
38836
|
+
if (typeof blocks === "number") {
|
|
38837
|
+
console.log(` blocks indexed: ${blocks.toLocaleString()}`);
|
|
38838
|
+
}
|
|
37004
38839
|
if (streams?.status)
|
|
37005
38840
|
console.log(` streams: ${streams.status}`);
|
|
37006
38841
|
if (index?.status)
|
|
@@ -37012,17 +38847,31 @@ async function runInstanceDoctor(jsonOutput) {
|
|
|
37012
38847
|
console.log(` ${green("None")}`);
|
|
37013
38848
|
} else {
|
|
37014
38849
|
for (const issue of issues) {
|
|
37015
|
-
console.log(` ${red("•")} ${issue}`);
|
|
38850
|
+
console.log(` ${red("•")} ${issue.title}`);
|
|
38851
|
+
for (const line of issue.detail ? wrapText(issue.detail, 72) : []) {
|
|
38852
|
+
console.log(dim(` ${line}`));
|
|
38853
|
+
}
|
|
38854
|
+
for (const step of issue.nextSteps) {
|
|
38855
|
+
console.log(dim(` → ${step}`));
|
|
38856
|
+
}
|
|
37016
38857
|
}
|
|
37017
38858
|
}
|
|
37018
38859
|
console.log("");
|
|
37019
38860
|
if (issues.length > 0) {
|
|
37020
|
-
warn(`${issues.length} issue
|
|
38861
|
+
warn(`${issues.length} issue${issues.length === 1 ? "" : "s"} found${doctorVerdictSuffix(diagnosis)}`);
|
|
37021
38862
|
} else {
|
|
37022
38863
|
success("All checks passed");
|
|
37023
38864
|
}
|
|
37024
38865
|
console.log("");
|
|
37025
38866
|
}
|
|
38867
|
+
function doctorVerdictSuffix(diagnosis) {
|
|
38868
|
+
if (!diagnosis)
|
|
38869
|
+
return " — the CLI could not reach this instance.";
|
|
38870
|
+
if (diagnosis.state === "empty-index") {
|
|
38871
|
+
return " — the instance is up but holds no chain data yet.";
|
|
38872
|
+
}
|
|
38873
|
+
return ` — the API reports status=${diagnosis.overall}.`;
|
|
38874
|
+
}
|
|
37026
38875
|
async function runLocalDoctor(jsonOutput) {
|
|
37027
38876
|
const config = await loadConfig2();
|
|
37028
38877
|
const report = await checkHealth();
|
|
@@ -39177,7 +41026,7 @@ function registerBootstrapCommand(program2) {
|
|
|
39177
41026
|
}
|
|
39178
41027
|
// src/commands/uninstall.ts
|
|
39179
41028
|
init_output();
|
|
39180
|
-
import { spawnSync as
|
|
41029
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
39181
41030
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
39182
41031
|
import { join as join17, resolve as resolve13 } from "node:path";
|
|
39183
41032
|
import { planUninstall, uninstallCommand } from "@secondlayer/shared/runtime";
|
|
@@ -39228,7 +41077,7 @@ function registerUninstallCommand(program2) {
|
|
|
39228
41077
|
});
|
|
39229
41078
|
return;
|
|
39230
41079
|
}
|
|
39231
|
-
const res =
|
|
41080
|
+
const res = spawnSync4("docker", cmd, { stdio: "inherit" });
|
|
39232
41081
|
if ((res.status ?? 1) !== 0) {
|
|
39233
41082
|
printError(`docker ${cmd.join(" ")} exited ${res.status}`);
|
|
39234
41083
|
process.exit(UNINSTALL_EXIT.FAILED);
|
|
@@ -39504,110 +41353,15 @@ function registerBackupCommand(program2) {
|
|
|
39504
41353
|
function registerRestoreCommand(program2) {
|
|
39505
41354
|
attachRestoreCommand(program2.command("restore").description("Restore an instance from a backup bundle"));
|
|
39506
41355
|
}
|
|
39507
|
-
// src/lib/instance-init.ts
|
|
39508
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
39509
|
-
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
|
|
39510
|
-
import { resolve as resolve15 } from "node:path";
|
|
39511
|
-
import { generateEd25519KeyPair } from "@secondlayer/shared/crypto/ed25519";
|
|
39512
|
-
import { INSTANCE_NETWORKS } from "@secondlayer/shared/db/queries/instance";
|
|
39513
|
-
var INSTANCE_ENV_FILE = ".env.local";
|
|
39514
|
-
function parseInstanceNetwork(value) {
|
|
39515
|
-
const network = value.trim().toLowerCase();
|
|
39516
|
-
if (INSTANCE_NETWORKS.includes(network)) {
|
|
39517
|
-
return network;
|
|
39518
|
-
}
|
|
39519
|
-
throw new Error(`network must be mainnet, testnet, or devnet (got ${value})`);
|
|
39520
|
-
}
|
|
39521
|
-
function generateInstanceToken() {
|
|
39522
|
-
return randomBytes3(32).toString("hex");
|
|
39523
|
-
}
|
|
39524
|
-
function generateSecretsKey() {
|
|
39525
|
-
return randomBytes3(32).toString("hex");
|
|
39526
|
-
}
|
|
39527
|
-
function generateSigningPrivateKey() {
|
|
39528
|
-
return generateEd25519KeyPair().privateKeyPem;
|
|
39529
|
-
}
|
|
39530
|
-
function escapeEnvValue(value) {
|
|
39531
|
-
if (!/[\s#"']/.test(value) && !value.includes("\\"))
|
|
39532
|
-
return value;
|
|
39533
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
|
|
39534
|
-
}
|
|
39535
|
-
function unescapeEnvValue(raw) {
|
|
39536
|
-
let value = raw.trim();
|
|
39537
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
39538
|
-
value = value.slice(1, -1);
|
|
39539
|
-
}
|
|
39540
|
-
return value.replace(/\\n/g, `
|
|
39541
|
-
`).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
39542
|
-
}
|
|
39543
|
-
function readEnvValue(envPath, key) {
|
|
39544
|
-
if (!existsSync10(envPath))
|
|
39545
|
-
return null;
|
|
39546
|
-
const match = readFileSync9(envPath, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
39547
|
-
return match ? unescapeEnvValue(match[1]) : null;
|
|
39548
|
-
}
|
|
39549
|
-
function buildInstanceEnv(input4) {
|
|
39550
|
-
const token = input4.existing?.INSTANCE_TOKEN || input4.existing?.SL_API_KEY || generateInstanceToken();
|
|
39551
|
-
const secrets = input4.existing?.SECONDLAYER_SECRETS_KEY || generateSecretsKey();
|
|
39552
|
-
const signing = input4.existing?.STREAMS_SIGNING_PRIVATE_KEY || input4.existing?.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY || generateSigningPrivateKey();
|
|
39553
|
-
const apiUrl = input4.apiUrl ?? "http://127.0.0.1:3800";
|
|
39554
|
-
const archiveKey = input4.existing?.ARCHIVE_SIGNING_PUBLIC_KEY;
|
|
39555
|
-
return {
|
|
39556
|
-
INSTANCE_MODE: "oss",
|
|
39557
|
-
STACKS_NETWORK: input4.network,
|
|
39558
|
-
INSTANCE_TOKEN: token,
|
|
39559
|
-
SECONDLAYER_SECRETS_KEY: secrets,
|
|
39560
|
-
STREAMS_SIGNING_PRIVATE_KEY: signing,
|
|
39561
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: signing,
|
|
39562
|
-
ALLOW_UNSIGNED_WEBHOOKS: "false",
|
|
39563
|
-
SL_API_URL: input4.existing?.SL_API_URL || apiUrl,
|
|
39564
|
-
SL_API_KEY: token,
|
|
39565
|
-
...archiveKey ? { ARCHIVE_SIGNING_PUBLIC_KEY: archiveKey } : {}
|
|
39566
|
-
};
|
|
39567
|
-
}
|
|
39568
|
-
function renderInstanceEnv(env2) {
|
|
39569
|
-
const lines2 = [
|
|
39570
|
-
"# Generated by secondlayer init. Do not commit.",
|
|
39571
|
-
`INSTANCE_MODE=${env2.INSTANCE_MODE}`,
|
|
39572
|
-
`STACKS_NETWORK=${env2.STACKS_NETWORK}`,
|
|
39573
|
-
`INSTANCE_TOKEN=${env2.INSTANCE_TOKEN}`,
|
|
39574
|
-
`SECONDLAYER_SECRETS_KEY=${env2.SECONDLAYER_SECRETS_KEY}`,
|
|
39575
|
-
`STREAMS_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.STREAMS_SIGNING_PRIVATE_KEY)}`,
|
|
39576
|
-
`SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY=${escapeEnvValue(env2.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY)}`,
|
|
39577
|
-
`ALLOW_UNSIGNED_WEBHOOKS=${env2.ALLOW_UNSIGNED_WEBHOOKS}`,
|
|
39578
|
-
`SL_API_URL=${env2.SL_API_URL}`,
|
|
39579
|
-
`SL_API_KEY=${env2.SL_API_KEY}`
|
|
39580
|
-
];
|
|
39581
|
-
if (env2.ARCHIVE_SIGNING_PUBLIC_KEY) {
|
|
39582
|
-
lines2.push(`ARCHIVE_SIGNING_PUBLIC_KEY=${escapeEnvValue(env2.ARCHIVE_SIGNING_PUBLIC_KEY)}`);
|
|
39583
|
-
}
|
|
39584
|
-
lines2.push("");
|
|
39585
|
-
return lines2.join(`
|
|
39586
|
-
`);
|
|
39587
|
-
}
|
|
39588
|
-
function writeInstanceEnv(cwd, env2, file = INSTANCE_ENV_FILE) {
|
|
39589
|
-
const path11 = resolve15(cwd, file);
|
|
39590
|
-
writeFileSync8(path11, renderInstanceEnv(env2), { mode: 384 });
|
|
39591
|
-
return path11;
|
|
39592
|
-
}
|
|
39593
|
-
function loadExistingInstanceEnv(cwd, file = INSTANCE_ENV_FILE) {
|
|
39594
|
-
const path11 = resolve15(cwd, file);
|
|
39595
|
-
return {
|
|
39596
|
-
INSTANCE_TOKEN: readEnvValue(path11, "INSTANCE_TOKEN"),
|
|
39597
|
-
SECONDLAYER_SECRETS_KEY: readEnvValue(path11, "SECONDLAYER_SECRETS_KEY"),
|
|
39598
|
-
STREAMS_SIGNING_PRIVATE_KEY: readEnvValue(path11, "STREAMS_SIGNING_PRIVATE_KEY"),
|
|
39599
|
-
SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY: readEnvValue(path11, "SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY"),
|
|
39600
|
-
SL_API_URL: readEnvValue(path11, "SL_API_URL"),
|
|
39601
|
-
SL_API_KEY: readEnvValue(path11, "SL_API_KEY"),
|
|
39602
|
-
ARCHIVE_SIGNING_PUBLIC_KEY: readEnvValue(path11, "ARCHIVE_SIGNING_PUBLIC_KEY")
|
|
39603
|
-
};
|
|
39604
|
-
}
|
|
39605
|
-
|
|
39606
41356
|
// src/commands/init.ts
|
|
41357
|
+
init_instance_init();
|
|
39607
41358
|
init_output();
|
|
39608
41359
|
function registerInitCommand(program2) {
|
|
39609
|
-
program2.command("init").description("Write local env (token, secrets key, webhook signing key). Idempotent.").option("--
|
|
39610
|
-
|
|
41360
|
+
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", `
|
|
41361
|
+
--network <mainnet|testnet|devnet> is the top-level global flag (see
|
|
41362
|
+
\`secondlayer --help\`) — default mainnet, same as before.
|
|
41363
|
+
`).action((opts) => {
|
|
41364
|
+
const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
|
|
39611
41365
|
const existing = opts.force ? {} : loadExistingInstanceEnv(process.cwd());
|
|
39612
41366
|
const env2 = buildInstanceEnv({
|
|
39613
41367
|
network,
|
|
@@ -39624,89 +41378,252 @@ function registerInitCommand(program2) {
|
|
|
39624
41378
|
note("Export these or pass --env-file to compose. Do not commit the file.");
|
|
39625
41379
|
});
|
|
39626
41380
|
}
|
|
39627
|
-
// src/
|
|
39628
|
-
|
|
39629
|
-
|
|
39630
|
-
|
|
39631
|
-
|
|
39632
|
-
|
|
41381
|
+
// src/commands/observer.ts
|
|
41382
|
+
init_instance_init();
|
|
41383
|
+
init_observer_stanza();
|
|
41384
|
+
init_output();
|
|
41385
|
+
function registerObserverCommand(program2) {
|
|
41386
|
+
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", `
|
|
41387
|
+
--network <mainnet|testnet|devnet> is the top-level global flag (see
|
|
41388
|
+
\`secondlayer --help\`) — default mainnet, same as before.
|
|
41389
|
+
`).action((opts) => {
|
|
41390
|
+
const mode = parseObserverMode(opts.mode);
|
|
41391
|
+
const network = parseInstanceNetwork(process.env.STACKS_NETWORK ?? "mainnet");
|
|
41392
|
+
const endpoint = opts.endpoint ?? defaultObserverEndpoint(network);
|
|
41393
|
+
const recovery = opts.recovery ? parseRecoverySource(opts.recovery) : undefined;
|
|
41394
|
+
if (mode === "signer-shared") {
|
|
41395
|
+
note("Signer-shared nodes skip observer retries. Pair with a journal or archive for completeness.");
|
|
41396
|
+
}
|
|
41397
|
+
writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
|
|
41398
|
+
});
|
|
39633
41399
|
}
|
|
39634
|
-
|
|
39635
|
-
|
|
39636
|
-
|
|
39637
|
-
|
|
41400
|
+
// src/commands/setup.ts
|
|
41401
|
+
init_output();
|
|
41402
|
+
|
|
41403
|
+
// src/lib/setup-prompts.ts
|
|
41404
|
+
init_instance_init();
|
|
41405
|
+
init_oss_compose();
|
|
41406
|
+
init_setup_wizard();
|
|
41407
|
+
import { confirm as confirm9, input as input4, select as select5 } from "@inquirer/prompts";
|
|
41408
|
+
|
|
41409
|
+
class SetupCancelledError extends Error {
|
|
41410
|
+
constructor() {
|
|
41411
|
+
super("Setup cancelled — nothing was changed.");
|
|
41412
|
+
this.name = "SetupCancelledError";
|
|
39638
41413
|
}
|
|
39639
|
-
throw new UnsupportedObserverError(`observer mode must be indexer or signer-shared (got ${value})`);
|
|
39640
41414
|
}
|
|
39641
|
-
function
|
|
39642
|
-
|
|
39643
|
-
|
|
39644
|
-
|
|
41415
|
+
function safeParse(value, parse2) {
|
|
41416
|
+
if (!value)
|
|
41417
|
+
return;
|
|
41418
|
+
try {
|
|
41419
|
+
return parse2(value);
|
|
41420
|
+
} catch {
|
|
41421
|
+
return;
|
|
39645
41422
|
}
|
|
39646
|
-
throw new UnsupportedObserverError(`recovery source must be journal or archive (got ${value})`);
|
|
39647
41423
|
}
|
|
39648
|
-
|
|
39649
|
-
|
|
39650
|
-
|
|
39651
|
-
|
|
39652
|
-
|
|
39653
|
-
|
|
39654
|
-
|
|
39655
|
-
|
|
41424
|
+
var NODE_MODE_DESCRIPTIONS = {
|
|
41425
|
+
external: "You run the Stacks node; paste an observer stanza into its Config.toml.",
|
|
41426
|
+
stacks: "You run the Stacks node yourself, no bundled bitcoind (same as external).",
|
|
41427
|
+
full: "Bundled Stacks node + bitcoind."
|
|
41428
|
+
};
|
|
41429
|
+
async function promptSetupConfig(flags) {
|
|
41430
|
+
const network = safeParse(flags.network, parseInstanceNetwork) ?? await select5({
|
|
41431
|
+
message: "Network",
|
|
41432
|
+
choices: [
|
|
41433
|
+
{
|
|
41434
|
+
name: "mainnet",
|
|
41435
|
+
value: "mainnet",
|
|
41436
|
+
description: "Production Stacks chain. Large: plan for hundreds of GB."
|
|
41437
|
+
},
|
|
41438
|
+
{
|
|
41439
|
+
name: "testnet",
|
|
41440
|
+
value: "testnet",
|
|
41441
|
+
description: "Stacks testnet. Small history."
|
|
41442
|
+
},
|
|
41443
|
+
{
|
|
41444
|
+
name: "devnet",
|
|
41445
|
+
value: "devnet",
|
|
41446
|
+
description: "Local Clarinet devnet."
|
|
41447
|
+
}
|
|
41448
|
+
]
|
|
41449
|
+
});
|
|
41450
|
+
const nodeMode = safeParse(flags.nodeMode, parseSetupNodeMode) ?? await select5({
|
|
41451
|
+
message: "Node mode",
|
|
41452
|
+
choices: SETUP_NODE_MODES.map((mode) => {
|
|
41453
|
+
const floor = guardrailPreview(mode, network);
|
|
41454
|
+
return {
|
|
41455
|
+
name: mode,
|
|
41456
|
+
value: mode,
|
|
41457
|
+
description: `${NODE_MODE_DESCRIPTIONS[mode]} needs ≥ ${(floor.ramFloorMb / 1024).toFixed(0)}GB RAM, ≥ ${floor.diskFloorGb}GB disk.`
|
|
41458
|
+
};
|
|
41459
|
+
})
|
|
41460
|
+
});
|
|
41461
|
+
let against = flags.against;
|
|
41462
|
+
let skipBootstrap = !!flags.skipBootstrap;
|
|
41463
|
+
if (!skipBootstrap && !against) {
|
|
41464
|
+
const choice = await select5({
|
|
41465
|
+
message: "Bootstrap from",
|
|
41466
|
+
choices: [
|
|
41467
|
+
{
|
|
41468
|
+
name: "Hosted archive (recommended)",
|
|
41469
|
+
value: "hosted",
|
|
41470
|
+
description: DEFAULT_ARCHIVE_MANIFEST
|
|
41471
|
+
},
|
|
41472
|
+
{
|
|
41473
|
+
name: "Custom manifest URL or path",
|
|
41474
|
+
value: "custom",
|
|
41475
|
+
description: "Enter your own"
|
|
41476
|
+
},
|
|
41477
|
+
{
|
|
41478
|
+
name: "Sync from genesis",
|
|
41479
|
+
value: "skip",
|
|
41480
|
+
description: "Skip bootstrap — slower, no download"
|
|
41481
|
+
}
|
|
41482
|
+
]
|
|
41483
|
+
});
|
|
41484
|
+
if (choice === "hosted") {
|
|
41485
|
+
against = DEFAULT_ARCHIVE_MANIFEST;
|
|
41486
|
+
} else if (choice === "skip") {
|
|
41487
|
+
skipBootstrap = true;
|
|
41488
|
+
} else {
|
|
41489
|
+
against = (await input4({
|
|
41490
|
+
message: "Manifest URL or local path",
|
|
41491
|
+
default: DEFAULT_ARCHIVE_MANIFEST
|
|
41492
|
+
})).trim();
|
|
41493
|
+
}
|
|
39656
41494
|
}
|
|
39657
|
-
const
|
|
39658
|
-
|
|
39659
|
-
|
|
41495
|
+
const config = {
|
|
41496
|
+
network,
|
|
41497
|
+
nodeMode,
|
|
41498
|
+
apiPort: flags.apiPort ?? "127.0.0.1:3800",
|
|
41499
|
+
indexerPort: "127.0.0.1:3700",
|
|
41500
|
+
postgresPort: "127.0.0.1:5432",
|
|
41501
|
+
dir: flags.dir ?? process.cwd(),
|
|
41502
|
+
against: skipBootstrap ? undefined : against,
|
|
41503
|
+
skipBootstrap,
|
|
41504
|
+
skipVerify: !!flags.skipVerify,
|
|
41505
|
+
yes: false,
|
|
41506
|
+
force: !!flags.force,
|
|
41507
|
+
owner: flags.owner ?? DEFAULT_IMAGE_OWNER,
|
|
41508
|
+
imageTag: flags.imageTag ?? DEFAULT_IMAGE_TAG
|
|
41509
|
+
};
|
|
41510
|
+
console.error("");
|
|
41511
|
+
console.error(` network ${config.network}`);
|
|
41512
|
+
console.error(` node mode ${config.nodeMode}`);
|
|
41513
|
+
console.error(` dir ${config.dir}`);
|
|
41514
|
+
console.error(` api port ${config.apiPort}`);
|
|
41515
|
+
console.error(` bootstrap ${config.skipBootstrap ? "skip (sync from genesis)" : config.against}`);
|
|
41516
|
+
console.error("");
|
|
41517
|
+
const proceed = await confirm9({ message: "Start setup?", default: true });
|
|
41518
|
+
if (!proceed)
|
|
41519
|
+
throw new SetupCancelledError;
|
|
41520
|
+
return config;
|
|
41521
|
+
}
|
|
41522
|
+
|
|
41523
|
+
// src/commands/setup.ts
|
|
41524
|
+
init_setup_wizard();
|
|
41525
|
+
function renderProgress(event) {
|
|
41526
|
+
switch (event.type) {
|
|
41527
|
+
case "step-start":
|
|
41528
|
+
note(`→ ${event.step}`);
|
|
41529
|
+
return;
|
|
41530
|
+
case "step-log":
|
|
41531
|
+
note(` ${event.line}`);
|
|
41532
|
+
return;
|
|
41533
|
+
case "step-done":
|
|
41534
|
+
success(event.step + (event.detail ? `: ${event.detail}` : ""));
|
|
41535
|
+
return;
|
|
41536
|
+
case "step-skip":
|
|
41537
|
+
note(` skipped ${event.step} (${event.reason})`);
|
|
41538
|
+
return;
|
|
41539
|
+
case "step-error":
|
|
41540
|
+
printError(`${event.step} failed: ${event.message}`);
|
|
41541
|
+
return;
|
|
39660
41542
|
}
|
|
39661
|
-
return trimmed;
|
|
39662
41543
|
}
|
|
39663
|
-
|
|
39664
|
-
|
|
39665
|
-
|
|
39666
|
-
|
|
39667
|
-
|
|
39668
|
-
|
|
41544
|
+
async function executeAndRender(config) {
|
|
41545
|
+
console.error(bold("Secondlayer setup"));
|
|
41546
|
+
console.error(dim(` network=${config.network} node-mode=${config.nodeMode} dir=${config.dir}`));
|
|
41547
|
+
console.error("");
|
|
41548
|
+
let failed = false;
|
|
41549
|
+
const result = await runSetup(config, (event) => {
|
|
41550
|
+
if (event.type === "step-error")
|
|
41551
|
+
failed = true;
|
|
41552
|
+
renderProgress(event);
|
|
41553
|
+
});
|
|
41554
|
+
if (!result.ok) {
|
|
41555
|
+
process.exit(1);
|
|
39669
41556
|
}
|
|
39670
|
-
if (
|
|
39671
|
-
|
|
41557
|
+
if (failed) {
|
|
41558
|
+
warn("Setup finished with at least one step reporting an error above.");
|
|
39672
41559
|
}
|
|
39673
|
-
|
|
41560
|
+
console.error("");
|
|
41561
|
+
if (result.summary)
|
|
41562
|
+
console.log(result.summary);
|
|
41563
|
+
process.exit(failed ? 1 : 0);
|
|
39674
41564
|
}
|
|
39675
|
-
function
|
|
39676
|
-
|
|
39677
|
-
|
|
39678
|
-
|
|
39679
|
-
|
|
39680
|
-
|
|
39681
|
-
|
|
39682
|
-
|
|
39683
|
-
|
|
39684
|
-
|
|
39685
|
-
|
|
39686
|
-
|
|
39687
|
-
|
|
39688
|
-
""
|
|
39689
|
-
].join(`
|
|
39690
|
-
`);
|
|
41565
|
+
async function runNonInteractive(flags) {
|
|
41566
|
+
let config;
|
|
41567
|
+
try {
|
|
41568
|
+
config = resolveNonInteractiveConfig(flags);
|
|
41569
|
+
} catch (err) {
|
|
41570
|
+
if (err instanceof MissingSetupFlagError) {
|
|
41571
|
+
printError(err.message, { hint: `Pass ${err.flag} and re-run.` });
|
|
41572
|
+
} else {
|
|
41573
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
41574
|
+
}
|
|
41575
|
+
process.exit(1);
|
|
41576
|
+
}
|
|
41577
|
+
await executeAndRender(config);
|
|
39691
41578
|
}
|
|
39692
|
-
function
|
|
39693
|
-
|
|
39694
|
-
|
|
39695
|
-
|
|
41579
|
+
async function runPromptedInteractive(flags) {
|
|
41580
|
+
let config;
|
|
41581
|
+
try {
|
|
41582
|
+
config = await promptSetupConfig(flags);
|
|
41583
|
+
} catch (err) {
|
|
41584
|
+
if (err instanceof SetupCancelledError) {
|
|
41585
|
+
note(err.message);
|
|
41586
|
+
process.exit(0);
|
|
41587
|
+
}
|
|
41588
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
41589
|
+
process.exit(1);
|
|
41590
|
+
}
|
|
41591
|
+
await executeAndRender(config);
|
|
39696
41592
|
}
|
|
41593
|
+
function registerSetupCommand(program2) {
|
|
41594
|
+
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", `
|
|
41595
|
+
--network <mainnet|testnet|devnet> is the top-level global flag (see
|
|
41596
|
+
\`secondlayer --help\`) — required here too, just parsed one level up.
|
|
39697
41597
|
|
|
39698
|
-
|
|
39699
|
-
|
|
39700
|
-
|
|
39701
|
-
|
|
39702
|
-
|
|
39703
|
-
|
|
39704
|
-
|
|
39705
|
-
|
|
39706
|
-
|
|
39707
|
-
|
|
41598
|
+
Examples:
|
|
41599
|
+
$ secondlayer setup
|
|
41600
|
+
$ secondlayer setup --yes --network mainnet --node-mode external --against ${DEFAULT_ARCHIVE_MANIFEST}
|
|
41601
|
+
$ secondlayer setup --yes --network testnet --node-mode external --skip-bootstrap --skip-verify
|
|
41602
|
+
|
|
41603
|
+
Without a TTY (piped, CI, an agent), or with --yes, the TUI is skipped: every
|
|
41604
|
+
decision with no safe default (--network, --node-mode, and --against unless
|
|
41605
|
+
--skip-bootstrap) must come from a flag or the command fails fast naming it.
|
|
41606
|
+
`).action(async (opts) => {
|
|
41607
|
+
const flags = {
|
|
41608
|
+
...opts,
|
|
41609
|
+
network: opts.network ?? process.env.STACKS_NETWORK
|
|
41610
|
+
};
|
|
41611
|
+
const interactive = process.stdout.isTTY && !flags.yes;
|
|
41612
|
+
if (!interactive) {
|
|
41613
|
+
await runNonInteractive(flags);
|
|
41614
|
+
return;
|
|
41615
|
+
}
|
|
41616
|
+
if (!isBunRuntime()) {
|
|
41617
|
+
await runPromptedInteractive(flags);
|
|
41618
|
+
return;
|
|
41619
|
+
}
|
|
41620
|
+
try {
|
|
41621
|
+
const { runSetupTui: runSetupTui2 } = await Promise.resolve().then(() => (init_setup_tui(), exports_setup_tui));
|
|
41622
|
+
await runSetupTui2(flags);
|
|
41623
|
+
} catch (err) {
|
|
41624
|
+
warn(`Interactive TUI unavailable (${err instanceof Error ? err.message : String(err)}) — falling back to guided prompts.`);
|
|
41625
|
+
await runPromptedInteractive(flags);
|
|
39708
41626
|
}
|
|
39709
|
-
writeData(renderObserverStanza({ mode, endpoint, network, recovery }));
|
|
39710
41627
|
});
|
|
39711
41628
|
}
|
|
39712
41629
|
// src/commands/start.ts
|
|
@@ -39732,7 +41649,7 @@ function registerStartCommand(program2) {
|
|
|
39732
41649
|
});
|
|
39733
41650
|
process.exit(1);
|
|
39734
41651
|
}
|
|
39735
|
-
const extra = parsed.config.NODE_MODE === "full" ? " --profile full-node" :
|
|
41652
|
+
const extra = parsed.config.NODE_MODE === "full" ? " --profile full-node" : "";
|
|
39736
41653
|
const cmd = `docker compose -f docker/oss/docker-compose.yml${extra} up -d`;
|
|
39737
41654
|
if (opts.print) {
|
|
39738
41655
|
console.log(cmd);
|
|
@@ -39745,24 +41662,30 @@ function registerStartCommand(program2) {
|
|
|
39745
41662
|
}
|
|
39746
41663
|
// src/cli.ts
|
|
39747
41664
|
var { version } = package_default;
|
|
39748
|
-
program.name("secondlayer").alias("sl").description("Secondlayer CLI — run a Stacks index on your own hardware").version(version).option("--network <network>", "Override network (local, testnet, mainnet)").option("--api-key <key>", "API credential (overrides
|
|
41665
|
+
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)");
|
|
39749
41666
|
program.hook("preAction", (thisCommand) => {
|
|
39750
41667
|
const { network, apiKey, apiUrl } = thisCommand.opts();
|
|
39751
41668
|
if (network)
|
|
39752
41669
|
process.env.STACKS_NETWORK = network;
|
|
39753
|
-
if (apiKey)
|
|
41670
|
+
if (apiKey) {
|
|
41671
|
+
process.env.INSTANCE_TOKEN = apiKey;
|
|
39754
41672
|
process.env.SL_API_KEY = apiKey;
|
|
41673
|
+
}
|
|
39755
41674
|
if (apiUrl)
|
|
39756
41675
|
process.env.SL_API_URL = apiUrl;
|
|
39757
41676
|
});
|
|
39758
41677
|
program.addHelpText("after", `
|
|
39759
41678
|
Quickstart:
|
|
39760
|
-
$ secondlayer
|
|
39761
|
-
$ secondlayer bootstrap --against <manifest>
|
|
41679
|
+
$ secondlayer setup
|
|
39762
41680
|
$ secondlayer subgraphs create my-watcher --from-contract SP....my-contract
|
|
39763
41681
|
$ secondlayer subgraphs deploy subgraphs/my-watcher.ts
|
|
41682
|
+
|
|
41683
|
+
Manual / scripted setup, one step at a time:
|
|
41684
|
+
$ secondlayer init --network mainnet
|
|
41685
|
+
$ secondlayer bootstrap --against <manifest>
|
|
39764
41686
|
`);
|
|
39765
41687
|
program.commandsGroup("Getting started:");
|
|
41688
|
+
registerSetupCommand(program);
|
|
39766
41689
|
registerInitCommand(program);
|
|
39767
41690
|
registerBootstrapCommand(program);
|
|
39768
41691
|
registerObserverCommand(program);
|
|
@@ -39778,19 +41701,6 @@ registerStreamsCommand(program);
|
|
|
39778
41701
|
registerIndexCommand(program);
|
|
39779
41702
|
program.commandsGroup("Project & codegen:");
|
|
39780
41703
|
registerCodegenCommand(program);
|
|
39781
|
-
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) => {
|
|
39782
|
-
const { deprecatedCodegenNotice: deprecatedCodegenNotice2 } = await Promise.resolve().then(() => (init_codegen(), exports_codegen));
|
|
39783
|
-
deprecatedCodegenNotice2("secondlayer contracts generate", "secondlayer codegen contracts");
|
|
39784
|
-
options3.out = options3.output;
|
|
39785
|
-
const { generate: generate2 } = await Promise.resolve().then(() => (init_generate(), exports_generate));
|
|
39786
|
-
await generate2(files, options3);
|
|
39787
|
-
});
|
|
39788
|
-
var contracts = program.command("contracts").description("Work with Clarity contracts");
|
|
39789
|
-
configureGenerate(contracts.command("generate [files...]").aliases(["gen"]).description("Generate TypeScript interfaces from Clarity contracts")).addHelpText("after", `
|
|
39790
|
-
Examples:
|
|
39791
|
-
$ secondlayer contracts generate
|
|
39792
|
-
$ secondlayer contracts generate ./contracts/pool.clar -o ./src/generated.ts
|
|
39793
|
-
$ secondlayer contracts generate --config secondlayer.config.ts --watch`);
|
|
39794
41704
|
program.commandsGroup("Local development:");
|
|
39795
41705
|
registerLocalCommand(program);
|
|
39796
41706
|
registerDevnetCommand(program);
|
|
@@ -39808,5 +41718,5 @@ program.commandsGroup("Archive:");
|
|
|
39808
41718
|
registerCreditsCommand(program);
|
|
39809
41719
|
program.parse();
|
|
39810
41720
|
|
|
39811
|
-
//# debugId=
|
|
41721
|
+
//# debugId=E4B3B7874FAC14F764756E2164756E21
|
|
39812
41722
|
//# sourceMappingURL=cli.js.map
|