Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "ckb/spore-contract"]
path = ckb/spore-contract
url = https://github.com/sporeprotocol/spore-contract.git
[submodule "ckb/ckb-js-vm"]
path = ckb/ckb-js-vm
url = https://github.com/nervosnetwork/ckb-js-vm.git
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: all omnilock anyone-can-pay xudt spore
.PHONY: all omnilock anyone-can-pay xudt spore ckb-js-vm

all: omnilock anyone-can-pay xudt spore
all: omnilock anyone-can-pay xudt spore ckb-js-vm

omnilock:
@echo "Building omnilock via submodule"
Expand Down Expand Up @@ -28,3 +28,8 @@ spore:
cp ckb/spore-contract/build/release/cluster_agent ckb/devnet/specs/spore-scripts/
cp ckb/spore-contract/build/release/cluster_proxy ckb/devnet/specs/spore-scripts/
cp ckb/spore-contract/build/release/spore_extension_lua ckb/devnet/specs/spore-scripts/

ckb-js-vm:
@echo "Building ckb-js-vm via submodule"
cd ckb/ckb-js-vm && git submodule update --init && make all
cp ckb/ckb-js-vm/build/ckb-js-vm ckb/devnet/specs/
1 change: 1 addition & 0 deletions ckb/ckb-js-vm
Submodule ckb-js-vm added at ba38a6
Binary file added ckb/devnet/specs/ckb-js-vm
Binary file not shown.
3 changes: 3 additions & 0 deletions ckb/devnet/specs/dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ create_type_id = false
[[genesis.system_cells]]
file = { file = "spore/spore_extension_lua" }
create_type_id = false
[[genesis.system_cells]]
file = { file = "ckb-js-vm" }
create_type_id = true

[genesis.system_cells_lock]
code_hash = "0x0000000000000000000000000000000000000000000000000000000000000000"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"build",
"account",
"ckb",
"templates/v3/offckb.config.example.ts"
"templates/v3/offckb.config.example.ts",
"templates/v4"
],
"private": false,
"publishConfig": {
Expand Down
26 changes: 10 additions & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DepositOptions, deposit } from './cmd/deposit';
import { DeployOptions, deploy } from './cmd/deploy';
import { TransferOptions, transfer } from './cmd/transfer';
import { BalanceOption, balanceOf } from './cmd/balance';
import { create, CreateOption, createScriptProject, createDAppProject } from './cmd/create';
import { createScriptProject, CreateScriptProjectOptions } from './cmd/create';
import { Config, ConfigItem } from './cmd/config';
import { debugSingleScript, debugTransaction, parseSingleScriptOption } from './cmd/debug';
import { printSystemScripts } from './cmd/system-scripts';
Expand All @@ -25,21 +25,15 @@ const program = new Command();
program.name('offckb').description(description).version(version);

program
.command('create [your-project-name]')
.description('Create a new dApp from bare templates')
.option('-s, --script', 'Only create the script project')
.option('-d, --dapp', 'Only create the ccc dapp project')
.action(async (projectName: string, option: CreateOption) => {
const name = projectName ?? 'my-first-ckb-project';
if (option.script) {
return await createScriptProject(name);
}

if (option.dapp) {
return await createDAppProject(name);
}

return create(name);
.command('create [project-name]')
.description('Create a new CKB Smart Contract project in JavaScript.')
.option('-m, --manager <manager>', 'Specify the package manager to use (npm, yarn, pnpm)')
.option('-l, --language <language>', 'Specify the language to use (typescript, javascript)')
.option('--no-interactive', 'Disable interactive prompts')
.option('--no-install', 'Skip dependency installation')
.option('--no-git', 'Skip git repository initialization')
.action(async (projectName: string, options: CreateScriptProjectOptions) => {
return await createScriptProject(projectName, options);
});

program
Expand Down
223 changes: 151 additions & 72 deletions src/cmd/create.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,166 @@
import path from 'path';
import { findFileInFolder } from '../util/fs';
import { injectConfig } from './inject-config';
import { confirm } from '@inquirer/prompts';
import { execSync } from 'child_process';
import { genMyScriptsJsonFile, genSystemScriptsJsonFile } from '../scripts/gen';
import { OffCKBConfigFile } from '../template/offckb-config';
const version = require('../../package.json').version;

export type ScriptOnly = {
script: true;
dapp?: false;
};

export type DappOnly = {
dapp: true;
script?: false;
};

export type CreateOption = ScriptOnly | DappOnly;

export function createScriptProject(name: string) {
const cmd = `pnpm create ckb-js-vm-app ${name}`;
try {
execSync(cmd, { encoding: 'utf-8', stdio: 'inherit' });
} catch (error: unknown) {
console.error('create script project failed, ', (error as Error).message);
}
}
import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';
import { TemplateProcessor } from '../templates/processor';
import { PackageManagerDetector } from '../templates/package-manager';
import { InteractivePrompts } from '../templates/prompts';
import { genSystemScriptsJsonFile } from '../scripts/gen';

export async function createDAppProject(name: string) {
const cmd = `npx create-ccc-app@latest ${name} --ts --}`;
try {
execSync(cmd, { encoding: 'utf-8', stdio: 'inherit' });
const dappFolderPath = path.resolve(process.cwd(), name);
await askForInjectOffckbConfig(dappFolderPath);
} catch (error: unknown) {
console.error('create ccc-appp project failed, ', (error as Error).message);
}
export interface CreateScriptProjectOptions {
manager?: 'pnpm' | 'yarn' | 'npm';
language?: 'typescript' | 'javascript' | 'ts' | 'js';
interactive?: boolean;
noInstall?: boolean;
noGit?: boolean;
}

export async function createFullstackProject(name: string) {
createScriptProject(name);
console.log("Now let's create the dapp part..");
const cmd = `pnpm create create-ccc-app@latest ${name}/packages/dapp --ts --}`;
export async function createScriptProject(name?: string, options: CreateScriptProjectOptions = {}) {
console.log(chalk.blue('🚀 Creating CKB JavaScript VM project...\n'));

try {
execSync(cmd, { encoding: 'utf-8', stdio: 'inherit' });
const dappFolderPath = path.resolve(process.cwd(), name, 'packages', 'dapp');
await askForInjectOffckbConfig(dappFolderPath);
return dappFolderPath;
// Initialize services
const prompts = new InteractivePrompts();
const packageManagerDetector = new PackageManagerDetector();

// Collect project information
const projectInfo = await prompts.collectProjectInfo(
name, // Pass the original name to let prompts handle path parsing in interactive mode
options.language,
options.manager,
options.interactive !== false, // Interactive by default, unless explicitly disabled
);

// Parse project name and path from the collected project info
const { projectName, projectPath } = parseProjectNameAndPath(projectInfo.projectName);

// Override install/git options if provided
if (options.noInstall) projectInfo.installDeps = false;
if (options.noGit) projectInfo.initGit = false;

// Update the project info with the correct project name (without path)
projectInfo.projectName = projectName;

// Use the parsed project path instead of just the name
const fullProjectPath = path.resolve(projectPath);

// Check if directory already exists
if (fs.existsSync(fullProjectPath)) {
console.error(chalk.red(`❌ Directory '${projectPath}' already exists!`));
process.exit(1);
}

console.log(chalk.gray(`📝 Project details:`));
console.log(chalk.gray(` Name: ${projectInfo.projectName}`));
console.log(chalk.gray(` Language: ${projectInfo.language}`));
console.log(chalk.gray(` Package Manager: ${projectInfo.packageManager}`));
console.log(chalk.gray(` Path: ${fullProjectPath}\n`));

// Try to find the template directory
const possiblePaths = [
path.join(__dirname, '../../templates/v4/base-template'), // from built files
path.join(process.cwd(), 'templates/v4/base-template'), // from project root
path.join(__dirname, '../../../templates/v4/base-template'), // alternative build location
path.join(__dirname, '../templates/v4/base-template'), // from source files
];

// Get template directory (adjust path based on whether we're running from source or built)
const templateDir = possiblePaths.find((p) => fs.existsSync(p)) || possiblePaths[0];

if (!fs.existsSync(templateDir)) {
console.error(chalk.red(`❌ Template directory not found: ${templateDir}`));
process.exit(1);
}

// Initialize template processor
const processor = new TemplateProcessor(templateDir);

// Generate project
console.log(chalk.blue('📦 Generating project files...'));
await processor.generateProject(fullProjectPath, projectInfo);

// Generate system-scripts.json
console.log(chalk.blue('🔧 Generating system scripts configuration...'));
try {
const systemScriptsPath = path.join(fullProjectPath, 'deployment', 'system-scripts.json');
genSystemScriptsJsonFile(systemScriptsPath);
console.log(chalk.green('✅ System scripts configuration generated successfully'));
} catch (error) {
console.warn(chalk.yellow('⚠️ Failed to generate system scripts configuration.'));
console.warn(
chalk.gray(
' You can generate it manually later with: offckb system-scripts -o deployment/system-scripts.json',
),
);
}

// Install dependencies
if (projectInfo.installDeps) {
console.log(chalk.blue('\n📥 Installing dependencies...'));
try {
packageManagerDetector.installDependencies(fullProjectPath, projectInfo.packageManager);
} catch (error) {
console.warn(chalk.yellow('⚠️ Failed to install dependencies. You can install them manually later.'));
console.warn(chalk.gray(` Run: cd ${projectPath} && ${projectInfo.packageManager} install`));
}
}

// Initialize git repository
if (projectInfo.initGit) {
console.log(chalk.blue('\n🔧 Initializing git repository...'));
try {
packageManagerDetector.initializeGit(fullProjectPath);
} catch (error) {
console.warn(chalk.yellow('⚠️ Failed to initialize git repository.'));
}
}

// Success message
console.log(chalk.green('\n🎉 Project created successfully!\n'));

console.log(chalk.bold('📖 Next steps:'));
console.log(chalk.gray(` 1. cd ${projectPath}`));

if (!projectInfo.installDeps) {
console.log(chalk.gray(` 2. ${projectInfo.packageManager} install`));
console.log(chalk.gray(` 3. ${projectInfo.packageManager} run build`));
console.log(chalk.gray(` 4. ${projectInfo.packageManager} test`));
} else {
console.log(chalk.gray(` 2. ${projectInfo.packageManager} run build`));
console.log(chalk.gray(` 3. ${projectInfo.packageManager} test`));
}

console.log(chalk.gray(`\n💡 To add a new contract:`));
console.log(chalk.gray(` ${projectInfo.packageManager} run add-contract <contract-name>`));
} catch (error: unknown) {
console.error('create fullstack appp project failed, ', (error as Error).message);
console.error(chalk.red('\n❌ Failed to create project:'), (error as Error).message);
process.exit(1);
}
}

export async function create(name: string) {
const dappFolderPath = await createFullstackProject(name);
/**
* Parse project name and path from the input string.
* Supports formats like:
* - "my-project" -> name: "my-project", path: "./my-project"
* - "path/to/my-project" -> name: "my-project", path: "./path/to/my-project"
* - "path\\to\\my-project" -> name: "my-project", path: "./path/to/my-project"
*/
export function parseProjectNameAndPath(input?: string): { projectName: string; projectPath: string } {
if (!input) {
return { projectName: 'my-ckb-project', projectPath: './my-ckb-project' };
}

// update the version
const targetConfigPath = findFileInFolder(dappFolderPath, 'offckb.config.ts');
if (targetConfigPath) {
OffCKBConfigFile.updateVersion(version, targetConfigPath);
const contractInfoFolder = OffCKBConfigFile.readContractInfoFolder(targetConfigPath);
if (!contractInfoFolder) {
throw new Error('No contract info folder found in offckb.config.ts!');
}
const normalizedInput = input.trim();

const systemJsonFilePath = path.resolve(contractInfoFolder, 'system-scripts.json');
genSystemScriptsJsonFile(systemJsonFilePath);
// Normalize path separators to forward slashes for consistency
const normalizedPath = normalizedInput.replace(/\\/g, '/');

const myScriptsJsonFilePath = path.resolve(contractInfoFolder, 'my-scripts.json');
genMyScriptsJsonFile(myScriptsJsonFilePath);
} else {
console.log("Couldn't find the offckb config file in project. abort.");
// If input contains path separators, extract the project name from the last part
if (normalizedPath.includes('/')) {
const projectName = path.basename(normalizedPath);
const projectPath = normalizedPath;
return { projectName, projectPath };
}
}

export async function askForInjectOffckbConfig(dappFolderPath: string) {
const answer = await confirm({
message: 'Do you want to inject offckb configs in your project so that it can work with local blockchain info?',
});
if (answer) {
const target = path.resolve(dappFolderPath, 'offckb.config.ts');
injectConfig({ target });
}
// If it's just a name, use it as both name and path
return { projectName: normalizedInput, projectPath: `./${normalizedInput}` };
}
46 changes: 0 additions & 46 deletions src/cmd/inject-config.ts

This file was deleted.

Loading
Loading