create-inferative 0.0.0 → 0.2.0-next.20261013180716

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/LICENSE ADDED
@@ -0,0 +1,70 @@
1
+ Copyright (c) 2024 WP Online Magyarország Kft.
2
+ Website: https://wpo.hu
3
+ All Rights Reserved.
4
+
5
+ ==============================================================================
6
+ WP Online Magyarország Kft. - Internal Use Software License
7
+ ==============================================================================
8
+
9
+ This software package and its accompanying documentation (collectively, "the Software")
10
+ are the proprietary and confidential information of WP Online Magyarország Kft.
11
+ ("the Company").
12
+
13
+ 1. GRANT OF LICENSE:
14
+ Subject to the terms and conditions of this License, the Company grants to
15
+ its employees and authorized contractors ("Authorized Users") a non-exclusive,
16
+ non-transferable, revocable license to install, use, copy, and modify the
17
+ Software solely for internal Company business purposes.
18
+
19
+ 2. RESTRICTIONS:
20
+ Authorized Users shall not:
21
+ a. Distribute, sublicense, sell, rent, lease, lend, or otherwise transfer
22
+ the Software, or any part thereof, to any third party outside of
23
+ WP Online Magyarország Kft. without the express prior written permission
24
+ of an authorized representative of the Company.
25
+ b. Use the Software for any purpose not expressly permitted by this License,
26
+ including any personal or commercial projects unrelated to the
27
+ Company's business.
28
+ c. Remove or alter any copyright, trademark, or other proprietary notices
29
+ contained in the Software.
30
+ d. Reverse engineer, decompile, or disassemble the Software, except and
31
+ only to the extent that such activity is expressly permitted by
32
+ applicable law notwithstanding this limitation.
33
+
34
+ 3. OWNERSHIP AND INTELLECTUAL PROPERTY:
35
+ The Company retains all right, title, and interest in and to the Software,
36
+ including all copyrights, patents, trade secrets, trademarks, and other
37
+ intellectual property rights therein. No rights are granted to Authorized
38
+ Users other than those expressly set forth in this License.
39
+
40
+ 4. CONFIDENTIALITY:
41
+ The Software, including its source code, design, and structure, is
42
+ confidential information of the Company. Authorized Users shall treat the
43
+ Software with the same degree of care as they treat other Company
44
+ confidential information and shall not disclose it to any third party
45
+ without the Company's prior written consent.
46
+
47
+ 5. NO WARRANTY:
48
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
51
+ THE COMPANY BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER
52
+ IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
53
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
54
+
55
+ 6. TERMINATION:
56
+ This License will terminate automatically if an Authorized User breaches any
57
+ of its terms and conditions. The license granted herein is also contingent
58
+ upon continued employment or contract with the Company and terminates
59
+ immediately upon the cessation of that relationship. Upon termination, the
60
+ Authorized User must cease all use of the Software and destroy all copies
61
+
62
+ of the Software in their possession or control.
63
+
64
+ 7. GOVERNING LAW:
65
+ This License shall be governed by and construed in accordance with the laws
66
+ of Hungary, without regard to its conflict of law principles. Any disputes
67
+ arising under or in connection with this License shall be subject to the
68
+ exclusive jurisdiction of the courts of Hungary.
69
+
70
+ For questions regarding this license, please contact office@wpo.hu.
package/README.md CHANGED
@@ -1 +1,56 @@
1
- # create-inferative
1
+ # `create-inferative`
2
+
3
+ Create a new Inferative project with nothing installed first. This package owns the `init` command, and
4
+ `inferative init` runs the same command, so every flag works the same way. It depends on packages from public npm only,
5
+ so `npm create inferative` needs no registry setting before it starts.
6
+
7
+ ## Create a project
8
+
9
+ Run one command for your package manager.
10
+
11
+ ```sh
12
+ npm create inferative@latest my-site
13
+ pnpm create inferative my-site
14
+ yarn create inferative my-site
15
+ bun create inferative my-site
16
+ ```
17
+
18
+ The command writes the project into the folder `my-site`. Leave the name out to fill the current folder. The folder
19
+ must not exist, or must be empty. The command never overwrites a file.
20
+
21
+ ## What it does
22
+
23
+ 1. It writes the `@inferative` and `@heximon` lines into the `.npmrc` of the project. Both scopes live on GitHub
24
+ Packages.
25
+ 2. It checks that npm can read both scopes. If it cannot, it removes what it wrote, prints the lines to add to your
26
+ `~/.npmrc`, and stops.
27
+ 3. It downloads the starter from the `inferative/starters` repository.
28
+ 4. It sets the `name` of the project and the version of `inferative` and of every `@inferative/*` package.
29
+ 5. It runs `pnpm install`.
30
+
31
+ The package and its dependencies come from public npm, so the command starts with an empty `~/.npmrc`. The project
32
+ install needs only the token, because step 1 writes the two scope lines into the `.npmrc` of the project:
33
+
34
+ ```ini
35
+ //npm.pkg.github.com/:_authToken=<a GitHub token with the read:packages scope>
36
+ ```
37
+
38
+ Add the token line to your `~/.npmrc`. If the token is missing, step 2 stops and prints the lines to add.
39
+
40
+ ## Flags
41
+
42
+ | Flag | What it does |
43
+ | :-- | :-- |
44
+ | `--starter <name>` | The starter to use. `nuxt-cloudflare` by default. |
45
+ | `--source <giget source>` | Download this source instead of the default starters. |
46
+ | `--local <folder>` | Copy the starter from this starters folder. Nothing is downloaded. |
47
+ | `--no-install` | Do not run `pnpm install`. |
48
+ | `--cwd <folder>` | The folder in which a named project folder is created. |
49
+
50
+ `pnpm`, `yarn` and `bun` pass a `--` to this package. The package drops the first one, so
51
+ `pnpm create inferative my-site -- --no-install` works as written.
52
+
53
+ ## Works with
54
+
55
+ - [`inferative`](../cli) depends on this package. `inferative init` and `inferative site doctor` use the `init`
56
+ command and the registry probe from here.
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/bin.mjs ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { t as CreateFrontDoor } from "./front-door-CJboxy5n.mjs";
3
+ import { runMain } from "citty";
4
+ //#region src/bin.ts
5
+ const argv = CreateFrontDoor.rawArgs(process.argv.slice(2));
6
+ if (CreateFrontDoor.isVersionRequest(argv)) process.stdout.write(`${CreateFrontDoor.version()}\n`);
7
+ else await runMain(CreateFrontDoor.command(), { rawArgs: [...argv] });
8
+ //#endregion
9
+ export {};
@@ -0,0 +1,932 @@
1
+ import { createRequire } from "node:module";
2
+ import { defineCommand } from "citty";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { installDependencies } from "nypm";
5
+ import { x } from "tinyexec";
6
+ import { cp, mkdir, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
7
+ //#region src/package-version.ts
8
+ /**
9
+ * The version in this package's own `package.json`.
10
+ *
11
+ * It selects the starter branch and the range stamped into a project. All packages share one version.
12
+ */
13
+ var PackageVersion = class {
14
+ /** The version. citty throws on `--version` without one. */
15
+ static current() {
16
+ return createRequire(import.meta.url)("../package.json").version;
17
+ }
18
+ /**
19
+ * The `major.minor` part of a version, which names the branch of the starters repository.
20
+ *
21
+ * @param version - A version such as `0.1.0` or `0.2.0-snapshot.20261009`.
22
+ */
23
+ static minor(version) {
24
+ const [major = "0", minor = "0"] = version.split(".");
25
+ return `${major}.${minor}`;
26
+ }
27
+ };
28
+ //#endregion
29
+ //#region src/error-code.ts
30
+ /** Reads the `code` of an error that Node raised, such as `ENOENT`. */
31
+ var ErrorCode = class ErrorCode {
32
+ /**
33
+ * @param error - A caught value of any type.
34
+ * @returns The `code` of the error, or `undefined` when the value is not an error with a text code.
35
+ */
36
+ static of(error) {
37
+ if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
38
+ }
39
+ /**
40
+ * @param error - A caught value of any type.
41
+ * @param code - The code to look for, for example `"ENOENT"`.
42
+ * @returns Whether the error carries exactly that code.
43
+ */
44
+ static is(error, code) {
45
+ return ErrorCode.of(error) === code;
46
+ }
47
+ };
48
+ //#endregion
49
+ //#region src/init/dependency-installer.ts
50
+ /** How an install ended. */
51
+ const InstallStatus = {
52
+ /** The user asked to skip the install. */
53
+ Skipped: "skipped",
54
+ /** The install finished. */
55
+ Installed: "installed",
56
+ /** `pnpm` is not installed. */
57
+ PackageManagerMissing: "package-manager-missing",
58
+ /** `pnpm install` exited with an error. */
59
+ Failed: "failed"
60
+ };
61
+ /** Installs the dependencies of a new project with pnpm. */
62
+ var DependencyInstaller = class DependencyInstaller {
63
+ /** The package manager. The starters pin their dependencies for pnpm. */
64
+ static command = "pnpm";
65
+ run;
66
+ /** @param run - Runs the install. nypm runs pnpm, through Corepack when Corepack exists, by default. */
67
+ constructor(run = (directory) => DependencyInstaller.installWithNypm(directory)) {
68
+ this.run = run;
69
+ }
70
+ /**
71
+ * Runs `pnpm install` in a project folder. It never throws for a failed install: the project is complete
72
+ * and the caller keeps it.
73
+ *
74
+ * @param directory - The project folder.
75
+ */
76
+ async install(directory) {
77
+ try {
78
+ await this.run(directory);
79
+ return { status: InstallStatus.Installed };
80
+ } catch (error) {
81
+ if (DependencyInstaller.isMissingProgram(error)) return { status: InstallStatus.PackageManagerMissing };
82
+ return {
83
+ status: InstallStatus.Failed,
84
+ reason: error instanceof Error ? error.message : String(error)
85
+ };
86
+ }
87
+ }
88
+ /** Whether an error says that the program does not exist: `spawn pnpm ENOENT`. */
89
+ static isMissingProgram(error) {
90
+ return ErrorCode.is(error, "ENOENT");
91
+ }
92
+ /** The default {@link InstallRunner}. The output goes to the terminal of the user. */
93
+ static async installWithNypm(directory) {
94
+ await installDependencies({
95
+ cwd: directory,
96
+ packageManager: DependencyInstaller.command
97
+ });
98
+ }
99
+ };
100
+ //#endregion
101
+ //#region src/init/init-messages.ts
102
+ /** The text for the end of an `init` run. */
103
+ var InitMessages = class InitMessages {
104
+ /** The command that starts the development server of a starter. */
105
+ static devCommand = "pnpm dev";
106
+ /** The command that writes the first migration of a starter. A starter commits no migration. */
107
+ static generateCommand = "pnpm db:generate";
108
+ /**
109
+ * The next steps after a successful run.
110
+ *
111
+ * @param changeDirectory - The folder to enter, relative to the current folder. Absent when the project is
112
+ * in the current folder.
113
+ * @param isInstalled - Whether the dependencies are installed.
114
+ */
115
+ static nextSteps(changeDirectory, isInstalled) {
116
+ return [
117
+ "Your Inferative project is ready.",
118
+ "Next steps:",
119
+ ...[
120
+ ...changeDirectory === void 0 ? [] : [`cd ${InitMessages.quote(changeDirectory)}`],
121
+ ...isInstalled ? [] : [`${DependencyInstaller.command} install`],
122
+ "cp .env.example .env",
123
+ InitMessages.generateCommand,
124
+ InitMessages.devCommand
125
+ ].map((step) => ` ${step}`),
126
+ "Fill in the values of .env first. README.md in the project explains the first run, with its first manifest sync (pnpm sync:manifest --first-sync)."
127
+ ].join("\n");
128
+ }
129
+ /** The text when pnpm is not installed. The project is complete. */
130
+ static packageManagerMissing(directory) {
131
+ return [
132
+ `The project is ready in ${directory}, but pnpm is not installed.`,
133
+ "Install pnpm: npm install --global pnpm",
134
+ `Then run "${DependencyInstaller.command} install" in the project folder.`
135
+ ].join("\n");
136
+ }
137
+ /** The text when `pnpm install` fails. The project is complete. */
138
+ static installFailed(directory, reason) {
139
+ return [`The project is ready in ${directory}, but "${DependencyInstaller.command} install" failed${reason === void 0 ? "" : `: ${reason}`}.`, "Read the messages above. Fix the cause, then run the command again in the project folder."].join("\n");
140
+ }
141
+ /** A folder name in quotes when it holds a space. */
142
+ static quote(path) {
143
+ return /\s/u.test(path) ? JSON.stringify(path) : path;
144
+ }
145
+ };
146
+ //#endregion
147
+ //#region src/process/npm-runner.ts
148
+ /**
149
+ * Starts a command with tinyexec and captures its output.
150
+ *
151
+ * It sets no `shell` option. On Windows tinyexec finds the `.cmd` shim of the command and escapes each argument for
152
+ * cmd.exe, so a folder with a space or an `&` in its name reaches npm as one argument.
153
+ */
154
+ var NpmRunner = class NpmRunner {
155
+ /** The exit code of cmd.exe for a command that it cannot find, in every language of Windows. */
156
+ static windowsCommandNotFoundExitCode = 9009;
157
+ execute;
158
+ platform;
159
+ /**
160
+ * @param execute - Starts the process. tinyexec's `x` by default.
161
+ * @param platform - The operating system. `process.platform` by default.
162
+ */
163
+ constructor(execute = x, platform = process.platform) {
164
+ this.execute = execute;
165
+ this.platform = platform;
166
+ }
167
+ /**
168
+ * Runs a command and captures its output. It never throws: a command that cannot start answers exit code 1 with
169
+ * the reason in `stderr` and `commandMissing` when the program does not exist.
170
+ *
171
+ * @param command - The program.
172
+ * @param commandArguments - The arguments, one element for each argument.
173
+ * @param workingDirectory - The directory to run in.
174
+ * @param environment - Variables added to the environment of the process.
175
+ */
176
+ async run(command, commandArguments, workingDirectory, environment) {
177
+ try {
178
+ const result = await this.execute(command, commandArguments, { nodeOptions: {
179
+ cwd: workingDirectory,
180
+ env: environment
181
+ } });
182
+ return {
183
+ exitCode: result.exitCode ?? 1,
184
+ stdout: result.stdout,
185
+ stderr: result.stderr,
186
+ ...this.isMissingInShell(result) ? { commandMissing: true } : {}
187
+ };
188
+ } catch (error) {
189
+ return {
190
+ exitCode: 1,
191
+ stdout: "",
192
+ stderr: error instanceof Error ? error.message : String(error),
193
+ commandMissing: NpmRunner.isMissingProgram(error)
194
+ };
195
+ }
196
+ }
197
+ /** Whether an error says that the program does not exist: `spawn npm ENOENT`. */
198
+ static isMissingProgram(error) {
199
+ return ErrorCode.is(error, "ENOENT");
200
+ }
201
+ /**
202
+ * Whether cmd.exe said that the program does not exist. On Windows tinyexec runs a command that it cannot find
203
+ * through cmd.exe, which exits with code 9009, where other systems throw `ENOENT`. The message is in the language
204
+ * of Windows, so the English text is only a second signal.
205
+ */
206
+ isMissingInShell(result) {
207
+ if (this.platform !== "win32" || result.exitCode === 0) return false;
208
+ return result.exitCode === NpmRunner.windowsCommandNotFoundExitCode || /is not recognized as an internal or external command/iu.test(result.stderr);
209
+ }
210
+ };
211
+ //#endregion
212
+ //#region src/registry/registry-access.ts
213
+ /** The verdict {@link RegistryAccess.probe} reaches. */
214
+ const RegistryAccessStatus = {
215
+ /** Both scopes reach GitHub Packages and the registry answered for each probe package. */
216
+ Ok: "ok",
217
+ /** A scope does not point at GitHub Packages. */
218
+ ScopeNotRouted: "scope-not-routed",
219
+ /** The registry refused the request for lacking, or holding invalid, credentials. */
220
+ Unauthorized: "unauthorized",
221
+ /** npm cannot be started: Node.js is installed without it, or not at all. */
222
+ NpmNotFound: "npm-not-found",
223
+ /** The registry could not be reached (DNS, network, timeout or an unknown failure). */
224
+ Unreachable: "unreachable",
225
+ /** The registry authorized the request, but the probe package is not there. */
226
+ NotFound: "not-found"
227
+ };
228
+ /**
229
+ * Probes whether `@inferative/*` and the `@heximon/*` packages it depends on are readable from GitHub
230
+ * Packages.
231
+ *
232
+ * It asks npm itself, so the verdict follows the same `.npmrc` files an install reads: the project's
233
+ * and the user's.
234
+ */
235
+ var RegistryAccess = class RegistryAccess {
236
+ /** The registry the `@inferative/*` and `@heximon/*` packages publish to. */
237
+ static gitHubPackagesRegistry = "https://npm.pkg.github.com";
238
+ /**
239
+ * The scopes that must resolve to {@link gitHubPackagesRegistry}, each with the package the probe reads. A
240
+ * token can read one organization and not the other, so each scope gets its own read.
241
+ */
242
+ static scopes = [{
243
+ scope: "@inferative",
244
+ probePackage: "@inferative/site"
245
+ }, {
246
+ scope: "@heximon",
247
+ probePackage: "@heximon/runtime"
248
+ }];
249
+ /** The scope line a project's `.npmrc` carries for a scope. */
250
+ static scopeLine(scope) {
251
+ return `${scope}:registry=${RegistryAccess.gitHubPackagesRegistry}`;
252
+ }
253
+ run;
254
+ workingDirectory;
255
+ isProjectRoot;
256
+ /**
257
+ * @param workingDirectory - The directory npm reads its project `.npmrc` from.
258
+ * @param run - The process runner. It spawns `npm` by default.
259
+ * @param isProjectRoot - Whether `workingDirectory` is the project root, even when it holds no `package.json`
260
+ * yet. npm then reads its `.npmrc`, and not the one of an ancestor project (`--prefix`). A caller that has
261
+ * just made the folder sets it.
262
+ */
263
+ constructor(workingDirectory = process.cwd(), run = (command, commandArguments, directory, environment) => new NpmRunner().run(command, commandArguments, directory, environment), isProjectRoot = false) {
264
+ this.workingDirectory = workingDirectory;
265
+ this.run = run;
266
+ this.isProjectRoot = isProjectRoot;
267
+ }
268
+ /**
269
+ * Checks that every scope points at GitHub Packages, then reads one probe package through each scope.
270
+ *
271
+ * @param environment - The environment npm runs with. An `.npmrc` that reads `${NODE_AUTH_TOKEN}` finds the
272
+ * token here, so a caller passes the environment it loaded from the site's `.env`.
273
+ * @returns The verdict, with the scope it is about and the raw output as `detail`.
274
+ */
275
+ async probe(environment = process.env) {
276
+ for (const { scope } of RegistryAccess.scopes) {
277
+ const configured = await this.run("npm", this.npmArguments([
278
+ "config",
279
+ "get",
280
+ `${scope}:registry`
281
+ ]), this.workingDirectory, environment);
282
+ if (configured.commandMissing === true) return {
283
+ status: RegistryAccessStatus.NpmNotFound,
284
+ detail: configured.stderr.trim()
285
+ };
286
+ if (RegistryAccess.withoutTrailingSlash(configured.stdout) !== RegistryAccess.gitHubPackagesRegistry) return {
287
+ status: RegistryAccessStatus.ScopeNotRouted,
288
+ scope,
289
+ detail: configured.stdout.trim()
290
+ };
291
+ }
292
+ for (const { scope, probePackage } of RegistryAccess.scopes) {
293
+ const result = await this.run("npm", this.npmArguments([
294
+ "view",
295
+ probePackage,
296
+ "version",
297
+ "--json",
298
+ "--fetch-retries=0",
299
+ "--fetch-timeout=15000"
300
+ ]), this.workingDirectory, environment);
301
+ if (result.commandMissing === true) return {
302
+ status: RegistryAccessStatus.NpmNotFound,
303
+ detail: result.stderr.trim()
304
+ };
305
+ if (result.exitCode !== 0) return RegistryAccess.failure(scope, `${result.stdout}\n${result.stderr}`);
306
+ }
307
+ return { status: RegistryAccessStatus.Ok };
308
+ }
309
+ /** The arguments of one npm call, with the prefix that pins the project root when the caller asked for it. */
310
+ npmArguments(commandArguments) {
311
+ return this.isProjectRoot ? [
312
+ ...commandArguments,
313
+ "--prefix",
314
+ this.workingDirectory
315
+ ] : commandArguments;
316
+ }
317
+ /**
318
+ * The lines that tell a user how to fix a verdict.
319
+ *
320
+ * @param report - The report that is not {@link RegistryAccessStatus.Ok}.
321
+ */
322
+ static fixInstructions(report) {
323
+ if (report.status === RegistryAccessStatus.NpmNotFound) return [
324
+ "Cannot start npm.",
325
+ "Install Node.js (version 24 or later) with npm, from https://nodejs.org, and make sure npm is on your PATH.",
326
+ "Then run the command again."
327
+ ].join("\n");
328
+ if (report.status === RegistryAccessStatus.Unreachable) return [
329
+ `Cannot reach ${RegistryAccess.gitHubPackagesRegistry}.`,
330
+ "Check your network connection.",
331
+ "Then run the command again."
332
+ ].join("\n");
333
+ const scope = report.scope ?? RegistryAccess.scopes[0]?.scope ?? "@inferative";
334
+ return [
335
+ report.status === RegistryAccessStatus.ScopeNotRouted ? `The scope ${scope} does not point at GitHub Packages.` : `Cannot read ${scope}/* from GitHub Packages.`,
336
+ "Add these lines to ~/.npmrc, or to the .npmrc of the project:",
337
+ ` ${RegistryAccess.scopeLine(scope)}`,
338
+ " //npm.pkg.github.com/:_authToken=<a GitHub token with the read:packages scope>",
339
+ "Then run the command again."
340
+ ].join("\n");
341
+ }
342
+ /** The verdict for a failed read of the probe package of `scope`. */
343
+ static failure(scope, output) {
344
+ const detail = output.trim();
345
+ if (/E401|E403|ENEEDAUTH/u.test(output)) return {
346
+ status: RegistryAccessStatus.Unauthorized,
347
+ scope,
348
+ detail
349
+ };
350
+ if (/E404/u.test(output)) return {
351
+ status: RegistryAccessStatus.NotFound,
352
+ scope,
353
+ detail
354
+ };
355
+ return {
356
+ status: RegistryAccessStatus.Unreachable,
357
+ scope,
358
+ detail
359
+ };
360
+ }
361
+ /** A registry address without the slash npm may add at its end. */
362
+ static withoutTrailingSlash(address) {
363
+ return address.trim().replace(/\/+$/u, "");
364
+ }
365
+ };
366
+ //#endregion
367
+ //#region src/init/init-error.ts
368
+ /** A failure that `inferative init` reports in plain words, without a stack trace. */
369
+ var InitError = class extends Error {
370
+ constructor(message, options) {
371
+ super(message, options);
372
+ this.name = "InitError";
373
+ }
374
+ };
375
+ //#endregion
376
+ //#region src/init/init-target.ts
377
+ /**
378
+ * An accepted target folder. It is absent or empty when accepted, so every entry in it later belongs to this run.
379
+ */
380
+ var InitTarget = class InitTarget {
381
+ /** The package name of a folder whose own name has no usable character. */
382
+ static fallbackPackageName = "inferative-app";
383
+ static packageNamePattern = /^[a-z0-9][a-z0-9._-]*$/u;
384
+ /** The absolute folder. */
385
+ directory;
386
+ /** The `name` that goes into the project's `package.json`. */
387
+ packageName;
388
+ /** The parents that this run created, the nearest first. Empty when the folder existed. */
389
+ createdParents;
390
+ isCreated;
391
+ constructor(directory, packageName, isCreated, createdParents) {
392
+ this.directory = directory;
393
+ this.packageName = packageName;
394
+ this.isCreated = isCreated;
395
+ this.createdParents = createdParents;
396
+ }
397
+ /**
398
+ * Checks a package name.
399
+ *
400
+ * @param name - The candidate name.
401
+ * @returns `undefined` when the name is valid, otherwise the problem in plain words.
402
+ */
403
+ static validatePackageName(name) {
404
+ if (InitTarget.packageNamePattern.test(name)) return;
405
+ return `"${name}" is not a valid package name. Use lowercase letters, digits, and . _ - only, and start with a letter or a digit.`;
406
+ }
407
+ /**
408
+ * Turns the name of an existing folder into a valid package name: lowercase, with each accent removed and
409
+ * its letter kept (`Café` gives `cafe`), and each run of other characters replaced by one `-`.
410
+ *
411
+ * @param folderName - The base name of the folder.
412
+ */
413
+ static normalizePackageName(folderName) {
414
+ const normalized = folderName.toLowerCase().normalize("NFKD").replace(/\p{Mn}/gu, "").replace(/[^a-z0-9._-]+/gu, "-").replace(/^[^a-z0-9]+/u, "").replace(/[^a-z0-9]+$/u, "");
415
+ return normalized === "" ? InitTarget.fallbackPackageName : normalized;
416
+ }
417
+ /**
418
+ * Accepts the target folder, and creates it when it does not exist.
419
+ *
420
+ * A folder that the user names must have a valid package name as its base name. The current folder keeps
421
+ * its name on disk, so its package name is normalised instead.
422
+ *
423
+ * @param request - Where the folder is.
424
+ * @throws {InitError} When the name is invalid, the path is a file, or the folder is not empty.
425
+ */
426
+ static async prepare(request) {
427
+ const base = resolve(request.workingDirectory);
428
+ const directory = request.directory === void 0 ? base : resolve(base, request.directory);
429
+ const isBaseFolder = directory === base;
430
+ const folderName = basename(directory);
431
+ const packageName = isBaseFolder ? InitTarget.normalizePackageName(folderName) : folderName;
432
+ const problem = InitTarget.validatePackageName(packageName);
433
+ if (problem !== void 0) throw new InitError(problem);
434
+ const entries = await InitTarget.readEntries(directory);
435
+ if (entries === void 0) {
436
+ const createdParents = await InitTarget.missingAncestors(directory);
437
+ await mkdir(directory, { recursive: true });
438
+ return new InitTarget(directory, packageName, true, createdParents);
439
+ }
440
+ if (entries.length > 0) throw new InitError(`The folder ${directory} is not empty. Choose an empty folder or a new name. Nothing was changed.`);
441
+ return new InitTarget(directory, packageName, false, []);
442
+ }
443
+ /**
444
+ * Removes everything this run wrote: the folder that it created, or else every entry of an existing folder.
445
+ * A parent that this run created goes too, but only while it is empty: another process may have put a
446
+ * sibling project there.
447
+ */
448
+ async discard() {
449
+ if (!this.isCreated) {
450
+ await this.clearExcept();
451
+ return;
452
+ }
453
+ await rm(this.directory, {
454
+ recursive: true,
455
+ force: true
456
+ });
457
+ for (const parent of this.createdParents) try {
458
+ await rmdir(parent);
459
+ } catch {
460
+ return;
461
+ }
462
+ }
463
+ /**
464
+ * Removes the entries of the folder, for example the half-written files of a failed download.
465
+ *
466
+ * @param keep - The name of one entry to keep.
467
+ */
468
+ async clearExcept(keep) {
469
+ for (const entry of await InitTarget.readEntries(this.directory) ?? []) if (entry !== keep) await rm(join(this.directory, entry), {
470
+ recursive: true,
471
+ force: true
472
+ });
473
+ }
474
+ /** The parents of `directory` that do not exist yet, the nearest first. */
475
+ static async missingAncestors(directory) {
476
+ const missing = [];
477
+ let current = dirname(directory);
478
+ while (current !== dirname(current) && await InitTarget.readEntries(current) === void 0) {
479
+ missing.push(current);
480
+ current = dirname(current);
481
+ }
482
+ return missing;
483
+ }
484
+ /** The entries of a folder, or `undefined` when it does not exist. */
485
+ static async readEntries(directory) {
486
+ try {
487
+ return await readdir(directory);
488
+ } catch (error) {
489
+ const code = ErrorCode.of(error);
490
+ if (code === "ENOENT") return;
491
+ if (code === "ENOTDIR") throw new InitError(`${directory} is a file, not a folder.`);
492
+ throw error;
493
+ }
494
+ }
495
+ };
496
+ //#endregion
497
+ //#region src/init/manifest-stamper.ts
498
+ /** Rewrites the `package.json` that a starter ships. */
499
+ var ManifestStamper = class ManifestStamper {
500
+ /** The dependency sections whose Inferative ranges follow the command line's version. */
501
+ static sections = ["dependencies", "devDependencies"];
502
+ /** The command line itself, which a starter may list beside the `@inferative/*` packages. */
503
+ static commandLineName = "inferative";
504
+ /** The scope prefix of the Inferative libraries. */
505
+ static scopePrefix = "@inferative/";
506
+ /**
507
+ * Sets `name`, and sets the range of `inferative` and of each `@inferative/*` dependency to `^<version>`.
508
+ * Every other range stays as the starter pins it.
509
+ *
510
+ * @param directory - The project folder.
511
+ * @param packageName - The new `name`.
512
+ * @param version - The version of the command line.
513
+ * @throws {InitError} When the starter has no readable `package.json`.
514
+ */
515
+ static async stamp(directory, packageName, version) {
516
+ const path = join(directory, "package.json");
517
+ const manifest = ManifestStamper.parse(await ManifestStamper.read(path), path);
518
+ manifest["name"] = packageName;
519
+ for (const section of ManifestStamper.sections) {
520
+ const dependencies = manifest[section];
521
+ if (!ManifestStamper.isRecord(dependencies)) continue;
522
+ for (const dependency of Object.keys(dependencies)) if (ManifestStamper.isInferative(dependency)) dependencies[dependency] = `^${version}`;
523
+ }
524
+ await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
525
+ }
526
+ /** Whether a dependency name is `inferative` or one of the `@inferative/*` libraries. */
527
+ static isInferative(name) {
528
+ return name === ManifestStamper.commandLineName || name.startsWith(ManifestStamper.scopePrefix);
529
+ }
530
+ static async read(path) {
531
+ try {
532
+ return await readFile(path, "utf8");
533
+ } catch (error) {
534
+ if (ErrorCode.is(error, "ENOENT")) throw new InitError("The starter has no package.json.");
535
+ throw error;
536
+ }
537
+ }
538
+ static parse(text, path) {
539
+ let parsed;
540
+ try {
541
+ parsed = JSON.parse(text);
542
+ } catch (error) {
543
+ throw new InitError(`${path} is not valid JSON.`, { cause: error });
544
+ }
545
+ if (!ManifestStamper.isRecord(parsed)) throw new InitError(`${path} does not hold a JSON object.`);
546
+ return parsed;
547
+ }
548
+ static isRecord(value) {
549
+ return typeof value === "object" && value !== null && !Array.isArray(value);
550
+ }
551
+ };
552
+ //#endregion
553
+ //#region src/init/npmrc-scopes.ts
554
+ /** Writes the scope lines of `@inferative` and `@heximon` into a project's `.npmrc`. It never writes a token. */
555
+ var NpmrcScopes = class NpmrcScopes {
556
+ /** The file name. */
557
+ static filename = ".npmrc";
558
+ /** The scope lines, one for each scope in {@link RegistryAccess.scopes}. */
559
+ static lines() {
560
+ return RegistryAccess.scopes.map((entry) => RegistryAccess.scopeLine(entry.scope));
561
+ }
562
+ /**
563
+ * Adds the missing scope lines to the text of an `.npmrc`. It keeps every existing line.
564
+ *
565
+ * @param content - The current text. It may be empty.
566
+ * @returns The text with each missing line appended, or `content` itself when none is missing.
567
+ */
568
+ static merge(content) {
569
+ const present = new Set(content.split(/\r?\n/u).map((line) => line.trim()));
570
+ const missing = NpmrcScopes.lines().filter((line) => !present.has(line));
571
+ if (missing.length === 0) return content;
572
+ return `${content}${content === "" || content.endsWith("\n") ? "" : "\n"}${missing.join("\n")}\n`;
573
+ }
574
+ /**
575
+ * Creates the `.npmrc` of a project folder, or completes the one that exists.
576
+ *
577
+ * @param directory - The project folder.
578
+ */
579
+ static async apply(directory) {
580
+ const path = join(directory, NpmrcScopes.filename);
581
+ const current = await NpmrcScopes.read(path);
582
+ const merged = NpmrcScopes.merge(current);
583
+ if (merged !== current) await writeFile(path, merged);
584
+ }
585
+ /** The text of a file, or an empty text when the file does not exist. */
586
+ static async read(path) {
587
+ try {
588
+ return await readFile(path, "utf8");
589
+ } catch (error) {
590
+ if (ErrorCode.is(error, "ENOENT")) return "";
591
+ throw error;
592
+ }
593
+ }
594
+ };
595
+ //#endregion
596
+ //#region src/init/starter-source.ts
597
+ /** A download that failed. Only this failure makes {@link StarterSource.fetch} try the next source. */
598
+ var StarterDownloadError = class StarterDownloadError extends InitError {
599
+ /** The giget sources that failed. */
600
+ sources;
601
+ /** Why the download failed, in one line. */
602
+ reason;
603
+ constructor(sources, reason, options) {
604
+ super(sources.length === 1 ? `Cannot download the starter from ${sources[0] ?? ""}: ${reason}` : `Cannot download the starter. ${reason}`, options);
605
+ this.name = "StarterDownloadError";
606
+ this.sources = sources;
607
+ this.reason = reason;
608
+ }
609
+ /**
610
+ * The failure of one source.
611
+ *
612
+ * @param source - The giget source.
613
+ * @param cause - What the download threw.
614
+ */
615
+ static of(source, cause) {
616
+ return new StarterDownloadError([source], cause instanceof Error ? cause.message : String(cause), { cause });
617
+ }
618
+ };
619
+ /** Fetches the files of a starter. */
620
+ var StarterSource = class StarterSource {
621
+ /** The public repository that holds the starters, one folder for each. */
622
+ static repository = "inferative/starters";
623
+ /** The starter that `init` uses when none is named. */
624
+ static defaultStarter = "nuxt-cloudflare";
625
+ /** The branch that the default sources fall back to. */
626
+ static fallbackBranch = "main";
627
+ static starterNamePattern = /^[a-z0-9][a-z0-9-]*$/u;
628
+ download;
629
+ /** @param download - The download function. It calls giget by default. */
630
+ constructor(download = (source, directory) => StarterSource.downloadWithGiget(source, directory)) {
631
+ this.download = download;
632
+ }
633
+ /**
634
+ * The giget sources to try, in order: the branch for this minor version, then `main`.
635
+ *
636
+ * @param starter - The starter folder name.
637
+ * @param version - The version of the command line.
638
+ */
639
+ static defaultSources(starter, version) {
640
+ const base = `gh:${StarterSource.repository}/${starter}`;
641
+ return [`${base}#v${PackageVersion.minor(version)}`, `${base}#${StarterSource.fallbackBranch}`];
642
+ }
643
+ /**
644
+ * Checks a starter name, which becomes a path segment.
645
+ *
646
+ * @throws {InitError} When the name is not a plain lowercase folder name.
647
+ */
648
+ static assertStarterName(starter) {
649
+ if (!StarterSource.starterNamePattern.test(starter)) throw new InitError(`"${starter}" is not a starter name. Use lowercase letters, digits, and - only.`);
650
+ }
651
+ /**
652
+ * Downloads the starter from the first source that works.
653
+ *
654
+ * Only a failed download moves on to the next source. Any other error stops the run, because the next
655
+ * source would fail the same way.
656
+ *
657
+ * @param sources - The giget sources, in order.
658
+ * @param directory - The folder to fill.
659
+ * @param resetDirectory - Called before each later source, to remove the half-written files of the last one.
660
+ * @returns The source that worked.
661
+ * @throws {StarterDownloadError} When every source failed. The message holds each failure.
662
+ */
663
+ async fetch(sources, directory, resetDirectory) {
664
+ const failures = [];
665
+ for (const source of sources) try {
666
+ await this.download(source, directory);
667
+ return source;
668
+ } catch (error) {
669
+ failures.push(StarterDownloadError.of(source, error));
670
+ await resetDirectory();
671
+ }
672
+ const [first] = failures;
673
+ if (failures.length === 1 && first !== void 0) throw first;
674
+ throw new StarterDownloadError(sources, `Tried ${failures.map((failure) => `${failure.sources.join("")} (${failure.reason})`).join(", then ")}.`);
675
+ }
676
+ /**
677
+ * Copies a starter from a local starters folder.
678
+ *
679
+ * @param localDirectory - The folder that holds the starters.
680
+ * @param starter - The starter folder name.
681
+ * @param directory - The folder to fill.
682
+ * @throws {InitError} When the starter folder does not exist.
683
+ */
684
+ async copyLocal(localDirectory, starter, directory) {
685
+ const source = resolve(localDirectory, starter);
686
+ try {
687
+ if (!(await stat(source)).isDirectory()) throw new InitError(`${source} is not a folder.`);
688
+ } catch (error) {
689
+ if (ErrorCode.is(error, "ENOENT")) throw new InitError(`The starter folder ${source} does not exist.`);
690
+ throw error;
691
+ }
692
+ await cp(source, directory, {
693
+ recursive: true,
694
+ filter: (path) => !path.split(/[\\/]/u).includes("node_modules")
695
+ });
696
+ return source;
697
+ }
698
+ /** The default {@link StarterDownload}. It loads giget on first use, so a local copy needs no giget. */
699
+ static async downloadWithGiget(source, directory) {
700
+ const { downloadTemplate } = await import("giget");
701
+ await downloadTemplate(source, {
702
+ dir: directory,
703
+ force: true
704
+ });
705
+ }
706
+ };
707
+ //#endregion
708
+ //#region src/init/project-initializer.ts
709
+ /** Creates a project from a starter. */
710
+ var ProjectInitializer = class ProjectInitializer {
711
+ starters;
712
+ runNpm;
713
+ installer;
714
+ /** @param dependencies - Replacements for the download, the `npm` calls and the install. */
715
+ constructor(dependencies = {}) {
716
+ this.starters = new StarterSource(dependencies.download);
717
+ this.runNpm = dependencies.runNpm;
718
+ this.installer = new DependencyInstaller(dependencies.runInstall);
719
+ }
720
+ /**
721
+ * Creates the project.
722
+ *
723
+ * Before the install, any failure removes what this run wrote and throws. A failed install keeps the
724
+ * project, because it is complete, and reports the failure in the result.
725
+ *
726
+ * @param request - What to create, and where.
727
+ * @throws {InitError} When the folder, the registry, the starter or the request is not usable.
728
+ */
729
+ async initialize(request) {
730
+ ProjectInitializer.assertRequest(request);
731
+ const target = await InitTarget.prepare(request);
732
+ let source;
733
+ try {
734
+ await this.assertRegistryAccess(target);
735
+ source = await this.fetchStarter(request, target);
736
+ await NpmrcScopes.apply(target.directory);
737
+ await ManifestStamper.stamp(target.directory, target.packageName, request.version);
738
+ } catch (error) {
739
+ await target.discard();
740
+ throw error;
741
+ }
742
+ const base = {
743
+ projectDirectory: target.directory,
744
+ packageName: target.packageName,
745
+ source
746
+ };
747
+ if (!request.install) return {
748
+ ...base,
749
+ install: InstallStatus.Skipped
750
+ };
751
+ const outcome = await this.installer.install(target.directory);
752
+ return {
753
+ ...base,
754
+ install: outcome.status,
755
+ ...outcome.reason === void 0 ? {} : { installError: outcome.reason }
756
+ };
757
+ }
758
+ /** Writes the scope lines, then fails when the registry is not readable. The download has not started. */
759
+ async assertRegistryAccess(target) {
760
+ await NpmrcScopes.apply(target.directory);
761
+ const report = await new RegistryAccess(target.directory, this.runNpm, true).probe();
762
+ if (report.status !== RegistryAccessStatus.Ok) throw new InitError(`${RegistryAccess.fixInstructions(report)}\nNothing was changed.`);
763
+ }
764
+ /** Fills the folder from the local copy or the download, and answers where the files came from. */
765
+ fetchStarter(request, target) {
766
+ if (request.local !== void 0) return this.starters.copyLocal(request.local, request.starter, target.directory);
767
+ const sources = request.source === void 0 ? StarterSource.defaultSources(request.starter, request.version) : [request.source];
768
+ return this.starters.fetch(sources, target.directory, () => target.clearExcept(NpmrcScopes.filename));
769
+ }
770
+ /** Refuses a request that cannot work before any file exists. */
771
+ static assertRequest(request) {
772
+ if (request.local !== void 0 && request.source !== void 0) throw new InitError("Use --local or --source, not both.");
773
+ StarterSource.assertStarterName(request.starter);
774
+ }
775
+ };
776
+ //#endregion
777
+ //#region src/init/init-command.ts
778
+ /** The `init` command. */
779
+ var InitCommand = class InitCommand {
780
+ /** The name it is registered under. */
781
+ static name = "init";
782
+ /** The exit code of a run that made a usable project. */
783
+ static okExitCode = 0;
784
+ /** The exit code of a run that failed, including a failed install. */
785
+ static failureExitCode = 1;
786
+ /** The command definition. */
787
+ static definition() {
788
+ return defineCommand({
789
+ meta: {
790
+ name: InitCommand.name,
791
+ description: "Create a new Inferative project from a starter."
792
+ },
793
+ args: InitCommand.args(),
794
+ run: async (context) => {
795
+ const args = context.args;
796
+ process.exitCode = await InitCommand.execute({
797
+ ...typeof args["directory"] === "string" ? { directory: args["directory"] } : {},
798
+ ...typeof args["starter"] === "string" ? { starter: args["starter"] } : {},
799
+ ...typeof args["source"] === "string" ? { source: args["source"] } : {},
800
+ ...typeof args["local"] === "string" ? { local: args["local"] } : {},
801
+ ...typeof args["cwd"] === "string" ? { cwd: args["cwd"] } : {},
802
+ install: args["install"] !== false
803
+ });
804
+ }
805
+ });
806
+ }
807
+ /** The argument definition, which `create-inferative` shares. */
808
+ static args() {
809
+ return {
810
+ directory: {
811
+ type: "positional",
812
+ description: "The folder to create. The current folder, which must be empty, when absent.",
813
+ required: false
814
+ },
815
+ starter: {
816
+ type: "string",
817
+ description: `The starter to use. ${StarterSource.defaultStarter} by default.`,
818
+ default: StarterSource.defaultStarter,
819
+ valueHint: "name"
820
+ },
821
+ source: {
822
+ type: "string",
823
+ description: "A giget source to download instead of the default starters.",
824
+ valueHint: "giget-source"
825
+ },
826
+ local: {
827
+ type: "string",
828
+ description: "Copy the starter from this starters folder. Nothing is downloaded.",
829
+ valueHint: "path"
830
+ },
831
+ install: {
832
+ type: "boolean",
833
+ description: "Run pnpm install after the files are written. Use --no-install to skip.",
834
+ default: true
835
+ },
836
+ cwd: {
837
+ type: "string",
838
+ description: "The folder that a named directory is created in. The current folder by default.",
839
+ valueHint: "path"
840
+ }
841
+ };
842
+ }
843
+ /**
844
+ * Creates one project and answers the exit code.
845
+ *
846
+ * @param args - What the invocation was asked to do.
847
+ * @param options - The working directory and the replaceable parts. A test supplies them.
848
+ * @returns {@link okExitCode} when the project is usable, otherwise {@link failureExitCode}.
849
+ */
850
+ static async execute(args, options = {}) {
851
+ const print = options.print ?? ((line) => console.log(line));
852
+ const fail = options.fail ?? ((line) => void process.stderr.write(`${line}\n`));
853
+ const currentDirectory = options.cwd ?? process.cwd();
854
+ try {
855
+ const result = await new ProjectInitializer(options.dependencies).initialize({
856
+ ...args.directory === void 0 ? {} : { directory: args.directory },
857
+ workingDirectory: resolve(currentDirectory, args.cwd ?? "."),
858
+ starter: args.starter ?? StarterSource.defaultStarter,
859
+ ...args.source === void 0 ? {} : { source: args.source },
860
+ ...args.local === void 0 ? {} : { local: resolve(currentDirectory, args.local) },
861
+ install: args.install !== false,
862
+ version: PackageVersion.current()
863
+ });
864
+ if (result.install === InstallStatus.PackageManagerMissing) {
865
+ fail(InitMessages.packageManagerMissing(result.projectDirectory));
866
+ return InitCommand.failureExitCode;
867
+ }
868
+ if (result.install === InstallStatus.Failed) {
869
+ fail(InitMessages.installFailed(result.projectDirectory, result.installError));
870
+ return InitCommand.failureExitCode;
871
+ }
872
+ const changeDirectory = relative(currentDirectory, result.projectDirectory);
873
+ print(InitMessages.nextSteps(changeDirectory === "" ? void 0 : changeDirectory, result.install === InstallStatus.Installed));
874
+ return InitCommand.okExitCode;
875
+ } catch (error) {
876
+ fail(error instanceof Error ? error.message : String(error));
877
+ return InitCommand.failureExitCode;
878
+ }
879
+ }
880
+ };
881
+ //#endregion
882
+ //#region src/front-door.ts
883
+ /**
884
+ * The front door of `npm create inferative`.
885
+ *
886
+ * It reuses the arguments and the `run` of {@link InitCommand}, which `inferative init` also runs, so the two names
887
+ * never differ. It sets its own `meta`: `--help` then prints the binary that the user typed, and `--version` has a
888
+ * version to print.
889
+ */
890
+ var CreateFrontDoor = class CreateFrontDoor {
891
+ /** The binary name, as the usage line of `--help` shows it. */
892
+ static binaryName = "create-inferative";
893
+ /** The end-of-options marker that some package managers pass on to the binary. */
894
+ static optionsSeparator = "--";
895
+ /** The command that citty runs. */
896
+ static command() {
897
+ return {
898
+ ...InitCommand.definition(),
899
+ meta: {
900
+ name: CreateFrontDoor.binaryName,
901
+ version: CreateFrontDoor.version(),
902
+ description: "Create a new Inferative project (the `npm create inferative` front door)."
903
+ }
904
+ };
905
+ }
906
+ /**
907
+ * The arguments that citty parses, without the first bare `--`.
908
+ *
909
+ * `npm create` removes the separator itself. `pnpm`, `yarn` and `bun` pass it on, and citty puts every
910
+ * argument after it into the positional list, where no flag reads it.
911
+ *
912
+ * @param argv - The arguments after the executable's name.
913
+ */
914
+ static rawArgs(argv) {
915
+ const separator = argv.indexOf(CreateFrontDoor.optionsSeparator);
916
+ return separator === -1 ? [...argv] : [...argv.slice(0, separator), ...argv.slice(separator + 1)];
917
+ }
918
+ /**
919
+ * Whether the arguments ask for the version and nothing else.
920
+ *
921
+ * @param argv - The arguments after the executable's name.
922
+ */
923
+ static isVersionRequest(argv) {
924
+ return argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v");
925
+ }
926
+ /** The version of this package, which `--version` prints. */
927
+ static version() {
928
+ return PackageVersion.current();
929
+ }
930
+ };
931
+ //#endregion
932
+ export { RegistryAccessStatus as i, InitCommand as n, RegistryAccess as r, CreateFrontDoor as t };
@@ -0,0 +1,200 @@
1
+ import { ArgsDef, CommandDef } from "citty";
2
+ //#region src/registry/registry-access.d.ts
3
+ /** The result of one spawned command, captured rather than inherited. */
4
+ interface ProcessRunResult {
5
+ /** The process exit code; a process that never exited counts as `1`. */
6
+ readonly exitCode: number;
7
+ /** Captured standard output. */
8
+ readonly stdout: string;
9
+ /** Captured standard error. */
10
+ readonly stderr: string;
11
+ /** Whether the program could not be started because it is not installed. */
12
+ readonly commandMissing?: boolean;
13
+ }
14
+ /** Spawns a command and resolves its captured result. A test injects one so no real `npm` runs. */
15
+ type ProcessRunner = (command: string, commandArguments: readonly string[], workingDirectory: string, environment: Readonly<Record<string, string | undefined>>) => Promise<ProcessRunResult>;
16
+ /** The verdict {@link RegistryAccess.probe} reaches. */
17
+ export declare const RegistryAccessStatus: {
18
+ /** Both scopes reach GitHub Packages and the registry answered for each probe package. */
19
+ readonly Ok: "ok";
20
+ /** A scope does not point at GitHub Packages. */
21
+ readonly ScopeNotRouted: "scope-not-routed";
22
+ /** The registry refused the request for lacking, or holding invalid, credentials. */
23
+ readonly Unauthorized: "unauthorized";
24
+ /** npm cannot be started: Node.js is installed without it, or not at all. */
25
+ readonly NpmNotFound: "npm-not-found";
26
+ /** The registry could not be reached (DNS, network, timeout or an unknown failure). */
27
+ readonly Unreachable: "unreachable";
28
+ /** The registry authorized the request, but the probe package is not there. */
29
+ readonly NotFound: "not-found";
30
+ };
31
+ /** One of {@link RegistryAccessStatus}. */
32
+ type RegistryAccessStatusValue = (typeof RegistryAccessStatus)[keyof typeof RegistryAccessStatus];
33
+ /** What {@link RegistryAccess.probe} reports. */
34
+ interface RegistryAccessReport {
35
+ /** The verdict. */
36
+ readonly status: RegistryAccessStatusValue;
37
+ /** The scope the verdict is about: the first one that is not routed or not readable. Absent for `Ok`. */
38
+ readonly scope?: string;
39
+ /** The raw output behind the verdict, for a diagnosis. */
40
+ readonly detail?: string;
41
+ }
42
+ /**
43
+ * Probes whether `@inferative/*` and the `@heximon/*` packages it depends on are readable from GitHub
44
+ * Packages.
45
+ *
46
+ * It asks npm itself, so the verdict follows the same `.npmrc` files an install reads: the project's
47
+ * and the user's.
48
+ */
49
+ export declare class RegistryAccess {
50
+ /** The registry the `@inferative/*` and `@heximon/*` packages publish to. */
51
+ static readonly gitHubPackagesRegistry: string;
52
+ /**
53
+ * The scopes that must resolve to {@link gitHubPackagesRegistry}, each with the package the probe reads. A
54
+ * token can read one organization and not the other, so each scope gets its own read.
55
+ */
56
+ static readonly scopes: readonly {
57
+ readonly scope: string;
58
+ readonly probePackage: string;
59
+ }[];
60
+ /** The scope line a project's `.npmrc` carries for a scope. */
61
+ static scopeLine(scope: string): string;
62
+ private readonly run;
63
+ private readonly workingDirectory;
64
+ private readonly isProjectRoot;
65
+ /**
66
+ * @param workingDirectory - The directory npm reads its project `.npmrc` from.
67
+ * @param run - The process runner. It spawns `npm` by default.
68
+ * @param isProjectRoot - Whether `workingDirectory` is the project root, even when it holds no `package.json`
69
+ * yet. npm then reads its `.npmrc`, and not the one of an ancestor project (`--prefix`). A caller that has
70
+ * just made the folder sets it.
71
+ */
72
+ constructor(workingDirectory?: string, run?: ProcessRunner, isProjectRoot?: boolean);
73
+ /**
74
+ * Checks that every scope points at GitHub Packages, then reads one probe package through each scope.
75
+ *
76
+ * @param environment - The environment npm runs with. An `.npmrc` that reads `${NODE_AUTH_TOKEN}` finds the
77
+ * token here, so a caller passes the environment it loaded from the site's `.env`.
78
+ * @returns The verdict, with the scope it is about and the raw output as `detail`.
79
+ */
80
+ probe(environment?: Readonly<Record<string, string | undefined>>): Promise<RegistryAccessReport>;
81
+ /** The arguments of one npm call, with the prefix that pins the project root when the caller asked for it. */
82
+ private npmArguments;
83
+ /**
84
+ * The lines that tell a user how to fix a verdict.
85
+ *
86
+ * @param report - The report that is not {@link RegistryAccessStatus.Ok}.
87
+ */
88
+ static fixInstructions(report: RegistryAccessReport): string;
89
+ /** The verdict for a failed read of the probe package of `scope`. */
90
+ private static failure;
91
+ /** A registry address without the slash npm may add at its end. */
92
+ private static withoutTrailingSlash;
93
+ }
94
+ //#endregion
95
+ //#region src/init/dependency-installer.d.ts
96
+ /**
97
+ * Installs the dependencies of a project folder, and throws when the install fails. A test injects one so no real
98
+ * package manager runs.
99
+ */
100
+ type InstallRunner = (directory: string) => Promise<void>;
101
+ //#endregion
102
+ //#region src/init/starter-source.d.ts
103
+ /** Downloads one giget source into an empty folder. A test injects one so nothing touches the network. */
104
+ type StarterDownload = (source: string, directory: string) => Promise<void>;
105
+ //#endregion
106
+ //#region src/init/project-initializer.d.ts
107
+ /** The injectable parts of {@link ProjectInitializer}. */
108
+ interface InitDependencies {
109
+ /** Downloads a giget source. Giget by default. */
110
+ readonly download?: StarterDownload;
111
+ /** Runs the `npm` calls of the registry probe. Spawns npm by default. */
112
+ readonly runNpm?: ProcessRunner;
113
+ /** Runs the install. Spawns pnpm by default. */
114
+ readonly runInstall?: InstallRunner;
115
+ }
116
+ //#endregion
117
+ //#region src/init/init-command.d.ts
118
+ /** What one `init` invocation was asked to do. */
119
+ interface InitArguments {
120
+ /** The folder to create. The current folder when absent. */
121
+ readonly directory?: string;
122
+ /** The starter folder name. */
123
+ readonly starter?: string;
124
+ /** One giget source to use alone. */
125
+ readonly source?: string;
126
+ /** A local starters folder to copy from. */
127
+ readonly local?: string;
128
+ /** The folder that a named `directory` is created in. */
129
+ readonly cwd?: string;
130
+ /** Whether to install the dependencies. `false` for `--no-install`. */
131
+ readonly install?: boolean;
132
+ }
133
+ /** The parts of an `init` run that a test replaces. */
134
+ interface InitOptions {
135
+ /** The working directory. The process's own by default. */
136
+ readonly cwd?: string;
137
+ /** The download, the `npm` calls of the registry probe and the install. */
138
+ readonly dependencies?: InitDependencies;
139
+ /** Writes a line to the output. */
140
+ readonly print?: (line: string) => void;
141
+ /** Writes a line to the error output. */
142
+ readonly fail?: (line: string) => void;
143
+ }
144
+ /** The `init` command. */
145
+ export declare class InitCommand {
146
+ /** The name it is registered under. */
147
+ static readonly name: string;
148
+ /** The exit code of a run that made a usable project. */
149
+ static readonly okExitCode: number;
150
+ /** The exit code of a run that failed, including a failed install. */
151
+ static readonly failureExitCode: number;
152
+ /** The command definition. */
153
+ static definition(): CommandDef;
154
+ /** The argument definition, which `create-inferative` shares. */
155
+ static args(): ArgsDef;
156
+ /**
157
+ * Creates one project and answers the exit code.
158
+ *
159
+ * @param args - What the invocation was asked to do.
160
+ * @param options - The working directory and the replaceable parts. A test supplies them.
161
+ * @returns {@link okExitCode} when the project is usable, otherwise {@link failureExitCode}.
162
+ */
163
+ static execute(args: InitArguments, options?: InitOptions): Promise<number>;
164
+ }
165
+ //#endregion
166
+ //#region src/front-door.d.ts
167
+ /**
168
+ * The front door of `npm create inferative`.
169
+ *
170
+ * It reuses the arguments and the `run` of {@link InitCommand}, which `inferative init` also runs, so the two names
171
+ * never differ. It sets its own `meta`: `--help` then prints the binary that the user typed, and `--version` has a
172
+ * version to print.
173
+ */
174
+ export declare class CreateFrontDoor {
175
+ /** The binary name, as the usage line of `--help` shows it. */
176
+ static readonly binaryName: string;
177
+ /** The end-of-options marker that some package managers pass on to the binary. */
178
+ private static readonly optionsSeparator;
179
+ /** The command that citty runs. */
180
+ static command(): CommandDef;
181
+ /**
182
+ * The arguments that citty parses, without the first bare `--`.
183
+ *
184
+ * `npm create` removes the separator itself. `pnpm`, `yarn` and `bun` pass it on, and citty puts every
185
+ * argument after it into the positional list, where no flag reads it.
186
+ *
187
+ * @param argv - The arguments after the executable's name.
188
+ */
189
+ static rawArgs(argv: readonly string[]): readonly string[];
190
+ /**
191
+ * Whether the arguments ask for the version and nothing else.
192
+ *
193
+ * @param argv - The arguments after the executable's name.
194
+ */
195
+ static isVersionRequest(argv: readonly string[]): boolean;
196
+ /** The version of this package, which `--version` prints. */
197
+ static version(): string;
198
+ }
199
+ //#endregion
200
+ export type { InitArguments, InitOptions, RegistryAccessReport, RegistryAccessStatusValue };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { i as RegistryAccessStatus, n as InitCommand, r as RegistryAccess, t as CreateFrontDoor } from "./front-door-CJboxy5n.mjs";
2
+ export { CreateFrontDoor, InitCommand, RegistryAccess, RegistryAccessStatus };
package/package.json CHANGED
@@ -1,11 +1,53 @@
1
1
  {
2
2
  "name": "create-inferative",
3
- "version": "0.0.0",
4
- "description": "Placeholder. The first release publishes the real package.",
5
- "license": "MIT",
3
+ "version": "0.2.0-next.20261013180716",
4
+ "description": "Create a new Inferative project: the `npm create inferative` command, which installs from public npm only.",
5
+ "keywords": [
6
+ "create",
7
+ "inferative",
8
+ "scaffolding",
9
+ "starter"
10
+ ],
6
11
  "repository": {
7
12
  "type": "git",
8
13
  "url": "git+https://github.com/inferative/inferative.git",
9
14
  "directory": "packages/create-inferative"
15
+ },
16
+ "bin": {
17
+ "create-inferative": "./dist/bin.mjs"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.mts",
27
+ "default": "./dist/index.mjs"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "citty": "0.1.6",
36
+ "giget": "^3.3.1",
37
+ "nypm": "^0.6.9",
38
+ "tinyexec": "^1.3.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.13.3",
42
+ "typescript": "^6.0.3",
43
+ "vite-plus": "1.0.0",
44
+ "vitest": "5.0.1"
45
+ },
46
+ "engines": {
47
+ "node": ">=24"
48
+ },
49
+ "scripts": {
50
+ "build": "vp pack",
51
+ "test": "vitest run"
10
52
  }
11
- }
53
+ }