@vlandoss/vland 0.2.1-git-74f39bb.0 → 0.2.1-git-87d22db.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 +4 -4
- package/dist/cli.usage.kdl +34 -0
- package/dist/run.mjs +474 -0
- package/package.json +9 -14
- package/src/actions/init.ts +39 -22
- package/src/actions/placeholders.ts +31 -13
- package/src/actions/template.ts +16 -3
- package/src/program/commands/completion.ts +1 -1
- package/src/program/commands/init.ts +4 -4
- package/src/program/index.ts +13 -9
- package/tsconfig.json +1 -1
- package/src/program/commands/usage.ts +0 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🦉 vland
|
|
2
2
|
|
|
3
|
-
The CLI to init a new project in [Variable Land](https://variable.land)
|
|
3
|
+
The CLI to init a new project in [Variable Land](https://variable.land)
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -35,11 +35,11 @@ See [`CLI.md`](./CLI.md) for the full reference (auto-generated per release).
|
|
|
35
35
|
|
|
36
36
|
| Template | What you get |
|
|
37
37
|
| ---------- | ----------------------------------------------------------------------------------------- |
|
|
38
|
-
| `library` | A standalone TypeScript library with
|
|
38
|
+
| `library` | A standalone TypeScript library with Vitest + Changesets release workflow. |
|
|
39
39
|
| `backend` | An Elysia (`@elysiajs/node`) backend with evlog, Vitest, Dockerfile, CI shape. |
|
|
40
40
|
| `monorepo` | pnpm + Turbo workspace with an Elysia API, a Vite-React SPA, and a few internal packages. |
|
|
41
41
|
|
|
42
|
-
All templates target Node.js, use pnpm, and
|
|
42
|
+
All templates target Node.js, use pnpm, and ship [`@rrlab/cli`](https://github.com/variableland/dx/tree/main/run-run) (`rr`) as the single entry point for lint, format, type-check, and build. The per-tool config files (`biome.json`, `tsconfig.json`, `tsdown.config.ts`) are not bundled — run `rr plugins add biome ts tsdown` (one at a time) in your new project to opt in. See the scaffolded README for the exact post-install setup.
|
|
43
43
|
|
|
44
44
|
## Shell completion
|
|
45
45
|
|
|
@@ -76,5 +76,5 @@ DEBUG=vland:* vland init my-app
|
|
|
76
76
|
To point `init` at local templates instead of fetching from GitHub (useful when developing inside this monorepo):
|
|
77
77
|
|
|
78
78
|
```sh
|
|
79
|
-
VLAND_TEMPLATES_DIR=/absolute/path/to/dx/templates vland init my-app -t library
|
|
79
|
+
VLAND_TEMPLATES_DIR=/absolute/path/to/dx/vland/templates vland init my-app -t library
|
|
80
80
|
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @generated by @usage-spec/commander from Commander.js metadata
|
|
2
|
+
name vland
|
|
3
|
+
bin vland
|
|
4
|
+
version "0.2.1-git-87d22db.0"
|
|
5
|
+
usage "[options] [command]"
|
|
6
|
+
flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
|
|
7
|
+
cmd completion help="print shell completion script (usage)" {
|
|
8
|
+
long_help "Prints a shell completion script for vland. Add to your shell rc file:\n\n bash: eval \"$(vland completion bash)\"\n zsh: eval \"$(vland completion zsh)\"\n fish: vland completion fish | source"
|
|
9
|
+
arg <shell> help="target shell" {
|
|
10
|
+
choices bash zsh fish
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
cmd init help="init a new project (giget)" {
|
|
14
|
+
long_help "Scaffold a new variableland project from one of the official templates."
|
|
15
|
+
flag "-t --template" help="template to use" {
|
|
16
|
+
arg <TEMPLATE> {
|
|
17
|
+
choices library backend monorepo
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
flag --visibility help="package visibility (library only)" {
|
|
21
|
+
arg <VISIBILITY> {
|
|
22
|
+
choices private public
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
flag "-d --dir" help="target directory (default: ./<name>)" {
|
|
26
|
+
arg <DIR>
|
|
27
|
+
}
|
|
28
|
+
flag --install help="install dependencies (skip prompt)"
|
|
29
|
+
flag --no-install help="skip dependency installation" negate=--install
|
|
30
|
+
flag --git help="initialise git repository (skip prompt)"
|
|
31
|
+
flag --no-git help="skip git init" negate=--git
|
|
32
|
+
flag "-f --force" help="overwrite existing directory"
|
|
33
|
+
arg "[name]" help="project name (also used as the target directory)" required=#false
|
|
34
|
+
}
|
package/dist/run.mjs
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import path, { extname, isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import { colorize, createPkg, createShellService, dirnameOf, hasTTY, palette, run, text } from "@vlandoss/clibuddy";
|
|
3
|
+
import { generateToStdout } from "@usage-spec/commander";
|
|
4
|
+
import { Argument, Command, Option, createCommand } from "commander";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { createLoggy } from "@vlandoss/loggy";
|
|
7
|
+
import { cp, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text as text$1 } from "@clack/prompts";
|
|
9
|
+
import { installDependencies } from "nypm";
|
|
10
|
+
import { downloadTemplate } from "giget";
|
|
11
|
+
//#region src/services/logger.ts
|
|
12
|
+
const logger = createLoggy({ namespace: "vland" });
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/services/ctx.ts
|
|
15
|
+
async function createContext(binDir) {
|
|
16
|
+
const debug = logger.subdebug("create-context");
|
|
17
|
+
const binPath = fs.realpathSync(binDir);
|
|
18
|
+
debug("bin path:", binPath);
|
|
19
|
+
const binPkg = await createPkg(binPath);
|
|
20
|
+
if (!binPkg) throw new Error("Could not find bin package.json");
|
|
21
|
+
debug("bin pkg info: %O", binPkg.info());
|
|
22
|
+
const shell = createShellService();
|
|
23
|
+
debug("shell service options: %O", shell.options);
|
|
24
|
+
return {
|
|
25
|
+
binPkg,
|
|
26
|
+
shell
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/program/ui.ts
|
|
31
|
+
const vlandColor = colorize("#a78bfa");
|
|
32
|
+
const usageColor = colorize("#24C55E");
|
|
33
|
+
const gigetColor = colorize("#F472B6");
|
|
34
|
+
const TOOL_LABELS = {
|
|
35
|
+
USAGE: usageColor("usage"),
|
|
36
|
+
GIGET: gigetColor("giget")
|
|
37
|
+
};
|
|
38
|
+
function getBannerText(version) {
|
|
39
|
+
return `
|
|
40
|
+
${vlandColor(`
|
|
41
|
+
██╗ ██╗██╗ █████╗ ███╗ ██╗██████╗
|
|
42
|
+
██║ ██║██║ ██╔══██╗████╗ ██║██╔══██╗
|
|
43
|
+
██║ ██║██║ ███████║██╔██╗ ██║██║ ██║
|
|
44
|
+
╚██╗ ██╔╝██║ ██╔══██║██║╚██╗██║██║ ██║
|
|
45
|
+
╚████╔╝ ███████╗██║ ██║██║ ╚████║██████╔╝
|
|
46
|
+
╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ${text.version(version)}
|
|
47
|
+
`.trim())}
|
|
48
|
+
|
|
49
|
+
🦉 ${palette.italic(palette.muted("The CLI to init a new project in"))} ${text.vland}\n`.trimStart();
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/program/commands/completion.ts
|
|
53
|
+
const SHELLS = [
|
|
54
|
+
"bash",
|
|
55
|
+
"zsh",
|
|
56
|
+
"fish"
|
|
57
|
+
];
|
|
58
|
+
function createCompletionCommand() {
|
|
59
|
+
return createCommand("completion").summary(`print shell completion script (${TOOL_LABELS.USAGE})`).description(`Prints a shell completion script for vland. Add to your shell rc file:
|
|
60
|
+
|
|
61
|
+
bash: eval "$(vland completion bash)"
|
|
62
|
+
zsh: eval "$(vland completion zsh)"
|
|
63
|
+
fish: vland completion fish | source`).addArgument(new Argument("<shell>", `target shell`).choices(SHELLS)).addHelpText("afterAll", `\nUnder the hood, this command uses ${TOOL_LABELS.USAGE} (https://usage.jdx.dev).
|
|
64
|
+
Make sure to have it installed and available in your PATH.`);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/actions/template.ts
|
|
68
|
+
const TEMPLATES = [
|
|
69
|
+
"library",
|
|
70
|
+
"backend",
|
|
71
|
+
"monorepo"
|
|
72
|
+
];
|
|
73
|
+
const VISIBILITIES = ["private", "public"];
|
|
74
|
+
const TEMPLATE_META = {
|
|
75
|
+
library: {
|
|
76
|
+
placeholder: "my-lib",
|
|
77
|
+
runScript: "test"
|
|
78
|
+
},
|
|
79
|
+
backend: {
|
|
80
|
+
placeholder: "my-api",
|
|
81
|
+
runScript: "dev"
|
|
82
|
+
},
|
|
83
|
+
monorepo: {
|
|
84
|
+
placeholder: "my-mono",
|
|
85
|
+
runScript: "dev"
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The npm scope a `library` template's package is published under, indexed by
|
|
90
|
+
* the user-selected visibility. Public libraries live under the open-source
|
|
91
|
+
* `@vlandoss` scope; private libraries under `@variableland`.
|
|
92
|
+
*/
|
|
93
|
+
const LIBRARY_SCOPES = {
|
|
94
|
+
private: "@variableland",
|
|
95
|
+
public: "@vlandoss"
|
|
96
|
+
};
|
|
97
|
+
const GITHUB_SOURCE = "github:variableland/dx";
|
|
98
|
+
const GITHUB_REF = "main";
|
|
99
|
+
/**
|
|
100
|
+
* Resolves the template into `dir`. Source order:
|
|
101
|
+
* 1. `VLAND_TEMPLATES_DIR` env var → copy from local path (used by E2E tests
|
|
102
|
+
* against the in-repo `vland/templates/`).
|
|
103
|
+
* 2. Otherwise → download via giget from `github:variableland/dx/vland/templates/<name>`.
|
|
104
|
+
*/
|
|
105
|
+
async function fetchTemplate(options) {
|
|
106
|
+
const debug = logger.subdebug("fetch-template");
|
|
107
|
+
const localRoot = process.env.VLAND_TEMPLATES_DIR;
|
|
108
|
+
if (localRoot) {
|
|
109
|
+
const sourceDir = resolve(localRoot, options.template);
|
|
110
|
+
debug("local source: %s", sourceDir);
|
|
111
|
+
await cp(sourceDir, options.dir, {
|
|
112
|
+
recursive: true,
|
|
113
|
+
force: options.force,
|
|
114
|
+
errorOnExist: !options.force,
|
|
115
|
+
filter: (src) => !src.includes("/node_modules") && !src.endsWith("/.turbo") && !src.endsWith("/dist")
|
|
116
|
+
});
|
|
117
|
+
return { source: sourceDir };
|
|
118
|
+
}
|
|
119
|
+
const source = `${GITHUB_SOURCE}/vland/templates/${options.template}#${GITHUB_REF}`;
|
|
120
|
+
debug("remote source: %s", source);
|
|
121
|
+
return { source: (await downloadTemplate(source, {
|
|
122
|
+
dir: options.dir,
|
|
123
|
+
force: options.force,
|
|
124
|
+
install: false
|
|
125
|
+
})).source };
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/actions/placeholders.ts
|
|
129
|
+
const TEXT_EXTENSIONS = new Set([
|
|
130
|
+
".ts",
|
|
131
|
+
".tsx",
|
|
132
|
+
".js",
|
|
133
|
+
".jsx",
|
|
134
|
+
".mjs",
|
|
135
|
+
".cjs",
|
|
136
|
+
".mts",
|
|
137
|
+
".cts",
|
|
138
|
+
".json",
|
|
139
|
+
".jsonc",
|
|
140
|
+
".md",
|
|
141
|
+
".mdx",
|
|
142
|
+
".yml",
|
|
143
|
+
".yaml",
|
|
144
|
+
".toml",
|
|
145
|
+
".css",
|
|
146
|
+
".html",
|
|
147
|
+
".sh",
|
|
148
|
+
".env",
|
|
149
|
+
".gitignore",
|
|
150
|
+
".gitattributes",
|
|
151
|
+
".npmrc",
|
|
152
|
+
".nvmrc",
|
|
153
|
+
".node-version",
|
|
154
|
+
".dockerignore",
|
|
155
|
+
".editorconfig",
|
|
156
|
+
".prettierrc"
|
|
157
|
+
]);
|
|
158
|
+
const TEXT_FILENAMES = new Set([
|
|
159
|
+
"Dockerfile",
|
|
160
|
+
"LICENSE",
|
|
161
|
+
"README",
|
|
162
|
+
"CHANGELOG",
|
|
163
|
+
".gitignore",
|
|
164
|
+
".gitattributes",
|
|
165
|
+
".npmrc",
|
|
166
|
+
".nvmrc",
|
|
167
|
+
".node-version",
|
|
168
|
+
".dockerignore",
|
|
169
|
+
".editorconfig",
|
|
170
|
+
".prettierrc",
|
|
171
|
+
"lefthook.yml",
|
|
172
|
+
"mise.toml"
|
|
173
|
+
]);
|
|
174
|
+
const SKIP_DIRS = new Set([
|
|
175
|
+
"node_modules",
|
|
176
|
+
".git",
|
|
177
|
+
"dist",
|
|
178
|
+
".turbo",
|
|
179
|
+
".next",
|
|
180
|
+
"build",
|
|
181
|
+
"coverage"
|
|
182
|
+
]);
|
|
183
|
+
function isTextFile(name) {
|
|
184
|
+
if (TEXT_FILENAMES.has(name)) return true;
|
|
185
|
+
return TEXT_EXTENSIONS.has(extname(name));
|
|
186
|
+
}
|
|
187
|
+
async function walk(root, onFile) {
|
|
188
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
189
|
+
await Promise.all(entries.map(async (entry) => {
|
|
190
|
+
const full = join(root, entry.name);
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
if (SKIP_DIRS.has(entry.name)) return;
|
|
193
|
+
await walk(full, onFile);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (entry.isFile()) await onFile(full);
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
function applyPlaceholders(content, values) {
|
|
200
|
+
return content.replaceAll("{{projectName}}", values.projectName).replaceAll("{{author}}", values.author).replaceAll("{{year}}", values.year);
|
|
201
|
+
}
|
|
202
|
+
async function replacePlaceholders(rootDir, values) {
|
|
203
|
+
const debug = logger.subdebug("placeholders");
|
|
204
|
+
let touched = 0;
|
|
205
|
+
await walk(rootDir, async (filePath) => {
|
|
206
|
+
if (!isTextFile(filePath.split("/").pop() ?? "")) return;
|
|
207
|
+
if ((await stat(filePath)).size > 1e6) return;
|
|
208
|
+
const original = await readFile(filePath, "utf8");
|
|
209
|
+
const replaced = applyPlaceholders(original, values);
|
|
210
|
+
if (replaced !== original) {
|
|
211
|
+
await writeFile(filePath, replaced);
|
|
212
|
+
touched += 1;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
debug("placeholders applied to %d file(s)", touched);
|
|
216
|
+
return { touched };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Writes the root `package.json`'s `name` and `private` fields based on the
|
|
220
|
+
* chosen template and visibility. Uses `pkg-types` so field ordering and
|
|
221
|
+
* JSON formatting survive.
|
|
222
|
+
*
|
|
223
|
+
* Naming rules:
|
|
224
|
+
* - `library` private → `@variableland/<projectName>`, `private: true`
|
|
225
|
+
* - `library` public → `@vlandoss/<projectName>`
|
|
226
|
+
* - `backend` → `<projectName>`, `private: true`
|
|
227
|
+
* - `monorepo` → `<projectName>`, `private: true` (root never publishes)
|
|
228
|
+
*/
|
|
229
|
+
async function applyRootPackage(rootDir, options) {
|
|
230
|
+
const debug = logger.subdebug("apply-root-package");
|
|
231
|
+
const pkg = await createPkg(fs.realpathSync(rootDir));
|
|
232
|
+
if (!pkg) throw new Error("Could not find package.json");
|
|
233
|
+
const { template, projectName, visibility } = options;
|
|
234
|
+
try {
|
|
235
|
+
if (template === "library") {
|
|
236
|
+
const scope = LIBRARY_SCOPES[visibility ?? "public"];
|
|
237
|
+
pkg.packageJson.name = `${scope}/${projectName}`;
|
|
238
|
+
if (visibility === "private") pkg.packageJson.private = true;
|
|
239
|
+
} else {
|
|
240
|
+
pkg.packageJson.name = projectName;
|
|
241
|
+
pkg.packageJson.private = true;
|
|
242
|
+
}
|
|
243
|
+
await pkg.write(pkg.packageJson);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
debug("skipped %s", error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/actions/init.ts
|
|
250
|
+
const PACKAGE_MANAGER = "pnpm";
|
|
251
|
+
const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
252
|
+
function validateProjectName(name) {
|
|
253
|
+
if (!name || !name.trim()) return "Name is required.";
|
|
254
|
+
if (/\s/.test(name)) return "Name cannot contain whitespace.";
|
|
255
|
+
if (name.startsWith(".") || name.startsWith("/") || name.startsWith("\\")) return "Name cannot start with '.', '/' or '\\'.";
|
|
256
|
+
if (name.includes("..")) return "Name cannot contain '..'.";
|
|
257
|
+
if (!NPM_NAME_RE.test(name)) return "Name must be a valid npm package name (lowercase, no spaces).";
|
|
258
|
+
}
|
|
259
|
+
async function isDirEmpty(dir) {
|
|
260
|
+
try {
|
|
261
|
+
return (await readdir(dir)).length === 0;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error.code === "ENOENT") return true;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function readGitAuthor(shell) {
|
|
268
|
+
try {
|
|
269
|
+
const [name, email] = await Promise.all([shell.runCaptured("git", [
|
|
270
|
+
"config",
|
|
271
|
+
"--get",
|
|
272
|
+
"user.name"
|
|
273
|
+
], { throwOnError: false }), shell.runCaptured("git", [
|
|
274
|
+
"config",
|
|
275
|
+
"--get",
|
|
276
|
+
"user.email"
|
|
277
|
+
], { throwOnError: false })]);
|
|
278
|
+
const trimmedName = name.stdout.trim();
|
|
279
|
+
const trimmedEmail = email.stdout.trim();
|
|
280
|
+
if (!trimmedName) return void 0;
|
|
281
|
+
return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName;
|
|
282
|
+
} catch {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function abort(message) {
|
|
287
|
+
cancel(message);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
async function runInit(ctx, options) {
|
|
291
|
+
const debug = logger.subdebug("init");
|
|
292
|
+
debug("options: %O", options);
|
|
293
|
+
const shell = ctx.shell;
|
|
294
|
+
intro(`${palette.label(" vland init ")}`);
|
|
295
|
+
let template = options.template;
|
|
296
|
+
if (!template) {
|
|
297
|
+
if (!hasTTY) abort("Template is required in non-interactive environments. Use --template <library|backend|monorepo>.");
|
|
298
|
+
const choice = await select({
|
|
299
|
+
message: "Pick a template",
|
|
300
|
+
options: TEMPLATES.map((value) => ({
|
|
301
|
+
value,
|
|
302
|
+
label: value
|
|
303
|
+
}))
|
|
304
|
+
});
|
|
305
|
+
if (isCancel(choice)) abort("Cancelled.");
|
|
306
|
+
template = choice;
|
|
307
|
+
}
|
|
308
|
+
let name = options.name;
|
|
309
|
+
if (!name) {
|
|
310
|
+
if (!hasTTY) abort("Project name is required in non-interactive environments. Pass it as the first argument.");
|
|
311
|
+
const value = await text$1({
|
|
312
|
+
message: "Project name",
|
|
313
|
+
placeholder: TEMPLATE_META[template].placeholder,
|
|
314
|
+
validate: (input) => validateProjectName(input ?? "")
|
|
315
|
+
});
|
|
316
|
+
if (isCancel(value)) abort("Cancelled.");
|
|
317
|
+
name = value;
|
|
318
|
+
}
|
|
319
|
+
const nameError = validateProjectName(name);
|
|
320
|
+
if (nameError) abort(nameError);
|
|
321
|
+
let visibility = options.visibility;
|
|
322
|
+
if (template === "library" && !visibility) if (!hasTTY) visibility = "public";
|
|
323
|
+
else {
|
|
324
|
+
const choice = await select({
|
|
325
|
+
message: "Library visibility",
|
|
326
|
+
options: [{
|
|
327
|
+
value: "public",
|
|
328
|
+
label: "public (@vlandoss/<name>)"
|
|
329
|
+
}, {
|
|
330
|
+
value: "private",
|
|
331
|
+
label: "private (@variableland/<name>, private: true)"
|
|
332
|
+
}],
|
|
333
|
+
initialValue: "public"
|
|
334
|
+
});
|
|
335
|
+
if (isCancel(choice)) abort("Cancelled.");
|
|
336
|
+
visibility = choice;
|
|
337
|
+
}
|
|
338
|
+
const dir = options.dir ? isAbsolute(options.dir) ? options.dir : resolve(process.cwd(), options.dir) : resolve(process.cwd(), name);
|
|
339
|
+
debug("target dir: %s", dir);
|
|
340
|
+
if (!await isDirEmpty(dir) && !options.force) abort(`Target directory ${palette.highlight(dir)} is not empty. Re-run with ${palette.highlight("--force")} to overwrite.`);
|
|
341
|
+
let author = await readGitAuthor(shell);
|
|
342
|
+
if (!author) if (!hasTTY) author = "";
|
|
343
|
+
else {
|
|
344
|
+
const value = await text$1({
|
|
345
|
+
message: "Author (used in package.json / LICENSE)",
|
|
346
|
+
placeholder: "Jane Doe <jane@example.com>",
|
|
347
|
+
defaultValue: ""
|
|
348
|
+
});
|
|
349
|
+
if (isCancel(value)) abort("Cancelled.");
|
|
350
|
+
author = value || "";
|
|
351
|
+
}
|
|
352
|
+
debug("author: %s", author || "<empty>");
|
|
353
|
+
const fetchSpin = spinner();
|
|
354
|
+
fetchSpin.start(`Fetching ${palette.highlight(template)} template`);
|
|
355
|
+
try {
|
|
356
|
+
const { source } = await fetchTemplate({
|
|
357
|
+
template,
|
|
358
|
+
dir,
|
|
359
|
+
force: options.force
|
|
360
|
+
});
|
|
361
|
+
fetchSpin.stop(`Fetched template from ${palette.muted(source)}`);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
fetchSpin.stop("Failed to fetch template", 1);
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
const placeholderSpin = spinner();
|
|
367
|
+
placeholderSpin.start("Applying placeholders");
|
|
368
|
+
await replacePlaceholders(dir, {
|
|
369
|
+
projectName: name,
|
|
370
|
+
author,
|
|
371
|
+
year: (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
372
|
+
});
|
|
373
|
+
await applyRootPackage(dir, {
|
|
374
|
+
template,
|
|
375
|
+
projectName: name,
|
|
376
|
+
visibility
|
|
377
|
+
});
|
|
378
|
+
placeholderSpin.stop("Placeholders applied");
|
|
379
|
+
const shouldInstall = await resolveYesNo(options.install, "Install dependencies?");
|
|
380
|
+
const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?");
|
|
381
|
+
if (shouldInstall) {
|
|
382
|
+
const installSpin = spinner();
|
|
383
|
+
installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
|
|
384
|
+
try {
|
|
385
|
+
await installDependencies({
|
|
386
|
+
cwd: dir,
|
|
387
|
+
packageManager: {
|
|
388
|
+
name: PACKAGE_MANAGER,
|
|
389
|
+
command: PACKAGE_MANAGER
|
|
390
|
+
},
|
|
391
|
+
silent: true
|
|
392
|
+
});
|
|
393
|
+
installSpin.stop(`Installed with ${palette.highlight(PACKAGE_MANAGER)}`);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
installSpin.stop("Failed to install dependencies", 1);
|
|
396
|
+
log.warn(`You can install manually later with \`cd <dir> && ${PACKAGE_MANAGER} install\`.`);
|
|
397
|
+
debug("install error: %O", error);
|
|
398
|
+
}
|
|
399
|
+
} else log.info(`Skipping ${palette.highlight("install")}.`);
|
|
400
|
+
if (shouldGit) {
|
|
401
|
+
const gitSpin = spinner();
|
|
402
|
+
gitSpin.start("Initialising git repository");
|
|
403
|
+
try {
|
|
404
|
+
const gitShell = shell.at(dir).child({ env: {
|
|
405
|
+
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland",
|
|
406
|
+
GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land",
|
|
407
|
+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland",
|
|
408
|
+
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land"
|
|
409
|
+
} });
|
|
410
|
+
await gitShell.runCaptured("git", ["init"]);
|
|
411
|
+
await gitShell.runCaptured("git", ["add", "-A"]);
|
|
412
|
+
await gitShell.runCaptured("git", [
|
|
413
|
+
"commit",
|
|
414
|
+
"-m",
|
|
415
|
+
"chore: initial commit from vland"
|
|
416
|
+
]);
|
|
417
|
+
gitSpin.stop("Initialised git repository");
|
|
418
|
+
} catch (error) {
|
|
419
|
+
gitSpin.stop("Failed to initialise git", 1);
|
|
420
|
+
debug("git error: %O", error);
|
|
421
|
+
}
|
|
422
|
+
} else log.info(`Skipping ${palette.highlight("git init")}.`);
|
|
423
|
+
const runScript = TEMPLATE_META[template].runScript;
|
|
424
|
+
outro([
|
|
425
|
+
palette.success("Done!"),
|
|
426
|
+
"",
|
|
427
|
+
palette.muted("Next steps:"),
|
|
428
|
+
` cd ${name}`,
|
|
429
|
+
shouldInstall ? ` ${PACKAGE_MANAGER} ${runScript}` : ` ${PACKAGE_MANAGER} install && ${PACKAGE_MANAGER} ${runScript}`
|
|
430
|
+
].join("\n"));
|
|
431
|
+
}
|
|
432
|
+
async function resolveYesNo(explicit, message) {
|
|
433
|
+
if (typeof explicit === "boolean") return explicit;
|
|
434
|
+
if (!hasTTY) return true;
|
|
435
|
+
const value = await confirm({
|
|
436
|
+
message,
|
|
437
|
+
initialValue: true
|
|
438
|
+
});
|
|
439
|
+
if (isCancel(value)) abort("Cancelled.");
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/program/commands/init.ts
|
|
444
|
+
function createInitCommand(ctx) {
|
|
445
|
+
return createCommand("init").summary(`init a new project (${TOOL_LABELS.GIGET})`).description("Scaffold a new variableland project from one of the official templates.").addArgument(new Argument("[name]", "project name (also used as the target directory)")).addOption(new Option("-t, --template <name>", "template to use").choices([...TEMPLATES])).addOption(new Option("--visibility <visibility>", "package visibility (library only)").choices([...VISIBILITIES])).addOption(new Option("-d, --dir <path>", "target directory (default: ./<name>)")).addOption(new Option("--install", "install dependencies (skip prompt)")).addOption(new Option("--no-install", "skip dependency installation")).addOption(new Option("--git", "initialise git repository (skip prompt)")).addOption(new Option("--no-git", "skip git init")).addOption(new Option("-f, --force", "overwrite existing directory").default(false)).action(async function(name, options) {
|
|
446
|
+
console.log(getBannerText(ctx.binPkg.version));
|
|
447
|
+
const installSource = this.getOptionValueSource("install");
|
|
448
|
+
const gitSource = this.getOptionValueSource("git");
|
|
449
|
+
await runInit(ctx, {
|
|
450
|
+
name,
|
|
451
|
+
...options,
|
|
452
|
+
install: installSource === "cli" ? options.install : void 0,
|
|
453
|
+
git: gitSource === "cli" ? options.git : void 0
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/program/index.ts
|
|
459
|
+
async function createProgram(options) {
|
|
460
|
+
const ctx = await createContext(options.binDir);
|
|
461
|
+
const version = ctx.binPkg.version;
|
|
462
|
+
return new Command("vland").version(version, "-v, --version").addOption(new Option("--usage", `print KDL spec for this CLI (${palette.muted(palette.link("https://kdl.dev"))})`)).on("option:usage", function onUsage() {
|
|
463
|
+
generateToStdout(this);
|
|
464
|
+
process.exit(0);
|
|
465
|
+
}).addHelpText("before", getBannerText(version)).addCommand(createCompletionCommand()).addCommand(createInitCommand(ctx));
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/run.ts
|
|
469
|
+
const BIN_DIR = path.dirname(dirnameOf(import.meta));
|
|
470
|
+
await run(async () => {
|
|
471
|
+
await (await createProgram({ binDir: BIN_DIR })).parseAsync();
|
|
472
|
+
}, logger);
|
|
473
|
+
//#endregion
|
|
474
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vlandoss/vland",
|
|
3
|
-
"version": "0.2.1-git-
|
|
3
|
+
"version": "0.2.1-git-87d22db.0",
|
|
4
4
|
"description": "The CLI to init a new project in Variable Land",
|
|
5
|
-
"homepage": "https://github.com/variableland/dx/tree/main/
|
|
5
|
+
"homepage": "https://github.com/variableland/dx/tree/main/vland/cli#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/variableland/dx/issues"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/variableland/dx.git",
|
|
12
|
-
"directory": "
|
|
12
|
+
"directory": "vland/cli"
|
|
13
13
|
},
|
|
14
14
|
"license": "MIT",
|
|
15
|
-
"author":
|
|
16
|
-
"name": "rcrd",
|
|
17
|
-
"email": "rcrd@variable.land"
|
|
18
|
-
},
|
|
15
|
+
"author": "rcrd <rcrd@variable.land>",
|
|
19
16
|
"type": "module",
|
|
20
17
|
"imports": {
|
|
21
18
|
"#src/*": "./src/*",
|
|
@@ -35,11 +32,11 @@
|
|
|
35
32
|
"dependencies": {
|
|
36
33
|
"@clack/prompts": "0.11.0",
|
|
37
34
|
"@usage-spec/commander": "1.1.0",
|
|
38
|
-
"@vlandoss/clibuddy": "0.6.1-git-74f39bb.0",
|
|
39
|
-
"@vlandoss/loggy": "0.2.1-git-74f39bb.0",
|
|
40
35
|
"commander": "14.0.3",
|
|
41
36
|
"giget": "2.0.0",
|
|
42
|
-
"nypm": "0.6.0"
|
|
37
|
+
"nypm": "0.6.0",
|
|
38
|
+
"@vlandoss/clibuddy": "0.6.1-git-87d22db.0",
|
|
39
|
+
"@vlandoss/loggy": "0.2.1-git-87d22db.0"
|
|
43
40
|
},
|
|
44
41
|
"publishConfig": {
|
|
45
42
|
"access": "public"
|
|
@@ -48,10 +45,8 @@
|
|
|
48
45
|
"node": ">=20.0.0"
|
|
49
46
|
},
|
|
50
47
|
"devDependencies": {
|
|
51
|
-
"@
|
|
48
|
+
"@rrlab/tsdown-config": "^0.0.1-git-87d22db.0"
|
|
52
49
|
},
|
|
53
|
-
"readme": "ERROR: No README data found!",
|
|
54
|
-
"_id": "@vlandoss/vland@0.2.0",
|
|
55
50
|
"scripts": {
|
|
56
51
|
"build": "tsdown && pnpm build:kdl",
|
|
57
52
|
"build:kdl": "./bin --usage > dist/cli.usage.kdl",
|
|
@@ -60,4 +55,4 @@
|
|
|
60
55
|
"test:integration": "vitest run --project integration",
|
|
61
56
|
"test:types": "rr tsc"
|
|
62
57
|
}
|
|
63
|
-
}
|
|
58
|
+
}
|
package/src/actions/init.ts
CHANGED
|
@@ -2,22 +2,24 @@ import { readdir } from "node:fs/promises";
|
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
4
4
|
import { hasTTY, palette } from "@vlandoss/clibuddy";
|
|
5
|
-
import {
|
|
5
|
+
import { installDependencies } from "nypm";
|
|
6
6
|
import type { Context } from "#src/services/ctx.ts";
|
|
7
7
|
import { logger } from "#src/services/logger.ts";
|
|
8
|
-
import {
|
|
9
|
-
import { fetchTemplate, TEMPLATE_META, TEMPLATES, type TemplateName } from "./template.ts";
|
|
8
|
+
import { applyRootPackage, replacePlaceholders } from "./placeholders.ts";
|
|
9
|
+
import { fetchTemplate, TEMPLATE_META, TEMPLATES, type TemplateName, type Visibility } from "./template.ts";
|
|
10
10
|
|
|
11
11
|
export type InitOptions = {
|
|
12
12
|
name?: string;
|
|
13
13
|
template?: TemplateName;
|
|
14
|
+
visibility?: Visibility;
|
|
14
15
|
dir?: string;
|
|
15
|
-
pm?: "npm" | "pnpm" | "yarn" | "bun";
|
|
16
16
|
install?: boolean;
|
|
17
17
|
git?: boolean;
|
|
18
18
|
force: boolean;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const PACKAGE_MANAGER = "pnpm" as const;
|
|
22
|
+
|
|
21
23
|
const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
22
24
|
|
|
23
25
|
function validateProjectName(name: string): string | undefined {
|
|
@@ -71,7 +73,6 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
71
73
|
|
|
72
74
|
intro(`${palette.label(" vland init ")}`);
|
|
73
75
|
|
|
74
|
-
// 1. Resolve template
|
|
75
76
|
let template = options.template;
|
|
76
77
|
if (!template) {
|
|
77
78
|
if (!hasTTY) abort("Template is required in non-interactive environments. Use --template <library|backend|monorepo>.");
|
|
@@ -86,7 +87,6 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
86
87
|
template = choice as TemplateName;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
// 2. Resolve project name
|
|
90
90
|
let name = options.name;
|
|
91
91
|
if (!name) {
|
|
92
92
|
if (!hasTTY) abort("Project name is required in non-interactive environments. Pass it as the first argument.");
|
|
@@ -101,7 +101,29 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
101
101
|
const nameError = validateProjectName(name);
|
|
102
102
|
if (nameError) abort(nameError);
|
|
103
103
|
|
|
104
|
-
//
|
|
104
|
+
// Visibility only meaningfully changes the library template (scope +
|
|
105
|
+
// private flag). For backend/monorepo we ignore it — those are always
|
|
106
|
+
// private at root. Prompt for it interactively when building a library
|
|
107
|
+
// and the user didn't pass --visibility; non-interactive default is
|
|
108
|
+
// "public" (the OSS-style scope, @vlandoss).
|
|
109
|
+
let visibility = options.visibility;
|
|
110
|
+
if (template === "library" && !visibility) {
|
|
111
|
+
if (!hasTTY) {
|
|
112
|
+
visibility = "public";
|
|
113
|
+
} else {
|
|
114
|
+
const choice = await select<Visibility>({
|
|
115
|
+
message: "Library visibility",
|
|
116
|
+
options: [
|
|
117
|
+
{ value: "public", label: "public (@vlandoss/<name>)" },
|
|
118
|
+
{ value: "private", label: "private (@variableland/<name>, private: true)" },
|
|
119
|
+
],
|
|
120
|
+
initialValue: "public",
|
|
121
|
+
});
|
|
122
|
+
if (isCancel(choice)) abort("Cancelled.");
|
|
123
|
+
visibility = choice as Visibility;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
const dir = options.dir
|
|
106
128
|
? isAbsolute(options.dir)
|
|
107
129
|
? options.dir
|
|
@@ -113,7 +135,6 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
113
135
|
abort(`Target directory ${palette.highlight(dir)} is not empty. Re-run with ${palette.highlight("--force")} to overwrite.`);
|
|
114
136
|
}
|
|
115
137
|
|
|
116
|
-
// 4. Resolve author
|
|
117
138
|
let author = await readGitAuthor(shell);
|
|
118
139
|
if (!author) {
|
|
119
140
|
if (!hasTTY) {
|
|
@@ -130,7 +151,6 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
130
151
|
}
|
|
131
152
|
debug("author: %s", author || "<empty>");
|
|
132
153
|
|
|
133
|
-
// 5. Download template
|
|
134
154
|
const fetchSpin = spinner();
|
|
135
155
|
fetchSpin.start(`Fetching ${palette.highlight(template)} template`);
|
|
136
156
|
try {
|
|
@@ -141,7 +161,6 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
141
161
|
throw error;
|
|
142
162
|
}
|
|
143
163
|
|
|
144
|
-
// 6. Replace placeholders
|
|
145
164
|
const placeholderSpin = spinner();
|
|
146
165
|
placeholderSpin.start("Applying placeholders");
|
|
147
166
|
await replacePlaceholders(dir, {
|
|
@@ -149,31 +168,31 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
149
168
|
author,
|
|
150
169
|
year: new Date().getFullYear().toString(),
|
|
151
170
|
});
|
|
152
|
-
await
|
|
171
|
+
await applyRootPackage(dir, { template, projectName: name, visibility });
|
|
153
172
|
placeholderSpin.stop("Placeholders applied");
|
|
154
173
|
|
|
155
|
-
// 7. Resolve install / git decisions (prompt with default-yes when not set on CLI)
|
|
156
174
|
const shouldInstall = await resolveYesNo(options.install, "Install dependencies?");
|
|
157
175
|
const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?");
|
|
158
176
|
|
|
159
|
-
// 8. Install deps
|
|
160
177
|
if (shouldInstall) {
|
|
161
|
-
const detected = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
|
|
162
178
|
const installSpin = spinner();
|
|
163
|
-
installSpin.start(`Installing dependencies with ${palette.highlight(
|
|
179
|
+
installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
|
|
164
180
|
try {
|
|
165
|
-
await installDependencies({
|
|
166
|
-
|
|
181
|
+
await installDependencies({
|
|
182
|
+
cwd: dir,
|
|
183
|
+
packageManager: { name: PACKAGE_MANAGER, command: PACKAGE_MANAGER },
|
|
184
|
+
silent: true,
|
|
185
|
+
});
|
|
186
|
+
installSpin.stop(`Installed with ${palette.highlight(PACKAGE_MANAGER)}`);
|
|
167
187
|
} catch (error) {
|
|
168
188
|
installSpin.stop("Failed to install dependencies", 1);
|
|
169
|
-
log.warn(
|
|
189
|
+
log.warn(`You can install manually later with \`cd <dir> && ${PACKAGE_MANAGER} install\`.`);
|
|
170
190
|
debug("install error: %O", error);
|
|
171
191
|
}
|
|
172
192
|
} else {
|
|
173
193
|
log.info(`Skipping ${palette.highlight("install")}.`);
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
// 9. Git init
|
|
177
196
|
if (shouldGit) {
|
|
178
197
|
const gitSpin = spinner();
|
|
179
198
|
gitSpin.start("Initialising git repository");
|
|
@@ -198,8 +217,6 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
198
217
|
log.info(`Skipping ${palette.highlight("git init")}.`);
|
|
199
218
|
}
|
|
200
219
|
|
|
201
|
-
// 10. Outro with next steps
|
|
202
|
-
const detectedPm = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
|
|
203
220
|
const runScript = TEMPLATE_META[template].runScript;
|
|
204
221
|
outro(
|
|
205
222
|
[
|
|
@@ -207,7 +224,7 @@ export async function runInit(ctx: Context, options: InitOptions) {
|
|
|
207
224
|
"",
|
|
208
225
|
palette.muted("Next steps:"),
|
|
209
226
|
` cd ${name}`,
|
|
210
|
-
shouldInstall ? ` ${
|
|
227
|
+
shouldInstall ? ` ${PACKAGE_MANAGER} ${runScript}` : ` ${PACKAGE_MANAGER} install && ${PACKAGE_MANAGER} ${runScript}`,
|
|
211
228
|
].join("\n"),
|
|
212
229
|
);
|
|
213
230
|
}
|
|
@@ -3,6 +3,7 @@ import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
4
|
import { createPkg } from "@vlandoss/clibuddy";
|
|
5
5
|
import { logger } from "#src/services/logger.ts";
|
|
6
|
+
import { LIBRARY_SCOPES, type TemplateName, type Visibility } from "./template.ts";
|
|
6
7
|
|
|
7
8
|
export type Placeholders = {
|
|
8
9
|
projectName: string;
|
|
@@ -111,27 +112,44 @@ export async function replacePlaceholders(rootDir: string, values: Placeholders)
|
|
|
111
112
|
return { touched };
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
export type ApplyRootPackageOptions = {
|
|
116
|
+
template: TemplateName;
|
|
117
|
+
projectName: string;
|
|
118
|
+
visibility?: Visibility;
|
|
119
|
+
};
|
|
120
|
+
|
|
114
121
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
122
|
+
* Writes the root `package.json`'s `name` and `private` fields based on the
|
|
123
|
+
* chosen template and visibility. Uses `pkg-types` so field ordering and
|
|
124
|
+
* JSON formatting survive.
|
|
125
|
+
*
|
|
126
|
+
* Naming rules:
|
|
127
|
+
* - `library` private → `@variableland/<projectName>`, `private: true`
|
|
128
|
+
* - `library` public → `@vlandoss/<projectName>`
|
|
129
|
+
* - `backend` → `<projectName>`, `private: true`
|
|
130
|
+
* - `monorepo` → `<projectName>`, `private: true` (root never publishes)
|
|
118
131
|
*/
|
|
119
|
-
export async function
|
|
120
|
-
const debug = logger.subdebug("
|
|
132
|
+
export async function applyRootPackage(rootDir: string, options: ApplyRootPackageOptions) {
|
|
133
|
+
const debug = logger.subdebug("apply-root-package");
|
|
121
134
|
|
|
122
135
|
const rootPath = fs.realpathSync(rootDir);
|
|
123
|
-
|
|
124
|
-
debug("root path:", rootPath);
|
|
125
|
-
debug("process cwd:", process.cwd());
|
|
126
|
-
|
|
127
136
|
const pkg = await createPkg(rootPath);
|
|
137
|
+
if (!pkg) throw new Error("Could not find package.json");
|
|
128
138
|
|
|
129
|
-
|
|
130
|
-
throw new Error("Could not find package.json");
|
|
131
|
-
}
|
|
139
|
+
const { template, projectName, visibility } = options;
|
|
132
140
|
|
|
133
141
|
try {
|
|
134
|
-
|
|
142
|
+
if (template === "library") {
|
|
143
|
+
const scope = LIBRARY_SCOPES[visibility ?? "public"];
|
|
144
|
+
pkg.packageJson.name = `${scope}/${projectName}`;
|
|
145
|
+
if (visibility === "private") {
|
|
146
|
+
pkg.packageJson.private = true;
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// backend + monorepo: bare project name, private root.
|
|
150
|
+
pkg.packageJson.name = projectName;
|
|
151
|
+
pkg.packageJson.private = true;
|
|
152
|
+
}
|
|
135
153
|
await pkg.write(pkg.packageJson);
|
|
136
154
|
} catch (error) {
|
|
137
155
|
debug("skipped %s", error);
|
package/src/actions/template.ts
CHANGED
|
@@ -6,12 +6,25 @@ import { logger } from "#src/services/logger.ts";
|
|
|
6
6
|
export const TEMPLATES = ["library", "backend", "monorepo"] as const;
|
|
7
7
|
export type TemplateName = (typeof TEMPLATES)[number];
|
|
8
8
|
|
|
9
|
+
export const VISIBILITIES = ["private", "public"] as const;
|
|
10
|
+
export type Visibility = (typeof VISIBILITIES)[number];
|
|
11
|
+
|
|
9
12
|
export const TEMPLATE_META: Record<TemplateName, { placeholder: string; runScript: string }> = {
|
|
10
13
|
library: { placeholder: "my-lib", runScript: "test" },
|
|
11
14
|
backend: { placeholder: "my-api", runScript: "dev" },
|
|
12
15
|
monorepo: { placeholder: "my-mono", runScript: "dev" },
|
|
13
16
|
};
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The npm scope a `library` template's package is published under, indexed by
|
|
20
|
+
* the user-selected visibility. Public libraries live under the open-source
|
|
21
|
+
* `@vlandoss` scope; private libraries under `@variableland`.
|
|
22
|
+
*/
|
|
23
|
+
export const LIBRARY_SCOPES: Record<Visibility, string> = {
|
|
24
|
+
private: "@variableland",
|
|
25
|
+
public: "@vlandoss",
|
|
26
|
+
};
|
|
27
|
+
|
|
15
28
|
const GITHUB_SOURCE = "github:variableland/dx";
|
|
16
29
|
const GITHUB_REF = "main";
|
|
17
30
|
|
|
@@ -24,8 +37,8 @@ type ResolveOptions = {
|
|
|
24
37
|
/**
|
|
25
38
|
* Resolves the template into `dir`. Source order:
|
|
26
39
|
* 1. `VLAND_TEMPLATES_DIR` env var → copy from local path (used by E2E tests
|
|
27
|
-
* against the in-repo `templates/`).
|
|
28
|
-
* 2. Otherwise → download via giget from `github:variableland/dx/templates/<name>`.
|
|
40
|
+
* against the in-repo `vland/templates/`).
|
|
41
|
+
* 2. Otherwise → download via giget from `github:variableland/dx/vland/templates/<name>`.
|
|
29
42
|
*/
|
|
30
43
|
export async function fetchTemplate(options: ResolveOptions): Promise<{ source: string }> {
|
|
31
44
|
const debug = logger.subdebug("fetch-template");
|
|
@@ -43,7 +56,7 @@ export async function fetchTemplate(options: ResolveOptions): Promise<{ source:
|
|
|
43
56
|
return { source: sourceDir };
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
const source = `${GITHUB_SOURCE}/templates/${options.template}#${GITHUB_REF}`;
|
|
59
|
+
const source = `${GITHUB_SOURCE}/vland/templates/${options.template}#${GITHUB_REF}`;
|
|
47
60
|
debug("remote source: %s", source);
|
|
48
61
|
const result = await downloadTemplate(source, {
|
|
49
62
|
dir: options.dir,
|
|
@@ -9,7 +9,7 @@ const SHELLS = ["bash", "zsh", "fish"] as const;
|
|
|
9
9
|
// dispatcher, which intercepts `vland completion <shell>` before reaching Node.
|
|
10
10
|
export function createCompletionCommand() {
|
|
11
11
|
return createCommand("completion")
|
|
12
|
-
.summary(`print shell completion script
|
|
12
|
+
.summary(`print shell completion script (${TOOL_LABELS.USAGE})`)
|
|
13
13
|
.description(
|
|
14
14
|
`Prints a shell completion script for vland. Add to your shell rc file:
|
|
15
15
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Argument, createCommand, Option } from "commander";
|
|
2
2
|
import { runInit } from "#src/actions/init.ts";
|
|
3
|
-
import { TEMPLATES, type TemplateName } from "#src/actions/template.ts";
|
|
3
|
+
import { TEMPLATES, type TemplateName, VISIBILITIES, type Visibility } from "#src/actions/template.ts";
|
|
4
4
|
import type { Context } from "#src/services/ctx.ts";
|
|
5
5
|
import { getBannerText, TOOL_LABELS } from "../ui.ts";
|
|
6
6
|
|
|
7
7
|
type InitOptions = {
|
|
8
8
|
dir?: string;
|
|
9
9
|
template?: TemplateName;
|
|
10
|
-
|
|
10
|
+
visibility?: Visibility;
|
|
11
11
|
install: boolean;
|
|
12
12
|
git: boolean;
|
|
13
13
|
force: boolean;
|
|
@@ -15,12 +15,12 @@ type InitOptions = {
|
|
|
15
15
|
|
|
16
16
|
export function createInitCommand(ctx: Context) {
|
|
17
17
|
return createCommand("init")
|
|
18
|
-
.summary(`init a new project
|
|
18
|
+
.summary(`init a new project (${TOOL_LABELS.GIGET})`)
|
|
19
19
|
.description("Scaffold a new variableland project from one of the official templates.")
|
|
20
20
|
.addArgument(new Argument("[name]", "project name (also used as the target directory)"))
|
|
21
21
|
.addOption(new Option("-t, --template <name>", "template to use").choices([...TEMPLATES]))
|
|
22
|
+
.addOption(new Option("--visibility <visibility>", "package visibility (library only)").choices([...VISIBILITIES]))
|
|
22
23
|
.addOption(new Option("-d, --dir <path>", "target directory (default: ./<name>)"))
|
|
23
|
-
.addOption(new Option("--pm <manager>", "package manager to use").choices(["npm", "pnpm", "yarn", "bun"]))
|
|
24
24
|
.addOption(new Option("--install", "install dependencies (skip prompt)"))
|
|
25
25
|
.addOption(new Option("--no-install", "skip dependency installation"))
|
|
26
26
|
.addOption(new Option("--git", "initialise git repository (skip prompt)"))
|
package/src/program/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateToStdout } from "@usage-spec/commander";
|
|
2
|
+
import { palette } from "@vlandoss/clibuddy";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
2
4
|
import { createContext } from "#src/services/ctx.ts";
|
|
3
5
|
import { createCompletionCommand } from "./commands/completion.ts";
|
|
4
6
|
import { createInitCommand } from "./commands/init.ts";
|
|
5
|
-
import { addUsage } from "./commands/usage.ts";
|
|
6
7
|
import { getBannerText } from "./ui.ts";
|
|
7
8
|
|
|
8
9
|
export type Options = {
|
|
@@ -13,11 +14,14 @@ export async function createProgram(options: Options) {
|
|
|
13
14
|
const ctx = await createContext(options.binDir);
|
|
14
15
|
const version = ctx.binPkg.version;
|
|
15
16
|
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
17
|
+
return new Command("vland")
|
|
18
|
+
.version(version, "-v, --version")
|
|
19
|
+
.addOption(new Option("--usage", `print KDL spec for this CLI (${palette.muted(palette.link("https://kdl.dev"))})`))
|
|
20
|
+
.on("option:usage", function onUsage(this: Command) {
|
|
21
|
+
generateToStdout(this);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
})
|
|
24
|
+
.addHelpText("before", getBannerText(version))
|
|
25
|
+
.addCommand(createCompletionCommand())
|
|
26
|
+
.addCommand(createInitCommand(ctx));
|
|
23
27
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { generateToStdout } from "@usage-spec/commander";
|
|
2
|
-
import { type Command, Option } from "commander";
|
|
3
|
-
|
|
4
|
-
export function addUsage(program: Command) {
|
|
5
|
-
return program.addOption(new Option("--usage", "print KDL spec for this CLI (https://kdl.dev)")).on("option:usage", () => {
|
|
6
|
-
generateToStdout(program);
|
|
7
|
-
process.exit(0);
|
|
8
|
-
});
|
|
9
|
-
}
|