create-snappy 0.0.5 ā 0.0.11
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 +18 -35
- package/dist/index.js +134 -0
- package/package.json +41 -22
- package/scripts/create-snappy/cli.js +0 -514
package/README.md
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
The official
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Features
|
|
22
|
-
|
|
23
|
-
- **Guided Infrastructure Setup**: Automatically configs your `.env` for Supabase Postgres and S3 Storage.
|
|
24
|
-
- **Private Templates**: Secured GitHub OAuth flow for accessing proprietary premium templates.
|
|
25
|
-
- **Lightning Fast**: Shallow git cloning for instant scaffolding.
|
|
26
|
-
|
|
27
|
-
## Requirements
|
|
28
|
-
|
|
29
|
-
- `Node.js` >= 20.x
|
|
30
|
-
- `pnpm` >= 9.x
|
|
31
|
-
- Git
|
|
32
|
-
|
|
33
|
-
## License Requirement
|
|
34
|
-
|
|
35
|
-
The underlying engine (`@snappy-stack/core`) requires a valid SNAPPY Development License to function.
|
|
1
|
+
# create-snappy
|
|
2
|
+
|
|
3
|
+
The official SNAPPY Stack CLI for project initialization.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-snappy@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Project Scaffolding**: Quickly set up a new SNAPPY site.
|
|
14
|
+
- **Environment Preparation**: Automatic `.npmrc` and `.env` setup.
|
|
15
|
+
- **Git Integration**: Direct integration with SNAPPY repositories.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
Built with š for the SNAPPY Ecosystem.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import kleur from "kleur";
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log(kleur.cyan("create-snappy v0.0.11 by Wicky.ID"));
|
|
11
|
+
console.log(kleur.white("wicky.id/snappy \u2014 hi@wicky.id"));
|
|
12
|
+
console.log("\u2500".repeat(44) + "\n");
|
|
13
|
+
try {
|
|
14
|
+
execSync("pnpm --version", { stdio: "ignore" });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error(kleur.red("\u274C Error: pnpm is required to use SNAPPY CMS."));
|
|
17
|
+
console.log(kleur.white(" Please install it: npm i -g pnpm\n"));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const response = await prompts([
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
name: "projectName",
|
|
24
|
+
message: "What is your project named?",
|
|
25
|
+
initial: "snappy-project",
|
|
26
|
+
validate: (value) => value.length > 0 || "Project name is required"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "select",
|
|
30
|
+
name: "template",
|
|
31
|
+
message: "Choose a starting template:",
|
|
32
|
+
choices: [
|
|
33
|
+
{ title: "Artist / Karya", value: "artist", description: "Musicians, singers, creatives" },
|
|
34
|
+
{ title: "Portfolio", value: "portfolio", description: "Personal branding & projects" },
|
|
35
|
+
{ title: "Portfolio + Services", value: "portfolio-services", description: "Portfolio with lead capture" },
|
|
36
|
+
{ title: "Institution", value: "institution", description: "Schools, NGOs, Organization" },
|
|
37
|
+
{ title: "Store", value: "store", description: "Simple e-commerce primitives" }
|
|
38
|
+
],
|
|
39
|
+
initial: 0
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: "password",
|
|
43
|
+
name: "token",
|
|
44
|
+
message: "Enter your SNAPPY_TOKEN:",
|
|
45
|
+
validate: (value) => value.startsWith("eyJ") || "Invalid SNAPPY_TOKEN format"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
name: "domain",
|
|
50
|
+
message: "Primary domain (for heatmap/heartbeat):",
|
|
51
|
+
initial: "example.com"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "password",
|
|
55
|
+
name: "githubToken",
|
|
56
|
+
message: "Enter your GitHub Token (for cloning):",
|
|
57
|
+
validate: (value) => value.length > 0 || "GitHub Token is required"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "select",
|
|
61
|
+
name: "locale",
|
|
62
|
+
message: "Default locale:",
|
|
63
|
+
choices: [
|
|
64
|
+
{ title: "English (EN)", value: "en" },
|
|
65
|
+
{ title: "Bahasa Indonesia (ID)", value: "id" },
|
|
66
|
+
{ title: "Both (Default EN)", value: "en-id" }
|
|
67
|
+
],
|
|
68
|
+
initial: 0
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
if (!response.projectName) {
|
|
72
|
+
console.log(kleur.yellow("Installation cancelled."));
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
const targetDir = path.resolve(process.cwd(), response.projectName);
|
|
76
|
+
if (fs.existsSync(targetDir)) {
|
|
77
|
+
const { overwrite } = await prompts({
|
|
78
|
+
type: "confirm",
|
|
79
|
+
name: "overwrite",
|
|
80
|
+
message: `Directory ${response.projectName} already exists. Wipe and overwrite?`,
|
|
81
|
+
initial: false
|
|
82
|
+
});
|
|
83
|
+
if (!overwrite) {
|
|
84
|
+
console.log(kleur.red("Aborted."));
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
console.log(kleur.blue(`
|
|
90
|
+
\u{1F4E6} Cloning [${response.template}] template...`));
|
|
91
|
+
try {
|
|
92
|
+
const cloneUrl = `https://${response.githubToken}@github.com/Snappy-Stack/Snappy_Template`;
|
|
93
|
+
execSync(`git clone --depth 1 -b ${response.template} ${cloneUrl} "${targetDir}"`, { stdio: "inherit" });
|
|
94
|
+
fs.rmSync(path.join(targetDir, ".git"), { recursive: true, force: true });
|
|
95
|
+
const npmrcContent = `//npm.pkg.github.com/:_authToken=${response.githubToken}
|
|
96
|
+
@snappy:registry=https://npm.pkg.github.com`;
|
|
97
|
+
fs.writeFileSync(path.join(targetDir, ".npmrc"), npmrcContent);
|
|
98
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
99
|
+
if (fs.existsSync(pkgPath)) {
|
|
100
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
101
|
+
pkg.name = response.projectName.toLowerCase().replace(/\s+/g, "-");
|
|
102
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error(kleur.red(`\u274C Failed to clone template.`));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const envContent = `# SNAPPY CMS - Site Configuration
|
|
109
|
+
SNAPPY_TOKEN="${response.token}"
|
|
110
|
+
SNAPPY_URL="https://core.wicky.id"
|
|
111
|
+
PUBLIC_LOCALE="${response.locale}"
|
|
112
|
+
PRIMARY_DOMAIN="${response.domain}"
|
|
113
|
+
`;
|
|
114
|
+
fs.writeFileSync(path.join(targetDir, ".env"), envContent);
|
|
115
|
+
const metadata = {
|
|
116
|
+
projectName: response.projectName,
|
|
117
|
+
template: response.template,
|
|
118
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
119
|
+
version: "2.0.0"
|
|
120
|
+
};
|
|
121
|
+
fs.writeFileSync(path.join(targetDir, "snappy.json"), JSON.stringify(metadata, null, 2));
|
|
122
|
+
console.log(kleur.green("\n\u2705 Project initialized successfully!"));
|
|
123
|
+
console.log(`
|
|
124
|
+
Next steps:`);
|
|
125
|
+
console.log(kleur.cyan(` cd ${response.projectName}`));
|
|
126
|
+
console.log(kleur.cyan(` pnpm install`));
|
|
127
|
+
console.log(kleur.cyan(` pnpm dev`));
|
|
128
|
+
console.log("\n");
|
|
129
|
+
}
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
console.error(kleur.red(`
|
|
132
|
+
\u274C Installation failed: ${err.message}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
package/package.json
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-snappy",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "The official
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-snappy",
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"description": "The official SNAPPY Stack CLI for project initialization.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-snappy": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
12
|
+
"dev": "tsup src/index.ts --format esm --watch"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"snappy",
|
|
16
|
+
"cms",
|
|
17
|
+
"astro",
|
|
18
|
+
"cli",
|
|
19
|
+
"headless"
|
|
20
|
+
],
|
|
21
|
+
"author": "Wicky <hi@wicky.id>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://wicky.id/snappy",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/Snappy-Stack/Snappy_Stack"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@clack/prompts": "^0.7.0",
|
|
34
|
+
"prompts": "^2.4.2",
|
|
35
|
+
"kleur": "^4.1.5"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"typescript": "^5.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,514 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* create-snappy
|
|
5
|
-
* The official CLI to bootstrap the SNAPPY stack.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import fs from 'fs'
|
|
9
|
-
import path from 'path'
|
|
10
|
-
import { fileURLToPath } from 'url'
|
|
11
|
-
import { execSync } from 'child_process'
|
|
12
|
-
import readline from 'readline'
|
|
13
|
-
import os from 'os'
|
|
14
|
-
import crypto from 'crypto'
|
|
15
|
-
import { Command } from 'commander'
|
|
16
|
-
import prompts from 'prompts'
|
|
17
|
-
import pc from 'picocolors'
|
|
18
|
-
import ora from 'ora'
|
|
19
|
-
|
|
20
|
-
const SNAPPY_LOGO = `
|
|
21
|
-
āāāāāāāāāāāā āāā āāāāāā āāāāāāā āāāāāāā āāā āāā
|
|
22
|
-
āāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā āāāā
|
|
23
|
-
āāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāā āāāāāāā
|
|
24
|
-
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā āāāāāāā āāāāā
|
|
25
|
-
āāāāāāāāāāā āāāāāāāāā āāāāāā āāā āāā
|
|
26
|
-
āāāāāāāāāāā āāāāāāāā āāāāāā āāā āāā
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
30
|
-
const __dirname = path.dirname(__filename)
|
|
31
|
-
|
|
32
|
-
const CONFIG_DIR = path.join(os.homedir(), '.snappy')
|
|
33
|
-
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json')
|
|
34
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
35
|
-
|
|
36
|
-
// This is the Client ID for GitHub Device OAuth
|
|
37
|
-
const CLIENT_ID = 'Ov23liWHoGiMponaUxrc'
|
|
38
|
-
|
|
39
|
-
const rl = readline.createInterface({
|
|
40
|
-
input: process.stdin,
|
|
41
|
-
output: process.stdout,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const question = (query) => new Promise((resolve) => rl.question(query, resolve))
|
|
45
|
-
|
|
46
|
-
// --- Auth & Config Utilities ---
|
|
47
|
-
|
|
48
|
-
function getSavedToken() {
|
|
49
|
-
if (fs.existsSync(AUTH_FILE)) {
|
|
50
|
-
try {
|
|
51
|
-
const data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8'))
|
|
52
|
-
return data.access_token
|
|
53
|
-
} catch (e) {
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function saveToken(token) {
|
|
61
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
62
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
63
|
-
}
|
|
64
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify({ access_token: token }, null, 2))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getSavedConfig() {
|
|
68
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
69
|
-
try {
|
|
70
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
|
|
71
|
-
} catch (e) {
|
|
72
|
-
return {}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return {}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function saveConfig(config) {
|
|
79
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
80
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
81
|
-
}
|
|
82
|
-
const current = getSavedConfig()
|
|
83
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getMachineId() {
|
|
87
|
-
const machineIdFile = path.join(CONFIG_DIR, 'machine-id')
|
|
88
|
-
const hostname = os.hostname()
|
|
89
|
-
|
|
90
|
-
if (fs.existsSync(machineIdFile)) {
|
|
91
|
-
return {
|
|
92
|
-
id: fs.readFileSync(machineIdFile, 'utf8').trim(),
|
|
93
|
-
hostname
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Generate a stable ID based on hardware info
|
|
98
|
-
const platform = os.platform()
|
|
99
|
-
const arch = os.arch()
|
|
100
|
-
const cpus = os.cpus().length
|
|
101
|
-
|
|
102
|
-
const rawId = `${platform}-${arch}-${cpus}-${hostname}`
|
|
103
|
-
const hash = crypto.createHash('sha256').update(rawId).digest('hex').substring(0, 12)
|
|
104
|
-
|
|
105
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
106
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
107
|
-
}
|
|
108
|
-
fs.writeFileSync(machineIdFile, hash)
|
|
109
|
-
return { id: hash, hostname }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function githubLogin() {
|
|
113
|
-
console.log('\nš Authenticating with GitHub...')
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const response = await fetch('https://github.com/login/device/code', {
|
|
117
|
-
method: 'POST',
|
|
118
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
119
|
-
body: JSON.stringify({ client_id: CLIENT_ID, scope: 'repo read:user' }),
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
if (response.status === 404) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
'GitHub returned 404. This usually means the Client ID is invalid or the app is not registered.',
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const data = await response.json()
|
|
129
|
-
if (data.error) throw new Error(`Auth request failed: ${data.error_description || data.error}`)
|
|
130
|
-
|
|
131
|
-
const { device_code, user_code, verification_uri, interval, expires_in } = data
|
|
132
|
-
|
|
133
|
-
console.log(`\n1. Go to: \x1b[34m${verification_uri}\x1b[0m`)
|
|
134
|
-
console.log(`2. Enter code: \x1b[1m\x1b[32m${user_code}\x1b[0m`)
|
|
135
|
-
console.log(
|
|
136
|
-
`\nWaiting for authorization... (Expires in ${Math.floor(expires_in / 60)} minutes)`,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
const poll = async () => {
|
|
140
|
-
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
141
|
-
method: 'POST',
|
|
142
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
143
|
-
body: JSON.stringify({
|
|
144
|
-
client_id: CLIENT_ID,
|
|
145
|
-
device_code,
|
|
146
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
147
|
-
}),
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const tokenData = await res.json()
|
|
151
|
-
|
|
152
|
-
if (tokenData.error) {
|
|
153
|
-
if (tokenData.error === 'authorization_pending') {
|
|
154
|
-
await new Promise((r) => setTimeout(r, (interval || 5) * 1000))
|
|
155
|
-
return poll()
|
|
156
|
-
}
|
|
157
|
-
throw new Error(`Polling failed: ${tokenData.error_description || tokenData.error}`)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return tokenData.access_token
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const accessToken = await poll()
|
|
164
|
-
saveToken(accessToken)
|
|
165
|
-
console.log('ā
Successfully authenticated!')
|
|
166
|
-
return accessToken
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.warn(`\x1b[33m\nā ļø GitHub OAuth failed: ${err.message}\x1b[0m`)
|
|
169
|
-
console.log('\x1b[36mFalling back to manual token entry...\x1b[0m')
|
|
170
|
-
const manualToken = await question('Paste your GitHub Personal Access Token (PAT): ')
|
|
171
|
-
|
|
172
|
-
if (!manualToken || manualToken.trim() === '') {
|
|
173
|
-
throw new Error('No token provided. Installation aborted.')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
saveToken(manualToken.trim())
|
|
177
|
-
console.log('ā
Token saved manually.')
|
|
178
|
-
return manualToken.trim()
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// --- Main CLI ---
|
|
183
|
-
|
|
184
|
-
function getPackageManager() {
|
|
185
|
-
return 'pnpm'
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function main() {
|
|
189
|
-
console.log(pc.cyan(SNAPPY_LOGO))
|
|
190
|
-
console.log(pc.cyan('š Welcome to the SNAPPY Stack Installer! (v0.0.1)'))
|
|
191
|
-
console.log('------------------------------------------')
|
|
192
|
-
|
|
193
|
-
const program = new Command()
|
|
194
|
-
program
|
|
195
|
-
.name('create-snappy')
|
|
196
|
-
.description('The official installer for the SNAPPY stack. (Private Access Required)')
|
|
197
|
-
.argument('[project-name]', 'Name of the project directory')
|
|
198
|
-
.option('-t, --template <type>', 'Template to use (e.g., portfolio)')
|
|
199
|
-
.option('--login', 'Login with GitHub')
|
|
200
|
-
.option('--guided', 'Force guided setup for environment variables')
|
|
201
|
-
.option('--license <key>', 'SNAPPY License Key')
|
|
202
|
-
.option('--name <name>', 'Project name')
|
|
203
|
-
.option('--description <desc>', 'Project description')
|
|
204
|
-
.option('--author <name>', 'Author name')
|
|
205
|
-
.parse(process.argv)
|
|
206
|
-
|
|
207
|
-
const options = program.opts()
|
|
208
|
-
let providedName = options.name || program.args[0]
|
|
209
|
-
|
|
210
|
-
if (options.login) {
|
|
211
|
-
try {
|
|
212
|
-
await githubLogin()
|
|
213
|
-
rl.close()
|
|
214
|
-
process.exit(0)
|
|
215
|
-
} catch (e) {
|
|
216
|
-
console.error(pc.red(e.message))
|
|
217
|
-
rl.close()
|
|
218
|
-
process.exit(1)
|
|
219
|
-
}
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const savedConfig = getSavedConfig()
|
|
224
|
-
const hasSavedConfig = Object.keys(savedConfig).length > 0
|
|
225
|
-
|
|
226
|
-
// INTERACTIVE PROMPTS
|
|
227
|
-
const questions = []
|
|
228
|
-
|
|
229
|
-
if (!providedName) {
|
|
230
|
-
questions.push({
|
|
231
|
-
type: 'text',
|
|
232
|
-
name: 'projectName',
|
|
233
|
-
message: 'What is your project named?',
|
|
234
|
-
initial: 'my-snappy-app',
|
|
235
|
-
validate: (value) => (value.length > 0 ? true : 'Please enter a project name.'),
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!options.description) {
|
|
240
|
-
questions.push({
|
|
241
|
-
type: 'text',
|
|
242
|
-
name: 'projectDescription',
|
|
243
|
-
message: 'Project description?',
|
|
244
|
-
initial: 'A fresh SNAPPY app',
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (!options.author) {
|
|
249
|
-
questions.push({
|
|
250
|
-
type: 'text',
|
|
251
|
-
name: 'authorName',
|
|
252
|
-
message: 'Author name?',
|
|
253
|
-
initial: savedConfig.authorName || 'Wicky',
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (!options.template) {
|
|
258
|
-
questions.push({
|
|
259
|
-
type: 'select',
|
|
260
|
-
name: 'template',
|
|
261
|
-
message: 'Which template would you like to use?',
|
|
262
|
-
choices: [
|
|
263
|
-
{ title: 'Portfolio', description: 'A sleek portfolio template', value: 'portfolio' },
|
|
264
|
-
],
|
|
265
|
-
initial: 0,
|
|
266
|
-
})
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (!options.license) {
|
|
270
|
-
questions.push({
|
|
271
|
-
type: 'password',
|
|
272
|
-
name: 'snappyLicenseKey',
|
|
273
|
-
message: 'SNAPPY License Key (leave blank for trial)?',
|
|
274
|
-
initial: savedConfig.snappyLicenseKey || '',
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Guided Setup: (Cloudflare R2 is now automated via Snappy Core)
|
|
279
|
-
const needsGuided = false;
|
|
280
|
-
|
|
281
|
-
const response = await prompts(questions, {
|
|
282
|
-
onCancel: () => {
|
|
283
|
-
console.log(pc.yellow('Installation cancelled.'))
|
|
284
|
-
process.exit(0)
|
|
285
|
-
},
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// Merge responses with saved config
|
|
289
|
-
const config = {
|
|
290
|
-
...savedConfig,
|
|
291
|
-
authorName: options.author || response.authorName || savedConfig.authorName,
|
|
292
|
-
snappyLicenseKey: options.license || response.snappyLicenseKey || savedConfig.snappyLicenseKey,
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Save if it was a guided session or forced
|
|
296
|
-
if (needsGuided) {
|
|
297
|
-
saveConfig(config)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const projectName = providedName || response.projectName
|
|
301
|
-
const projectDescription = options.description || response.projectDescription
|
|
302
|
-
const authorName = config.authorName
|
|
303
|
-
const selectedTemplate = options.template || response.template || 'portfolio'
|
|
304
|
-
|
|
305
|
-
const targetDir = path.resolve(process.cwd(), projectName)
|
|
306
|
-
|
|
307
|
-
// Target directory check (but don't create yet to keep it empty for git clone)
|
|
308
|
-
if (fs.existsSync(targetDir)) {
|
|
309
|
-
const { overwrite } = await prompts({
|
|
310
|
-
type: 'confirm',
|
|
311
|
-
name: 'overwrite',
|
|
312
|
-
message: pc.yellow(`Directory ${projectName} already exists. Overwrite?`),
|
|
313
|
-
initial: false,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
if (!overwrite) {
|
|
317
|
-
console.log('Aborted.')
|
|
318
|
-
process.exit(0)
|
|
319
|
-
}
|
|
320
|
-
fs.rmSync(targetDir, { recursive: true, force: true })
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let finalLicenseToken = config.snappyLicenseKey;
|
|
324
|
-
let isTrial = false;
|
|
325
|
-
let { id: machineId, hostname } = getMachineId();
|
|
326
|
-
let machineIdToSave = machineId;
|
|
327
|
-
let skippedLicense = false;
|
|
328
|
-
|
|
329
|
-
if (!isTrial && (!finalLicenseToken || finalLicenseToken.trim() === '')) {
|
|
330
|
-
const trialResponse = await prompts({
|
|
331
|
-
type: 'confirm',
|
|
332
|
-
name: 'startTrial',
|
|
333
|
-
message: 'šÆ Start 2-hour free trial?',
|
|
334
|
-
initial: true
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
if (trialResponse.startTrial) {
|
|
338
|
-
console.log(pc.cyan('\nā³ Setting up 2-hour free trial...'));
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const res = await fetch('https://snappycore.wicky.id/api/trial', {
|
|
342
|
-
method: 'POST',
|
|
343
|
-
headers: { 'Content-Type': 'application/json' },
|
|
344
|
-
body: JSON.stringify({ machineId, hostname })
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const data = await res.json();
|
|
348
|
-
if (res.ok && data.token) {
|
|
349
|
-
finalLicenseToken = data.token;
|
|
350
|
-
isTrial = true;
|
|
351
|
-
machineIdToSave = machineId;
|
|
352
|
-
} else {
|
|
353
|
-
console.error(pc.red(`\nā Trial generation failed: ${data.error || 'Unknown error'}`));
|
|
354
|
-
console.log(pc.gray('The installer cannot proceed without a valid trial license.'));
|
|
355
|
-
process.exit(1);
|
|
356
|
-
}
|
|
357
|
-
} catch (err) {
|
|
358
|
-
console.error(pc.red(`\nā Could not reach licensing server: ${err.message}`));
|
|
359
|
-
process.exit(1);
|
|
360
|
-
}
|
|
361
|
-
} else {
|
|
362
|
-
skippedLicense = true;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// (Removed premature file writing)
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
// 1. Initialize from repository
|
|
370
|
-
const repoUrl = 'https://github.com/snappy-stack/snappy.git'
|
|
371
|
-
const cloneSpinner = ora(`Cloning template (${selectedTemplate})...`).start()
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
// Use inherit to see actual errors if it fails
|
|
375
|
-
cloneSpinner.stop()
|
|
376
|
-
execSync(`git clone --depth 1 -b ${selectedTemplate} ${repoUrl} "${targetDir}"`, {
|
|
377
|
-
stdio: 'inherit',
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
// Template already has correct payload.config.ts ā no patching needed.
|
|
381
|
-
console.log(pc.green('ā Project cloned successfully.'))
|
|
382
|
-
} catch (err) {
|
|
383
|
-
console.error(pc.red("\nā Installation failed during project cloning."))
|
|
384
|
-
console.error(pc.gray(`Template: ${selectedTemplate}`))
|
|
385
|
-
console.error(pc.gray(`Repository: ${repoUrl}`))
|
|
386
|
-
throw err
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Now safe to write metadata files
|
|
390
|
-
if (machineIdToSave) {
|
|
391
|
-
fs.writeFileSync(path.join(targetDir, '.snappy-machine-id'), machineIdToSave);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (fs.existsSync(path.join(targetDir, '.git'))) {
|
|
395
|
-
fs.rmSync(path.join(targetDir, '.git'), { recursive: true, force: true })
|
|
396
|
-
}
|
|
397
|
-
console.log(pc.green('ā
Project initialized from secure source.'))
|
|
398
|
-
|
|
399
|
-
// 2. Customize package.json and README.md
|
|
400
|
-
console.log('\nš Customizing project files...')
|
|
401
|
-
const pkgPath = path.join(targetDir, 'package.json')
|
|
402
|
-
if (fs.existsSync(pkgPath)) {
|
|
403
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
404
|
-
pkg.name = projectName.toLowerCase().replace(/\s+/g, '-')
|
|
405
|
-
pkg.description = projectDescription || pkg.description
|
|
406
|
-
pkg.author = authorName || pkg.author
|
|
407
|
-
if (pkg.bin) delete pkg.bin
|
|
408
|
-
|
|
409
|
-
// FIX LEXICAL MISMATCH - FORCE 0.41.0
|
|
410
|
-
if (pkg.dependencies) {
|
|
411
|
-
const targetLexical = "0.35.0";
|
|
412
|
-
console.log(pc.yellow(`š Forcing Lexical dependencies to ${targetLexical}...`));
|
|
413
|
-
|
|
414
|
-
// Fix main dependency
|
|
415
|
-
pkg.dependencies.lexical = targetLexical;
|
|
416
|
-
|
|
417
|
-
// Update @snappy-stack/core to latest
|
|
418
|
-
if (pkg.dependencies['@snappy-stack/core']) {
|
|
419
|
-
pkg.dependencies['@snappy-stack/core'] = '^0.1.11';
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Fix all @lexical/* sub-dependencies
|
|
423
|
-
Object.keys(pkg.dependencies).forEach(dep => {
|
|
424
|
-
if (dep.startsWith('@lexical/')) {
|
|
425
|
-
pkg.dependencies[dep] = targetLexical;
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// Add overrides for pnpm/npm to be absolutely sure
|
|
430
|
-
pkg.overrides = { ...pkg.overrides, lexical: targetLexical };
|
|
431
|
-
pkg.resolutions = { ...pkg.resolutions, lexical: targetLexical }; // For yarn
|
|
432
|
-
pkg.pnpm = {
|
|
433
|
-
...pkg.pnpm,
|
|
434
|
-
overrides: { ...pkg.pnpm?.overrides, lexical: targetLexical }
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// CLEANUP LOCKFILES
|
|
439
|
-
['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'].forEach(lock => {
|
|
440
|
-
const lockPath = path.join(targetDir, lock);
|
|
441
|
-
if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const readmePath = path.join(targetDir, 'README.md')
|
|
448
|
-
if (fs.existsSync(readmePath)) {
|
|
449
|
-
const readmeContent = `# ${projectName}\n\n${projectDescription}\n\nGenerated with \`create-snappy\`.`
|
|
450
|
-
fs.writeFileSync(readmePath, readmeContent)
|
|
451
|
-
}
|
|
452
|
-
console.log(pc.green('ā
Project details injected.'))
|
|
453
|
-
|
|
454
|
-
// 3. Configure .env
|
|
455
|
-
console.log('\nš ļø Configuring environment variables...')
|
|
456
|
-
|
|
457
|
-
const finalLicenseToken = config.snappyLicenseKey;
|
|
458
|
-
|
|
459
|
-
const payloadSecret = crypto.randomBytes(32).toString('hex')
|
|
460
|
-
|
|
461
|
-
let envContent = `# SNAPPY STACK - Zero-Config Environment
|
|
462
|
-
SNAPPY_LICENSE_TOKEN="${finalLicenseToken || ''}"
|
|
463
|
-
# SNAPPY_API_URL is hardcoded in @snappy-stack/core
|
|
464
|
-
|
|
465
|
-
# Payload CMS
|
|
466
|
-
PAYLOAD_SECRET="${payloadSecret}"
|
|
467
|
-
PUBLIC_FRONTEND_URL="http://localhost:3000"
|
|
468
|
-
`
|
|
469
|
-
fs.writeFileSync(path.join(targetDir, '.env'), envContent)
|
|
470
|
-
console.log(pc.green('ā
.env generated.\n'))
|
|
471
|
-
|
|
472
|
-
if (process.env.SKIP_INSTALL !== 'true') {
|
|
473
|
-
const pm = getPackageManager()
|
|
474
|
-
const installCmd = pm === 'npm' ? 'npm install' : `${pm} install`
|
|
475
|
-
const installSpinner = ora(`Installing dependencies using ${pm}...`).start()
|
|
476
|
-
try {
|
|
477
|
-
execSync(installCmd, { cwd: targetDir, stdio: 'ignore' })
|
|
478
|
-
installSpinner.succeed('Dependencies installed successfully.')
|
|
479
|
-
} catch (e) {
|
|
480
|
-
installSpinner.fail('Failed to install dependencies.')
|
|
481
|
-
console.warn(pc.yellow(`Warning: ${installCmd} failed.`))
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const pmRun = getPackageManager() === 'npm' ? 'npm run dev' : `${getPackageManager()} run dev`
|
|
486
|
-
|
|
487
|
-
if (skippedLicense) {
|
|
488
|
-
console.log(pc.red('\n āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
489
|
-
console.log(` š ${pc.bold(pc.white('Add your license key to .env'))}`);
|
|
490
|
-
console.log(pc.gray(' SNAPPY_LICENSE_TOKEN=sk_snappy_...'));
|
|
491
|
-
console.log('');
|
|
492
|
-
console.log(` š” ${pc.cyan('Get your key: wicky.id')}`);
|
|
493
|
-
console.log(` š ${pc.cyan('Docs: snappycore.wicky.id')}`);
|
|
494
|
-
console.log(pc.red(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
495
|
-
console.log(pc.bold(pc.red('\n THERE IS NO MERCY IN PRODUCTION š¹ \n')));
|
|
496
|
-
} else if (isTrial) {
|
|
497
|
-
console.log(pc.green('\nā
2-hour free trial active! Your trial license has been added to .env.'));
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
console.log(pc.green('\nā
SNAPPY Stack is perfectly prepared and ready to launch!'))
|
|
501
|
-
console.log(`\nNext steps:\n cd ${pc.bold(projectName)}\n ${pmRun}\n`)
|
|
502
|
-
|
|
503
|
-
} catch (err) {
|
|
504
|
-
console.error(pc.red(`Installation failed: ${err.message || err}`))
|
|
505
|
-
} finally {
|
|
506
|
-
if (rl) rl.close()
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
main().catch((err) => {
|
|
511
|
-
console.error(pc.red(err))
|
|
512
|
-
if (rl) rl.close()
|
|
513
|
-
process.exit(1)
|
|
514
|
-
})
|