@variablelab/vland 0.3.6 → 0.3.8-git-fdf054c.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
@@ -43,23 +43,25 @@ See [`CLI.md`](./CLI.md) for the full reference (auto-generated per release).
43
43
  | `backend` | An Elysia (`@elysiajs/node`) backend with evlog, Vitest, Dockerfile, CI shape. |
44
44
  | `monorepo` | pnpm + Turbo workspace with an Elysia API, a Vite-React SPA, and a few internal packages. |
45
45
 
46
- All templates target Node.js, use pnpm, and ship [`@rrlab/cli`](https://github.com/withvariablelab/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.
46
+ All templates target Node.js, pin Node.js and pnpm with [mise](https://mise.jdx.dev) (`mise.toml` and `mise.lock`), run their GitHub Actions on SHA-pinned actions checked by [zizmor](https://docs.zizmor.sh), and ship [`@rrlab/cli`](https://github.com/withvariablelab/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.
47
47
 
48
48
  ## Shell completion
49
49
 
50
- `vland` ships a `completion` subcommand that prints a shell-specific script. Add it to your shell rc file:
50
+ `vland` ships a `completion` subcommand that prints a shell-specific script. Add a guarded eval to your shell rc file:
51
51
 
52
52
  ```sh
53
53
  # zsh — ~/.zshrc
54
- eval "$(vland completion zsh)"
54
+ command -v vland >/dev/null 2>&1 && eval "$(vland completion zsh 2>/dev/null)"
55
55
 
56
56
  # bash — ~/.bashrc
57
- eval "$(vland completion bash)"
57
+ command -v vland >/dev/null 2>&1 && eval "$(vland completion bash 2>/dev/null)"
58
58
 
59
59
  # fish — ~/.config/fish/config.fish
60
- vland completion fish | source
60
+ command -v vland >/dev/null 2>&1; and vland completion fish 2>/dev/null | source
61
61
  ```
62
62
 
63
+ The `command -v` guard skips the line when no `vland` is on `PATH`. The `2>/dev/null` covers a mise shim with no active version: it passes the guard, then fails on stderr. If completion doesn't load, run `vland completion zsh` by hand to see why.
64
+
63
65
  **Prerequisite:** the [`usage`](https://usage.jdx.dev) CLI must be on your `PATH` (it powers completion at runtime). Install via one of:
64
66
 
65
67
  ```sh
@@ -1,11 +1,11 @@
1
1
  // @generated by @usage-spec/commander from Commander.js metadata
2
2
  name vland
3
3
  bin vland
4
- version "0.3.6"
4
+ version "0.3.8-git-fdf054c.0"
5
5
  usage "[options] [command]"
6
6
  flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
7
7
  cmd completion help="print shell completion script (usage)" {
8
- long_help "Prints a shell completion script for vland. Add to your shell rc file:\n\n bash: eval \"$(vland completion bash)\"\n zsh: eval \"$(vland completion zsh)\"\n fish: vland completion fish | source"
8
+ long_help "Prints a shell completion script for vland. Add your shell's line to its rc file:\n\n # bash — ~/.bashrc\n command -v vland >/dev/null 2>&1 && eval \"$(vland completion bash 2>/dev/null)\"\n\n # zsh — ~/.zshrc\n command -v vland >/dev/null 2>&1 && eval \"$(vland completion zsh 2>/dev/null)\"\n\n # fish — ~/.config/fish/config.fish\n command -v vland >/dev/null 2>&1; and vland completion fish 2>/dev/null | source"
9
9
  arg <shell> help="target shell" {
10
10
  choices bash zsh fish
11
11
  }
package/dist/run.mjs CHANGED
@@ -56,11 +56,16 @@ 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 your shell's line to its rc file:
60
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).
61
+ # bash — ~/.bashrc
62
+ command -v vland >/dev/null 2>&1 && eval "$(vland completion bash 2>/dev/null)"
63
+
64
+ # zsh — ~/.zshrc
65
+ command -v vland >/dev/null 2>&1 && eval "$(vland completion zsh 2>/dev/null)"
66
+
67
+ # fish — ~/.config/fish/config.fish
68
+ command -v vland >/dev/null 2>&1; and vland completion fish 2>/dev/null | source`).configureHelp({ helpWidth: Number.POSITIVE_INFINITY }).addArgument(new Argument("<shell>", `target shell`).choices(SHELLS)).addHelpText("afterAll", `\nUnder the hood, this command uses ${TOOL_LABELS.USAGE} (https://usage.jdx.dev).
64
69
  Make sure to have it installed and available in your PATH.`);
65
70
  }
66
71
  //#endregion
@@ -94,6 +99,15 @@ const LIBRARY_SCOPES = {
94
99
  private: "@variableland",
95
100
  public: "@variablelab"
96
101
  };
102
+ /**
103
+ * The GitHub owner of a project's repository, indexed by visibility: public
104
+ * projects live in the open-source `withvariablelab` org, private ones in
105
+ * `variableland`.
106
+ */
107
+ const GITHUB_OWNERS = {
108
+ private: "variableland",
109
+ public: "withvariablelab"
110
+ };
97
111
  const GITHUB_SOURCE = "github:withvariablelab/dx";
98
112
  const GITHUB_REF = "main";
99
113
  /**
@@ -197,7 +211,7 @@ async function walk(root, onFile) {
197
211
  }));
198
212
  }
199
213
  function applyPlaceholders(content, values) {
200
- return content.replaceAll("{{projectName}}", values.projectName).replaceAll("{{author}}", values.author).replaceAll("{{year}}", values.year);
214
+ return content.replaceAll("{{projectName}}", values.projectName).replaceAll("{{author}}", values.author).replaceAll("{{year}}", values.year).replaceAll("{{packageName}}", values.packageName).replaceAll("{{githubOwner}}", values.githubOwner);
201
215
  }
202
216
  async function replacePlaceholders(rootDir, values) {
203
217
  const debug = logger.subdebug("placeholders");
@@ -216,30 +230,29 @@ async function replacePlaceholders(rootDir, values) {
216
230
  return { touched };
217
231
  }
218
232
  /**
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`
233
+ * The root package's name:
234
+ * - `library` private → `@variableland/<projectName>`
225
235
  * - `library` public → `@variablelab/<projectName>`
226
- * - `backend` → `<projectName>`, `private: true`
227
- * - `monorepo` → `<projectName>`, `private: true` (root never publishes)
236
+ * - `backend`, `monorepo` → `<projectName>`
237
+ */
238
+ function rootPackageName({ template, projectName, visibility }) {
239
+ return template === "library" ? `${LIBRARY_SCOPES[visibility ?? "public"]}/${projectName}` : projectName;
240
+ }
241
+ /**
242
+ * Writes the root `package.json`'s `name` ({@link rootPackageName}) and
243
+ * `private` fields based on the chosen template and visibility. Uses
244
+ * `pkg-types` so field ordering and JSON formatting survive. `private: true`
245
+ * goes on a private library and on every backend and monorepo root, which
246
+ * never publishes.
228
247
  */
229
248
  async function applyRootPackage(rootDir, options) {
230
249
  const debug = logger.subdebug("apply-root-package");
231
250
  const pkg = await createPkg(fs.realpathSync(rootDir));
232
251
  if (!pkg) throw new Error("Could not find package.json");
233
- const { template, projectName, visibility } = options;
252
+ const { template, visibility } = options;
234
253
  try {
235
- if (template === "library") {
236
- const scope = LIBRARY_SCOPES[visibility ?? "public"];
237
- pkg.packageJson.name = `${scope}/${projectName}`;
238
- if (visibility === "private") pkg.packageJson.private = true;
239
- } else {
240
- pkg.packageJson.name = projectName;
241
- pkg.packageJson.private = true;
242
- }
254
+ pkg.packageJson.name = rootPackageName(options);
255
+ if (template !== "library" || visibility === "private") pkg.packageJson.private = true;
243
256
  await pkg.write(pkg.packageJson);
244
257
  } catch (error) {
245
258
  debug("skipped %s", error);
@@ -264,7 +277,12 @@ async function isDirEmpty(dir) {
264
277
  throw error;
265
278
  }
266
279
  }
267
- async function readGitAuthor(shell) {
280
+ const FALLBACK_GIT_IDENTITY = {
281
+ name: "vland",
282
+ email: "noreply@variable.land"
283
+ };
284
+ /** The `user.name` and `user.email` git resolves from the shell's cwd. Each is empty when unset. */
285
+ async function readGitIdentity(shell) {
268
286
  try {
269
287
  const [name, email] = await Promise.all([shell.runCaptured("git", [
270
288
  "config",
@@ -275,14 +293,31 @@ async function readGitAuthor(shell) {
275
293
  "--get",
276
294
  "user.email"
277
295
  ], { throwOnError: false })]);
278
- const trimmedName = name.stdout.trim();
279
- const trimmedEmail = email.stdout.trim();
280
- if (!trimmedName) return void 0;
281
- return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName;
296
+ return {
297
+ name: name.stdout.trim(),
298
+ email: email.stdout.trim()
299
+ };
282
300
  } catch {
283
- return;
301
+ return {
302
+ name: "",
303
+ email: ""
304
+ };
284
305
  }
285
306
  }
307
+ async function readGitAuthor(shell) {
308
+ const { name, email } = await readGitIdentity(shell);
309
+ if (!name) return void 0;
310
+ return email ? `${name} <${email}>` : name;
311
+ }
312
+ /**
313
+ * `-c` flags that fill in only the half of the identity git can't resolve. A
314
+ * configured identity is left alone, so `commit.gpgsign` signs with the user's
315
+ * own email.
316
+ */
317
+ async function gitIdentityFallbackArgs(shell) {
318
+ const { name, email } = await readGitIdentity(shell);
319
+ return [...name ? [] : ["-c", `user.name=${FALLBACK_GIT_IDENTITY.name}`], ...email ? [] : ["-c", `user.email=${FALLBACK_GIT_IDENTITY.email}`]];
320
+ }
286
321
  function abort(message) {
287
322
  cancel(message);
288
323
  process.exit(1);
@@ -368,7 +403,13 @@ async function runInit(ctx, options) {
368
403
  await replacePlaceholders(dir, {
369
404
  projectName: name,
370
405
  author,
371
- year: (/* @__PURE__ */ new Date()).getFullYear().toString()
406
+ year: (/* @__PURE__ */ new Date()).getFullYear().toString(),
407
+ packageName: rootPackageName({
408
+ template,
409
+ projectName: name,
410
+ visibility
411
+ }),
412
+ githubOwner: GITHUB_OWNERS[template === "library" ? visibility ?? "public" : "private"]
372
413
  });
373
414
  await applyRootPackage(dir, {
374
415
  template,
@@ -378,6 +419,13 @@ async function runInit(ctx, options) {
378
419
  placeholderSpin.stop("Placeholders applied");
379
420
  const shouldInstall = await resolveYesNo(options.install, "Install dependencies?");
380
421
  const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?");
422
+ const gitShell = shell.at(dir);
423
+ let gitInitError;
424
+ if (shouldGit) try {
425
+ await gitShell.runCaptured("git", ["init"]);
426
+ } catch (error) {
427
+ gitInitError = error;
428
+ }
381
429
  if (shouldInstall) {
382
430
  const installSpin = spinner();
383
431
  installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
@@ -401,16 +449,13 @@ async function runInit(ctx, options) {
401
449
  const gitSpin = spinner();
402
450
  gitSpin.start("Initialising git repository");
403
451
  try {
404
- const gitShell = shell.at(dir).child({ env: {
405
- GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland",
406
- GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land",
407
- GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland",
408
- GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land"
409
- } });
410
- await gitShell.runCaptured("git", ["init"]);
452
+ if (gitInitError) throw gitInitError;
411
453
  await gitShell.runCaptured("git", ["add", "-A"]);
454
+ const identityArgs = await gitIdentityFallbackArgs(gitShell);
412
455
  await gitShell.runCaptured("git", [
456
+ ...identityArgs,
413
457
  "commit",
458
+ "--no-verify",
414
459
  "-m",
415
460
  "chore: initial commit from vland"
416
461
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variablelab/vland",
3
- "version": "0.3.6",
3
+ "version": "0.3.8-git-fdf054c.0",
4
4
  "description": "The CLI to init a new project in Variable Land",
5
5
  "homepage": "https://github.com/withvariablelab/dx/tree/main/vland/cli#readme",
6
6
  "bugs": {
@@ -35,8 +35,8 @@
35
35
  "commander": "14.0.3",
36
36
  "giget": "2.0.0",
37
37
  "nypm": "0.6.0",
38
- "@variablelab/clibuddy": "0.7.4",
39
- "@variablelab/loggy": "0.2.3"
38
+ "@variablelab/loggy": "0.2.3",
39
+ "@variablelab/clibuddy": "0.7.4"
40
40
  },
41
41
  "publishConfig": {
42
42
  "access": "public"
@@ -5,8 +5,8 @@ import { hasTTY, palette } from "@variablelab/clibuddy";
5
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 { applyRootPackage, replacePlaceholders } from "./placeholders.ts";
9
- import { fetchTemplate, TEMPLATE_META, TEMPLATES, type TemplateName, type Visibility } from "./template.ts";
8
+ import { applyRootPackage, replacePlaceholders, rootPackageName } from "./placeholders.ts";
9
+ import { fetchTemplate, GITHUB_OWNERS, TEMPLATE_META, TEMPLATES, type TemplateName, type Visibility } from "./template.ts";
10
10
 
11
11
  export type InitOptions = {
12
12
  name?: string;
@@ -45,21 +45,42 @@ async function isDirEmpty(dir: string): Promise<boolean> {
45
45
  }
46
46
  }
47
47
 
48
- async function readGitAuthor(shell: Context["shell"]): Promise<string | undefined> {
48
+ type GitIdentity = { name: string; email: string };
49
+
50
+ const FALLBACK_GIT_IDENTITY: GitIdentity = { name: "vland", email: "noreply@variable.land" };
51
+
52
+ /** The `user.name` and `user.email` git resolves from the shell's cwd. Each is empty when unset. */
53
+ async function readGitIdentity(shell: Context["shell"]): Promise<GitIdentity> {
49
54
  try {
50
55
  const [name, email] = await Promise.all([
51
56
  shell.runCaptured("git", ["config", "--get", "user.name"], { throwOnError: false }),
52
57
  shell.runCaptured("git", ["config", "--get", "user.email"], { throwOnError: false }),
53
58
  ]);
54
- const trimmedName = name.stdout.trim();
55
- const trimmedEmail = email.stdout.trim();
56
- if (!trimmedName) return undefined;
57
- return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName;
59
+ return { name: name.stdout.trim(), email: email.stdout.trim() };
58
60
  } catch {
59
- return undefined;
61
+ return { name: "", email: "" };
60
62
  }
61
63
  }
62
64
 
65
+ async function readGitAuthor(shell: Context["shell"]): Promise<string | undefined> {
66
+ const { name, email } = await readGitIdentity(shell);
67
+ if (!name) return undefined;
68
+ return email ? `${name} <${email}>` : name;
69
+ }
70
+
71
+ /**
72
+ * `-c` flags that fill in only the half of the identity git can't resolve. A
73
+ * configured identity is left alone, so `commit.gpgsign` signs with the user's
74
+ * own email.
75
+ */
76
+ async function gitIdentityFallbackArgs(shell: Context["shell"]): Promise<string[]> {
77
+ const { name, email } = await readGitIdentity(shell);
78
+ return [
79
+ ...(name ? [] : ["-c", `user.name=${FALLBACK_GIT_IDENTITY.name}`]),
80
+ ...(email ? [] : ["-c", `user.email=${FALLBACK_GIT_IDENTITY.email}`]),
81
+ ];
82
+ }
83
+
63
84
  function abort(message: string): never {
64
85
  cancel(message);
65
86
  process.exit(1);
@@ -167,6 +188,9 @@ export async function runInit(ctx: Context, options: InitOptions) {
167
188
  projectName: name,
168
189
  author,
169
190
  year: new Date().getFullYear().toString(),
191
+ packageName: rootPackageName({ template, projectName: name, visibility }),
192
+ // Only a library picks its visibility: backend and monorepo roots are always private.
193
+ githubOwner: GITHUB_OWNERS[template === "library" ? (visibility ?? "public") : "private"],
170
194
  });
171
195
  await applyRootPackage(dir, { template, projectName: name, visibility });
172
196
  placeholderSpin.stop("Placeholders applied");
@@ -174,6 +198,17 @@ export async function runInit(ctx: Context, options: InitOptions) {
174
198
  const shouldInstall = await resolveYesNo(options.install, "Install dependencies?");
175
199
  const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?");
176
200
 
201
+ const gitShell = shell.at(dir);
202
+ let gitInitError: unknown;
203
+ if (shouldGit) {
204
+ // Before the install, so a postinstall that sets up git hooks (lefthook's) finds the repo.
205
+ try {
206
+ await gitShell.runCaptured("git", ["init"]);
207
+ } catch (error) {
208
+ gitInitError = error;
209
+ }
210
+ }
211
+
177
212
  if (shouldInstall) {
178
213
  const installSpin = spinner();
179
214
  installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
@@ -197,17 +232,12 @@ export async function runInit(ctx: Context, options: InitOptions) {
197
232
  const gitSpin = spinner();
198
233
  gitSpin.start("Initialising git repository");
199
234
  try {
200
- const gitShell = shell.at(dir).child({
201
- env: {
202
- GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland",
203
- GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land",
204
- GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland",
205
- GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land",
206
- },
207
- });
208
- await gitShell.runCaptured("git", ["init"]);
235
+ if (gitInitError) throw gitInitError;
209
236
  await gitShell.runCaptured("git", ["add", "-A"]);
210
- await gitShell.runCaptured("git", ["commit", "-m", "chore: initial commit from vland"]);
237
+ // Read after `git init`, so an `includeIf "gitdir:…"` identity for the new repo applies.
238
+ const identityArgs = await gitIdentityFallbackArgs(gitShell);
239
+ // --no-verify: the hooks the install just set up are for the user's commits, not the template as generated.
240
+ await gitShell.runCaptured("git", [...identityArgs, "commit", "--no-verify", "-m", "chore: initial commit from vland"]);
211
241
  gitSpin.stop("Initialised git repository");
212
242
  } catch (error) {
213
243
  gitSpin.stop("Failed to initialise git", 1);
@@ -9,6 +9,8 @@ export type Placeholders = {
9
9
  projectName: string;
10
10
  author: string;
11
11
  year: string;
12
+ packageName: string;
13
+ githubOwner: string;
12
14
  };
13
15
 
14
16
  const TEXT_EXTENSIONS = new Set([
@@ -86,7 +88,9 @@ function applyPlaceholders(content: string, values: Placeholders) {
86
88
  return content
87
89
  .replaceAll("{{projectName}}", values.projectName)
88
90
  .replaceAll("{{author}}", values.author)
89
- .replaceAll("{{year}}", values.year);
91
+ .replaceAll("{{year}}", values.year)
92
+ .replaceAll("{{packageName}}", values.packageName)
93
+ .replaceAll("{{githubOwner}}", values.githubOwner);
90
94
  }
91
95
 
92
96
  export async function replacePlaceholders(rootDir: string, values: Placeholders) {
@@ -119,15 +123,21 @@ export type ApplyRootPackageOptions = {
119
123
  };
120
124
 
121
125
  /**
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`
126
+ * The root package's name:
127
+ * - `library` private → `@variableland/<projectName>`
128
128
  * - `library` public → `@variablelab/<projectName>`
129
- * - `backend` → `<projectName>`, `private: true`
130
- * - `monorepo` → `<projectName>`, `private: true` (root never publishes)
129
+ * - `backend`, `monorepo` → `<projectName>`
130
+ */
131
+ export function rootPackageName({ template, projectName, visibility }: ApplyRootPackageOptions): string {
132
+ return template === "library" ? `${LIBRARY_SCOPES[visibility ?? "public"]}/${projectName}` : projectName;
133
+ }
134
+
135
+ /**
136
+ * Writes the root `package.json`'s `name` ({@link rootPackageName}) and
137
+ * `private` fields based on the chosen template and visibility. Uses
138
+ * `pkg-types` so field ordering and JSON formatting survive. `private: true`
139
+ * goes on a private library and on every backend and monorepo root, which
140
+ * never publishes.
131
141
  */
132
142
  export async function applyRootPackage(rootDir: string, options: ApplyRootPackageOptions) {
133
143
  const debug = logger.subdebug("apply-root-package");
@@ -136,18 +146,11 @@ export async function applyRootPackage(rootDir: string, options: ApplyRootPackag
136
146
  const pkg = await createPkg(rootPath);
137
147
  if (!pkg) throw new Error("Could not find package.json");
138
148
 
139
- const { template, projectName, visibility } = options;
149
+ const { template, visibility } = options;
140
150
 
141
151
  try {
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;
152
+ pkg.packageJson.name = rootPackageName(options);
153
+ if (template !== "library" || visibility === "private") {
151
154
  pkg.packageJson.private = true;
152
155
  }
153
156
  await pkg.write(pkg.packageJson);
@@ -25,6 +25,16 @@ export const LIBRARY_SCOPES: Record<Visibility, string> = {
25
25
  public: "@variablelab",
26
26
  };
27
27
 
28
+ /**
29
+ * The GitHub owner of a project's repository, indexed by visibility: public
30
+ * projects live in the open-source `withvariablelab` org, private ones in
31
+ * `variableland`.
32
+ */
33
+ export const GITHUB_OWNERS: Record<Visibility, string> = {
34
+ private: "variableland",
35
+ public: "withvariablelab",
36
+ };
37
+
28
38
  const GITHUB_SOURCE = "github:withvariablelab/dx";
29
39
  const GITHUB_REF = "main";
30
40
 
@@ -11,12 +11,21 @@ export function createCompletionCommand() {
11
11
  return createCommand("completion")
12
12
  .summary(`print shell completion script (${TOOL_LABELS.USAGE})`)
13
13
  .description(
14
- `Prints a shell completion script for vland. Add to your shell rc file:
14
+ `Prints a shell completion script for vland. Add your shell's line to its rc file:
15
15
 
16
- bash: eval "$(vland completion bash)"
17
- zsh: eval "$(vland completion zsh)"
18
- fish: vland completion fish | source`,
16
+ # bash — ~/.bashrc
17
+ command -v vland >/dev/null 2>&1 && eval "$(vland completion bash 2>/dev/null)"
18
+
19
+ # zsh — ~/.zshrc
20
+ command -v vland >/dev/null 2>&1 && eval "$(vland completion zsh 2>/dev/null)"
21
+
22
+ # fish — ~/.config/fish/config.fish
23
+ command -v vland >/dev/null 2>&1; and vland completion fish 2>/dev/null | source`,
19
24
  )
25
+ .configureHelp({
26
+ // Commander hard-wraps help at the terminal width; a wrapped rc line breaks when pasted.
27
+ helpWidth: Number.POSITIVE_INFINITY,
28
+ })
20
29
  .addArgument(new Argument("<shell>", `target shell`).choices(SHELLS))
21
30
  .addHelpText(
22
31
  "afterAll",