@basaltkit/cli 1.6.0 → 1.7.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
@@ -41,6 +41,10 @@ app's, at its pinned versions. Arguments and the exit code pass through
41
41
  unchanged. Outside an app it says so and exits 1. The launcher loads nothing of
42
42
  the framework, only Node built-ins.
43
43
 
44
+ `basalt new <name> [flags]` is the launcher's own command: it scaffolds an app
45
+ with `create-basalt@latest` (through `pnpm dlx`, else `npx`; `--pm=` picks the
46
+ manager), so it always gets the latest scaffolder.
47
+
44
48
  ## Get started in 5 minutes
45
49
 
46
50
  1. Create a `bin/basalt.ts` file at the root of the project — this will be the entry point for the `basalt` command:
@@ -7,6 +7,10 @@
7
7
  * app's own package manager — so the code that runs is always the app's, at
8
8
  * the versions its lockfile pins, never the launcher's. Node built-ins only:
9
9
  * nothing of the framework is loaded.
10
+ *
11
+ * One command is the launcher's own: `basalt new <name> [flags]` scaffolds an
12
+ * app through `create-basalt@latest` (`pnpm dlx`, else `npx`) — the latest
13
+ * scaffolder, whatever version the launcher is.
10
14
  */
11
15
  export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
12
16
  export interface LauncherProject {
@@ -41,6 +45,17 @@ export interface RunLauncherOptions {
41
45
  /** Where the launcher's own lines go (never the app's output). */
42
46
  error?: (line: string) => void;
43
47
  log?: (line: string) => void;
48
+ /** Whether pnpm is installed — what `basalt new` runs create-basalt with. Default: `pnpm --version` answers. */
49
+ hasPnpm?: () => boolean;
44
50
  }
51
+ /**
52
+ * How `basalt new` runs `create-basalt@latest`: through the manager `--pm=`
53
+ * names, else pnpm when it is installed, else npx. create-basalt reads the
54
+ * manager it was started with, so the new app uses the same one.
55
+ */
56
+ export declare function scaffoldCommand(args: readonly string[], hasPnpm: () => boolean): {
57
+ command: string;
58
+ args: string[];
59
+ };
45
60
  /** Runs `basalt <argv>`: the app's `basalt` script, through the app's package manager. Resolves to the exit code. */
46
61
  export declare function runLauncher(argv: readonly string[], options?: RunLauncherOptions): Promise<number>;
package/dist/launcher.js CHANGED
@@ -7,8 +7,12 @@
7
7
  * app's own package manager — so the code that runs is always the app's, at
8
8
  * the versions its lockfile pins, never the launcher's. Node built-ins only:
9
9
  * nothing of the framework is loaded.
10
+ *
11
+ * One command is the launcher's own: `basalt new <name> [flags]` scaffolds an
12
+ * app through `create-basalt@latest` (`pnpm dlx`, else `npx`) — the latest
13
+ * scaffolder, whatever version the launcher is.
10
14
  */
11
- import { spawn } from 'node:child_process';
15
+ import { spawn, spawnSync } from 'node:child_process';
12
16
  import { existsSync, readFileSync } from 'node:fs';
13
17
  import { dirname, join, resolve } from 'node:path';
14
18
  /** Set on the script's environment: a `basalt` script that calls `basalt` again would loop. */
@@ -93,9 +97,24 @@ function launcherVersion() {
93
97
  return 'unknown';
94
98
  }
95
99
  }
100
+ /**
101
+ * How `basalt new` runs `create-basalt@latest`: through the manager `--pm=`
102
+ * names, else pnpm when it is installed, else npx. create-basalt reads the
103
+ * manager it was started with, so the new app uses the same one.
104
+ */
105
+ export function scaffoldCommand(args, hasPnpm) {
106
+ const pm = args.find((arg) => arg.startsWith('--pm='))?.slice('--pm='.length);
107
+ const pkg = 'create-basalt@latest';
108
+ if (pm === 'pnpm' || (pm === undefined && hasPnpm()))
109
+ return { command: 'pnpm', args: ['--silent', 'dlx', pkg, ...args] };
110
+ if (pm === 'bun')
111
+ return { command: 'bunx', args: [pkg, ...args] };
112
+ return { command: 'npx', args: ['--yes', pkg, ...args] }; // npm, and yarn (create-basalt honours --pm=yarn)
113
+ }
114
+ const pnpmInstalled = () => spawnSync('pnpm', ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' }).status === 0;
96
115
  const NOT_AN_APP = [
97
116
  'basalt: no Basalt app here — no package.json with a "basalt" script in this directory or above it.',
98
- ' Start one: npm create basalt@latest my-app',
117
+ ' Start one: basalt new my-app',
99
118
  ' Or cd into an app first; `basalt <command>` then runs `<pm> basalt <command>` there.',
100
119
  ];
101
120
  /** Runs `basalt <argv>`: the app's `basalt` script, through the app's package manager. Resolves to the exit code. */
@@ -104,6 +123,11 @@ export async function runLauncher(argv, options = {}) {
104
123
  const env = options.env ?? process.env;
105
124
  const error = options.error ?? ((line) => process.stderr.write(`${line}\n`));
106
125
  const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
126
+ // The launcher's own command, inside an app too: a new app is never the current one's business.
127
+ if (argv[0] === 'new') {
128
+ const scaffold = scaffoldCommand(argv.slice(1), options.hasPnpm ?? pnpmInstalled);
129
+ return spawnAndWait(scaffold.command, scaffold.args, { cwd, env, error, what: 'create a new app' });
130
+ }
107
131
  const project = findProject(cwd);
108
132
  if (!project) {
109
133
  if (argv[0] === '--version' || argv[0] === '-v') {
@@ -120,14 +144,24 @@ export async function runLauncher(argv, options = {}) {
120
144
  return 1;
121
145
  }
122
146
  const { command, args } = launchCommand(project.pm, argv);
147
+ return spawnAndWait(command, args, {
148
+ cwd: project.root,
149
+ env: { ...env, [LAUNCHER_ENV]: project.root },
150
+ error,
151
+ what: `run this app's basalt script (${project.root})`,
152
+ });
153
+ }
154
+ /** Runs one command with the terminal attached; resolves to its exit code. */
155
+ function spawnAndWait(command, args, options) {
156
+ const { error } = options;
123
157
  return new Promise((done) => {
124
158
  const child = spawn(command, args, {
125
- cwd: project.root,
126
- env: { ...env, [LAUNCHER_ENV]: project.root },
159
+ cwd: options.cwd,
160
+ env: options.env,
127
161
  stdio: 'inherit',
128
162
  shell: process.platform === 'win32',
129
163
  });
130
- // Ctrl-C reaches the whole foreground group: the app handles it; the
164
+ // Ctrl-C reaches the whole foreground group: the child handles it; the
131
165
  // launcher only waits for it to finish and reports its code.
132
166
  const ignore = () => { };
133
167
  const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
@@ -140,7 +174,7 @@ export async function runLauncher(argv, options = {}) {
140
174
  };
141
175
  child.on('error', (cause) => {
142
176
  error(cause.code === 'ENOENT'
143
- ? `basalt: ${command} is not installed, and this app uses it (${project.root}).`
177
+ ? `basalt: ${command} is not installed, and it is needed to ${options.what}.`
144
178
  : `basalt: could not start ${command}: ${cause.message}`);
145
179
  settle(1);
146
180
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },