create-packkit 0.1.1
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/LICENSE +21 -0
- package/README.md +63 -0
- package/bin/cli.js +7 -0
- package/package.json +59 -0
- package/src/cli/args.js +62 -0
- package/src/cli/index.js +112 -0
- package/src/cli/wizard.js +59 -0
- package/src/cli/write.js +46 -0
- package/src/core/features/agents.js +57 -0
- package/src/core/features/bundler.js +118 -0
- package/src/core/features/cli.js +35 -0
- package/src/core/features/community.js +175 -0
- package/src/core/features/gitfiles.js +50 -0
- package/src/core/features/githooks.js +59 -0
- package/src/core/features/index.js +32 -0
- package/src/core/features/lint.js +76 -0
- package/src/core/features/meta.js +99 -0
- package/src/core/features/release.js +44 -0
- package/src/core/features/test.js +104 -0
- package/src/core/features/typescript.js +48 -0
- package/src/core/features/vscode.js +33 -0
- package/src/core/features/workflows.js +216 -0
- package/src/core/index.js +73 -0
- package/src/core/options.js +193 -0
- package/src/core/pkg.js +31 -0
- package/src/core/presets.js +23 -0
- package/src/core/render.js +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DanMat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Packkit 📦
|
|
2
|
+
|
|
3
|
+
> A highly configurable scaffolder for modern **npm packages and CLIs** — pick your stack from a CLI **or** a web configurator, and get a ready-to-ship repo.
|
|
4
|
+
|
|
5
|
+
[](https://danmat.github.io/create-packkit/)
|
|
6
|
+
[](https://www.npmjs.com/package/create-packkit)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Most scaffolders lock you into one stack, one language, and the terminal. Packkit lets you **choose** — TypeScript or JavaScript, library or CLI, ESM/CJS/dual, your bundler, test runner, linter, git hooks, release flow, GitHub Actions and more — and it works from a CLI **or** a browser page that downloads your project as a zip.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
# interactive wizard
|
|
15
|
+
npm create packkit@latest
|
|
16
|
+
# or the short alias
|
|
17
|
+
npx packkit
|
|
18
|
+
|
|
19
|
+
# skip the wizard with a preset
|
|
20
|
+
npx packkit ts-lib my-lib
|
|
21
|
+
npx packkit cli my-tool
|
|
22
|
+
npx packkit --preset full my-pkg --pm pnpm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then `cd`, and you already have a working project — `build`, `test`, and `lint` all pass out of the box.
|
|
26
|
+
|
|
27
|
+
## Or configure it on the web
|
|
28
|
+
|
|
29
|
+
No install needed: **[danmat.github.io/create-packkit](https://danmat.github.io/create-packkit/)** — tick the options, preview the file tree, and **download a zip** (or copy the equivalent `npx packkit` command). Everything runs in your browser.
|
|
30
|
+
|
|
31
|
+
## What you can pick
|
|
32
|
+
|
|
33
|
+
| Area | Options |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Language** | TypeScript (strict) · JavaScript (ESM) |
|
|
36
|
+
| **Module format** | ESM · CJS · dual (proper `exports` map) |
|
|
37
|
+
| **Target** | Library · CLI tool · both |
|
|
38
|
+
| **Bundler** | tsup · tsdown · unbuild · rollup · none (tsc) |
|
|
39
|
+
| **Tests** | Vitest · Jest · node:test · none (+ coverage) |
|
|
40
|
+
| **Lint/format** | ESLint + Prettier · Biome · oxlint · none |
|
|
41
|
+
| **Git hooks** | simple-git-hooks · husky + lint-staged · lefthook · none |
|
|
42
|
+
| **Release** | Changesets · release-it · np · none |
|
|
43
|
+
| **GitHub Actions** | CI · npm publish (provenance) · Pages · CodeQL · Codecov · stale bot |
|
|
44
|
+
| **Deps** | Renovate · Dependabot · none |
|
|
45
|
+
| **Repo** | LICENSE · community files · **AGENTS.md + CLAUDE.md** · VS Code · `.editorconfig` |
|
|
46
|
+
| **Package manager** | npm · pnpm · yarn · bun |
|
|
47
|
+
|
|
48
|
+
## Presets
|
|
49
|
+
|
|
50
|
+
`ts-lib` · `js-lib` · `ts-cli` / `cli` · `minimal` · `full` — named bundles of the options above.
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
Packkit is a pure `config → { files }` **core** that runs in both Node and the browser:
|
|
55
|
+
|
|
56
|
+
- the **CLI** writes the files to disk, runs `git init`, and installs dependencies;
|
|
57
|
+
- the **web configurator** zips the same files client-side (no server).
|
|
58
|
+
|
|
59
|
+
Both drive from one options schema ([`src/core/options.js`](src/core/options.js)), so the CLI and the web page always stay in sync.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
[MIT](LICENSE) © DanMat
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-packkit",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Highly configurable scaffolder for modern npm packages and CLIs — pick your stack (TS/JS, bundler, tests, linter, CI, releases) from a CLI or a web configurator.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-packkit": "bin/cli.js",
|
|
8
|
+
"packkit": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/core/index.js",
|
|
12
|
+
"./core": "./src/core/index.js",
|
|
13
|
+
"./cli": "./src/cli/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node bin/cli.js",
|
|
26
|
+
"test": "node --test",
|
|
27
|
+
"build:web": "esbuild src/core/index.js --bundle --format=esm --outfile=docs/packkit-core.js"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/DanMat/create-packkit.git"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"create",
|
|
35
|
+
"scaffold",
|
|
36
|
+
"scaffolding",
|
|
37
|
+
"generator",
|
|
38
|
+
"boilerplate",
|
|
39
|
+
"starter",
|
|
40
|
+
"template",
|
|
41
|
+
"npm-package",
|
|
42
|
+
"typescript",
|
|
43
|
+
"cli",
|
|
44
|
+
"tsup",
|
|
45
|
+
"vitest"
|
|
46
|
+
],
|
|
47
|
+
"author": "DanMat <dannymatthew@gmail.com>",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/DanMat/create-packkit/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://danmat.github.io/create-packkit/",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@clack/prompts": "^0.7.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"esbuild": "^0.23.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { PRESET_NAMES } from '../core/presets.js';
|
|
3
|
+
|
|
4
|
+
// Map friendly flag names to config keys for non-interactive use.
|
|
5
|
+
const OVERRIDE_FLAGS = {
|
|
6
|
+
language: 'language',
|
|
7
|
+
module: 'moduleFormat',
|
|
8
|
+
bundler: 'bundler',
|
|
9
|
+
test: 'test',
|
|
10
|
+
lint: 'lint',
|
|
11
|
+
hooks: 'gitHooks',
|
|
12
|
+
release: 'release',
|
|
13
|
+
license: 'license',
|
|
14
|
+
pm: 'packageManager',
|
|
15
|
+
node: 'nodeVersion',
|
|
16
|
+
author: 'author',
|
|
17
|
+
description: 'description',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function parseCliArgs(argv) {
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
args: argv,
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
options: {
|
|
25
|
+
preset: { type: 'string' },
|
|
26
|
+
name: { type: 'string' },
|
|
27
|
+
yes: { type: 'boolean', short: 'y' },
|
|
28
|
+
here: { type: 'boolean' },
|
|
29
|
+
'no-install': { type: 'boolean' },
|
|
30
|
+
'no-git': { type: 'boolean' },
|
|
31
|
+
target: { type: 'string', multiple: true },
|
|
32
|
+
help: { type: 'boolean', short: 'h' },
|
|
33
|
+
version: { type: 'boolean', short: 'v' },
|
|
34
|
+
...Object.fromEntries(Object.keys(OVERRIDE_FLAGS).map((k) => [k, { type: 'string' }])),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// First positional may be a known preset; otherwise it's the project name.
|
|
39
|
+
let preset = values.preset;
|
|
40
|
+
const pos = [...positionals];
|
|
41
|
+
if (!preset && pos.length && PRESET_NAMES.includes(pos[0])) preset = pos.shift();
|
|
42
|
+
const name = values.name || pos[0];
|
|
43
|
+
|
|
44
|
+
const overrides = {};
|
|
45
|
+
for (const [flag, key] of Object.entries(OVERRIDE_FLAGS)) {
|
|
46
|
+
if (values[flag] != null) overrides[key] = values[flag];
|
|
47
|
+
}
|
|
48
|
+
if (values.target) overrides.target = values.target;
|
|
49
|
+
if (name) overrides.name = name;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
preset,
|
|
53
|
+
name,
|
|
54
|
+
here: !!values.here,
|
|
55
|
+
yes: !!values.yes,
|
|
56
|
+
install: !values['no-install'],
|
|
57
|
+
git: !values['no-git'],
|
|
58
|
+
help: !!values.help,
|
|
59
|
+
version: !!values.version,
|
|
60
|
+
overrides,
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { resolve, basename } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { generate, fromPreset, normalizeConfig, PRESET_NAMES } from '../core/index.js';
|
|
6
|
+
import { parseCliArgs } from './args.js';
|
|
7
|
+
import { runWizard } from './wizard.js';
|
|
8
|
+
import { writeProject, dirIsEmptyOrMissing, gitInit, installDeps } from './write.js';
|
|
9
|
+
|
|
10
|
+
const pkgVersion = () => {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL('../../package.json', import.meta.url);
|
|
13
|
+
return JSON.parse(readFileSync(fileURLToPath(url), 'utf8')).version;
|
|
14
|
+
} catch {
|
|
15
|
+
return '0.0.0';
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const HELP = `
|
|
20
|
+
packkit — scaffold a modern npm package or CLI
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
npm create packkit@latest [name] [options]
|
|
24
|
+
npx packkit [preset] [name] [options]
|
|
25
|
+
|
|
26
|
+
Presets: ${PRESET_NAMES.join(', ')}
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--preset <name> Use a preset (skips the wizard)
|
|
30
|
+
--name <name> Package name
|
|
31
|
+
--here Scaffold into the current directory
|
|
32
|
+
-y, --yes Accept defaults / preset, no prompts
|
|
33
|
+
--no-install Skip dependency install
|
|
34
|
+
--no-git Skip git init
|
|
35
|
+
--pm <manager> npm | pnpm | yarn | bun
|
|
36
|
+
--language <ts|js> --module <esm|cjs|dual> --bundler <tsup|tsdown|unbuild|rollup|none>
|
|
37
|
+
--test <vitest|jest|node|none> --lint <eslint-prettier|biome|oxlint|none>
|
|
38
|
+
--license <MIT|Apache-2.0|ISC|none>
|
|
39
|
+
-h, --help Show this help
|
|
40
|
+
-v, --version Show version
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
npx packkit ts-lib my-lib
|
|
44
|
+
npm create packkit@latest my-cli -- --preset cli
|
|
45
|
+
npx packkit --preset full my-pkg --pm pnpm
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
49
|
+
const args = parseCliArgs(argv);
|
|
50
|
+
if (args.help) return void console.log(HELP);
|
|
51
|
+
if (args.version) return void console.log(pkgVersion());
|
|
52
|
+
|
|
53
|
+
const interactive = !args.preset && !args.yes && process.stdout.isTTY;
|
|
54
|
+
|
|
55
|
+
let config;
|
|
56
|
+
if (interactive) {
|
|
57
|
+
p.intro('📦 Packkit');
|
|
58
|
+
config = normalizeConfig(await runWizard(args.overrides));
|
|
59
|
+
} else if (args.preset) {
|
|
60
|
+
config = fromPreset(args.preset, args.overrides);
|
|
61
|
+
} else {
|
|
62
|
+
config = normalizeConfig(args.overrides);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
config.gitInit = args.git;
|
|
66
|
+
config.install = args.install;
|
|
67
|
+
|
|
68
|
+
// Resolve the target directory.
|
|
69
|
+
const targetDir = args.here ? process.cwd() : resolve(process.cwd(), config.name);
|
|
70
|
+
if (!args.here && !config.name) {
|
|
71
|
+
console.error('A package name is required (pass one, or run interactively).');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (!dirIsEmptyOrMissing(targetDir)) {
|
|
75
|
+
console.error(`Target directory "${basename(targetDir)}" is not empty. Aborting.`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generate + write.
|
|
80
|
+
const { files, summary } = generate(config);
|
|
81
|
+
await writeProject(targetDir, files);
|
|
82
|
+
|
|
83
|
+
// Post steps.
|
|
84
|
+
if (config.gitInit) gitInit(targetDir);
|
|
85
|
+
if (config.install) {
|
|
86
|
+
const s = p.spinner();
|
|
87
|
+
s.start(`Installing dependencies with ${config.packageManager}`);
|
|
88
|
+
const ok = installDeps(config.packageManager, targetDir);
|
|
89
|
+
s.stop(ok ? 'Dependencies installed' : 'Install skipped (run it manually)');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rel = args.here ? '.' : config.name;
|
|
93
|
+
const next = [
|
|
94
|
+
args.here ? null : `cd ${rel}`,
|
|
95
|
+
config.install ? null : `${config.packageManager} install`,
|
|
96
|
+
summary.stack.includes('tsup') || summary.stack.includes('tsdown') ? `${runWord(config)} build` : null,
|
|
97
|
+
summary.stack.some((s) => ['vitest', 'jest'].includes(s)) ? `${runWord(config)} test` : null,
|
|
98
|
+
].filter(Boolean);
|
|
99
|
+
|
|
100
|
+
const done = `Created ${summary.name} — ${summary.fileCount} files · ${summary.stack.join(' · ')}`;
|
|
101
|
+
if (interactive) {
|
|
102
|
+
p.note(next.join('\n') || 'You are all set.', 'Next steps');
|
|
103
|
+
p.outro(done);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(done);
|
|
106
|
+
if (next.length) console.log('\nNext steps:\n ' + next.join('\n '));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runWord(config) {
|
|
111
|
+
return config.packageManager === 'npm' ? 'npm run' : config.packageManager;
|
|
112
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { OPTIONS } from '../core/options.js';
|
|
3
|
+
|
|
4
|
+
function bail(value) {
|
|
5
|
+
if (p.isCancel(value)) {
|
|
6
|
+
p.cancel('Cancelled.');
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const asOptions = (key) => OPTIONS[key].choices.map((c) => ({ value: c.value, label: c.label }));
|
|
13
|
+
|
|
14
|
+
/** Run the interactive wizard, returning a partial config. `seed` pre-fills. */
|
|
15
|
+
export async function runWizard(seed = {}) {
|
|
16
|
+
const cfg = { ...seed };
|
|
17
|
+
|
|
18
|
+
cfg.name = bail(
|
|
19
|
+
await p.text({
|
|
20
|
+
message: 'Package name',
|
|
21
|
+
placeholder: 'my-package',
|
|
22
|
+
initialValue: seed.name || '',
|
|
23
|
+
validate: (v) => (!v ? 'Required' : /^(@[\w.-]+\/)?[\w.-]+$/.test(v) ? undefined : 'Invalid npm name'),
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
cfg.description = bail(await p.text({ message: 'Description', placeholder: '(optional)', defaultValue: '' }));
|
|
28
|
+
cfg.author = bail(await p.text({ message: 'Author', placeholder: '(optional)', defaultValue: seed.author || '' }));
|
|
29
|
+
|
|
30
|
+
cfg.language = bail(await p.select({ message: 'Language', options: asOptions('language'), initialValue: OPTIONS.language.default }));
|
|
31
|
+
cfg.target = bail(await p.multiselect({ message: 'What are you building?', options: asOptions('target'), initialValues: ['library'], required: true }));
|
|
32
|
+
cfg.moduleFormat = bail(await p.select({ message: 'Module format', options: asOptions('moduleFormat'), initialValue: OPTIONS.moduleFormat.default }));
|
|
33
|
+
cfg.packageManager = bail(await p.select({ message: 'Package manager', options: asOptions('packageManager'), initialValue: OPTIONS.packageManager.default }));
|
|
34
|
+
cfg.bundler = bail(await p.select({ message: 'Build / bundler', options: asOptions('bundler'), initialValue: OPTIONS.bundler.default }));
|
|
35
|
+
cfg.test = bail(await p.select({ message: 'Test runner', options: asOptions('test'), initialValue: OPTIONS.test.default }));
|
|
36
|
+
cfg.lint = bail(await p.select({ message: 'Lint / format', options: asOptions('lint'), initialValue: OPTIONS.lint.default }));
|
|
37
|
+
cfg.gitHooks = bail(await p.select({ message: 'Git hooks', options: asOptions('gitHooks'), initialValue: OPTIONS.gitHooks.default }));
|
|
38
|
+
cfg.release = bail(await p.select({ message: 'Release / versioning', options: asOptions('release'), initialValue: OPTIONS.release.default }));
|
|
39
|
+
cfg.workflows = bail(await p.multiselect({ message: 'GitHub Actions (space to toggle)', options: asOptions('workflows'), initialValues: OPTIONS.workflows.default, required: false }));
|
|
40
|
+
cfg.deps = bail(await p.select({ message: 'Dependency updates', options: asOptions('deps'), initialValue: OPTIONS.deps.default }));
|
|
41
|
+
cfg.license = bail(await p.select({ message: 'License', options: asOptions('license'), initialValue: OPTIONS.license.default }));
|
|
42
|
+
|
|
43
|
+
const extras = bail(
|
|
44
|
+
await p.multiselect({
|
|
45
|
+
message: 'Extras (space to toggle)',
|
|
46
|
+
options: [
|
|
47
|
+
{ value: 'community', label: 'Community files (CONTRIBUTING, CoC, SECURITY, templates)' },
|
|
48
|
+
{ value: 'agents', label: 'AI agent instructions (AGENTS.md + CLAUDE.md)' },
|
|
49
|
+
{ value: 'vscode', label: 'VS Code settings' },
|
|
50
|
+
{ value: 'editorconfig', label: '.editorconfig' },
|
|
51
|
+
],
|
|
52
|
+
initialValues: ['community', 'agents', 'vscode', 'editorconfig'],
|
|
53
|
+
required: false,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
for (const key of ['community', 'agents', 'vscode', 'editorconfig']) cfg[key] = extras.includes(key);
|
|
57
|
+
|
|
58
|
+
return cfg;
|
|
59
|
+
}
|
package/src/cli/write.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
/** Write a { path: contents } map under `targetDir`. Returns the count. */
|
|
7
|
+
export async function writeProject(targetDir, files) {
|
|
8
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
9
|
+
const full = join(targetDir, rel);
|
|
10
|
+
await mkdir(dirname(full), { recursive: true });
|
|
11
|
+
await writeFile(full, contents);
|
|
12
|
+
}
|
|
13
|
+
return Object.keys(files).length;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True if the directory doesn't exist or contains no entries. */
|
|
17
|
+
export function dirIsEmptyOrMissing(dir) {
|
|
18
|
+
if (!existsSync(dir)) return true;
|
|
19
|
+
try {
|
|
20
|
+
return readdirSync(dir).length === 0;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Run a command in `cwd` (arg array — no shell parsing). Never throws. */
|
|
27
|
+
export function run(cmd, args, cwd, { quiet = true } = {}) {
|
|
28
|
+
const res = spawnSync(cmd, args, {
|
|
29
|
+
cwd,
|
|
30
|
+
stdio: quiet ? 'ignore' : 'inherit',
|
|
31
|
+
shell: process.platform === 'win32',
|
|
32
|
+
});
|
|
33
|
+
return res.status === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** git init + initial commit. Returns true on success. */
|
|
37
|
+
export function gitInit(cwd) {
|
|
38
|
+
if (!run('git', ['init', '--quiet'], cwd)) return false;
|
|
39
|
+
run('git', ['add', '-A'], cwd);
|
|
40
|
+
return run('git', ['commit', '-m', 'Initial commit from Packkit', '--quiet'], cwd);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Install dependencies with the chosen package manager. */
|
|
44
|
+
export function installDeps(pm, cwd) {
|
|
45
|
+
return run(pm, ['install'], cwd, { quiet: false });
|
|
46
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// AGENTS.md (the emerging cross-tool standard) + CLAUDE.md pointer. Describes
|
|
2
|
+
// the stack and the exact commands so coding agents work correctly out of the box.
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
id: 'agents',
|
|
6
|
+
active: (cfg) => cfg.agents,
|
|
7
|
+
apply(cfg) {
|
|
8
|
+
const run = (s) => (cfg.packageManager === 'npm' ? `npm run ${s}` : `${cfg.packageManager} ${s}`);
|
|
9
|
+
const test = cfg.packageManager === 'npm' ? 'npm test' : `${cfg.packageManager} test`;
|
|
10
|
+
|
|
11
|
+
const commands = [];
|
|
12
|
+
if (cfg.isTs) commands.push(`- Type-check: \`${run('typecheck')}\``);
|
|
13
|
+
if (cfg.lint !== 'none') commands.push(`- Lint: \`${run('lint')}\``);
|
|
14
|
+
if (cfg.test !== 'none') commands.push(`- Test: \`${test}\``);
|
|
15
|
+
if (cfg.bundler !== 'none' || cfg.isTs) commands.push(`- Build: \`${run('build')}\``);
|
|
16
|
+
|
|
17
|
+
const stack = [
|
|
18
|
+
`- Language: ${cfg.isTs ? 'TypeScript (strict)' : 'JavaScript (ESM)'}`,
|
|
19
|
+
`- Module format: ${cfg.moduleFormat.toUpperCase()}`,
|
|
20
|
+
`- Package manager: ${cfg.packageManager}`,
|
|
21
|
+
cfg.bundler !== 'none' ? `- Bundler: ${cfg.bundler}` : `- Build: ${cfg.isTs ? 'tsc' : 'none'}`,
|
|
22
|
+
cfg.test !== 'none' ? `- Tests: ${cfg.test}` : null,
|
|
23
|
+
cfg.lint !== 'none' ? `- Lint/format: ${cfg.lint}` : null,
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
|
|
26
|
+
const agents = [
|
|
27
|
+
`# AGENTS.md`,
|
|
28
|
+
``,
|
|
29
|
+
`Guidance for AI coding agents working in **${cfg.name}**.`,
|
|
30
|
+
``,
|
|
31
|
+
`## Stack`,
|
|
32
|
+
``,
|
|
33
|
+
...stack,
|
|
34
|
+
``,
|
|
35
|
+
`## Commands`,
|
|
36
|
+
``,
|
|
37
|
+
...commands,
|
|
38
|
+
``,
|
|
39
|
+
`## Conventions`,
|
|
40
|
+
``,
|
|
41
|
+
`- Source lives in \`src/\`. Keep the public API in \`src/index.${cfg.ext}\`.`,
|
|
42
|
+
`- Add or update tests for any behavior change.`,
|
|
43
|
+
cfg.isTs ? `- Prefer explicit types on exported functions; keep \`strict\` passing.` : `- Use ESM syntax and JSDoc types where helpful.`,
|
|
44
|
+
cfg.release === 'changesets' ? `- Run \`npx changeset\` after a user-facing change.` : null,
|
|
45
|
+
`- Do not commit \`dist/\` or \`node_modules/\`.`,
|
|
46
|
+
``,
|
|
47
|
+
].filter((l) => l !== null).join('\n');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
files: {
|
|
51
|
+
'AGENTS.md': agents,
|
|
52
|
+
'CLAUDE.md': `See [AGENTS.md](./AGENTS.md) for build/test commands and conventions.\n`,
|
|
53
|
+
},
|
|
54
|
+
pkg: {},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Always-on. Owns the package entry points (exports/main/module/types/files)
|
|
2
|
+
// and the build tooling for the chosen bundler.
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
id: 'bundler',
|
|
6
|
+
active: () => true,
|
|
7
|
+
apply(cfg) {
|
|
8
|
+
const files = {};
|
|
9
|
+
const pkg = { scripts: {} };
|
|
10
|
+
const build = cfg.bundler !== 'none';
|
|
11
|
+
|
|
12
|
+
// ---- entry points ----
|
|
13
|
+
if (!build && !cfg.isTs) {
|
|
14
|
+
// Plain JS, no build: ship source directly.
|
|
15
|
+
pkg.files = ['src'];
|
|
16
|
+
pkg.exports = { '.': `./src/index.js` };
|
|
17
|
+
pkg.main = './src/index.js';
|
|
18
|
+
if (cfg.hasEsm) pkg.module = './src/index.js';
|
|
19
|
+
} else {
|
|
20
|
+
pkg.files = ['dist'];
|
|
21
|
+
const esm = './dist/index.js';
|
|
22
|
+
const cjs = './dist/index.cjs';
|
|
23
|
+
const dts = './dist/index.d.ts';
|
|
24
|
+
const exp = {};
|
|
25
|
+
if (cfg.isTs) exp.types = dts;
|
|
26
|
+
if (cfg.hasEsm) exp.import = esm;
|
|
27
|
+
if (cfg.hasCjs) exp.require = build ? cjs : './dist/index.cjs';
|
|
28
|
+
pkg.exports = { '.': exp };
|
|
29
|
+
if (cfg.hasCjs) pkg.main = exp.require;
|
|
30
|
+
else pkg.main = esm;
|
|
31
|
+
if (cfg.hasEsm) pkg.module = esm;
|
|
32
|
+
if (cfg.isTs) pkg.types = dts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- build tooling ----
|
|
36
|
+
const entries = ['src/index.' + cfg.ext];
|
|
37
|
+
if (cfg.hasCli) entries.push('src/cli.' + cfg.ext);
|
|
38
|
+
const formats = [cfg.hasEsm && 'esm', cfg.hasCjs && 'cjs'].filter(Boolean);
|
|
39
|
+
|
|
40
|
+
if (cfg.bundler === 'tsup' || cfg.bundler === 'tsdown') {
|
|
41
|
+
const tool = cfg.bundler;
|
|
42
|
+
files[`${tool}.config.${cfg.ext}`] = tsupConfig(cfg, entries, formats, tool);
|
|
43
|
+
pkg.scripts.build = tool;
|
|
44
|
+
pkg.scripts.dev = `${tool} --watch`;
|
|
45
|
+
pkg.devDependencies = { [tool]: tool === 'tsup' ? '^8.0.0' : '^0.6.0' };
|
|
46
|
+
} else if (cfg.bundler === 'unbuild') {
|
|
47
|
+
files['build.config.ts'] = unbuildConfig(cfg);
|
|
48
|
+
pkg.scripts.build = 'unbuild';
|
|
49
|
+
pkg.scripts.dev = 'unbuild --stub';
|
|
50
|
+
pkg.devDependencies = { unbuild: '^2.0.0' };
|
|
51
|
+
} else if (cfg.bundler === 'rollup') {
|
|
52
|
+
files[`rollup.config.${cfg.ext === 'ts' ? 'ts' : 'js'}`] = rollupConfig(cfg, formats);
|
|
53
|
+
pkg.scripts.build = 'rollup -c';
|
|
54
|
+
pkg.scripts.dev = 'rollup -c -w';
|
|
55
|
+
pkg.devDependencies = {
|
|
56
|
+
rollup: '^4.0.0',
|
|
57
|
+
...(cfg.isTs ? { '@rollup/plugin-typescript': '^11.0.0', tslib: '^2.6.0' } : {}),
|
|
58
|
+
};
|
|
59
|
+
} else if (cfg.bundler === 'none' && cfg.isTs) {
|
|
60
|
+
// tsc-only build for TypeScript.
|
|
61
|
+
pkg.scripts.build = 'tsc';
|
|
62
|
+
pkg.scripts.dev = 'tsc --watch';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (build) pkg.scripts.prepublishOnly = pkg.scripts.build;
|
|
66
|
+
pkg.scripts.clean = 'rimraf dist';
|
|
67
|
+
if (build) pkg.devDependencies = { ...pkg.devDependencies, rimraf: '^6.0.0' };
|
|
68
|
+
|
|
69
|
+
return { files, pkg };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function tsupConfig(cfg, entries, formats, tool) {
|
|
74
|
+
return [
|
|
75
|
+
`import { defineConfig } from '${tool}';`,
|
|
76
|
+
``,
|
|
77
|
+
`export default defineConfig({`,
|
|
78
|
+
`\tentry: [${entries.map((e) => `'${e}'`).join(', ')}],`,
|
|
79
|
+
`\tformat: [${formats.map((f) => `'${f}'`).join(', ')}],`,
|
|
80
|
+
cfg.isTs ? `\tdts: true,` : null,
|
|
81
|
+
`\tsourcemap: true,`,
|
|
82
|
+
`\tclean: true,`,
|
|
83
|
+
`\ttreeshake: true,`,
|
|
84
|
+
`});`,
|
|
85
|
+
``,
|
|
86
|
+
].filter((l) => l !== null).join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unbuildConfig(cfg) {
|
|
90
|
+
return [
|
|
91
|
+
`import { defineBuildConfig } from 'unbuild';`,
|
|
92
|
+
``,
|
|
93
|
+
`export default defineBuildConfig({`,
|
|
94
|
+
`\tentries: ['src/index'],`,
|
|
95
|
+
`\tdeclaration: ${cfg.isTs},`,
|
|
96
|
+
`\tclean: true,`,
|
|
97
|
+
`\trollup: { emitCJS: ${cfg.hasCjs} },`,
|
|
98
|
+
`});`,
|
|
99
|
+
``,
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rollupConfig(cfg, formats) {
|
|
104
|
+
const out = formats
|
|
105
|
+
.map((f) => `\t\t{ file: 'dist/index.${f === 'cjs' ? 'cjs' : 'js'}', format: '${f}', sourcemap: true }`)
|
|
106
|
+
.join(',\n');
|
|
107
|
+
const tsPlugin = cfg.isTs ? `\n\tplugins: [typescript()],` : '';
|
|
108
|
+
const tsImport = cfg.isTs ? `import typescript from '@rollup/plugin-typescript';\n` : '';
|
|
109
|
+
return [
|
|
110
|
+
tsImport + `export default {`,
|
|
111
|
+
`\tinput: 'src/index.${cfg.ext}',`,
|
|
112
|
+
`\toutput: [`,
|
|
113
|
+
out,
|
|
114
|
+
`\t],${tsPlugin}`,
|
|
115
|
+
`};`,
|
|
116
|
+
``,
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// CLI target: a `bin` entry + a minimal command scaffold with a shebang.
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: 'cli',
|
|
5
|
+
active: (cfg) => cfg.hasCli,
|
|
6
|
+
apply(cfg) {
|
|
7
|
+
const build = cfg.bundler !== 'none';
|
|
8
|
+
const binPath = build || cfg.isTs ? './dist/cli.js' : './src/cli.js';
|
|
9
|
+
const files = {};
|
|
10
|
+
|
|
11
|
+
files[`src/cli.${cfg.ext}`] = cliScaffold(cfg);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
files,
|
|
15
|
+
pkg: {
|
|
16
|
+
bin: { [binName(cfg.name)]: binPath },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function binName(name) {
|
|
23
|
+
// scoped packages -> use the part after the slash as the command
|
|
24
|
+
return name.startsWith('@') ? name.split('/')[1] : name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cliScaffold(cfg) {
|
|
28
|
+
const importLine = cfg.hasLibrary
|
|
29
|
+
? `import { greet } from './index.${cfg.isTs ? 'js' : 'js'}';\n`
|
|
30
|
+
: '';
|
|
31
|
+
const body = cfg.hasLibrary
|
|
32
|
+
? `const name = process.argv[2] ?? 'world';\nconsole.log(greet(name));\n`
|
|
33
|
+
: `console.log('${cfg.name} — hello from your CLI');\n`;
|
|
34
|
+
return `#!/usr/bin/env node\n${importLine}\n${body}`;
|
|
35
|
+
}
|