@better-sol/cli 0.1.0-alpha.6 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,10 +4,11 @@ import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } f
4
4
  import { Command } from "commander";
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
7
- import { homedir } from "node:os";
8
- import { execSync } from "node:child_process";
7
+ import { homedir, tmpdir } from "node:os";
8
+ import { execFile, execSync } from "node:child_process";
9
9
  import { generateKeyPairSigner, getAddressDecoder } from "@solana/kit";
10
- import { access, mkdir, opendir, readFile, writeFile } from "node:fs/promises";
10
+ import { access, mkdir, mkdtemp, opendir, readFile, rm, writeFile } from "node:fs/promises";
11
+ import { promisify } from "node:util";
11
12
  import { fileURLToPath, pathToFileURL } from "node:url";
12
13
  import { parseSync } from "oxc-parser";
13
14
  import { AnchorProvider, Program } from "@coral-xyz/anchor";
@@ -460,11 +461,23 @@ async function compileProgram(params) {
460
461
  })
461
462
  });
462
463
  if (!response.ok) {
463
- const message = await response.text();
464
- throw new Error(`Compile failed (${response.status}): ${message}`);
464
+ const error = await readJson(response);
465
+ if (response.status === 429 && error.retryAfterSeconds !== void 0) throw new Error(`Rate limit exceeded. Try again in ${formatDuration(error.retryAfterSeconds)}.`);
466
+ throw new Error(`Compile failed (${response.status}): ${error.error}`);
465
467
  }
468
+ return readJson(response);
469
+ }
470
+ async function readJson(response) {
466
471
  return await response.json();
467
472
  }
473
+ function formatDuration(totalSeconds) {
474
+ const seconds = Math.max(0, Math.ceil(totalSeconds));
475
+ const minutes = Math.floor(seconds / 60);
476
+ const remainder = seconds % 60;
477
+ if (minutes === 0) return `${remainder}s`;
478
+ if (remainder === 0) return `${minutes}m`;
479
+ return `${minutes}m ${remainder}s`;
480
+ }
468
481
  //#endregion
469
482
  //#region src/lib/solana-rpc.ts
470
483
  const CLUSTER_URLS = {
@@ -3103,6 +3116,7 @@ async function discoverProgramsWithSpinner(src) {
3103
3116
  }
3104
3117
  //#endregion
3105
3118
  //#region src/commands/deploy.ts
3119
+ const execFileAsync = promisify(execFile);
3106
3120
  const DEFAULT_PAYER_PATH = "keypair.json";
3107
3121
  const AIRDROP_LAMPORTS = 2000000000n;
3108
3122
  const AIRDROP_RETRIES = 3;
@@ -3118,9 +3132,10 @@ async function deploy(options) {
3118
3132
  const payerPath = resolvePayerPath(options.payer, config.payer);
3119
3133
  const payer = await readKeypair(payerPath);
3120
3134
  const rpcUrl = clusterUrl(cluster);
3135
+ const writesRust = options.verify || options.dryRun || options.output !== void 0;
3121
3136
  log.step(`Cluster: ${cluster}`);
3122
3137
  log.step(`Source: ${src}`);
3123
- log.step(`Output: ${out}`);
3138
+ if (writesRust) log.step(`Output: ${out}`);
3124
3139
  await ensureFunded(payer.publicKey, cluster, rpcUrl);
3125
3140
  const programs = await discoverProgramsWithSpinner(src);
3126
3141
  const s = spinner();
@@ -3131,7 +3146,7 @@ async function deploy(options) {
3131
3146
  throw new Error(`No program named '${options.program}' found in ${src}.\nAvailable programs:\n${available}`);
3132
3147
  }
3133
3148
  const projects = matched.map((program) => generateAnchorProject(program));
3134
- if (options.verify || options.dryRun || options.output !== void 0) {
3149
+ if (writesRust) {
3135
3150
  s.message(`Writing generated Anchor projects`);
3136
3151
  await ensureDirectory(outDir);
3137
3152
  await Promise.all(projects.map(async (project) => {
@@ -3147,44 +3162,51 @@ async function deploy(options) {
3147
3162
  outro(`Dry run complete — Rust written to ${out}/. No compilation or deployment performed.`);
3148
3163
  return;
3149
3164
  }
3150
- s.message(`Compiling ${matched.length === 1 ? matched[0]?.name : matched.length + " programs"}`);
3151
- const compileResults = await Promise.all(projects.map((project) => compileProgram({
3152
- apiKey,
3153
- program: project.program,
3154
- libRs: project.libRs,
3155
- cargoToml: project.cargoToml,
3156
- idl: project.idl
3157
- })));
3158
- s.stop("Compilation completed");
3165
+ const compileSpinner = spinner();
3166
+ compileSpinner.start(`Compiling ${matched.length === 1 ? matched[0]?.name : matched.length + " programs"}`);
3167
+ let compileResults;
3168
+ try {
3169
+ compileResults = await Promise.all(projects.map((project) => compileProgram({
3170
+ apiKey,
3171
+ program: project.program,
3172
+ libRs: project.libRs,
3173
+ cargoToml: project.cargoToml,
3174
+ idl: project.idl
3175
+ })));
3176
+ compileSpinner.stop("Compilation completed");
3177
+ } catch (error) {
3178
+ compileSpinner.stop("Compilation failed");
3179
+ throw error;
3180
+ }
3159
3181
  for (const [i, project] of projects.entries()) {
3160
3182
  const result = compileResults[i];
3161
- printProgramSummary(project.program, cluster, outDir, options.verify);
3162
- const statusLabel = result.status === "success" ? "✓ Success" : `✗ ${result.status}`;
3163
3183
  const compileTime = `${(result.compileTimeMs / 1e3).toFixed(1)}s`;
3164
- log.info(`${statusLabel} compiled in ${compileTime}`);
3165
- if (result.status === "failed" && result.logs) log.info(`Compile logs:\n${result.logs}`);
3166
- log.step(`Explorer: https://explorer.solana.com/address/${project.program.address}?cluster=${cluster}`);
3167
- if (result.status === "success" && result.bytecode !== null) {
3168
- const soDir = join(outDir, project.program.name, "target", "deploy");
3169
- const soPath = join(soDir, `${project.program.name}.so`);
3170
- const programKeypairPath = cwdJoin(BETTER_SOL_DIR, `${project.program.name}.json`);
3171
- mkdirSync(soDir, { recursive: true });
3172
- writeFileSync(soPath, Buffer.from(result.bytecode, "base64"));
3173
- s.message(`Deploying ${project.program.name} to ${cluster}`);
3174
- const solanaPath = ensureSolanaCli();
3175
- try {
3176
- execSync(`"${solanaPath}" program deploy "${soPath}" --program-id "${programKeypairPath}" --keypair "${payerPath}" --url ${cluster}`, {
3177
- encoding: "utf8",
3178
- timeout: 12e4,
3179
- stdio: "pipe"
3180
- });
3181
- s.stop(`Deployed to ${cluster}`);
3182
- log.step(`Deployed: ${project.program.address}`);
3183
- } catch (error) {
3184
- s.stop("Deployment failed");
3185
- const message = error instanceof Error && "stderr" in error ? error.stderr.trim() : String(error);
3186
- throw new Error(`Deployment failed: ${message}`, { cause: error });
3187
- }
3184
+ log.info(`Program: ${project.program.name}`);
3185
+ log.step(`Address: ${project.program.address}`);
3186
+ if (result.status === "failed" || result.bytecode === null) {
3187
+ const logs = result.logs !== void 0 && result.logs.length > 0 ? `\n${result.logs}` : "";
3188
+ throw new Error(`Compilation failed for ${project.program.name}.${logs}`);
3189
+ }
3190
+ log.step(`Compiled: ${compileTime}`);
3191
+ const programKeypairPath = cwdJoin(BETTER_SOL_DIR, `${project.program.name}.json`);
3192
+ const solanaPath = ensureSolanaCli();
3193
+ const deploySpinner = spinner();
3194
+ deploySpinner.start(`Deploying ${project.program.name} to ${cluster}`);
3195
+ try {
3196
+ const signature = await deployCompiledProgram({
3197
+ bytecode: result.bytecode,
3198
+ programName: project.program.name,
3199
+ programKeypairPath,
3200
+ payerPath,
3201
+ cluster,
3202
+ solanaPath
3203
+ });
3204
+ deploySpinner.stop("Deployment completed");
3205
+ log.step(`Signature: ${signature}`);
3206
+ log.step(`Explorer: https://explorer.solana.com/address/${project.program.address}?cluster=${cluster}`);
3207
+ } catch (error) {
3208
+ deploySpinner.stop("Deployment failed");
3209
+ throw new Error(`Deployment failed: ${extractProcessErrorMessage(error)}`, { cause: error });
3188
3210
  }
3189
3211
  }
3190
3212
  if (options.verify) {
@@ -3194,6 +3216,44 @@ async function deploy(options) {
3194
3216
  }
3195
3217
  outro("Deploy complete.");
3196
3218
  }
3219
+ async function deployCompiledProgram(params) {
3220
+ const deployDir = await mkdtemp(join(tmpdir(), "better-sol-deploy-"));
3221
+ const soPath = join(deployDir, `${params.programName}.so`);
3222
+ try {
3223
+ writeFileSync(soPath, Buffer.from(params.bytecode, "base64"));
3224
+ const { stdout } = await execFileAsync(params.solanaPath, [
3225
+ "program",
3226
+ "deploy",
3227
+ soPath,
3228
+ "--program-id",
3229
+ params.programKeypairPath,
3230
+ "--keypair",
3231
+ params.payerPath,
3232
+ "--url",
3233
+ params.cluster
3234
+ ], {
3235
+ encoding: "utf8",
3236
+ timeout: 12e4
3237
+ });
3238
+ return extractDeploymentSignature(stdout);
3239
+ } finally {
3240
+ await rm(deployDir, {
3241
+ recursive: true,
3242
+ force: true
3243
+ });
3244
+ }
3245
+ }
3246
+ function extractDeploymentSignature(stdout) {
3247
+ return stdout.split("\n").map((line) => line.trim()).find((line) => line.startsWith("Signature:"))?.replace("Signature:", "").trim() ?? "submitted";
3248
+ }
3249
+ function extractProcessErrorMessage(error) {
3250
+ if (typeof error === "object" && error !== null) {
3251
+ const record = error;
3252
+ if (typeof record.stderr === "string" && record.stderr.trim().length > 0) return record.stderr.trim();
3253
+ if (typeof record.stdout === "string" && record.stdout.trim().length > 0) return record.stdout.trim();
3254
+ }
3255
+ return error instanceof Error ? error.message : String(error);
3256
+ }
3197
3257
  function resolvePayerPath(payerFlag, configPayer) {
3198
3258
  if (payerFlag !== void 0) return cwdPath(payerFlag);
3199
3259
  if (configPayer !== void 0) return configPayer;
@@ -69260,7 +69320,7 @@ async function gitCommit() {
69260
69320
  //#endregion
69261
69321
  //#region src/index.ts
69262
69322
  const cli = new Command();
69263
- cli.name("better-sol").description("TypeScript-first Solana program tooling — run with npx @better-sol/cli").version("0.1.0-alpha.6");
69323
+ cli.name("better-sol").description("TypeScript-first Solana program tooling — run with npx @better-sol/cli").version("0.1.0-alpha.7");
69264
69324
  cli.command("init").description("Initialize a better-sol project").option("--force", "overwrite existing files", false).option("--skip-install", "skip installing dependencies", false).action((options) => run(() => init(options)));
69265
69325
  cli.command("create").description("Create a new better-sol program").argument("[name]", "program name").option("--dir <dir>", "program directory", "programs").option("--force", "overwrite existing files", false).action((name, options) => run(() => create(name, options)));
69266
69326
  cli.command("login").description("Save your compiler API key").action(() => run(() => login()));