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.
Files changed (3) hide show
  1. package/README.md +18 -60
  2. package/bin/tackl.js +275 -173
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,75 +1,33 @@
1
1
  # Tackl CLI
2
2
 
3
- The ultimate Next.js starter kit with Waffl Grid system.
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
- tackl my-app
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
- ### Options
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
- ### Examples
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
- ```bash
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's Included
18
+ ## What it does
38
19
 
39
- - ⚡ **Next.js 15** with App Router
40
- - 🎨 **Waffl Grid System** - Responsive grid with smart breakpoint inheritance
41
- - 🎭 **Styled Components** - CSS-in-JS with TypeScript
42
- - 🎬 **GSAP** - Professional animations
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
- ## Features
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
- ## Repository
69
-
70
- - **GitHub**: https://github.com/12-studio/tackl
71
- - **Issues**: https://github.com/12-studio/tackl/issues
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 DEFAULT_REF = 'main';
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`; // Deep vibrant purple
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
- tackl [project-name] [options]
165
+ Run inside an empty directory (or a freshly-initialised git repo):
59
166
 
60
- ${pc.bold('Options:')}
61
- --no-install Skip dependency installation (default: install)
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('Repository:')} https://github.com/12-studio/tackl
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 argName = args.find(a => !a.startsWith('-'));
77
-
78
- const initial = {
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
- // Filter out null prompts
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
- const answers = await prompts(promptQuestions, {
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
- `Directory "${projectName}" already exists and is not empty.`
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
- log.step(`Creating project: ${pc.bold(pc.cyan(projectName))}`);
134
- log.step(`Template: ${pc.bold(pc.yellow(`${OWNER}/${REPO}@${ref}`))}`);
135
- console.log();
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 tarUrl = `https://codeload.github.com/${OWNER}/${REPO}/tar.gz/${encodeURIComponent(ref)}`;
138
- const tmpTar = path.join(os.tmpdir(), `tackl-${Date.now()}.tgz`);
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
- try {
141
- log.step('📥 Downloading template...');
142
- const res = await fetch(tarUrl);
143
- if (!res.ok)
144
- throw new Error(`Download failed: ${res.status} ${res.statusText}`);
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
- // Convert response to buffer and write to file
147
- const arrayBuffer = await res.arrayBuffer();
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
- log.ok('Template downloaded successfully!');
152
- } catch (e) {
153
- log.error(`Download failed: ${e.message}`);
154
- log.error('Please check your internet connection and try again.');
155
- process.exit(1);
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 successfully!');
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
- try {
168
- fs.unlinkSync(tmpTar);
169
- } catch {}
279
+ if (!process.env.TACKL_TEMPLATE_TAR) {
280
+ try {
281
+ fs.unlinkSync(tmpTar);
282
+ } catch {}
283
+ }
170
284
  }
171
285
 
172
- // Remove template .git if present
173
- try {
174
- const gitPath = path.join(targetDir, '.git');
175
- if (fs.existsSync(gitPath))
176
- fs.rmSync(gitPath, { recursive: true, force: true });
177
- } catch {}
178
-
179
- // Remove any existing lock files to ensure npm is used
180
- try {
181
- const yarnLock = path.join(targetDir, 'yarn.lock');
182
- const pnpmLock = path.join(targetDir, 'pnpm-lock.yaml');
183
- if (fs.existsSync(yarnLock)) fs.unlinkSync(yarnLock);
184
- if (fs.existsSync(pnpmLock)) fs.unlinkSync(pnpmLock);
185
- log.step('🧹 Cleaned up lock files to ensure npm usage');
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(pkg, 'utf8'));
194
- json.name = projectName.toLowerCase();
195
- fs.writeFileSync(pkg, JSON.stringify(json, null, 2));
196
- log.ok('Project configuration updated!');
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
- // Create .npmrc to ensure npm is used
203
- try {
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('🔧 Initializing git repository...');
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
- cwd: targetDir,
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 npm...');
233
-
234
- // Disable Husky during installation to prevent Git issues
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
- const rel = path.relative(process.cwd(), targetDir) || '.';
266
- console.log(
267
- '\n' + pc.bold(pc.green('Success!')) + ' Project scaffolded.\n'
268
- );
269
- console.log(` Next steps:`);
270
- console.log(` 1) cd ${rel}`);
271
- if (!doInstall) console.log(` 2) npm install`);
272
- console.log(` ${doInstall ? '2' : '3'}) npm run dev`);
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": "1.0.10",
4
- "description": "The ultimate Next.js starter kit with Waffl Grid system",
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": "./bin/tackl.js"
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": {