import { parseArgs } from "@std/cli/parse-args"; import { join, dirname } from "@std/path"; import { ensureDir } from "@std/fs"; import { cyan, green, bold, dim } from "@std/fmt/colors"; import { logger } from "../utils/logger.ts"; import { getTemplatesForName, AVAILABLE_TEMPLATES } from "../templates/embedded.ts"; import { generateSliceName, generateDomain } from "../utils/name_generator.ts"; import { createAuthenticatedClient } from "../utils/client.ts"; import { ConfigManager } from "../auth/config.ts"; import { pullCommand } from "./lexicon/pull.ts"; import { pushCommand } from "./lexicon/push.ts"; import { codegenCommand } from "./codegen.ts"; import { dasherize } from "../utils/strings.ts"; import { SYSTEM_SLICE_URI, REFERENCE_SLICE_URI, DEFAULT_API_URL, DEFAULT_AIP_BASE_URL, } from "../utils/constants.ts"; export async function initCommand(args: string[], _globalArgs: unknown) { const parsed = parseArgs(args, { string: ["name", "template"], boolean: ["help"], alias: { n: "name", h: "help", t: "template", }, default: { template: "deno-ssr", }, }); if (parsed.help) { showInitHelp(); return; } // Validate template name const templateName = parsed.template as string; if (!AVAILABLE_TEMPLATES.includes(templateName)) { logger.error(`Invalid template: ${templateName}`); logger.info(`Available templates: ${AVAILABLE_TEMPLATES.join(", ")}`); Deno.exit(1); } // Check if we're inside an existing project const currentDir = Deno.cwd(); const projectIndicators = ["deno.json", "slices.json", ".git"]; for (const indicator of projectIndicators) { try { await Deno.stat(join(currentDir, indicator)); logger.error( `Cannot initialize a new project here - found ${indicator} in current directory.` ); logger.info( "Please run this command from outside your project directory." ); Deno.exit(1); } catch { // File doesn't exist, which is what we want } } let projectName = parsed.name || (parsed._[0] as string); let wasNameGenerated = false; // If no project name provided, generate a random one if (!projectName) { projectName = generateSliceName(); wasNameGenerated = true; console.log( `\n${cyan("📦 No project name provided, generated one for you:")}` ); console.log(` Project name: ${bold(projectName)}`); // Ask for confirmation const proceed = confirm("\nDo you want to proceed with this name?"); if (!proceed) { logger.info( "Initialization cancelled. Run 'slices init ' to specify your own name." ); return; } } else { // Dasherize user-provided name projectName = dasherize(projectName); } // Validate project name if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) { logger.error( "Project name can only contain letters, numbers, hyphens, and underscores" ); Deno.exit(1); } const targetDir = join(Deno.cwd(), projectName); // Check if directory already exists try { const stat = await Deno.stat(targetDir); if (stat.isDirectory) { logger.error(`Directory '${projectName}' already exists`); Deno.exit(1); } } catch { // Directory doesn't exist, which is what we want } logger.info(`Creating project: ${projectName}`); // Try to create a slice with the same name let sliceUri: string | undefined; let sliceDomain: string | undefined; let oauthClientId: string | undefined; let oauthClientSecret: string | undefined; let oauthRedirectUri: string | undefined; const config = new ConfigManager(); await config.load(); if (config.isAuthenticated()) { try { // If we generated the name, use it for the domain too // Otherwise, prompt for domain if (wasNameGenerated) { sliceDomain = `network.slices.${projectName}`; } else { // User provided a name, ask for domain console.log(`\n${cyan("🌐 Setting up your slice domain:")}`); console.log( ` ${dim("Format: com.example (reverse domain notation)")}` ); console.log(` ${dim("Leave blank to generate one automatically")}`); const userDomain = prompt( "\nEnter your domain (or press Enter to generate):" ); if (userDomain && userDomain.trim()) { // Validate domain format (basic check for at least one dot) if (!userDomain.includes(".")) { logger.error( "Domain must contain at least one dot (e.g., com.example)" ); Deno.exit(1); } sliceDomain = userDomain.trim(); } else { // Generate a domain sliceDomain = generateDomain(); console.log(` Generated domain: ${bold(sliceDomain)}`); } } console.log(`\n${cyan("🌐 Creating a slice on the Slices network:")}`); console.log(` Slice name: ${bold(projectName)}`); console.log(` Domain: ${bold(sliceDomain)}`); console.log( ` ${dim("This will be your unique namespace for collections")}` ); const createSlice = confirm("\nCreate this slice?"); if (!createSlice) { logger.info( "Skipping slice creation. You can create one later at https://slices.network" ); } else { logger.info("Creating slice..."); const client = await createAuthenticatedClient( SYSTEM_SLICE_URI, DEFAULT_API_URL ); // Check if domain already exists in the system slice const existingSlices = await client.network.slices.slice.getRecords({ where: { domain: { eq: sliceDomain } }, limit: 1, }); if (existingSlices.records.length > 0) { logger.error(`A slice with domain '${sliceDomain}' already exists`); logger.info("Please try again with a different name or domain"); Deno.exit(1); } const result = await client.network.slices.slice.createRecord({ name: projectName, domain: sliceDomain, createdAt: new Date().toISOString(), }); sliceUri = result.uri; logger.success(`Created slice: ${sliceUri}`); // Ask if they want to create OAuth client const createOAuth = confirm( "\nWould you like to create OAuth credentials for this slice?" ); if (createOAuth) { logger.info("Creating OAuth client..."); // Default redirect URI for local development const redirectUri = "http://localhost:8080/oauth/callback"; try { const oauthResult = await client.network.slices.slice.createOAuthClient({ sliceUri: sliceUri, clientName: `${projectName} Development`, redirectUris: [redirectUri], grantTypes: ["authorization_code", "refresh_token"], responseTypes: ["code"], scope: "profile openid atproto transition:generic", }); // Store OAuth credentials to add to .env later oauthClientId = oauthResult.clientId; oauthClientSecret = oauthResult.clientSecret; oauthRedirectUri = redirectUri; logger.success("Created OAuth client!"); console.log(` Client ID: ${cyan(oauthResult.clientId)}`); console.log( ` ${dim("Client secret will be added to your .env file")}` ); } catch (oauthError) { const err = oauthError as Error; logger.warn(`Could not create OAuth client: ${err.message}`); logger.info( "You can create OAuth credentials later at https://slices.network" ); } } } } catch (error) { const err = error as Error; logger.warn(`Could not create slice: ${err.message}`); logger.info("You can create a slice later at https://slices.network"); } } else { logger.info("Not authenticated - skipping slice creation"); logger.info( "Run 'slices login' to authenticate, then create a slice at https://slices.network" ); } try { // Create target directory await ensureDir(targetDir); // Extract embedded templates await extractEmbeddedTemplates(targetDir, projectName, templateName); // Update .env.example with slice URI if we created one if (sliceUri) { const envExamplePath = join(targetDir, ".env.example"); try { let envContent = await Deno.readTextFile(envExamplePath); envContent = envContent.replace( /SLICE_URI=.*/, `SLICE_URI="${sliceUri}"` ); await Deno.writeTextFile(envExamplePath, envContent); } catch { // Ignore if can't update .env.example } } // Create .env file with OAuth credentials if we have them if (oauthClientId && oauthClientSecret) { const envPath = join(targetDir, ".env"); const envExamplePath = join(targetDir, ".env.example"); try { // Read the example file as a template let envContent = await Deno.readTextFile(envExamplePath); // Replace with actual values if (sliceUri) { envContent = envContent.replace( /SLICE_URI=.*/, `SLICE_URI="${sliceUri}"` ); } envContent = envContent.replace( /OAUTH_CLIENT_ID=.*/, `OAUTH_CLIENT_ID="${oauthClientId}"` ); envContent = envContent.replace( /OAUTH_CLIENT_SECRET=.*/, `OAUTH_CLIENT_SECRET="${oauthClientSecret}"` ); envContent = envContent.replace( /OAUTH_REDIRECT_URI=.*/, `OAUTH_REDIRECT_URI="${oauthRedirectUri}"` ); envContent = envContent.replace( /OAUTH_AIP_BASE_URL=.*/, `OAUTH_AIP_BASE_URL="${DEFAULT_AIP_BASE_URL}"` ); envContent = envContent.replace( /API_URL=.*/, `API_URL="${DEFAULT_API_URL}"` ); // Write the actual .env file await Deno.writeTextFile(envPath, envContent); logger.success("Created .env file with your OAuth credentials"); } catch (error) { const err = error as Error; logger.warn(`Could not create .env file: ${err.message}`); } } // Create slices.json file with actual slice URI if (sliceUri) { const slicesJsonPath = join(targetDir, "slices.json"); const slicesConfig = { slice: sliceUri, lexiconPath: "./lexicons", clientOutputPath: "./src/generated_client.ts", }; try { await Deno.writeTextFile( slicesJsonPath, JSON.stringify(slicesConfig, null, 2) + "\n" ); logger.success("Created slices.json with your slice configuration"); } catch (error) { const err = error as Error; logger.warn(`Could not create slices.json file: ${err.message}`); } } // Pull lexicons from the base slice w/ Bsky Profile Lexicons try { logger.info("Pulling base lexicons..."); // Change to the project directory and run pull const originalCwd = Deno.cwd(); Deno.chdir(targetDir); // Pull from the base Slices platform slice which has all the proper lexicons await pullCommand(["--slice", REFERENCE_SLICE_URI], {}); logger.success("Pulled base lexicons"); // If we created a slice, push the lexicons to it if (sliceUri) { logger.info("Pushing lexicons to your slice..."); await pushCommand([], {}); logger.success("Pushed lexicons to your slice"); } // Generate TypeScript client from lexicons logger.info("Generating TypeScript client..."); await codegenCommand([], {}); logger.success("Generated TypeScript client"); // Change back to original directory Deno.chdir(originalCwd); } catch (error) { const err = error as Error; logger.warn(`Could not pull lexicons: ${err.message}`); // Create empty lexicons folder as fallback const lexiconsPath = join(targetDir, "lexicons"); try { await ensureDir(lexiconsPath); await Deno.writeTextFile(join(lexiconsPath, ".gitkeep"), ""); } catch { // Ignore if we can't create the folder } } // Initialize git repository try { const gitInit = new Deno.Command("git", { args: ["init"], cwd: targetDir, stdout: "piped", stderr: "piped", }); const result = await gitInit.output(); if (result.success) { logger.success("Initialized git repository"); } else { const stderr = new TextDecoder().decode(result.stderr); logger.warn(`Could not initialize git repository: ${stderr}`); } } catch (error) { const err = error as Error; logger.warn(`Could not initialize git repository: ${err.message}`); } logger.success(`Created project: ${projectName}`); console.log(`\n${green("✨ Your project is ready!")}\n`); console.log(`${bold("Next steps:")}`); console.log(` 1. cd ${cyan(projectName)}`); if (oauthClientId && oauthClientSecret) { console.log(` 2. deno task dev`); console.log(` ${dim(" Your .env file is already configured!")}`); } else { console.log(` 2. cp .env.example .env`); console.log(` 3. ${dim("# Add your OAuth credentials to .env")}`); console.log(` 4. deno task dev`); } if (sliceUri && sliceDomain) { console.log(`\n${bold("Your slice details:")}`); console.log(` Name: ${cyan(projectName)}`); console.log(` Domain: ${cyan(sliceDomain)}`); console.log(` URI: ${dim(sliceUri)}`); } console.log(`\n${bold("Available commands:")}`); console.log(` ${cyan("deno task dev")} Start development server`); console.log(` ${cyan("deno task start")} Start production server`); console.log(` ${cyan("deno fmt")} Format code`); } catch (error) { const err = error as Error; logger.error("Failed to create project:", err.message); Deno.exit(1); } } async function extractEmbeddedTemplates( targetDir: string, projectName: string, templateName: string ) { try { const templates = getTemplatesForName(templateName); if (templates.size === 0) { throw new Error(`No templates found for: ${templateName}`); } for (const [relativePath, content] of templates.entries()) { const fullPath = join(targetDir, relativePath); const dir = dirname(fullPath); // Ensure directory exists await ensureDir(dir); // Write file await Deno.writeFile(fullPath, content); } // Process template files for variable replacement await processTemplateFiles(targetDir, projectName); logger.info(`Template files extracted and processed (${templateName})`); } catch (error) { const err = error as Error; logger.error("Failed to extract templates:", err.message); throw error; } } async function processTemplateFiles(targetDir: string, projectName: string) { // Process files that need variable replacement const filesToProcess = ["README.md", "src/config.ts"]; for (const file of filesToProcess) { const filePath = join(targetDir, file); try { const content = await Deno.readTextFile(filePath); const processed = content.replace(/{{PROJECT_NAME}}/g, projectName); await Deno.writeTextFile(filePath, processed); } catch (error) { const err = error as Error; logger.warn(`Could not process template file ${file}:`, err.message); } } } function showInitHelp() { console.log(` Initialize a new Deno project with OAuth authentication USAGE: slices init Create project with specified name slices init Create project with random generated name slices init --name Create project with specified name slices init --template Specify template to use ARGUMENTS: Name of the project to create (optional) OPTIONS: -n, --name Project name -t, --template Template to use (default: deno-ssr) Available: ${AVAILABLE_TEMPLATES.join(", ")} -h, --help Show this help message EXAMPLES: slices init my-app Creates "my-app" project with deno-ssr template slices init Creates project with random name FEATURES: • Automatically creates a matching slice (if authenticated) • Generates domain in format: network.slices. • Updates .env.example with slice URI • Deno SSR with Preact and JSX • OAuth authentication with PKCE flow • Session management with SQLite • HTMX for interactive components • Tailwind CSS for styling • Feature-based architecture `); }