tackl 1.0.10 → 2.2.0
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 -60
- package/bin/tackl.js +275 -173
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,75 +1,33 @@
|
|
|
1
1
|
# Tackl CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g tackl
|
|
9
|
-
```
|
|
3
|
+
Scaffolds [Tackl](https://github.com/12-studio/tackl) — the animation-first Next.js starter kit — **into the current directory**. Made for the "empty git repo" workflow: create your repo, clone it, run one command.
|
|
10
4
|
|
|
11
5
|
## Usage
|
|
12
6
|
|
|
13
7
|
```bash
|
|
14
|
-
|
|
8
|
+
mkdir my-app && cd my-app # or clone your empty repo and cd in
|
|
9
|
+
bunx tackl # or: bun create tackl / npm create tackl@latest
|
|
15
10
|
```
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- `--ref <branch>` - Template ref (branch or tag) (default: main)
|
|
20
|
-
- `--no-install` - Skip dependency installation
|
|
21
|
-
- `--no-git` - Skip git initialization
|
|
22
|
-
- `--help` - Show help message
|
|
12
|
+
You'll be asked which CMS to use — **DatoCMS**, **Sanity**, or **None** — and the scaffold is pruned to match: only the chosen adapter, its dependencies, its docs, and its `.env.example` block survive.
|
|
23
13
|
|
|
24
|
-
|
|
14
|
+
Choosing **Sanity** sets the whole thing up: an embedded Studio at `/studio`, example schemas, and draft-mode preview routes ship in the scaffold, and the CLI offers to run `sanity init` for you — logging in, creating/linking a project, and writing your `.env`. Scaffold → `bun run dev` → `/studio` → start editing.
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
# Create a new project
|
|
28
|
-
tackl my-app
|
|
29
|
-
|
|
30
|
-
# Create with specific branch
|
|
31
|
-
tackl my-app --ref develop
|
|
32
|
-
|
|
33
|
-
# Skip installation and git
|
|
34
|
-
tackl my-app --no-install --no-git
|
|
35
|
-
```
|
|
16
|
+
On your first `bun run dev`, a **setup wizard** greets you in the browser: it walks through every theme token (colours, spacing, motion, fonts, the type scale), writes your answers into `src/theme`, then deletes itself — skip it to keep the starter defaults. The scaffold commit the CLI makes is your restore point if you ever want to run it again.
|
|
36
17
|
|
|
37
|
-
## What
|
|
18
|
+
## What it does
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
- 📱 **Responsive Design** - Mobile-first approach
|
|
44
|
-
- 🔧 **TypeScript** - Full type safety
|
|
45
|
-
- 🎯 **Performance** - Optimized for speed
|
|
46
|
-
- 📦 **Modern Tooling** - ESLint, Prettier, Husky
|
|
20
|
+
1. Verifies the directory is empty (an existing `.git`, `.gitignore`, `README.md` or `LICENSE` is fine — your repo's `.git` and remote are kept)
|
|
21
|
+
2. Downloads the latest tagged release of the template (falls back to `main` if none)
|
|
22
|
+
3. Prunes repo-only cargo (the CLI itself, `.github`) and the unused CMS adapter
|
|
23
|
+
4. Sets your package name, initialises git if needed, and runs `bun install`
|
|
47
24
|
|
|
48
|
-
##
|
|
25
|
+
## Options
|
|
49
26
|
|
|
50
|
-
### Waffl Grid System
|
|
51
|
-
|
|
52
|
-
- Smart breakpoint inheritance (only specify what changes!)
|
|
53
|
-
- Responsive grid columns
|
|
54
|
-
- Semantic components
|
|
55
|
-
- CSS custom properties
|
|
56
|
-
|
|
57
|
-
### Example Usage
|
|
58
|
-
|
|
59
|
-
```jsx
|
|
60
|
-
<Waffl>
|
|
61
|
-
<Section $s='1/-1' $l='3/9'>
|
|
62
|
-
// Mobile: full width, Desktop: 3/9 // Tablet inherits mobile (full
|
|
63
|
-
width) Content
|
|
64
|
-
</Section>
|
|
65
|
-
</Waffl>
|
|
66
27
|
```
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
## License
|
|
74
|
-
|
|
75
|
-
MIT
|
|
28
|
+
--cms <dato|sanity|none> Skip the CMS prompt
|
|
29
|
+
--name <name> Package name (default: directory name)
|
|
30
|
+
--ref <tag|branch> Template ref (default: latest release)
|
|
31
|
+
--no-install Skip dependency installation
|
|
32
|
+
--no-git Skip git init (an existing .git is always kept)
|
|
33
|
+
```
|
package/bin/tackl.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import path from 'path';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
7
|
import prompts from 'prompts';
|
|
8
8
|
import * as tar from 'tar';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = path.dirname(__filename);
|
|
13
9
|
|
|
14
10
|
const OWNER = '12-studio';
|
|
15
11
|
const REPO = 'tackl';
|
|
16
|
-
const
|
|
12
|
+
const FALLBACK_REF = 'main';
|
|
17
13
|
|
|
18
14
|
const log = {
|
|
19
15
|
info: m => console.log(pc.cyan('•'), m),
|
|
@@ -21,11 +17,10 @@ const log = {
|
|
|
21
17
|
warn: m => console.log(pc.yellow('▲'), m),
|
|
22
18
|
error: m => console.log(pc.red('✖'), m),
|
|
23
19
|
step: m => console.log(pc.blue('→'), m),
|
|
24
|
-
success: m => console.log(pc.green('🎉'), m),
|
|
25
20
|
};
|
|
26
21
|
|
|
27
22
|
const showBanner = () => {
|
|
28
|
-
const purple = str => `\x1b[38;5;99m${str}\x1b[0m`;
|
|
23
|
+
const purple = str => `\x1b[38;5;99m${str}\x1b[0m`;
|
|
29
24
|
console.log(
|
|
30
25
|
pc.bold(
|
|
31
26
|
purple(`
|
|
@@ -40,221 +35,303 @@ const showBanner = () => {
|
|
|
40
35
|
║ ║
|
|
41
36
|
║ The Ultimate Next.js Starter Kit ║
|
|
42
37
|
║ ║
|
|
43
|
-
╚═════════════════════════════════════════════════╝
|
|
38
|
+
╚═════════════════════════════════════════════════╝
|
|
44
39
|
`)
|
|
45
40
|
)
|
|
46
41
|
);
|
|
47
42
|
};
|
|
48
43
|
|
|
44
|
+
// SECTION • Directory checks
|
|
45
|
+
// NOTE • The CLI scaffolds INTO the current directory — run it inside an
|
|
46
|
+
// empty (or freshly-initialised) git repo. These files are allowed to
|
|
47
|
+
// already exist without counting as "not empty".
|
|
48
|
+
const ALLOWED_EXISTING = new Set(['.git', '.gitignore', 'README.md', 'LICENSE', '.DS_Store']);
|
|
49
|
+
|
|
50
|
+
const dirIsScaffoldable = dir => {
|
|
51
|
+
if (!fs.existsSync(dir)) return true;
|
|
52
|
+
return fs.readdirSync(dir).every(entry => ALLOWED_EXISTING.has(entry));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// SECTION • Template ref resolution
|
|
56
|
+
// NOTE • Prefer the latest GitHub release so every scaffold is a tested,
|
|
57
|
+
// reproducible snapshot; fall back to main (with a warning) when no
|
|
58
|
+
// release exists. --ref overrides both.
|
|
59
|
+
const resolveRef = async explicitRef => {
|
|
60
|
+
if (explicitRef) return { ref: explicitRef, source: 'flag' };
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`https://api.github.com/repos/${OWNER}/${REPO}/releases/latest`, {
|
|
63
|
+
headers: { accept: 'application/vnd.github+json' },
|
|
64
|
+
});
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
const json = await res.json();
|
|
67
|
+
if (json?.tag_name) return { ref: json.tag_name, source: 'release' };
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return { ref: FALLBACK_REF, source: 'fallback' };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// SECTION • Pruning
|
|
74
|
+
const rmrf = (root, rel) => {
|
|
75
|
+
const p = path.join(root, rel);
|
|
76
|
+
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const stripEnvBlock = (root, marker) => {
|
|
80
|
+
const p = path.join(root, '.env.example');
|
|
81
|
+
if (!fs.existsSync(p)) return;
|
|
82
|
+
const lines = fs.readFileSync(p, 'utf8').split('\n');
|
|
83
|
+
const out = [];
|
|
84
|
+
let skipping = false;
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.startsWith(marker)) {
|
|
87
|
+
skipping = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (skipping && line.trim() === '') {
|
|
91
|
+
skipping = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!skipping) out.push(line);
|
|
95
|
+
}
|
|
96
|
+
fs.writeFileSync(p, out.join('\n'));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const stripDeps = (root, deps) => {
|
|
100
|
+
const p = path.join(root, 'package.json');
|
|
101
|
+
if (!fs.existsSync(p)) return;
|
|
102
|
+
const json = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
103
|
+
for (const d of deps) {
|
|
104
|
+
delete json.dependencies?.[d];
|
|
105
|
+
delete json.devDependencies?.[d];
|
|
106
|
+
}
|
|
107
|
+
fs.writeFileSync(p, `${JSON.stringify(json, null, '\t')}\n`);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// NOTE • Everything that exists solely for the embedded Sanity Studio
|
|
111
|
+
const SANITY_STUDIO_PATHS = ['app/(studio)', 'app/api/draft-mode', 'sanity.config.ts', 'sanity'];
|
|
112
|
+
const SANITY_DEPS = ['next-sanity', 'sanity', '@sanity/vision'];
|
|
113
|
+
|
|
114
|
+
const CMS = {
|
|
115
|
+
dato: {
|
|
116
|
+
title: 'DatoCMS',
|
|
117
|
+
prune: root => {
|
|
118
|
+
rmrf(root, 'src/cms/sanity');
|
|
119
|
+
rmrf(root, 'docs/Sanity');
|
|
120
|
+
for (const p of SANITY_STUDIO_PATHS) rmrf(root, p);
|
|
121
|
+
stripDeps(root, SANITY_DEPS);
|
|
122
|
+
stripEnvBlock(root, '# CMS — Sanity');
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
sanity: {
|
|
126
|
+
title: 'Sanity',
|
|
127
|
+
prune: root => {
|
|
128
|
+
rmrf(root, 'src/cms/dato');
|
|
129
|
+
rmrf(root, 'docs/DatoCMS');
|
|
130
|
+
stripDeps(root, ['@datocms/cda-client', 'react-datocms', 'graphql', 'graphql-tag']);
|
|
131
|
+
stripEnvBlock(root, '# CMS — DatoCMS');
|
|
132
|
+
// Rewire the adapter entry to the kept adapter
|
|
133
|
+
const entry = path.join(root, 'src/cms/index.ts');
|
|
134
|
+
if (fs.existsSync(entry)) {
|
|
135
|
+
fs.writeFileSync(entry, fs.readFileSync(entry, 'utf8').replace(`'./dato'`, `'./sanity'`));
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
none: {
|
|
140
|
+
title: 'No CMS',
|
|
141
|
+
prune: root => {
|
|
142
|
+
rmrf(root, 'src/cms');
|
|
143
|
+
rmrf(root, 'docs/Sanity');
|
|
144
|
+
rmrf(root, 'docs/DatoCMS');
|
|
145
|
+
rmrf(root, 'docs/CMS.md');
|
|
146
|
+
for (const p of SANITY_STUDIO_PATHS) rmrf(root, p);
|
|
147
|
+
stripDeps(root, [...SANITY_DEPS, '@datocms/cda-client', 'react-datocms', 'graphql', 'graphql-tag']);
|
|
148
|
+
stripEnvBlock(root, '# CMS — Sanity');
|
|
149
|
+
stripEnvBlock(root, '# CMS — DatoCMS');
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// NOTE • Repo-only cargo that should never land in a scaffolded project
|
|
155
|
+
const REPO_CARGO = ['tackl', '.github', 'bun.lock'];
|
|
156
|
+
|
|
49
157
|
async function main() {
|
|
50
158
|
const args = process.argv.slice(2);
|
|
51
159
|
|
|
52
|
-
// Handle help flag
|
|
53
160
|
if (args.includes('--help') || args.includes('-h')) {
|
|
54
161
|
console.log(`
|
|
55
162
|
${pc.bold('Tackl CLI')} - The ultimate Next.js starter kit
|
|
56
163
|
|
|
57
164
|
${pc.bold('Usage:')}
|
|
58
|
-
|
|
165
|
+
Run inside an empty directory (or a freshly-initialised git repo):
|
|
59
166
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
--no-git Skip git initialization (default: init)
|
|
63
|
-
-h, --help Show this help message
|
|
64
|
-
|
|
65
|
-
${pc.bold('Examples:')}
|
|
66
|
-
tackl my-app # Creates project with npm install + git init
|
|
67
|
-
tackl my-app --no-install # Creates project with git init only
|
|
68
|
-
tackl my-app --no-git # Creates project with npm install only
|
|
69
|
-
tackl my-app --no-install --no-git # Creates project only
|
|
167
|
+
mkdir my-app && cd my-app
|
|
168
|
+
bunx tackl
|
|
70
169
|
|
|
71
|
-
${pc.bold('
|
|
170
|
+
${pc.bold('Options:')}
|
|
171
|
+
--cms <dato|sanity|none> Skip the CMS prompt
|
|
172
|
+
--name <name> Package name (default: the directory name)
|
|
173
|
+
--ref <tag|branch> Template ref (default: latest release)
|
|
174
|
+
--no-install Skip dependency installation
|
|
175
|
+
--no-git Skip git init (existing .git is always kept)
|
|
176
|
+
-h, --help Show this help message
|
|
177
|
+
|
|
178
|
+
${pc.bold('Repository:')} https://github.com/${OWNER}/${REPO}
|
|
72
179
|
`);
|
|
73
180
|
process.exit(0);
|
|
74
181
|
}
|
|
75
182
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name: argName,
|
|
80
|
-
ref: DEFAULT_REF, // Always use main branch
|
|
81
|
-
install: !args.includes('--no-install'), // Default: true (install)
|
|
82
|
-
git: !args.includes('--no-git'), // Default: true (init git)
|
|
183
|
+
const flagValue = name => {
|
|
184
|
+
const i = args.indexOf(name);
|
|
185
|
+
return i !== -1 ? args[i + 1] : undefined;
|
|
83
186
|
};
|
|
84
187
|
|
|
85
|
-
|
|
86
|
-
const promptQuestions = [
|
|
87
|
-
{
|
|
88
|
-
type: initial.name ? null : 'text',
|
|
89
|
-
name: 'name',
|
|
90
|
-
message: 'Project directory',
|
|
91
|
-
initial: 'my-tackl-app',
|
|
92
|
-
validate: v =>
|
|
93
|
-
/^[a-z0-9\-._]+$/.test(v)
|
|
94
|
-
? true
|
|
95
|
-
: 'Use lowercase letters, numbers, - . _',
|
|
96
|
-
},
|
|
97
|
-
].filter(q => q.type !== null);
|
|
188
|
+
const targetDir = process.cwd();
|
|
98
189
|
|
|
99
|
-
|
|
100
|
-
onCancel: () => {
|
|
101
|
-
log.warn('Cancelled.');
|
|
102
|
-
process.exit(1);
|
|
103
|
-
},
|
|
104
|
-
onSubmit: () => {
|
|
105
|
-
// Clean up terminal
|
|
106
|
-
process.stdout.write('\x1b[?25h');
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const projectName = (answers.name || initial.name).trim();
|
|
111
|
-
const ref = initial.ref; // Always use main branch
|
|
112
|
-
const doInstall = initial.install; // Default: true
|
|
113
|
-
const doGit = initial.git; // Default: true
|
|
114
|
-
|
|
115
|
-
// Validate project name
|
|
116
|
-
if (!projectName) {
|
|
117
|
-
log.error('Project name is required.');
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const targetDir = path.resolve(process.cwd(), projectName);
|
|
122
|
-
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
190
|
+
if (!dirIsScaffoldable(targetDir)) {
|
|
123
191
|
log.error(
|
|
124
|
-
|
|
192
|
+
'This directory is not empty. Run tackl inside an empty directory (a .git folder, .gitignore, README.md and LICENSE are fine).'
|
|
125
193
|
);
|
|
126
194
|
process.exit(1);
|
|
127
195
|
}
|
|
128
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
129
196
|
|
|
130
197
|
showBanner();
|
|
131
|
-
console.log(pc.bold(pc.white('🚀 Starting project creation...\n')));
|
|
132
198
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
199
|
+
// SECTION • Gather choices
|
|
200
|
+
const cmsFlag = flagValue('--cms');
|
|
201
|
+
if (cmsFlag && !CMS[cmsFlag]) {
|
|
202
|
+
log.error(`Unknown --cms value "${cmsFlag}". Use dato, sanity or none.`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
136
205
|
|
|
137
|
-
const
|
|
138
|
-
|
|
206
|
+
const answers = await prompts(
|
|
207
|
+
[
|
|
208
|
+
{
|
|
209
|
+
type: cmsFlag ? null : 'select',
|
|
210
|
+
name: 'cms',
|
|
211
|
+
message: 'Which CMS should this project use?',
|
|
212
|
+
choices: [
|
|
213
|
+
{ title: 'DatoCMS', value: 'dato' },
|
|
214
|
+
{ title: 'Sanity', value: 'sanity' },
|
|
215
|
+
{ title: 'None', value: 'none' },
|
|
216
|
+
],
|
|
217
|
+
initial: 0,
|
|
218
|
+
},
|
|
219
|
+
].filter(q => q.type !== null),
|
|
220
|
+
{
|
|
221
|
+
onCancel: () => {
|
|
222
|
+
log.warn('Cancelled.');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
);
|
|
139
227
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
228
|
+
const cms = cmsFlag || answers.cms;
|
|
229
|
+
const rawName = flagValue('--name') || path.basename(targetDir);
|
|
230
|
+
const projectName =
|
|
231
|
+
rawName
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/[^a-z0-9\-._]/g, '-')
|
|
234
|
+
.replace(/^[-.]+|[-.]+$/g, '') || 'tackl-app';
|
|
235
|
+
const doInstall = !args.includes('--no-install');
|
|
236
|
+
const doGit = !args.includes('--no-git');
|
|
237
|
+
const hadGit = fs.existsSync(path.join(targetDir, '.git'));
|
|
145
238
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
149
|
-
fs.writeFileSync(tmpTar, buffer);
|
|
239
|
+
const { ref, source } = await resolveRef(flagValue('--ref'));
|
|
240
|
+
if (source === 'fallback') log.warn(`No release found — using ${FALLBACK_REF} (untagged).`);
|
|
150
241
|
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
console.log();
|
|
243
|
+
log.step(`Scaffolding ${pc.bold(pc.cyan(projectName))} in ${pc.bold(targetDir)}`);
|
|
244
|
+
log.step(`Template: ${pc.bold(pc.yellow(`${OWNER}/${REPO}@${ref}`))} · CMS: ${pc.bold(CMS[cms].title)}`);
|
|
245
|
+
console.log();
|
|
246
|
+
|
|
247
|
+
// NOTE • If the repo already has its own LICENSE, that choice wins —
|
|
248
|
+
// stash it and restore it after extraction (the template ships MIT)
|
|
249
|
+
const licensePath = path.join(targetDir, 'LICENSE');
|
|
250
|
+
const existingLicense = fs.existsSync(licensePath) ? fs.readFileSync(licensePath, 'utf8') : null;
|
|
251
|
+
|
|
252
|
+
// SECTION • Download + extract
|
|
253
|
+
// NOTE • TACKL_TEMPLATE_TAR points at a local tarball — used by tests/CI
|
|
254
|
+
const tmpTar = process.env.TACKL_TEMPLATE_TAR || path.join(os.tmpdir(), `tackl-${Date.now()}.tgz`);
|
|
255
|
+
|
|
256
|
+
if (!process.env.TACKL_TEMPLATE_TAR) {
|
|
257
|
+
const tarUrl = `https://codeload.github.com/${OWNER}/${REPO}/tar.gz/${encodeURIComponent(ref)}`;
|
|
258
|
+
try {
|
|
259
|
+
log.step('📥 Downloading template...');
|
|
260
|
+
const res = await fetch(tarUrl);
|
|
261
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
262
|
+
fs.writeFileSync(tmpTar, Buffer.from(await res.arrayBuffer()));
|
|
263
|
+
log.ok('Template downloaded!');
|
|
264
|
+
} catch (e) {
|
|
265
|
+
log.error(`Download failed: ${e.message}`);
|
|
266
|
+
log.error('Please check your internet connection and try again.');
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
156
269
|
}
|
|
157
270
|
|
|
158
271
|
try {
|
|
159
272
|
log.step('📦 Extracting files...');
|
|
160
273
|
await tar.x({ file: tmpTar, cwd: targetDir, strip: 1 });
|
|
161
|
-
log.ok('Files extracted
|
|
274
|
+
log.ok('Files extracted!');
|
|
162
275
|
} catch (e) {
|
|
163
276
|
log.error(`Extraction failed: ${e.message}`);
|
|
164
|
-
log.error('Please try again or check if the template is valid.');
|
|
165
277
|
process.exit(1);
|
|
166
278
|
} finally {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
279
|
+
if (!process.env.TACKL_TEMPLATE_TAR) {
|
|
280
|
+
try {
|
|
281
|
+
fs.unlinkSync(tmpTar);
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
170
284
|
}
|
|
171
285
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
} catch {}
|
|
187
|
-
|
|
188
|
-
// Patch package name
|
|
189
|
-
log.step('📝 Updating project configuration...');
|
|
190
|
-
const pkg = path.join(targetDir, 'package.json');
|
|
191
|
-
if (fs.existsSync(pkg)) {
|
|
286
|
+
// SECTION • Prune
|
|
287
|
+
log.step('🧹 Pruning template...');
|
|
288
|
+
if (existingLicense !== null) {
|
|
289
|
+
fs.writeFileSync(licensePath, existingLicense);
|
|
290
|
+
log.info('Kept your existing LICENSE.');
|
|
291
|
+
}
|
|
292
|
+
for (const cargo of REPO_CARGO) rmrf(targetDir, cargo);
|
|
293
|
+
for (const lock of ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml']) rmrf(targetDir, lock);
|
|
294
|
+
CMS[cms].prune(targetDir);
|
|
295
|
+
log.ok(`Template pruned for ${CMS[cms].title}.`);
|
|
296
|
+
|
|
297
|
+
// SECTION • Patch package.json
|
|
298
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
299
|
+
if (fs.existsSync(pkgPath)) {
|
|
192
300
|
try {
|
|
193
|
-
const json = JSON.parse(fs.readFileSync(
|
|
194
|
-
json.name = projectName
|
|
195
|
-
|
|
196
|
-
|
|
301
|
+
const json = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
302
|
+
json.name = projectName;
|
|
303
|
+
json.version = '0.1.0';
|
|
304
|
+
delete json.repository;
|
|
305
|
+
delete json.author;
|
|
306
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(json, null, '\t')}\n`);
|
|
307
|
+
log.ok('package.json updated.');
|
|
197
308
|
} catch (e) {
|
|
198
309
|
log.warn(`Could not update package.json: ${e.message}`);
|
|
199
310
|
}
|
|
200
311
|
}
|
|
201
312
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
const npmrcPath = path.join(targetDir, '.npmrc');
|
|
205
|
-
fs.writeFileSync(npmrcPath, 'package-lock=true\n');
|
|
206
|
-
log.step('📝 Created .npmrc to ensure npm usage');
|
|
207
|
-
} catch (e) {
|
|
208
|
-
log.warn(`Could not create .npmrc: ${e.message}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (doGit) {
|
|
313
|
+
// SECTION • Git
|
|
314
|
+
if (doGit && !hadGit) {
|
|
212
315
|
try {
|
|
213
|
-
log.step('🔧
|
|
316
|
+
log.step('🔧 Initialising git repository...');
|
|
214
317
|
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
|
|
215
|
-
|
|
216
|
-
// Set default branch to main (not master)
|
|
217
318
|
execSync('git branch -M main', { cwd: targetDir, stdio: 'ignore' });
|
|
218
|
-
|
|
219
319
|
execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
|
|
220
|
-
execSync('git commit -m "chore: scaffold with tackl"', {
|
|
221
|
-
|
|
222
|
-
stdio: 'ignore',
|
|
223
|
-
});
|
|
224
|
-
log.ok('Git repository initialized!');
|
|
320
|
+
execSync('git commit -m "chore: scaffold with tackl" --no-verify', { cwd: targetDir, stdio: 'ignore' });
|
|
321
|
+
log.ok('Git repository initialised!');
|
|
225
322
|
} catch (e) {
|
|
226
323
|
log.warn(`Git init failed: ${e.message}`);
|
|
227
324
|
}
|
|
325
|
+
} else if (hadGit) {
|
|
326
|
+
log.info('Existing .git kept — review and commit the scaffold yourself.');
|
|
228
327
|
}
|
|
229
328
|
|
|
329
|
+
// SECTION • Install
|
|
230
330
|
if (doInstall) {
|
|
231
331
|
try {
|
|
232
|
-
log.step('📦 Installing dependencies with
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const originalEnv = { ...process.env };
|
|
236
|
-
process.env.HUSKY = '0';
|
|
237
|
-
|
|
238
|
-
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
|
|
239
|
-
|
|
240
|
-
// Restore environment
|
|
241
|
-
process.env = originalEnv;
|
|
242
|
-
|
|
243
|
-
// Try to fix audit issues automatically
|
|
244
|
-
try {
|
|
245
|
-
log.step('🔧 Fixing security vulnerabilities...');
|
|
246
|
-
execSync('npm audit fix', { cwd: targetDir, stdio: 'inherit' });
|
|
247
|
-
log.ok('Security vulnerabilities fixed!');
|
|
248
|
-
} catch (e) {
|
|
249
|
-
log.warn(
|
|
250
|
-
'Some vulnerabilities could not be automatically fixed.'
|
|
251
|
-
);
|
|
252
|
-
log.info(
|
|
253
|
-
'You can run "npm audit fix --force" manually if needed.'
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
log.ok('Dependencies installed successfully!');
|
|
332
|
+
log.step('📦 Installing dependencies with bun...');
|
|
333
|
+
execSync('bun install', { cwd: targetDir, stdio: 'inherit', env: { ...process.env, HUSKY: '0' } });
|
|
334
|
+
log.ok('Dependencies installed!');
|
|
258
335
|
} catch (e) {
|
|
259
336
|
log.warn(`Install failed: ${e.message}`);
|
|
260
337
|
}
|
|
@@ -262,14 +339,39 @@ ${pc.bold('Repository:')} https://github.com/12-studio/tackl
|
|
|
262
339
|
log.info('Skipped dependency install.');
|
|
263
340
|
}
|
|
264
341
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
342
|
+
// SECTION • Sanity project link
|
|
343
|
+
// NOTE • A Sanity scaffold isn't usable until a project id exists —
|
|
344
|
+
// sanity's own init logs in, creates/links a project and writes .env.
|
|
345
|
+
let sanityLinked = false;
|
|
346
|
+
if (cms === 'sanity' && process.stdout.isTTY) {
|
|
347
|
+
const { link } = await prompts({
|
|
348
|
+
type: 'confirm',
|
|
349
|
+
name: 'link',
|
|
350
|
+
message: 'Link a Sanity project now? (opens a browser login, writes .env)',
|
|
351
|
+
initial: true,
|
|
352
|
+
});
|
|
353
|
+
if (link) {
|
|
354
|
+
try {
|
|
355
|
+
execSync('bunx sanity@latest init --env .env', { cwd: targetDir, stdio: 'inherit' });
|
|
356
|
+
sanityLinked = true;
|
|
357
|
+
log.ok('Sanity project linked!');
|
|
358
|
+
} catch (e) {
|
|
359
|
+
log.warn(`sanity init did not complete: ${e.message}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log(`\n${pc.bold(pc.green('Success!'))} Project scaffolded.\n`);
|
|
365
|
+
console.log(' Next steps:');
|
|
366
|
+
let step = 1;
|
|
367
|
+
if (cms === 'sanity' && !sanityLinked) {
|
|
368
|
+
console.log(` ${step++}) bunx sanity init --env .env # create/link your Sanity project`);
|
|
369
|
+
} else if (cms !== 'sanity') {
|
|
370
|
+
console.log(` ${step++}) cp .env.example .env # then fill in your values`);
|
|
371
|
+
}
|
|
372
|
+
if (!doInstall) console.log(` ${step++}) bun install`);
|
|
373
|
+
console.log(` ${step++}) bun run dev # a first-run setup wizard walks you through your theme`);
|
|
374
|
+
if (cms === 'sanity') console.log(` ${step}) open http://localhost:3000/studio to start editing content`);
|
|
273
375
|
}
|
|
274
376
|
|
|
275
377
|
main().catch(e => {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tackl",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Scaffold a Tackl project into the current directory — Next.js, styled-components, Waffl grid, DatoCMS/Sanity adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tackl": "
|
|
7
|
+
"tackl": "bin/tackl.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"scripts": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/12-studio/tackl"
|
|
30
|
+
"url": "git+https://github.com/12-studio/tackl.git"
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/12-studio/tackl",
|
|
33
33
|
"bugs": {
|