@vlandoss/vland 0.2.1-git-a1181c2.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 +8 -8
- package/dist/run.mjs +119 -88
- package/package.json +6 -6
- 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
|
```
|
package/dist/cli.usage.kdl
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
// @generated by @usage-spec/commander from Commander.js metadata
|
|
2
2
|
name vland
|
|
3
3
|
bin vland
|
|
4
|
-
version "0.2.1-git-
|
|
4
|
+
version "0.2.1-git-87d22db.0"
|
|
5
5
|
usage "[options] [command]"
|
|
6
6
|
flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
|
|
7
|
-
cmd completion help="print shell completion script
|
|
7
|
+
cmd completion help="print shell completion script (usage)" {
|
|
8
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
9
|
arg <shell> help="target shell" {
|
|
10
10
|
choices bash zsh fish
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
cmd init help="init a new project
|
|
13
|
+
cmd init help="init a new project (giget)" {
|
|
14
14
|
long_help "Scaffold a new variableland project from one of the official templates."
|
|
15
15
|
flag "-t --template" help="template to use" {
|
|
16
16
|
arg <TEMPLATE> {
|
|
17
17
|
choices library backend monorepo
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
flag --visibility help="package visibility (library only)" {
|
|
21
|
+
arg <VISIBILITY> {
|
|
22
|
+
choices private public
|
|
23
|
+
}
|
|
24
|
+
}
|
|
20
25
|
flag "-d --dir" help="target directory (default: ./<name>)" {
|
|
21
26
|
arg <DIR>
|
|
22
27
|
}
|
|
23
|
-
flag --pm help="package manager to use" {
|
|
24
|
-
arg <PM> {
|
|
25
|
-
choices npm pnpm yarn bun
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
28
|
flag --install help="install dependencies (skip prompt)"
|
|
29
29
|
flag --no-install help="skip dependency installation" negate=--install
|
|
30
30
|
flag --git help="initialise git repository (skip prompt)"
|
package/dist/run.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path, { extname, isAbsolute, join, resolve } from "node:path";
|
|
2
2
|
import { colorize, createPkg, createShellService, dirnameOf, hasTTY, palette, run, text } from "@vlandoss/clibuddy";
|
|
3
|
+
import { generateToStdout } from "@usage-spec/commander";
|
|
3
4
|
import { Argument, Command, Option, createCommand } from "commander";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import { createLoggy } from "@vlandoss/loggy";
|
|
6
7
|
import { cp, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
7
8
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text as text$1 } from "@clack/prompts";
|
|
8
|
-
import {
|
|
9
|
+
import { installDependencies } from "nypm";
|
|
9
10
|
import { downloadTemplate } from "giget";
|
|
10
|
-
import { generateToStdout } from "@usage-spec/commander";
|
|
11
11
|
//#region src/services/logger.ts
|
|
12
12
|
const logger = createLoggy({ namespace: "vland" });
|
|
13
13
|
//#endregion
|
|
@@ -56,7 +56,7 @@ const SHELLS = [
|
|
|
56
56
|
"fish"
|
|
57
57
|
];
|
|
58
58
|
function createCompletionCommand() {
|
|
59
|
-
return createCommand("completion").summary(`print shell completion script
|
|
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
60
|
|
|
61
61
|
bash: eval "$(vland completion bash)"
|
|
62
62
|
zsh: eval "$(vland completion zsh)"
|
|
@@ -64,6 +64,67 @@ function createCompletionCommand() {
|
|
|
64
64
|
Make sure to have it installed and available in your PATH.`);
|
|
65
65
|
}
|
|
66
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
|
|
67
128
|
//#region src/actions/placeholders.ts
|
|
68
129
|
const TEXT_EXTENSIONS = new Set([
|
|
69
130
|
".ts",
|
|
@@ -155,77 +216,38 @@ async function replacePlaceholders(rootDir, values) {
|
|
|
155
216
|
return { touched };
|
|
156
217
|
}
|
|
157
218
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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)
|
|
161
228
|
*/
|
|
162
|
-
async function
|
|
163
|
-
const debug = logger.subdebug("
|
|
164
|
-
const
|
|
165
|
-
debug("root path:", rootPath);
|
|
166
|
-
debug("process cwd:", process.cwd());
|
|
167
|
-
const pkg = await createPkg(rootPath);
|
|
229
|
+
async function applyRootPackage(rootDir, options) {
|
|
230
|
+
const debug = logger.subdebug("apply-root-package");
|
|
231
|
+
const pkg = await createPkg(fs.realpathSync(rootDir));
|
|
168
232
|
if (!pkg) throw new Error("Could not find package.json");
|
|
233
|
+
const { template, projectName, visibility } = options;
|
|
169
234
|
try {
|
|
170
|
-
|
|
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
|
+
}
|
|
171
243
|
await pkg.write(pkg.packageJson);
|
|
172
244
|
} catch (error) {
|
|
173
245
|
debug("skipped %s", error);
|
|
174
246
|
}
|
|
175
247
|
}
|
|
176
248
|
//#endregion
|
|
177
|
-
//#region src/actions/template.ts
|
|
178
|
-
const TEMPLATES = [
|
|
179
|
-
"library",
|
|
180
|
-
"backend",
|
|
181
|
-
"monorepo"
|
|
182
|
-
];
|
|
183
|
-
const TEMPLATE_META = {
|
|
184
|
-
library: {
|
|
185
|
-
placeholder: "my-lib",
|
|
186
|
-
runScript: "test"
|
|
187
|
-
},
|
|
188
|
-
backend: {
|
|
189
|
-
placeholder: "my-api",
|
|
190
|
-
runScript: "dev"
|
|
191
|
-
},
|
|
192
|
-
monorepo: {
|
|
193
|
-
placeholder: "my-mono",
|
|
194
|
-
runScript: "dev"
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
const GITHUB_SOURCE = "github:variableland/dx";
|
|
198
|
-
const GITHUB_REF = "main";
|
|
199
|
-
/**
|
|
200
|
-
* Resolves the template into `dir`. Source order:
|
|
201
|
-
* 1. `VLAND_TEMPLATES_DIR` env var → copy from local path (used by E2E tests
|
|
202
|
-
* against the in-repo `templates/`).
|
|
203
|
-
* 2. Otherwise → download via giget from `github:variableland/dx/templates/<name>`.
|
|
204
|
-
*/
|
|
205
|
-
async function fetchTemplate(options) {
|
|
206
|
-
const debug = logger.subdebug("fetch-template");
|
|
207
|
-
const localRoot = process.env.VLAND_TEMPLATES_DIR;
|
|
208
|
-
if (localRoot) {
|
|
209
|
-
const sourceDir = resolve(localRoot, options.template);
|
|
210
|
-
debug("local source: %s", sourceDir);
|
|
211
|
-
await cp(sourceDir, options.dir, {
|
|
212
|
-
recursive: true,
|
|
213
|
-
force: options.force,
|
|
214
|
-
errorOnExist: !options.force,
|
|
215
|
-
filter: (src) => !src.includes("/node_modules") && !src.endsWith("/.turbo") && !src.endsWith("/dist")
|
|
216
|
-
});
|
|
217
|
-
return { source: sourceDir };
|
|
218
|
-
}
|
|
219
|
-
const source = `${GITHUB_SOURCE}/templates/${options.template}#${GITHUB_REF}`;
|
|
220
|
-
debug("remote source: %s", source);
|
|
221
|
-
return { source: (await downloadTemplate(source, {
|
|
222
|
-
dir: options.dir,
|
|
223
|
-
force: options.force,
|
|
224
|
-
install: false
|
|
225
|
-
})).source };
|
|
226
|
-
}
|
|
227
|
-
//#endregion
|
|
228
249
|
//#region src/actions/init.ts
|
|
250
|
+
const PACKAGE_MANAGER = "pnpm";
|
|
229
251
|
const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
230
252
|
function validateProjectName(name) {
|
|
231
253
|
if (!name || !name.trim()) return "Name is required.";
|
|
@@ -296,6 +318,23 @@ async function runInit(ctx, options) {
|
|
|
296
318
|
}
|
|
297
319
|
const nameError = validateProjectName(name);
|
|
298
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
|
+
}
|
|
299
338
|
const dir = options.dir ? isAbsolute(options.dir) ? options.dir : resolve(process.cwd(), options.dir) : resolve(process.cwd(), name);
|
|
300
339
|
debug("target dir: %s", dir);
|
|
301
340
|
if (!await isDirEmpty(dir) && !options.force) abort(`Target directory ${palette.highlight(dir)} is not empty. Re-run with ${palette.highlight("--force")} to overwrite.`);
|
|
@@ -331,27 +370,30 @@ async function runInit(ctx, options) {
|
|
|
331
370
|
author,
|
|
332
371
|
year: (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
333
372
|
});
|
|
334
|
-
await
|
|
373
|
+
await applyRootPackage(dir, {
|
|
374
|
+
template,
|
|
375
|
+
projectName: name,
|
|
376
|
+
visibility
|
|
377
|
+
});
|
|
335
378
|
placeholderSpin.stop("Placeholders applied");
|
|
336
379
|
const shouldInstall = await resolveYesNo(options.install, "Install dependencies?");
|
|
337
380
|
const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?");
|
|
338
381
|
if (shouldInstall) {
|
|
339
|
-
const detected = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
|
|
340
382
|
const installSpin = spinner();
|
|
341
|
-
installSpin.start(`Installing dependencies with ${palette.highlight(
|
|
383
|
+
installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
|
|
342
384
|
try {
|
|
343
385
|
await installDependencies({
|
|
344
386
|
cwd: dir,
|
|
345
387
|
packageManager: {
|
|
346
|
-
name:
|
|
347
|
-
command:
|
|
388
|
+
name: PACKAGE_MANAGER,
|
|
389
|
+
command: PACKAGE_MANAGER
|
|
348
390
|
},
|
|
349
391
|
silent: true
|
|
350
392
|
});
|
|
351
|
-
installSpin.stop(`Installed with ${palette.highlight(
|
|
393
|
+
installSpin.stop(`Installed with ${palette.highlight(PACKAGE_MANAGER)}`);
|
|
352
394
|
} catch (error) {
|
|
353
395
|
installSpin.stop("Failed to install dependencies", 1);
|
|
354
|
-
log.warn(
|
|
396
|
+
log.warn(`You can install manually later with \`cd <dir> && ${PACKAGE_MANAGER} install\`.`);
|
|
355
397
|
debug("install error: %O", error);
|
|
356
398
|
}
|
|
357
399
|
} else log.info(`Skipping ${palette.highlight("install")}.`);
|
|
@@ -378,14 +420,13 @@ async function runInit(ctx, options) {
|
|
|
378
420
|
debug("git error: %O", error);
|
|
379
421
|
}
|
|
380
422
|
} else log.info(`Skipping ${palette.highlight("git init")}.`);
|
|
381
|
-
const detectedPm = options.pm ?? (await detectPackageManager(dir, { ignorePackageJSON: false }))?.name ?? "pnpm";
|
|
382
423
|
const runScript = TEMPLATE_META[template].runScript;
|
|
383
424
|
outro([
|
|
384
425
|
palette.success("Done!"),
|
|
385
426
|
"",
|
|
386
427
|
palette.muted("Next steps:"),
|
|
387
428
|
` cd ${name}`,
|
|
388
|
-
shouldInstall ? ` ${
|
|
429
|
+
shouldInstall ? ` ${PACKAGE_MANAGER} ${runScript}` : ` ${PACKAGE_MANAGER} install && ${PACKAGE_MANAGER} ${runScript}`
|
|
389
430
|
].join("\n"));
|
|
390
431
|
}
|
|
391
432
|
async function resolveYesNo(explicit, message) {
|
|
@@ -401,12 +442,7 @@ async function resolveYesNo(explicit, message) {
|
|
|
401
442
|
//#endregion
|
|
402
443
|
//#region src/program/commands/init.ts
|
|
403
444
|
function createInitCommand(ctx) {
|
|
404
|
-
return createCommand("init").summary(`init a new project
|
|
405
|
-
"npm",
|
|
406
|
-
"pnpm",
|
|
407
|
-
"yarn",
|
|
408
|
-
"bun"
|
|
409
|
-
])).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) {
|
|
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) {
|
|
410
446
|
console.log(getBannerText(ctx.binPkg.version));
|
|
411
447
|
const installSource = this.getOptionValueSource("install");
|
|
412
448
|
const gitSource = this.getOptionValueSource("git");
|
|
@@ -419,19 +455,14 @@ function createInitCommand(ctx) {
|
|
|
419
455
|
});
|
|
420
456
|
}
|
|
421
457
|
//#endregion
|
|
422
|
-
//#region src/program/commands/usage.ts
|
|
423
|
-
function addUsage(program) {
|
|
424
|
-
return program.addOption(new Option("--usage", "print KDL spec for this CLI (https://kdl.dev)")).on("option:usage", () => {
|
|
425
|
-
generateToStdout(program);
|
|
426
|
-
process.exit(0);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
//#endregion
|
|
430
458
|
//#region src/program/index.ts
|
|
431
459
|
async function createProgram(options) {
|
|
432
460
|
const ctx = await createContext(options.binDir);
|
|
433
461
|
const version = ctx.binPkg.version;
|
|
434
|
-
return
|
|
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));
|
|
435
466
|
}
|
|
436
467
|
//#endregion
|
|
437
468
|
//#region src/run.ts
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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
15
|
"author": "rcrd <rcrd@variable.land>",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"commander": "14.0.3",
|
|
36
36
|
"giget": "2.0.0",
|
|
37
37
|
"nypm": "0.6.0",
|
|
38
|
-
"@vlandoss/clibuddy": "0.6.1-git-
|
|
39
|
-
"@vlandoss/loggy": "0.2.1-git-
|
|
38
|
+
"@vlandoss/clibuddy": "0.6.1-git-87d22db.0",
|
|
39
|
+
"@vlandoss/loggy": "0.2.1-git-87d22db.0"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"node": ">=20.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@
|
|
48
|
+
"@rrlab/tsdown-config": "^0.0.1-git-87d22db.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsdown && pnpm build:kdl",
|
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
|
-
}
|