@vlandoss/vland 0.0.1-git-53b6b02.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 ADDED
@@ -0,0 +1,80 @@
1
+ # πŸ¦‰ vland
2
+
3
+ The CLI to init a new project in Variable Land πŸ‘Š
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js >= 20.0.0
8
+
9
+ ## Installation
10
+
11
+ Run it directly with `npx`:
12
+
13
+ ```sh
14
+ npx @vlandoss/vland init
15
+ ```
16
+
17
+ …or install it globally:
18
+
19
+ ```sh
20
+ pnpm add -g @vlandoss/vland
21
+ vland init
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```sh
27
+ vland init # interactive
28
+ vland init my-app -t library # explicit template
29
+ vland init my-app -t backend --no-install --no-git
30
+ ```
31
+
32
+ See [`CLI.md`](./CLI.md) for the full reference (auto-generated per release).
33
+
34
+ ### Templates
35
+
36
+ | Template | What you get |
37
+ |------------|--------------|
38
+ | `library` | A standalone TypeScript library with tsdown, Vitest, biome, Changesets release workflow. |
39
+ | `backend` | An Elysia (`@elysiajs/node`) backend with evlog, Vitest, Dockerfile, CI shape. |
40
+ | `monorepo` | pnpm + Turbo workspace with an Elysia API, a Vite-React SPA, and a few internal packages. |
41
+
42
+ All templates target Node.js, use pnpm, and extend `@vlandoss/config` for biome and tsconfig.
43
+
44
+ ## Shell completion
45
+
46
+ `vland` ships a `completion` subcommand that prints a shell-specific script. Add it to your shell rc file:
47
+
48
+ ```sh
49
+ # zsh β€” ~/.zshrc
50
+ eval "$(vland completion zsh)"
51
+
52
+ # bash β€” ~/.bashrc
53
+ eval "$(vland completion bash)"
54
+
55
+ # fish β€” ~/.config/fish/config.fish
56
+ vland completion fish | source
57
+ ```
58
+
59
+ **Prerequisite:** the [`usage`](https://usage.jdx.dev) CLI must be on your `PATH` (it powers completion at runtime). Install via one of:
60
+
61
+ ```sh
62
+ mise use -g usage
63
+ brew install usage
64
+ ```
65
+
66
+ When you upgrade `@vlandoss/vland`, the next shell session will pick up new commands automatically β€” no need to re-run anything.
67
+
68
+ ## Troubleshooting
69
+
70
+ To enable debug mode, set the `DEBUG` environment variable to `vland:*` before running *any* command.
71
+
72
+ ```sh
73
+ DEBUG=vland:* vland init my-app
74
+ ```
75
+
76
+ To point `init` at local templates instead of fetching from GitHub (useful when developing inside this monorepo):
77
+
78
+ ```sh
79
+ VLAND_TEMPLATES_DIR=/absolute/path/to/dx/templates vland init my-app -t library
80
+ ```
package/bin ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [ -L "$SOURCE" ]; do
6
+ TARGET="$(readlink "$SOURCE")"
7
+ case "$TARGET" in
8
+ /*) SOURCE="$TARGET" ;;
9
+ *) SOURCE="$(dirname "$SOURCE")/$TARGET" ;;
10
+ esac
11
+ done
12
+ DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
13
+
14
+ if [ "$1" = "completion" ]; then
15
+ case "$2" in
16
+ bash | zsh | fish)
17
+ kdl="$DIR/dist/cli.usage.kdl"
18
+ if [ ! -f "$kdl" ]; then
19
+ echo "vland completion: missing $kdl. Reinstall @vlandoss/vland or run \`pnpm build\`." >&2
20
+ exit 1
21
+ fi
22
+ if ! command -v usage >/dev/null 2>&1; then
23
+ echo "vland completion: 'usage' CLI not found in PATH." >&2
24
+ echo "Install via: mise use -g usage | brew install usage" >&2
25
+ exit 1
26
+ fi
27
+ exec usage generate completion "$2" vland --file "$kdl"
28
+ ;;
29
+ esac
30
+ # Unknown shell or `--help`: fall through to Node so Commander prints help/errors.
31
+ fi
32
+
33
+ # In the source repo (tsdown.config.ts present, NOT shipped in the npm tarball),
34
+ # always run from src/ so live edits show up. In a published install, use the
35
+ # bundled dist/run.mjs.
36
+ if [ -f "$DIR/tsdown.config.ts" ]; then
37
+ exec node "$DIR/src/run.ts" "$@"
38
+ else
39
+ exec node "$DIR/dist/run.mjs" "$@"
40
+ fi
@@ -0,0 +1,32 @@
1
+ // @generated by @usage-spec/commander from Commander.js metadata
2
+ name vland
3
+ bin vland
4
+ version "0.0.1-git-53b6b02.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 🐚 (\u{1b}[38;2;36;197;94musage\u{1b}[39m)" {
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 πŸš€ (\u{1b}[38;2;244;114;182mgiget\u{1b}[39m)" {
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 "-d --dir" help="target directory (default: ./<name>)" {
21
+ arg <DIR>
22
+ }
23
+ flag --pm help="package manager to use" {
24
+ arg <PM> {
25
+ choices npm pnpm yarn bun
26
+ }
27
+ }
28
+ flag --no-install help="skip dependency installation" negate=--install
29
+ flag --no-git help="skip git init" negate=--git
30
+ flag "-f --force" help="overwrite existing directory"
31
+ arg "[name]" help="project name (also used as the target directory)" required=#false
32
+ }
package/dist/run.mjs ADDED
@@ -0,0 +1,399 @@
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 { Argument, Command, Option, createCommand } from "commander";
4
+ import fs from "node:fs";
5
+ import { createLoggy } from "@vlandoss/loggy";
6
+ import { cp, readFile, readdir, stat, writeFile } from "node:fs/promises";
7
+ import { cancel, intro, isCancel, log, outro, select, spinner, text as text$1 } from "@clack/prompts";
8
+ import { detectPackageManager, installDependencies } from "nypm";
9
+ import { downloadTemplate } from "giget";
10
+ import { generateToStdout } from "@usage-spec/commander";
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({ localBaseBinPath: [binDir] });
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/placeholders.ts
68
+ const TEXT_EXTENSIONS = new Set([
69
+ ".ts",
70
+ ".tsx",
71
+ ".js",
72
+ ".jsx",
73
+ ".mjs",
74
+ ".cjs",
75
+ ".mts",
76
+ ".cts",
77
+ ".json",
78
+ ".jsonc",
79
+ ".md",
80
+ ".mdx",
81
+ ".yml",
82
+ ".yaml",
83
+ ".toml",
84
+ ".css",
85
+ ".html",
86
+ ".sh",
87
+ ".env",
88
+ ".gitignore",
89
+ ".gitattributes",
90
+ ".npmrc",
91
+ ".nvmrc",
92
+ ".node-version",
93
+ ".dockerignore",
94
+ ".editorconfig",
95
+ ".prettierrc"
96
+ ]);
97
+ const TEXT_FILENAMES = new Set([
98
+ "Dockerfile",
99
+ "LICENSE",
100
+ "README",
101
+ "CHANGELOG",
102
+ ".gitignore",
103
+ ".gitattributes",
104
+ ".npmrc",
105
+ ".nvmrc",
106
+ ".node-version",
107
+ ".dockerignore",
108
+ ".editorconfig",
109
+ ".prettierrc",
110
+ "lefthook.yml",
111
+ "mise.toml"
112
+ ]);
113
+ const SKIP_DIRS = new Set([
114
+ "node_modules",
115
+ ".git",
116
+ "dist",
117
+ ".turbo",
118
+ ".next",
119
+ "build",
120
+ "coverage"
121
+ ]);
122
+ function isTextFile(name) {
123
+ if (TEXT_FILENAMES.has(name)) return true;
124
+ return TEXT_EXTENSIONS.has(extname(name));
125
+ }
126
+ async function walk(root, onFile) {
127
+ const entries = await readdir(root, { withFileTypes: true });
128
+ await Promise.all(entries.map(async (entry) => {
129
+ const full = join(root, entry.name);
130
+ if (entry.isDirectory()) {
131
+ if (SKIP_DIRS.has(entry.name)) return;
132
+ await walk(full, onFile);
133
+ return;
134
+ }
135
+ if (entry.isFile()) await onFile(full);
136
+ }));
137
+ }
138
+ function applyPlaceholders(content, values) {
139
+ return content.replaceAll("{{projectName}}", values.projectName).replaceAll("{{author}}", values.author).replaceAll("{{year}}", values.year);
140
+ }
141
+ async function replacePlaceholders(rootDir, values) {
142
+ const debug = logger.subdebug("placeholders");
143
+ let touched = 0;
144
+ await walk(rootDir, async (filePath) => {
145
+ if (!isTextFile(filePath.split("/").pop() ?? "")) return;
146
+ if ((await stat(filePath)).size > 1e6) return;
147
+ const original = await readFile(filePath, "utf8");
148
+ const replaced = applyPlaceholders(original, values);
149
+ if (replaced !== original) {
150
+ await writeFile(filePath, replaced);
151
+ touched += 1;
152
+ }
153
+ });
154
+ debug("placeholders applied to %d file(s)", touched);
155
+ return { touched };
156
+ }
157
+ /**
158
+ * Updates the root `package.json` `name` field via `pkg-types`. Safer than a
159
+ * regex pass because it preserves field ordering and JSON formatting handled
160
+ * by `pkg-types`.
161
+ */
162
+ async function updateRootPackageName(rootDir, projectName) {
163
+ const debug = logger.subdebug("update-root-package-name");
164
+ const rootPath = fs.realpathSync(rootDir);
165
+ debug("root path:", rootPath);
166
+ debug("process cwd:", process.cwd());
167
+ const pkg = await createPkg(rootPath);
168
+ if (!pkg) throw new Error("Could not find package.json");
169
+ try {
170
+ pkg.packageJson.name = projectName;
171
+ await pkg.write(pkg.packageJson);
172
+ } catch (error) {
173
+ debug("skipped %s", error);
174
+ }
175
+ }
176
+ //#endregion
177
+ //#region src/actions/template.ts
178
+ const TEMPLATES = [
179
+ "library",
180
+ "backend",
181
+ "monorepo"
182
+ ];
183
+ const GITHUB_SOURCE = "github:variableland/dx";
184
+ const GITHUB_REF = "main";
185
+ /**
186
+ * Resolves the template into `dir`. Source order:
187
+ * 1. `VLAND_TEMPLATES_DIR` env var β†’ copy from local path (used by E2E tests
188
+ * against the in-repo `templates/`).
189
+ * 2. Otherwise β†’ download via giget from `github:variableland/dx/templates/<name>`.
190
+ */
191
+ async function fetchTemplate(options) {
192
+ const debug = logger.subdebug("fetch-template");
193
+ const localRoot = process.env.VLAND_TEMPLATES_DIR;
194
+ if (localRoot) {
195
+ const sourceDir = resolve(localRoot, options.template);
196
+ debug("local source: %s", sourceDir);
197
+ await cp(sourceDir, options.dir, {
198
+ recursive: true,
199
+ force: options.force,
200
+ errorOnExist: !options.force,
201
+ filter: (src) => !src.includes("/node_modules") && !src.endsWith("/.turbo") && !src.endsWith("/dist")
202
+ });
203
+ return { source: sourceDir };
204
+ }
205
+ const source = `${GITHUB_SOURCE}/templates/${options.template}#${GITHUB_REF}`;
206
+ debug("remote source: %s", source);
207
+ return { source: (await downloadTemplate(source, {
208
+ dir: options.dir,
209
+ force: options.force,
210
+ install: false
211
+ })).source };
212
+ }
213
+ //#endregion
214
+ //#region src/actions/init.ts
215
+ const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
216
+ function validateProjectName(name) {
217
+ if (!name || !name.trim()) return "Name is required.";
218
+ if (/\s/.test(name)) return "Name cannot contain whitespace.";
219
+ if (name.startsWith(".") || name.startsWith("/") || name.startsWith("\\")) return "Name cannot start with '.', '/' or '\\'.";
220
+ if (name.includes("..")) return "Name cannot contain '..'.";
221
+ if (!NPM_NAME_RE.test(name)) return "Name must be a valid npm package name (lowercase, no spaces).";
222
+ }
223
+ async function isDirEmpty(dir) {
224
+ try {
225
+ return (await readdir(dir)).length === 0;
226
+ } catch (error) {
227
+ if (error.code === "ENOENT") return true;
228
+ throw error;
229
+ }
230
+ }
231
+ async function readGitAuthor(shell) {
232
+ try {
233
+ const [name, email] = await Promise.all([shell.$`git config --get user.name`.nothrow(), shell.$`git config --get user.email`.nothrow()]);
234
+ const trimmedName = name.stdout.trim();
235
+ const trimmedEmail = email.stdout.trim();
236
+ if (!trimmedName) return void 0;
237
+ return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName;
238
+ } catch {
239
+ return;
240
+ }
241
+ }
242
+ function abort(message) {
243
+ cancel(message);
244
+ process.exit(1);
245
+ }
246
+ async function runInit(ctx, options) {
247
+ const debug = logger.subdebug("init");
248
+ debug("options: %O", options);
249
+ const shell = ctx.shell.mute();
250
+ intro(`${palette.label(" vland init ")}`);
251
+ let template = options.template;
252
+ if (!template) {
253
+ if (!hasTTY) abort("Template is required in non-interactive environments. Use --template <library|backend|monorepo>.");
254
+ const choice = await select({
255
+ message: "Pick a template",
256
+ options: TEMPLATES.map((value) => ({
257
+ value,
258
+ label: value
259
+ }))
260
+ });
261
+ if (isCancel(choice)) abort("Cancelled.");
262
+ template = choice;
263
+ }
264
+ let name = options.name;
265
+ if (!name) {
266
+ if (!hasTTY) abort("Project name is required in non-interactive environments. Pass it as the first argument.");
267
+ const value = await text$1({
268
+ message: "Project name",
269
+ placeholder: "my-app",
270
+ validate: (input) => validateProjectName(input ?? "")
271
+ });
272
+ if (isCancel(value)) abort("Cancelled.");
273
+ name = value;
274
+ }
275
+ const nameError = validateProjectName(name);
276
+ if (nameError) abort(nameError);
277
+ const dir = options.dir ? isAbsolute(options.dir) ? options.dir : resolve(process.cwd(), options.dir) : resolve(process.cwd(), name);
278
+ debug("target dir: %s", dir);
279
+ if (!await isDirEmpty(dir) && !options.force) abort(`Target directory ${palette.highlight(dir)} is not empty. Re-run with ${palette.highlight("--force")} to overwrite.`);
280
+ let author = await readGitAuthor(shell);
281
+ if (!author) if (!hasTTY) author = "";
282
+ else {
283
+ const value = await text$1({
284
+ message: "Author (used in package.json / LICENSE)",
285
+ placeholder: "Jane Doe <jane@example.com>",
286
+ defaultValue: ""
287
+ });
288
+ if (isCancel(value)) abort("Cancelled.");
289
+ author = value || "";
290
+ }
291
+ debug("author: %s", author || "<empty>");
292
+ const fetchSpin = spinner();
293
+ fetchSpin.start(`Fetching ${palette.highlight(template)} template`);
294
+ try {
295
+ const { source } = await fetchTemplate({
296
+ template,
297
+ dir,
298
+ force: options.force
299
+ });
300
+ fetchSpin.stop(`Fetched template from ${palette.muted(source)}`);
301
+ } catch (error) {
302
+ fetchSpin.stop("Failed to fetch template", 1);
303
+ throw error;
304
+ }
305
+ const placeholderSpin = spinner();
306
+ placeholderSpin.start("Applying placeholders");
307
+ await replacePlaceholders(dir, {
308
+ projectName: name,
309
+ author,
310
+ year: (/* @__PURE__ */ new Date()).getFullYear().toString()
311
+ });
312
+ await updateRootPackageName(dir, name);
313
+ placeholderSpin.stop("Placeholders applied");
314
+ if (options.install) {
315
+ const detected = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
316
+ const installSpin = spinner();
317
+ installSpin.start(`Installing dependencies with ${palette.highlight(detected)}`);
318
+ try {
319
+ await installDependencies({
320
+ cwd: dir,
321
+ packageManager: {
322
+ name: detected,
323
+ command: detected
324
+ }
325
+ });
326
+ installSpin.stop(`Installed with ${palette.highlight(detected)}`);
327
+ } catch (error) {
328
+ installSpin.stop("Failed to install dependencies", 1);
329
+ log.warn("You can install manually later with `cd <dir> && <pm> install`.");
330
+ debug("install error: %O", error);
331
+ }
332
+ } else log.info(`Skipping ${palette.highlight("install")} (--no-install).`);
333
+ if (options.git) {
334
+ const gitSpin = spinner();
335
+ gitSpin.start("Initialising git repository");
336
+ try {
337
+ const gitShell = shell.at(dir).child({ env: {
338
+ ...process.env,
339
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland",
340
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land",
341
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland",
342
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land"
343
+ } });
344
+ await gitShell.$`git init`;
345
+ await gitShell.$`git add -A`;
346
+ await gitShell.$`git commit -m ${"chore: initial commit from vland"}`;
347
+ gitSpin.stop("Initialised git repository");
348
+ } catch (error) {
349
+ gitSpin.stop("Failed to initialise git", 1);
350
+ debug("git error: %O", error);
351
+ }
352
+ } else log.info(`Skipping ${palette.highlight("git init")} (--no-git).`);
353
+ const detectedPm = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
354
+ outro([
355
+ palette.success("Done!"),
356
+ "",
357
+ palette.muted("Next steps:"),
358
+ ` cd ${name}`,
359
+ options.install ? ` ${detectedPm} dev` : ` ${detectedPm} install && ${detectedPm} dev`
360
+ ].join("\n"));
361
+ }
362
+ //#endregion
363
+ //#region src/program/commands/init.ts
364
+ function createInitCommand(ctx) {
365
+ 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("-d, --dir <path>", "target directory (default: ./<name>)")).addOption(new Option("--pm <manager>", "package manager to use").choices([
366
+ "npm",
367
+ "pnpm",
368
+ "yarn",
369
+ "bun"
370
+ ])).addOption(new Option("--no-install", "skip dependency installation")).addOption(new Option("--no-git", "skip git init")).addOption(new Option("-f, --force", "overwrite existing directory").default(false)).action(async (name, options) => {
371
+ await runInit(ctx, {
372
+ name,
373
+ ...options
374
+ });
375
+ });
376
+ }
377
+ //#endregion
378
+ //#region src/program/commands/usage.ts
379
+ function addUsage(program) {
380
+ return program.addOption(new Option("--usage", "print KDL spec for this CLI (https://kdl.dev)")).on("option:usage", () => {
381
+ generateToStdout(program);
382
+ process.exit(0);
383
+ });
384
+ }
385
+ //#endregion
386
+ //#region src/program/index.ts
387
+ async function createProgram(options) {
388
+ const ctx = await createContext(options.binDir);
389
+ const version = ctx.binPkg.version;
390
+ return addUsage(new Command("vland").version(version, "-v, --version").addHelpText("before", getBannerText(version)).addCommand(createCompletionCommand()).addCommand(createInitCommand(ctx)));
391
+ }
392
+ //#endregion
393
+ //#region src/run.ts
394
+ const BIN_DIR = path.dirname(dirnameOf(import.meta));
395
+ await run(async () => {
396
+ await (await createProgram({ binDir: BIN_DIR })).parseAsync();
397
+ }, logger);
398
+ //#endregion
399
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@vlandoss/vland",
3
+ "version": "0.0.1-git-53b6b02.0",
4
+ "description": "The CLI to init a new project in Variable Land",
5
+ "homepage": "https://github.com/variableland/dx/tree/main/packages/vland#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/variableland/dx/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/variableland/dx.git",
12
+ "directory": "packages/vland"
13
+ },
14
+ "license": "MIT",
15
+ "author": "rcrd <rcrd@variable.land>",
16
+ "type": "module",
17
+ "imports": {
18
+ "#src/*": "./src/*",
19
+ "#test/*": "./test/*"
20
+ },
21
+ "bin": {
22
+ "vland": "./bin"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "dist",
27
+ "src",
28
+ "!src/**/__tests__",
29
+ "!src/**/*.test.*",
30
+ "tsconfig.json"
31
+ ],
32
+ "dependencies": {
33
+ "@clack/prompts": "0.11.0",
34
+ "@usage-spec/commander": "1.1.0",
35
+ "commander": "14.0.3",
36
+ "giget": "2.0.0",
37
+ "nypm": "0.6.0",
38
+ "@vlandoss/clibuddy": "0.4.1-git-53b6b02.0",
39
+ "@vlandoss/loggy": "0.2.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "engines": {
45
+ "node": ">=20.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@vlandoss/tsdown-config": "^0.0.1"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown && pnpm build:kdl",
52
+ "build:kdl": "./bin --usage > dist/cli.usage.kdl",
53
+ "test": "vitest run",
54
+ "test:unit": "vitest run --project unit",
55
+ "test:integration": "vitest run --project integration",
56
+ "test:types": "rr tsc"
57
+ }
58
+ }
@@ -0,0 +1,210 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { cancel, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
4
+ import { hasTTY, palette } from "@vlandoss/clibuddy";
5
+ import { detectPackageManager, installDependencies } from "nypm";
6
+ import type { Context } from "#src/services/ctx.ts";
7
+ import { logger } from "#src/services/logger.ts";
8
+ import { replacePlaceholders, updateRootPackageName } from "./placeholders.ts";
9
+ import { fetchTemplate, TEMPLATES, type TemplateName } from "./template.ts";
10
+
11
+ export type InitOptions = {
12
+ name?: string;
13
+ template?: TemplateName;
14
+ dir?: string;
15
+ pm?: "npm" | "pnpm" | "yarn" | "bun";
16
+ install: boolean;
17
+ git: boolean;
18
+ force: boolean;
19
+ };
20
+
21
+ const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
22
+
23
+ function validateProjectName(name: string): string | undefined {
24
+ if (!name || !name.trim()) return "Name is required.";
25
+ if (/\s/.test(name)) return "Name cannot contain whitespace.";
26
+ if (name.startsWith(".") || name.startsWith("/") || name.startsWith("\\")) {
27
+ return "Name cannot start with '.', '/' or '\\'.";
28
+ }
29
+ if (name.includes("..")) return "Name cannot contain '..'.";
30
+ if (!NPM_NAME_RE.test(name)) {
31
+ return "Name must be a valid npm package name (lowercase, no spaces).";
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ async function isDirEmpty(dir: string): Promise<boolean> {
37
+ try {
38
+ const entries = await readdir(dir);
39
+ return entries.length === 0;
40
+ } catch (error) {
41
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async function readGitAuthor(shell: Context["shell"]): Promise<string | undefined> {
47
+ try {
48
+ const [name, email] = await Promise.all([
49
+ shell.$`git config --get user.name`.nothrow(),
50
+ shell.$`git config --get user.email`.nothrow(),
51
+ ]);
52
+ const trimmedName = name.stdout.trim();
53
+ const trimmedEmail = email.stdout.trim();
54
+ if (!trimmedName) return undefined;
55
+ return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName;
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ function abort(message: string): never {
62
+ cancel(message);
63
+ process.exit(1);
64
+ }
65
+
66
+ export async function runInit(ctx: Context, options: InitOptions) {
67
+ const debug = logger.subdebug("init");
68
+ debug("options: %O", options);
69
+
70
+ // Mute zx so the @clack/prompts UI stays clean while git output is captured.
71
+ const shell = ctx.shell.mute();
72
+
73
+ intro(`${palette.label(" vland init ")}`);
74
+
75
+ // 1. Resolve template
76
+ let template = options.template;
77
+ if (!template) {
78
+ if (!hasTTY) abort("Template is required in non-interactive environments. Use --template <library|backend|monorepo>.");
79
+ const choice = await select({
80
+ message: "Pick a template",
81
+ options: TEMPLATES.map((value) => ({
82
+ value,
83
+ label: value,
84
+ })),
85
+ });
86
+ if (isCancel(choice)) abort("Cancelled.");
87
+ template = choice as TemplateName;
88
+ }
89
+
90
+ // 2. Resolve project name
91
+ let name = options.name;
92
+ if (!name) {
93
+ if (!hasTTY) abort("Project name is required in non-interactive environments. Pass it as the first argument.");
94
+ const value = await text({
95
+ message: "Project name",
96
+ placeholder: "my-app",
97
+ validate: (input) => validateProjectName(input ?? ""),
98
+ });
99
+ if (isCancel(value)) abort("Cancelled.");
100
+ name = value as string;
101
+ }
102
+ const nameError = validateProjectName(name);
103
+ if (nameError) abort(nameError);
104
+
105
+ // 3. Resolve target dir
106
+ const dir = options.dir
107
+ ? isAbsolute(options.dir)
108
+ ? options.dir
109
+ : resolve(process.cwd(), options.dir)
110
+ : resolve(process.cwd(), name);
111
+ debug("target dir: %s", dir);
112
+
113
+ if (!(await isDirEmpty(dir)) && !options.force) {
114
+ abort(`Target directory ${palette.highlight(dir)} is not empty. Re-run with ${palette.highlight("--force")} to overwrite.`);
115
+ }
116
+
117
+ // 4. Resolve author
118
+ let author = await readGitAuthor(shell);
119
+ if (!author) {
120
+ if (!hasTTY) {
121
+ author = "";
122
+ } else {
123
+ const value = await text({
124
+ message: "Author (used in package.json / LICENSE)",
125
+ placeholder: "Jane Doe <jane@example.com>",
126
+ defaultValue: "",
127
+ });
128
+ if (isCancel(value)) abort("Cancelled.");
129
+ author = (value as string) || "";
130
+ }
131
+ }
132
+ debug("author: %s", author || "<empty>");
133
+
134
+ // 5. Download template
135
+ const fetchSpin = spinner();
136
+ fetchSpin.start(`Fetching ${palette.highlight(template)} template`);
137
+ try {
138
+ const { source } = await fetchTemplate({ template, dir, force: options.force });
139
+ fetchSpin.stop(`Fetched template from ${palette.muted(source)}`);
140
+ } catch (error) {
141
+ fetchSpin.stop("Failed to fetch template", 1);
142
+ throw error;
143
+ }
144
+
145
+ // 6. Replace placeholders
146
+ const placeholderSpin = spinner();
147
+ placeholderSpin.start("Applying placeholders");
148
+ await replacePlaceholders(dir, {
149
+ projectName: name,
150
+ author,
151
+ year: new Date().getFullYear().toString(),
152
+ });
153
+ await updateRootPackageName(dir, name);
154
+ placeholderSpin.stop("Placeholders applied");
155
+
156
+ // 7. Install deps
157
+ if (options.install) {
158
+ const detected = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
159
+ const installSpin = spinner();
160
+ installSpin.start(`Installing dependencies with ${palette.highlight(detected)}`);
161
+ try {
162
+ await installDependencies({ cwd: dir, packageManager: { name: detected, command: detected } });
163
+ installSpin.stop(`Installed with ${palette.highlight(detected)}`);
164
+ } catch (error) {
165
+ installSpin.stop("Failed to install dependencies", 1);
166
+ log.warn("You can install manually later with `cd <dir> && <pm> install`.");
167
+ debug("install error: %O", error);
168
+ }
169
+ } else {
170
+ log.info(`Skipping ${palette.highlight("install")} (--no-install).`);
171
+ }
172
+
173
+ // 8. Git init
174
+ if (options.git) {
175
+ const gitSpin = spinner();
176
+ gitSpin.start("Initialising git repository");
177
+ try {
178
+ const gitShell = shell.at(dir).child({
179
+ env: {
180
+ ...process.env,
181
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland",
182
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land",
183
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland",
184
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land",
185
+ },
186
+ });
187
+ await gitShell.$`git init`;
188
+ await gitShell.$`git add -A`;
189
+ await gitShell.$`git commit -m ${"chore: initial commit from vland"}`;
190
+ gitSpin.stop("Initialised git repository");
191
+ } catch (error) {
192
+ gitSpin.stop("Failed to initialise git", 1);
193
+ debug("git error: %O", error);
194
+ }
195
+ } else {
196
+ log.info(`Skipping ${palette.highlight("git init")} (--no-git).`);
197
+ }
198
+
199
+ // 9. Outro with next steps
200
+ const detectedPm = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
201
+ outro(
202
+ [
203
+ palette.success("Done!"),
204
+ "",
205
+ palette.muted("Next steps:"),
206
+ ` cd ${name}`,
207
+ options.install ? ` ${detectedPm} dev` : ` ${detectedPm} install && ${detectedPm} dev`,
208
+ ].join("\n"),
209
+ );
210
+ }
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs";
2
+ import { readdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { extname, join } from "node:path";
4
+ import { createPkg } from "@vlandoss/clibuddy";
5
+ import { logger } from "#src/services/logger.ts";
6
+
7
+ export type Placeholders = {
8
+ projectName: string;
9
+ author: string;
10
+ year: string;
11
+ };
12
+
13
+ const TEXT_EXTENSIONS = new Set([
14
+ ".ts",
15
+ ".tsx",
16
+ ".js",
17
+ ".jsx",
18
+ ".mjs",
19
+ ".cjs",
20
+ ".mts",
21
+ ".cts",
22
+ ".json",
23
+ ".jsonc",
24
+ ".md",
25
+ ".mdx",
26
+ ".yml",
27
+ ".yaml",
28
+ ".toml",
29
+ ".css",
30
+ ".html",
31
+ ".sh",
32
+ ".env",
33
+ ".gitignore",
34
+ ".gitattributes",
35
+ ".npmrc",
36
+ ".nvmrc",
37
+ ".node-version",
38
+ ".dockerignore",
39
+ ".editorconfig",
40
+ ".prettierrc",
41
+ ]);
42
+
43
+ const TEXT_FILENAMES = new Set([
44
+ "Dockerfile",
45
+ "LICENSE",
46
+ "README",
47
+ "CHANGELOG",
48
+ ".gitignore",
49
+ ".gitattributes",
50
+ ".npmrc",
51
+ ".nvmrc",
52
+ ".node-version",
53
+ ".dockerignore",
54
+ ".editorconfig",
55
+ ".prettierrc",
56
+ "lefthook.yml",
57
+ "mise.toml",
58
+ ]);
59
+
60
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".turbo", ".next", "build", "coverage"]);
61
+
62
+ function isTextFile(name: string) {
63
+ if (TEXT_FILENAMES.has(name)) return true;
64
+ return TEXT_EXTENSIONS.has(extname(name));
65
+ }
66
+
67
+ async function walk(root: string, onFile: (path: string) => Promise<void>) {
68
+ const entries = await readdir(root, { withFileTypes: true });
69
+ await Promise.all(
70
+ entries.map(async (entry) => {
71
+ const full = join(root, entry.name);
72
+ if (entry.isDirectory()) {
73
+ if (SKIP_DIRS.has(entry.name)) return;
74
+ await walk(full, onFile);
75
+ return;
76
+ }
77
+ if (entry.isFile()) {
78
+ await onFile(full);
79
+ }
80
+ }),
81
+ );
82
+ }
83
+
84
+ function applyPlaceholders(content: string, values: Placeholders) {
85
+ return content
86
+ .replaceAll("{{projectName}}", values.projectName)
87
+ .replaceAll("{{author}}", values.author)
88
+ .replaceAll("{{year}}", values.year);
89
+ }
90
+
91
+ export async function replacePlaceholders(rootDir: string, values: Placeholders) {
92
+ const debug = logger.subdebug("placeholders");
93
+ let touched = 0;
94
+
95
+ await walk(rootDir, async (filePath) => {
96
+ const name = filePath.split("/").pop() ?? "";
97
+ if (!isTextFile(name)) return;
98
+
99
+ const fileStat = await stat(filePath);
100
+ if (fileStat.size > 1_000_000) return; // skip files >1MB; templates shouldn't have those
101
+
102
+ const original = await readFile(filePath, "utf8");
103
+ const replaced = applyPlaceholders(original, values);
104
+ if (replaced !== original) {
105
+ await writeFile(filePath, replaced);
106
+ touched += 1;
107
+ }
108
+ });
109
+
110
+ debug("placeholders applied to %d file(s)", touched);
111
+ return { touched };
112
+ }
113
+
114
+ /**
115
+ * Updates the root `package.json` `name` field via `pkg-types`. Safer than a
116
+ * regex pass because it preserves field ordering and JSON formatting handled
117
+ * by `pkg-types`.
118
+ */
119
+ export async function updateRootPackageName(rootDir: string, projectName: string) {
120
+ const debug = logger.subdebug("update-root-package-name");
121
+
122
+ const rootPath = fs.realpathSync(rootDir);
123
+
124
+ debug("root path:", rootPath);
125
+ debug("process cwd:", process.cwd());
126
+
127
+ const pkg = await createPkg(rootPath);
128
+
129
+ if (!pkg) {
130
+ throw new Error("Could not find package.json");
131
+ }
132
+
133
+ try {
134
+ pkg.packageJson.name = projectName;
135
+ await pkg.write(pkg.packageJson);
136
+ } catch (error) {
137
+ debug("skipped %s", error);
138
+ }
139
+ }
@@ -0,0 +1,48 @@
1
+ import { cp } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { downloadTemplate } from "giget";
4
+ import { logger } from "#src/services/logger.ts";
5
+
6
+ export const TEMPLATES = ["library", "backend", "monorepo"] as const;
7
+ export type TemplateName = (typeof TEMPLATES)[number];
8
+
9
+ const GITHUB_SOURCE = "github:variableland/dx";
10
+ const GITHUB_REF = "main";
11
+
12
+ type ResolveOptions = {
13
+ template: TemplateName;
14
+ dir: string;
15
+ force: boolean;
16
+ };
17
+
18
+ /**
19
+ * Resolves the template into `dir`. Source order:
20
+ * 1. `VLAND_TEMPLATES_DIR` env var β†’ copy from local path (used by E2E tests
21
+ * against the in-repo `templates/`).
22
+ * 2. Otherwise β†’ download via giget from `github:variableland/dx/templates/<name>`.
23
+ */
24
+ export async function fetchTemplate(options: ResolveOptions): Promise<{ source: string }> {
25
+ const debug = logger.subdebug("fetch-template");
26
+ const localRoot = process.env.VLAND_TEMPLATES_DIR;
27
+
28
+ if (localRoot) {
29
+ const sourceDir = resolve(localRoot, options.template);
30
+ debug("local source: %s", sourceDir);
31
+ await cp(sourceDir, options.dir, {
32
+ recursive: true,
33
+ force: options.force,
34
+ errorOnExist: !options.force,
35
+ filter: (src) => !src.includes("/node_modules") && !src.endsWith("/.turbo") && !src.endsWith("/dist"),
36
+ });
37
+ return { source: sourceDir };
38
+ }
39
+
40
+ const source = `${GITHUB_SOURCE}/templates/${options.template}#${GITHUB_REF}`;
41
+ debug("remote source: %s", source);
42
+ const result = await downloadTemplate(source, {
43
+ dir: options.dir,
44
+ force: options.force,
45
+ install: false,
46
+ });
47
+ return { source: result.source };
48
+ }
@@ -0,0 +1,26 @@
1
+ import { Argument, createCommand } from "commander";
2
+ import { TOOL_LABELS } from "../ui.ts";
3
+
4
+ const SHELLS = ["bash", "zsh", "fish"] as const;
5
+
6
+ // Ghost command: registered with Commander purely for discoverability β€” it surfaces
7
+ // in `vland --help` and is baked into dist/cli.usage.kdl so the completion itself can
8
+ // suggest "completion" after `vland <TAB>`. The actual handler lives in the bash bin
9
+ // dispatcher, which intercepts `vland completion <shell>` before reaching Node.
10
+ export function createCompletionCommand() {
11
+ return createCommand("completion")
12
+ .summary(`print shell completion script 🐚 (${TOOL_LABELS.USAGE})`)
13
+ .description(
14
+ `Prints a shell completion script for vland. Add to your shell rc file:
15
+
16
+ bash: eval "$(vland completion bash)"
17
+ zsh: eval "$(vland completion zsh)"
18
+ fish: vland completion fish | source`,
19
+ )
20
+ .addArgument(new Argument("<shell>", `target shell`).choices(SHELLS))
21
+ .addHelpText(
22
+ "afterAll",
23
+ `\nUnder the hood, this command uses ${TOOL_LABELS.USAGE} (https://usage.jdx.dev).
24
+ Make sure to have it installed and available in your PATH.`,
25
+ );
26
+ }
@@ -0,0 +1,33 @@
1
+ import { Argument, createCommand, Option } from "commander";
2
+ import { runInit } from "#src/actions/init.ts";
3
+ import { TEMPLATES, type TemplateName } from "#src/actions/template.ts";
4
+ import type { Context } from "#src/services/ctx.ts";
5
+ import { TOOL_LABELS } from "../ui.ts";
6
+
7
+ type InitOptions = {
8
+ dir?: string;
9
+ template?: TemplateName;
10
+ pm?: "npm" | "pnpm" | "yarn" | "bun";
11
+ install: boolean;
12
+ git: boolean;
13
+ force: boolean;
14
+ };
15
+
16
+ export function createInitCommand(ctx: Context) {
17
+ return createCommand("init")
18
+ .summary(`init a new project πŸš€ (${TOOL_LABELS.GIGET})`)
19
+ .description("Scaffold a new variableland project from one of the official templates.")
20
+ .addArgument(new Argument("[name]", "project name (also used as the target directory)"))
21
+ .addOption(new Option("-t, --template <name>", "template to use").choices([...TEMPLATES]))
22
+ .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
+ .addOption(new Option("--no-install", "skip dependency installation"))
25
+ .addOption(new Option("--no-git", "skip git init"))
26
+ .addOption(new Option("-f, --force", "overwrite existing directory").default(false))
27
+ .action(async (name: string | undefined, options: InitOptions) => {
28
+ await runInit(ctx, {
29
+ name,
30
+ ...options,
31
+ });
32
+ });
33
+ }
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1,23 @@
1
+ import { Command } from "commander";
2
+ import { createContext } from "#src/services/ctx.ts";
3
+ import { createCompletionCommand } from "./commands/completion.ts";
4
+ import { createInitCommand } from "./commands/init.ts";
5
+ import { addUsage } from "./commands/usage.ts";
6
+ import { getBannerText } from "./ui.ts";
7
+
8
+ export type Options = {
9
+ binDir: string;
10
+ };
11
+
12
+ export async function createProgram(options: Options) {
13
+ const ctx = await createContext(options.binDir);
14
+ const version = ctx.binPkg.version;
15
+
16
+ return addUsage(
17
+ new Command("vland")
18
+ .version(version, "-v, --version")
19
+ .addHelpText("before", getBannerText(version))
20
+ .addCommand(createCompletionCommand())
21
+ .addCommand(createInitCommand(ctx)),
22
+ );
23
+ }
@@ -0,0 +1,29 @@
1
+ import { colorize, palette, text } from "@vlandoss/clibuddy";
2
+
3
+ const vlandColor = colorize("#a78bfa");
4
+ const usageColor = colorize("#24C55E");
5
+ const gigetColor = colorize("#F472B6");
6
+
7
+ export const TOOL_LABELS = {
8
+ USAGE: usageColor("usage"),
9
+ GIGET: gigetColor("giget"),
10
+ };
11
+
12
+ // npx figlet -f "ANSI Shadow" "vland"
13
+ export function getBannerText(version: string) {
14
+ const uiLogo = vlandColor(
15
+ `
16
+ β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
17
+ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
18
+ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘
19
+ β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘
20
+ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
21
+ β•šβ•β•β•β• β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•β•β•šβ•β•β•β•β•β• ${text.version(version)}
22
+ `.trim(),
23
+ );
24
+
25
+ return `
26
+ ${uiLogo}
27
+
28
+ πŸ¦‰ ${palette.italic(palette.muted("The CLI to init a new project in"))} ${text.vland}\n`.trimStart();
29
+ }
package/src/run.ts ADDED
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ import { dirnameOf, run } from "@vlandoss/clibuddy";
3
+ import { createProgram } from "./program/index.ts";
4
+ import { logger } from "./services/logger.ts";
5
+
6
+ const BIN_DIR = path.dirname(dirnameOf(import.meta));
7
+
8
+ await run(async () => {
9
+ const program = await createProgram({ binDir: BIN_DIR });
10
+ await program.parseAsync();
11
+ }, logger);
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ import { createPkg, createShellService, type Pkg, type ShellService } from "@vlandoss/clibuddy";
3
+ import { logger } from "./logger.ts";
4
+
5
+ export type Context = {
6
+ binPkg: Pkg;
7
+ shell: ShellService;
8
+ };
9
+
10
+ export async function createContext(binDir: string): Promise<Context> {
11
+ const debug = logger.subdebug("create-context");
12
+
13
+ const binPath = fs.realpathSync(binDir);
14
+
15
+ debug("bin path:", binPath);
16
+
17
+ const binPkg = await createPkg(binPath);
18
+
19
+ if (!binPkg) {
20
+ throw new Error("Could not find bin package.json");
21
+ }
22
+
23
+ debug("bin pkg info: %O", binPkg.info());
24
+
25
+ const shell = createShellService({
26
+ localBaseBinPath: [binDir],
27
+ });
28
+
29
+ debug("shell service options: %O", shell.options);
30
+
31
+ return { binPkg, shell };
32
+ }
@@ -0,0 +1,5 @@
1
+ import { createLoggy } from "@vlandoss/loggy";
2
+
3
+ export const logger = createLoggy({
4
+ namespace: "vland",
5
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["@vlandoss/config/ts/no-dom/app"]
3
+ }