@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 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 tsdown, Vitest, biome, Changesets release workflow. |
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 extend `@vlandoss/config` for biome and tsconfig.
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
  ```
@@ -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-a1181c2.0"
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 🐚 (usage)" {
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 🚀 (giget)" {
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 { detectPackageManager, installDependencies } from "nypm";
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 🐚 (${TOOL_LABELS.USAGE})`).description(`Prints a shell completion script for vland. Add to your shell rc file:
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
- * 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`.
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 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);
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
- pkg.packageJson.name = projectName;
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 updateRootPackageName(dir, name);
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(detected)}`);
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: detected,
347
- command: detected
388
+ name: PACKAGE_MANAGER,
389
+ command: PACKAGE_MANAGER
348
390
  },
349
391
  silent: true
350
392
  });
351
- installSpin.stop(`Installed with ${palette.highlight(detected)}`);
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("You can install manually later with `cd <dir> && <pm> install`.");
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 ? ` ${detectedPm} ${runScript}` : ` ${detectedPm} install && ${detectedPm} ${runScript}`
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 🚀 (${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([
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 addUsage(new Command("vland").version(version, "-v, --version").addHelpText("before", getBannerText(version)).addCommand(createCompletionCommand()).addCommand(createInitCommand(ctx)));
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-a1181c2.0",
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/packages/vland#readme",
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": "packages/vland"
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-a1181c2.0",
39
- "@vlandoss/loggy": "0.2.1-git-a1181c2.0"
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
- "@vlandoss/tsdown-config": "^0.0.1"
48
+ "@rrlab/tsdown-config": "^0.0.1-git-87d22db.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsdown && pnpm build:kdl",
@@ -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 { detectPackageManager, installDependencies } from "nypm";
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 { replacePlaceholders, updateRootPackageName } from "./placeholders.ts";
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
- // 3. Resolve target dir
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 updateRootPackageName(dir, name);
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(detected)}`);
179
+ installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
164
180
  try {
165
- await installDependencies({ cwd: dir, packageManager: { name: detected, command: detected }, silent: true });
166
- installSpin.stop(`Installed with ${palette.highlight(detected)}`);
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("You can install manually later with `cd <dir> && <pm> install`.");
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 ? ` ${detectedPm} ${runScript}` : ` ${detectedPm} install && ${detectedPm} ${runScript}`,
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
- * 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`.
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 updateRootPackageName(rootDir: string, projectName: string) {
120
- const debug = logger.subdebug("update-root-package-name");
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
- if (!pkg) {
130
- throw new Error("Could not find package.json");
131
- }
139
+ const { template, projectName, visibility } = options;
132
140
 
133
141
  try {
134
- pkg.packageJson.name = projectName;
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);
@@ -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 🐚 (${TOOL_LABELS.USAGE})`)
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
- pm?: "npm" | "pnpm" | "yarn" | "bun";
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 🚀 (${TOOL_LABELS.GIGET})`)
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)"))
@@ -1,8 +1,9 @@
1
- import { Command } from "commander";
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 addUsage(
17
- new Command("vland")
18
- .version(version, "-v, --version")
19
- .addHelpText("before", getBannerText(version))
20
- .addCommand(createCompletionCommand())
21
- .addCommand(createInitCommand(ctx)),
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,3 +1,3 @@
1
1
  {
2
- "extends": ["@vlandoss/config/ts/no-dom/app"]
2
+ "extends": ["@rrlab/ts-config/no-dom/app"]
3
3
  }
@@ -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
- }