@aventara/cli 1.0.0-rc.11 → 1.0.0-rc.13

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.
@@ -16,6 +16,12 @@ const OPTIONS = [
16
16
  commands: ["new"],
17
17
  summary: "create no git repository (passed to nest new)",
18
18
  },
19
+ {
20
+ flag: "--skip-lint",
21
+ key: "skipLint",
22
+ commands: ["new"],
23
+ summary: "leave out nest new's linter and formatter (oxlint, prettier, their files and scripts)",
24
+ },
19
25
  {
20
26
  flag: "--yes",
21
27
  alias: "-y",
@@ -236,6 +242,7 @@ export function parseCliCommand(argv) {
236
242
  given,
237
243
  skipInstall: options.skipInstall === true,
238
244
  skipGit: options.skipGit === true,
245
+ ...(options.skipLint === true ? { skipLint: true } : {}),
239
246
  yes: options.yes === true,
240
247
  ...(options.verbose === true ? { verbose: true } : {}),
241
248
  ...(host === undefined ? {} : { host }),
@@ -38,9 +38,8 @@ const E2E_SCRIPTS = {
38
38
  };
39
39
  const NEST = hostOf("nest");
40
40
  export function frontendInitCommand(packageManager, cliVersion) {
41
- const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
42
41
  const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
43
- return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
42
+ return `${runner} @aventara/client@${cliVersion} init`;
44
43
  }
45
44
  export function planNestHost(builder, input) {
46
45
  const { inspection, scaffold, packageManager } = input;
@@ -102,6 +101,7 @@ export function planNestHost(builder, input) {
102
101
  `pre${script}`,
103
102
  `${packageManager} run ${PREPARE_SCRIPT}`,
104
103
  ])),
104
+ typecheck: "tsc --noEmit",
105
105
  rewrites: inspection.usesNestConfig
106
106
  ? {}
107
107
  : {
@@ -122,6 +122,7 @@ export function planNextHost(builder, input) {
122
122
  },
123
123
  build: "aventara-build --client",
124
124
  scripts: { prebuild: `${input.packageManager} run ${PREPARE_SCRIPT}` },
125
+ typecheck: "next typegen && tsc --noEmit",
125
126
  rewrites: {},
126
127
  gitignore: [
127
128
  `/${layout.generated}/`,
@@ -1,4 +1,4 @@
1
- import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
1
+ import { allowBuildsEntries, allowBuildsGuidance, appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
2
2
  import { planAgentSkill, SKILL_DIRECTORY, } from "../skill/agent-skill.planner.js";
3
3
  import { PREPARE_SCRIPT, } from "./host-plan.interface.js";
4
4
  import { planNestHost } from "./nest-host.planner.js";
@@ -16,6 +16,25 @@ function writeOf(path, before, after) {
16
16
  },
17
17
  ];
18
18
  }
19
+ export function rootAllowBuilds(before, workspace, builds) {
20
+ if (!workspace.matched || builds.length === 0) {
21
+ return { kind: "none" };
22
+ }
23
+ const merged = mergePnpmWorkspace(before, builds);
24
+ if (merged.unsafe !== undefined) {
25
+ return {
26
+ kind: "print",
27
+ instruction: `The workspace root's ${workspace.file} was left as it is (${merged.unsafe}); add whichever of these are missing under your \`allowBuilds:\` (an explicit \`false\` is yours to keep):\n${allowBuildsEntries(builds)}`,
28
+ };
29
+ }
30
+ return merged.text === before
31
+ ? { kind: "none" }
32
+ : {
33
+ kind: "edit",
34
+ text: merged.text,
35
+ conflict: `the workspace root's ${workspace.file} (its allowBuilds gains ${merged.added.join(", ")}, for their install scripts)`,
36
+ };
37
+ }
19
38
  export function planInit(input) {
20
39
  const { inspection, hosted, packageManager } = input;
21
40
  const { scaffold } = hosted;
@@ -93,10 +112,15 @@ export function planInit(input) {
93
112
  postinstall: `${packageManager} run ${PREPARE_SCRIPT}`,
94
113
  ...host.scripts,
95
114
  },
115
+ defaults: { typecheck: host.typecheck },
96
116
  rewrites: host.rewrites,
97
117
  }, replace));
98
118
  const env = input.read(".env");
99
119
  const { envEntry } = host;
120
+ const writesEnv = scaffoldWritesEnv ||
121
+ (envEntry !== undefined &&
122
+ env !== undefined &&
123
+ appendEnvEntryIfAbsent(env, envEntry.key, envEntry.value) !== env);
100
124
  if (scaffoldWritesEnv || (envEntry !== undefined && env !== undefined)) {
101
125
  builder.both(".env", env, (replace) => {
102
126
  const merged = scaffoldWritesEnv
@@ -114,15 +138,39 @@ export function planInit(input) {
114
138
  builder.both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
115
139
  ...scaffold.gitignore,
116
140
  ...host.gitignore,
117
- ...(scaffoldWritesEnv ? [".env"] : []),
141
+ ...(writesEnv ? [".env"] : []),
118
142
  ]));
119
- if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
120
- const workspace = input.read("pnpm-workspace.yaml");
121
- builder.both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
143
+ const { workspace } = input;
144
+ if (packageManager === "pnpm" && workspace !== undefined) {
145
+ const before = input.read(workspace.file);
146
+ const edit = rootAllowBuilds(before, workspace, scaffold.nativeBuilds);
147
+ if (edit.kind === "edit") {
148
+ builder.both(workspace.file, before, (replace) => ({
149
+ text: replace ? edit.text : (before ?? ""),
150
+ conflicts: [edit.conflict],
151
+ }));
152
+ }
153
+ else if (edit.kind === "print") {
154
+ builder.instruct(edit.instruction);
155
+ }
156
+ if (!workspace.matched) {
157
+ warnings.push(`${workspace.member} is not matched by the packages in ${workspace.file}, so pnpm install at the root does not install it; add it there`);
158
+ }
159
+ }
160
+ else if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
161
+ const workspaceFile = input.read("pnpm-workspace.yaml");
162
+ const merged = mergePnpmWorkspace(workspaceFile, scaffold.nativeBuilds);
163
+ if (merged.unsafe === undefined) {
164
+ builder.both("pnpm-workspace.yaml", workspaceFile, () => merged);
165
+ }
166
+ else {
167
+ builder.instruct(`pnpm-workspace.yaml was left as it is (${merged.unsafe}); ${allowBuildsGuidance(workspaceFile, scaffold.nativeBuilds)}`);
168
+ }
122
169
  }
123
170
  const skill = planAgentSkill({
124
171
  read: input.read,
125
172
  coreVersion: input.aventaraVersion,
173
+ packageManager,
126
174
  force: false,
127
175
  });
128
176
  keeping.push(...skill.writes);
@@ -0,0 +1,113 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const WORKSPACE_FILE = "pnpm-workspace.yaml";
4
+ function packagesOf(text) {
5
+ const lines = text.split("\n");
6
+ const at = lines.findIndex((line) => /^packages:\s*(\[.*\])?\s*$/.test(line));
7
+ if (at === -1) {
8
+ return [];
9
+ }
10
+ const unquote = (value) => value.trim().replace(/^["']|["']$/g, "");
11
+ const flow = /^packages:\s*\[(.*)\]\s*$/.exec(lines[at])?.[1];
12
+ if (flow !== undefined) {
13
+ return flow
14
+ .split(",")
15
+ .map(unquote)
16
+ .filter((value) => value !== "");
17
+ }
18
+ const globs = [];
19
+ for (let line = at + 1; line < lines.length; line += 1) {
20
+ const item = /^\s+-\s+(.+?)\s*(#.*)?$/.exec(lines[line]);
21
+ if (item === null) {
22
+ if (/^\S/.test(lines[line])) {
23
+ break;
24
+ }
25
+ continue;
26
+ }
27
+ globs.push(unquote(item[1]));
28
+ }
29
+ return globs;
30
+ }
31
+ function segmentPattern(part) {
32
+ let pattern = "";
33
+ for (let at = 0; at < part.length; at += 1) {
34
+ const char = part[at];
35
+ if (char === "*") {
36
+ pattern += "[^/]*";
37
+ }
38
+ else if (char === "?") {
39
+ pattern += "[^/]";
40
+ }
41
+ else if (char === "{") {
42
+ const close = part.indexOf("}", at);
43
+ if (close === -1) {
44
+ pattern += "\\{";
45
+ continue;
46
+ }
47
+ pattern += `(?:${part
48
+ .slice(at + 1, close)
49
+ .split(",")
50
+ .map(segmentPattern)
51
+ .join("|")})`;
52
+ at = close;
53
+ }
54
+ else {
55
+ pattern += char.replace(/[.+^$()|[\]\\]/g, "\\$&");
56
+ }
57
+ }
58
+ return pattern;
59
+ }
60
+ function globPattern(glob) {
61
+ const body = glob
62
+ .replace(/^\.\//, "")
63
+ .replace(/\/+$/, "")
64
+ .split("/")
65
+ .map((part) => (part === "**" ? "(?:.*)" : segmentPattern(part)))
66
+ .join("/");
67
+ return new RegExp(`^${body.replaceAll("/(?:.*)", "(?:/.*)?")}$`);
68
+ }
69
+ export function workspaceMatches(globs, member) {
70
+ let matched = false;
71
+ for (const glob of globs) {
72
+ if (glob.startsWith("!")) {
73
+ if (globPattern(glob.slice(1)).test(member)) {
74
+ matched = false;
75
+ }
76
+ }
77
+ else if (globPattern(glob).test(member)) {
78
+ matched = true;
79
+ }
80
+ }
81
+ return matched;
82
+ }
83
+ const LINT_CONFIG = /^(biome\.jsonc?|eslint\.config\.[cm]?[jt]s|\.eslintrc(\..+)?|\.prettierrc(\..+)?|prettier\.config\.[cm]?[jt]s)$/;
84
+ export function enclosingPnpmWorkspace(directory) {
85
+ const project = path.resolve(directory);
86
+ let root = project;
87
+ for (;;) {
88
+ const file = path.join(root, WORKSPACE_FILE);
89
+ if (existsSync(file)) {
90
+ if (root === project) {
91
+ return undefined;
92
+ }
93
+ const member = path.relative(root, project).split(path.sep).join("/");
94
+ let entries = [];
95
+ try {
96
+ entries = readdirSync(root);
97
+ }
98
+ catch {
99
+ }
100
+ return {
101
+ root,
102
+ file: path.relative(project, file).split(path.sep).join("/"),
103
+ member,
104
+ matched: workspaceMatches(packagesOf(readFileSync(file, "utf8")), member),
105
+ lintConfigs: entries.filter((entry) => LINT_CONFIG.test(entry)).sort(),
106
+ };
107
+ }
108
+ if (root === path.dirname(root)) {
109
+ return undefined;
110
+ }
111
+ root = path.dirname(root);
112
+ }
113
+ }
@@ -13,7 +13,12 @@ const LOCKFILES = [
13
13
  ["package-lock.json", "npm"],
14
14
  ["pnpm-lock.yaml", "pnpm"],
15
15
  ["yarn.lock", "yarn"],
16
+ ["bun.lock", "bun"],
17
+ ["bun.lockb", "bun"],
16
18
  ];
19
+ export function lockfilesIn(directory) {
20
+ return LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
21
+ }
17
22
  function dependencyRange(manifest, name) {
18
23
  return manifest.dependencies?.[name] ?? manifest.devDependencies?.[name];
19
24
  }
@@ -82,8 +87,8 @@ export function inspectProject(directory, catalog) {
82
87
  typescriptMajor >= FIRST_UNSUPPORTED_TYPESCRIPT) {
83
88
  throw new AvProjectRefusedError(`this project declares typescript ${typescript}, and TypeScript ${FIRST_UNSUPPORTED_TYPESCRIPT} has no classic compiler API for Aventara's generate steps to read types with; use TypeScript 5.7 to 6`);
84
89
  }
85
- const lockfiles = LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
86
- const known = lockfiles.filter((manager) => manager !== "yarn");
90
+ const lockfiles = lockfilesIn(directory);
91
+ const known = lockfiles.filter((manager) => manager === "npm" || manager === "pnpm");
87
92
  if (lockfiles.includes("yarn") && known.length === 0) {
88
93
  throw new AvProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
89
94
  }
@@ -0,0 +1,63 @@
1
+ export function channelOf(version) {
2
+ return /-rc\.\d+$/.test(version) ? "rc" : "latest";
3
+ }
4
+ const PACKAGE = "@aventara/cli";
5
+ const TIMEOUT_MS = 1500;
6
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
7
+ export function registryNewestRelease(registry) {
8
+ const base = registry === undefined || registry === "" ? DEFAULT_REGISTRY : registry;
9
+ return async (channel) => {
10
+ try {
11
+ const response = await fetch(new URL(`-/package/${PACKAGE.replace("/", "%2f")}/dist-tags`, base.endsWith("/") ? base : `${base}/`), { signal: AbortSignal.timeout(TIMEOUT_MS) });
12
+ if (!response.ok) {
13
+ return undefined;
14
+ }
15
+ const tags = (await response.json());
16
+ const named = tags[channel];
17
+ return typeof named === "string" ? named : undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ };
23
+ }
24
+ function parse(version) {
25
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(version);
26
+ if (match === null) {
27
+ return undefined;
28
+ }
29
+ const [, major, minor, patch, rc] = match;
30
+ return {
31
+ core: [Number(major), Number(minor), Number(patch)],
32
+ ...(rc === undefined ? {} : { rc: Number(rc) }),
33
+ };
34
+ }
35
+ export function isNewer(candidate, pinned) {
36
+ const a = parse(candidate);
37
+ const b = parse(pinned);
38
+ if (a === undefined || b === undefined) {
39
+ return false;
40
+ }
41
+ for (let at = 0; at < 3; at += 1) {
42
+ const difference = a.core[at] - b.core[at];
43
+ if (difference !== 0) {
44
+ return difference > 0;
45
+ }
46
+ }
47
+ if (a.rc === undefined || b.rc === undefined) {
48
+ return a.rc === undefined && b.rc !== undefined;
49
+ }
50
+ return a.rc > b.rc;
51
+ }
52
+ export async function releaseLines(input) {
53
+ const newest = input.read === undefined
54
+ ? undefined
55
+ : await input.read(channelOf(input.pinned));
56
+ if (newest === undefined || !isNewer(newest, input.pinned)) {
57
+ return `Aventara ${input.pinned}\n`;
58
+ }
59
+ const take = input.packageManager === "pnpm"
60
+ ? `pnpm --config.minimum-release-age=0 dlx ${PACKAGE}@${newest} ${input.command} …`
61
+ : `npx ${PACKAGE}@${newest} ${input.command} …, with @aventara/* excluded from npm's min-release-age`;
62
+ return `Aventara ${input.pinned} (newest: ${newest}, held back by a release-age delay or not yet in your registry; to take it: ${take})\n`;
63
+ }
@@ -0,0 +1,120 @@
1
+ import { closeSync, constants, existsSync, fstatSync, openSync, readSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ const SCOPE = "@aventara";
5
+ export function npmRegistryOf(input) {
6
+ const scopeKey = `${SCOPE}:registry`;
7
+ const layers = [
8
+ environmentLayer(input.env),
9
+ fileLayer(path.join(localPrefixOf(input.cwd), ".npmrc"), undefined),
10
+ fileLayer(nonEmpty(input.env.npm_config_userconfig) ??
11
+ nonEmpty(input.env.NPM_CONFIG_USERCONFIG) ??
12
+ path.join(input.home ?? homedir(), ".npmrc"), input.env),
13
+ ];
14
+ for (const key of [scopeKey, "registry"]) {
15
+ for (const layer of layers) {
16
+ const value = layer.get(key);
17
+ if (value !== undefined) {
18
+ return value;
19
+ }
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ const KEYS = new Set([`${SCOPE}:registry`, "registry"]);
25
+ const MAX_NPMRC_BYTES = 64 * 1024;
26
+ function environmentLayer(env) {
27
+ const layer = new Map();
28
+ for (const key of KEYS) {
29
+ const value = nonEmpty(env[`npm_config_${key}`]) ??
30
+ nonEmpty(env[`NPM_CONFIG_${key.toUpperCase()}`]);
31
+ if (value === undefined) {
32
+ layer.delete(key);
33
+ }
34
+ else {
35
+ layer.set(key, value);
36
+ }
37
+ }
38
+ return layer;
39
+ }
40
+ function fileLayer(file, env) {
41
+ const layer = new Map();
42
+ const text = regularFileText(file);
43
+ if (text === undefined) {
44
+ return layer;
45
+ }
46
+ for (const raw of text.split(/\r?\n/)) {
47
+ const line = raw.trim();
48
+ const equals = line.indexOf("=");
49
+ if (equals <= 0 || line.startsWith(";") || line.startsWith("#")) {
50
+ continue;
51
+ }
52
+ const key = line.slice(0, equals).trim();
53
+ if (!KEYS.has(key)) {
54
+ continue;
55
+ }
56
+ const written = unquote(line.slice(equals + 1).trim());
57
+ const value = nonEmpty(env === undefined
58
+ ? written.includes("${")
59
+ ? undefined
60
+ : written
61
+ : expand(written, env));
62
+ if (value === undefined) {
63
+ layer.delete(key);
64
+ }
65
+ else {
66
+ layer.set(key, value);
67
+ }
68
+ }
69
+ return layer;
70
+ }
71
+ function regularFileText(file) {
72
+ let descriptor;
73
+ try {
74
+ descriptor = openSync(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ try {
80
+ const stats = fstatSync(descriptor);
81
+ if (!stats.isFile() || stats.size > MAX_NPMRC_BYTES) {
82
+ return undefined;
83
+ }
84
+ const buffer = Buffer.alloc(MAX_NPMRC_BYTES + 1);
85
+ const read = readSync(descriptor, buffer, 0, buffer.length, 0);
86
+ return read > MAX_NPMRC_BYTES
87
+ ? undefined
88
+ : buffer.toString("utf8", 0, read);
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ finally {
94
+ closeSync(descriptor);
95
+ }
96
+ }
97
+ function localPrefixOf(cwd) {
98
+ for (let at = path.resolve(cwd);; at = path.dirname(at)) {
99
+ if (existsSync(path.join(at, "package.json")) ||
100
+ existsSync(path.join(at, "node_modules"))) {
101
+ return at;
102
+ }
103
+ if (path.dirname(at) === at) {
104
+ return path.resolve(cwd);
105
+ }
106
+ }
107
+ }
108
+ function unquote(value) {
109
+ return value.length >= 2 &&
110
+ (value.startsWith('"') || value.startsWith("'")) &&
111
+ value.endsWith(value[0])
112
+ ? value.slice(1, -1)
113
+ : value;
114
+ }
115
+ function expand(value, env) {
116
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? "");
117
+ }
118
+ function nonEmpty(value) {
119
+ return value === undefined || value === "" ? undefined : value;
120
+ }
@@ -4,6 +4,7 @@ import { confirmConflicts } from "../apply/conflict.confirmer.js";
4
4
  import { matchInstalledOrm, matchOrmSetup, } from "../catalog/catalog.matcher.js";
5
5
  import { planInit } from "../plan/project.planner.js";
6
6
  import { detectPackageManager } from "../project/package-manager.detector.js";
7
+ import { enclosingPnpmWorkspace } from "../project/pnpm-workspace.locator.js";
7
8
  import { AvProjectRefusedError, inspectProject, } from "../project/project.inspector.js";
8
9
  import { scanServices } from "../project/service.detector.js";
9
10
  import { templatesFor } from "../templates/template.registry.js";
@@ -40,6 +41,9 @@ export async function initProject(command, io) {
40
41
  ? await prepareReuse(command, io, inspection, detectedPackageManager)
41
42
  : await prepareFresh(command, io, inspection, detectedPackageManager);
42
43
  const { entry, provider, hosted, packageManager } = prepared;
44
+ const workspace = packageManager === "pnpm"
45
+ ? enclosingPnpmWorkspace(io.directory)
46
+ : undefined;
43
47
  const plan = planInit({
44
48
  inspection,
45
49
  entry,
@@ -49,12 +53,17 @@ export async function initProject(command, io) {
49
53
  skipInstall: command.skipInstall,
50
54
  read,
51
55
  warnings: prepared.warnings,
56
+ ...(workspace === undefined ? {} : { workspace }),
52
57
  });
53
58
  await confirmConflicts(plan.conflicts, {
54
59
  yes: command.yes,
55
60
  prompter: io.prompter,
61
+ command: "init",
56
62
  });
57
63
  const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
64
+ if (io.release !== undefined) {
65
+ io.stdout(await io.release());
66
+ }
58
67
  const progress = io.progress ??
59
68
  createStepReporter({
60
69
  write: io.stderr,
@@ -81,7 +90,7 @@ export async function initProject(command, io) {
81
90
  if (!command.skipInstall) {
82
91
  await progress.step(`Installing dependencies (${packageManager} install)`, async () => {
83
92
  const installed = await io.runner(packageManager, ["install"], {
84
- cwd: io.directory,
93
+ cwd: workspace?.root ?? io.directory,
85
94
  onOutput: progress.output,
86
95
  });
87
96
  if (installed.code !== 0) {
@@ -1,10 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { confirmConflicts } from "../apply/conflict.confirmer.js";
5
+ import { stripLintTooling } from "../apply/lint-tooling.stripper.js";
4
6
  import { DEFAULT_HOST, hostOf, } from "../catalog/host-entry.interface.js";
5
7
  import { AvCliCommandError, } from "../command/command.parser.js";
8
+ import { rootAllowBuilds } from "../plan/project.planner.js";
6
9
  import { detectPackageManager } from "../project/package-manager.detector.js";
10
+ import { enclosingPnpmWorkspace } from "../project/pnpm-workspace.locator.js";
7
11
  import { AvProjectRefusedError } from "../project/project.inspector.js";
12
+ import { templatesFor, } from "../templates/template.registry.js";
8
13
  import { resolveAnswers } from "../wizard/answer.resolver.js";
9
14
  import { NEW_QUESTIONS, } from "../wizard/wizard.questions.js";
10
15
  import { AvInstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
@@ -14,6 +19,7 @@ const NEST_CLI = "@nestjs/cli@12.0.8";
14
19
  const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
15
20
  const NEXT_VERSION_VARIABLE = "AVENTARA_NEXT_VERSION";
16
21
  const NEXT_GENERATOR = "create-next-app";
22
+ const NEXT_MAJOR = "16";
17
23
  const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]*$/;
18
24
  const NEXT_LAYOUT_FLAGS = [
19
25
  "--ts",
@@ -58,14 +64,17 @@ const GENERATIONS = {
58
64
  };
59
65
  },
60
66
  next: ({ command, io, name, packageManager }) => {
67
+ if (command.skipLint === true) {
68
+ throw new AvCliCommandError("--skip-lint takes out nest new's linter and formatter; create-next-app asks about its own linter, so answer it there or pass its flag after --");
69
+ }
61
70
  const pinned = io.env[NEXT_VERSION_VARIABLE];
62
71
  if (pinned !== undefined && pinned !== "" && !VERSION.test(pinned)) {
63
- throw new AvCliCommandError(`${NEXT_VERSION_VARIABLE} is ${JSON.stringify(pinned)}, which is not a version of ${NEXT_GENERATOR}; set it to one such as 16.4.0, or unset it for the latest`);
72
+ throw new AvCliCommandError(`${NEXT_VERSION_VARIABLE} is ${JSON.stringify(pinned)}, which is not a version of ${NEXT_GENERATOR}; set it to one such as 16.4.0, or unset it for the newest ${NEXT_MAJOR}.x`);
64
73
  }
65
74
  if (!command.yes && !io.prompter.interactive) {
66
75
  throw new AvCliCommandError(`${NEXT_GENERATOR} asks its own questions, and nobody can answer them here (stdin is not a terminal); pass --yes to take its defaults, and its own flags after --`);
67
76
  }
68
- const generator = `${NEXT_GENERATOR}@${pinned === undefined || pinned === "" ? "latest" : pinned}`;
77
+ const generator = `${NEXT_GENERATOR}@${pinned === undefined || pinned === "" ? NEXT_MAJOR : pinned}`;
69
78
  const [runner, prefix] = oneOff(packageManager);
70
79
  return {
71
80
  title: `Creating the Next.js project (${runner} ${[...prefix, generator, name].join(" ")})`,
@@ -86,6 +95,9 @@ const GENERATIONS = {
86
95
  };
87
96
  },
88
97
  };
98
+ function withoutRelease({ release: _, ...io }) {
99
+ return io;
100
+ }
89
101
  export async function runNew(command, io) {
90
102
  const host = hostOf((command.host ?? DEFAULT_HOST));
91
103
  const answers = await resolveAnswers(NEW_QUESTIONS, {
@@ -106,8 +118,13 @@ export async function runNew(command, io) {
106
118
  packageManager === undefined) {
107
119
  throw new Error("the new questions settled without every answer");
108
120
  }
121
+ GENERATIONS[host.alias]({ command, io, name, packageManager });
122
+ const surroundings = command.skipGit
123
+ ? undefined
124
+ : await gitSurroundings(io.runner, io.cwd);
125
+ const skipGit = command.skipGit || surroundings === "inside-work-tree";
109
126
  const generation = GENERATIONS[host.alias]({
110
- command,
127
+ command: { ...command, skipGit },
111
128
  io,
112
129
  name,
113
130
  packageManager,
@@ -116,15 +133,34 @@ export async function runNew(command, io) {
116
133
  if (existsSync(directory) && readdirSync(directory).length > 0) {
117
134
  throw new AvProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
118
135
  }
119
- const surroundings = command.skipGit
120
- ? undefined
121
- : await gitSurroundings(io.runner, io.cwd);
136
+ const workspace = packageManager === "pnpm" ? enclosingPnpmWorkspace(directory) : undefined;
137
+ if (workspace !== undefined) {
138
+ const edit = rootAllowBuilds(readFileSync(path.join(workspace.root, "pnpm-workspace.yaml"), "utf8"), workspace, templatesFor(orm).providers[db]?.nativeBuilds ?? []);
139
+ if (edit.kind === "edit") {
140
+ await confirmConflicts([edit.conflict], {
141
+ yes: command.yes,
142
+ prompter: io.prompter,
143
+ command: "new",
144
+ });
145
+ }
146
+ }
147
+ if (io.release !== undefined) {
148
+ io.stdout(await io.release());
149
+ }
150
+ if (host.alias === "nest" &&
151
+ command.skipLint !== true &&
152
+ workspace !== undefined &&
153
+ workspace.lintConfigs.length > 0) {
154
+ io.stderr(`aventara: the workspace root has its own ${workspace.lintConfigs.join(", ")}; pass --skip-lint to leave out nest new's oxlint and prettier.\n`);
155
+ }
122
156
  const progress = createStepReporter({
123
157
  write: io.stderr,
124
158
  mode: io.progressMode,
125
159
  total: (generation.interactive ? 1 : 2) +
126
160
  (command.skipInstall ? 0 : 1) +
127
- (command.skipGit ? 0 : 1),
161
+ (surroundings === undefined || surroundings === "inside-work-tree"
162
+ ? 0
163
+ : 1),
128
164
  verbose: command.verbose === true,
129
165
  });
130
166
  const generate = async () => {
@@ -148,6 +184,9 @@ export async function runNew(command, io) {
148
184
  await progress.step(generation.title, generate);
149
185
  }
150
186
  generation.check(directory);
187
+ if (command.skipLint === true) {
188
+ stripLintTooling(directory);
189
+ }
151
190
  const { nextSteps } = await initProject({
152
191
  command: "init",
153
192
  given: {
@@ -159,11 +198,14 @@ export async function runNew(command, io) {
159
198
  skipGit: false,
160
199
  yes: true,
161
200
  }, {
162
- ...io,
201
+ ...withoutRelease(io),
163
202
  directory,
164
203
  progress,
165
204
  });
166
- if (surroundings !== undefined) {
205
+ if (surroundings === "inside-work-tree") {
206
+ io.stdout(`aventara: ${await commitScaffold(io.runner, directory, surroundings, io.cwd)}.\n`);
207
+ }
208
+ else if (surroundings !== undefined) {
167
209
  const committed = await progress.step("Committing the scaffold", () => commitScaffold(io.runner, directory, surroundings, io.cwd));
168
210
  io.stdout(`aventara: ${committed}.\n`);
169
211
  }
@@ -1,9 +1,22 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { detectPackageManager } from "../project/package-manager.detector.js";
4
+ import { lockfilesIn } from "../project/project.inspector.js";
3
5
  import { planAgentSkill } from "../skill/agent-skill.planner.js";
4
6
  export class AvSkillRefusedError extends Error {
5
7
  name = "AvSkillRefusedError";
6
8
  }
9
+ function lockfileManager(directory) {
10
+ for (let at = directory;; at = path.dirname(at)) {
11
+ const found = lockfilesIn(at);
12
+ if (found.length > 0) {
13
+ return found.find((manager) => manager === "npm" || manager === "pnpm");
14
+ }
15
+ if (path.dirname(at) === at || existsSync(path.join(at, ".git"))) {
16
+ return undefined;
17
+ }
18
+ }
19
+ }
7
20
  function readText(file) {
8
21
  try {
9
22
  return readFileSync(file, "utf8");
@@ -34,6 +47,10 @@ export function runSkill(command, io) {
34
47
  const plan = planAgentSkill({
35
48
  read: (file) => readText(path.join(io.directory, file)),
36
49
  coreVersion,
50
+ packageManager: detectPackageManager({
51
+ lockfile: lockfileManager(io.directory),
52
+ userAgent: io.env?.npm_config_user_agent,
53
+ }),
37
54
  ...(errors === undefined ? {} : { errors }),
38
55
  force: command.force,
39
56
  });