@variablelab/vland 0.3.7 → 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,7 +43,7 @@ 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
 
@@ -1,7 +1,7 @@
1
1
  // @generated by @usage-spec/commander from Commander.js metadata
2
2
  name vland
3
3
  bin vland
4
- version "0.3.7"
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)" {
package/dist/run.mjs CHANGED
@@ -99,6 +99,15 @@ const LIBRARY_SCOPES = {
99
99
  private: "@variableland",
100
100
  public: "@variablelab"
101
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
+ };
102
111
  const GITHUB_SOURCE = "github:withvariablelab/dx";
103
112
  const GITHUB_REF = "main";
104
113
  /**
@@ -202,7 +211,7 @@ async function walk(root, onFile) {
202
211
  }));
203
212
  }
204
213
  function applyPlaceholders(content, values) {
205
- 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);
206
215
  }
207
216
  async function replacePlaceholders(rootDir, values) {
208
217
  const debug = logger.subdebug("placeholders");
@@ -221,30 +230,29 @@ async function replacePlaceholders(rootDir, values) {
221
230
  return { touched };
222
231
  }
223
232
  /**
224
- * Writes the root `package.json`'s `name` and `private` fields based on the
225
- * chosen template and visibility. Uses `pkg-types` so field ordering and
226
- * JSON formatting survive.
227
- *
228
- * Naming rules:
229
- * - `library` private → `@variableland/<projectName>`, `private: true`
233
+ * The root package's name:
234
+ * - `library` private → `@variableland/<projectName>`
230
235
  * - `library` public → `@variablelab/<projectName>`
231
- * - `backend` → `<projectName>`, `private: true`
232
- * - `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.
233
247
  */
234
248
  async function applyRootPackage(rootDir, options) {
235
249
  const debug = logger.subdebug("apply-root-package");
236
250
  const pkg = await createPkg(fs.realpathSync(rootDir));
237
251
  if (!pkg) throw new Error("Could not find package.json");
238
- const { template, projectName, visibility } = options;
252
+ const { template, visibility } = options;
239
253
  try {
240
- if (template === "library") {
241
- const scope = LIBRARY_SCOPES[visibility ?? "public"];
242
- pkg.packageJson.name = `${scope}/${projectName}`;
243
- if (visibility === "private") pkg.packageJson.private = true;
244
- } else {
245
- pkg.packageJson.name = projectName;
246
- pkg.packageJson.private = true;
247
- }
254
+ pkg.packageJson.name = rootPackageName(options);
255
+ if (template !== "library" || visibility === "private") pkg.packageJson.private = true;
248
256
  await pkg.write(pkg.packageJson);
249
257
  } catch (error) {
250
258
  debug("skipped %s", error);
@@ -269,7 +277,12 @@ async function isDirEmpty(dir) {
269
277
  throw error;
270
278
  }
271
279
  }
272
- 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) {
273
286
  try {
274
287
  const [name, email] = await Promise.all([shell.runCaptured("git", [
275
288
  "config",
@@ -280,14 +293,31 @@ async function readGitAuthor(shell) {
280
293
  "--get",
281
294
  "user.email"
282
295
  ], { throwOnError: false })]);
283
- const trimmedName = name.stdout.trim();
284
- const trimmedEmail = email.stdout.trim();
285
- if (!trimmedName) return void 0;
286
- return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName;
296
+ return {
297
+ name: name.stdout.trim(),
298
+ email: email.stdout.trim()
299
+ };
287
300
  } catch {
288
- return;
301
+ return {
302
+ name: "",
303
+ email: ""
304
+ };
289
305
  }
290
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
+ }
291
321
  function abort(message) {
292
322
  cancel(message);
293
323
  process.exit(1);
@@ -373,7 +403,13 @@ async function runInit(ctx, options) {
373
403
  await replacePlaceholders(dir, {
374
404
  projectName: name,
375
405
  author,
376
- 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"]
377
413
  });
378
414
  await applyRootPackage(dir, {
379
415
  template,
@@ -383,6 +419,13 @@ async function runInit(ctx, options) {
383
419
  placeholderSpin.stop("Placeholders applied");
384
420
  const shouldInstall = await resolveYesNo(options.install, "Install dependencies?");
385
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
+ }
386
429
  if (shouldInstall) {
387
430
  const installSpin = spinner();
388
431
  installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`);
@@ -406,16 +449,13 @@ async function runInit(ctx, options) {
406
449
  const gitSpin = spinner();
407
450
  gitSpin.start("Initialising git repository");
408
451
  try {
409
- const gitShell = shell.at(dir).child({ env: {
410
- GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland",
411
- GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land",
412
- GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland",
413
- GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land"
414
- } });
415
- await gitShell.runCaptured("git", ["init"]);
452
+ if (gitInitError) throw gitInitError;
416
453
  await gitShell.runCaptured("git", ["add", "-A"]);
454
+ const identityArgs = await gitIdentityFallbackArgs(gitShell);
417
455
  await gitShell.runCaptured("git", [
456
+ ...identityArgs,
418
457
  "commit",
458
+ "--no-verify",
419
459
  "-m",
420
460
  "chore: initial commit from vland"
421
461
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variablelab/vland",
3
- "version": "0.3.7",
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