@lenne.tech/cli 1.32.1 → 1.34.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/build/commands/codex/codex.js +23 -0
- package/build/commands/codex/plugins.js +103 -0
- package/build/commands/codex/shortcuts.js +110 -0
- package/build/commands/dev/test.js +10 -4
- package/build/commands/dev/up.js +9 -3
- package/build/commands/frontend/nuxt.js +4 -7
- package/build/commands/fullstack/init.js +26 -11
- package/build/commands/fullstack/update.js +13 -0
- package/build/commands/ticket/start.js +6 -4
- package/build/extensions/frontend-helper.js +49 -59
- package/build/extensions/server.js +220 -32
- package/build/lib/check-freshness-hooks.js +53 -0
- package/build/lib/codex-cli.js +56 -0
- package/build/lib/codex-plugin-utils.js +102 -0
- package/build/lib/dev-package-manager.js +89 -0
- package/build/lib/dev-patches.js +4 -1
- package/build/lib/dev-test-session.js +30 -16
- package/build/lib/dev-ticket.js +12 -6
- package/build/lib/heal-check-wrapper.js +59 -0
- package/build/lib/hoist-workspace-pnpm-config.js +105 -92
- package/build/lib/vendor-claude-md.js +7 -2
- package/build/templates/check/check.mjs +512 -0
- package/package.json +1 -1
|
@@ -15,40 +15,41 @@ const js_yaml_1 = require("js-yaml");
|
|
|
15
15
|
* overrides defined only in projects/api/package.json never reach the
|
|
16
16
|
* install resolver. Hoisting them to the root fixes both the warning
|
|
17
17
|
* and the actual dependency-resolution behavior.
|
|
18
|
+
*
|
|
19
|
+
* Object-valued fields (`overrides`, `allowBuilds`) merge key-by-key;
|
|
20
|
+
* array-valued fields union + dedupe + sort. `minimumReleaseAgeExclude`
|
|
21
|
+
* is hoisted too so a sub-project's first-party exemption (e.g.
|
|
22
|
+
* `@lenne.tech/*`) keeps working in the monorepo — otherwise the
|
|
23
|
+
* minimum-release-age gate would block freshly published own packages.
|
|
18
24
|
*/
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* usually carries BOTH for cross-version compatibility, but we must not
|
|
24
|
-
* rely on the array twin always being present: we normalise `allowBuilds`
|
|
25
|
-
* back into `onlyBuiltDependencies` before hoisting (see
|
|
26
|
-
* `normalizeAllowBuilds`) so the build-allowlist survives into the pnpm-10
|
|
27
|
-
* monorepo root even when the file only carries the pnpm-11 object form.
|
|
28
|
-
*/
|
|
29
|
-
const PNPM11_BUILD_KEY = 'allowBuilds';
|
|
25
|
+
const OBJECT_FIELDS = ['overrides', 'allowBuilds'];
|
|
26
|
+
const ARRAY_FIELDS = ['onlyBuiltDependencies', 'ignoredOptionalDependencies', 'minimumReleaseAgeExclude'];
|
|
27
|
+
const WORKSPACE_SCOPED_PNPM_FIELDS = [...OBJECT_FIELDS, ...ARRAY_FIELDS];
|
|
28
|
+
const isArrayField = (field) => ARRAY_FIELDS.includes(field);
|
|
30
29
|
/**
|
|
31
|
-
* Hoist workspace-scoped pnpm config from sub-projects into the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* Hoist workspace-scoped pnpm config from sub-projects into the monorepo
|
|
31
|
+
* root `pnpm-workspace.yaml`. After this runs, sub-project pnpm config
|
|
32
|
+
* (package.json#pnpm or a settings-only pnpm-workspace.yaml) is gone, and
|
|
33
|
+
* the root pnpm-workspace.yaml carries the merged union next to `packages:`.
|
|
34
|
+
*
|
|
35
|
+
* Why pnpm-workspace.yaml and not package.json#pnpm: the monorepo root
|
|
36
|
+
* pins pnpm 11 (via `packageManager`), and pnpm 11 SILENTLY IGNORES the
|
|
37
|
+
* `pnpm` block in package.json — overrides/build-allowlists/etc. declared
|
|
38
|
+
* there never take effect, regressing `pnpm audit` and the minimum-release
|
|
39
|
+
* -age exemptions. pnpm-workspace.yaml is the pnpm-recommended home and is
|
|
40
|
+
* read by both pnpm 10 and 11.
|
|
36
41
|
*
|
|
37
42
|
* Two sources are read per sub-project, because the two starters store
|
|
38
43
|
* their pnpm config differently:
|
|
39
44
|
*
|
|
40
|
-
* 1. `<sub>/package.json` `pnpm` block — nest-server-starter, and
|
|
41
|
-
*
|
|
42
|
-
* 2. `<sub>/pnpm-workspace.yaml` — nuxt-base-template
|
|
43
|
-
*
|
|
44
|
-
* package.json,
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* the
|
|
48
|
-
* conflicts with the monorepo's own pnpm-workspace.yaml. We hoist
|
|
49
|
-
* its fields into the root package.json (the lt-monorepo root pins
|
|
50
|
-
* pnpm@10 via `packageManager`, where package.json#pnpm IS honored)
|
|
51
|
-
* and remove the now-redundant nested file.
|
|
45
|
+
* 1. `<sub>/package.json` `pnpm` block — nest-server-starter, and any
|
|
46
|
+
* template that has not migrated to the pnpm-11 layout yet.
|
|
47
|
+
* 2. `<sub>/pnpm-workspace.yaml` — nuxt-base-template (pnpm-11 layout).
|
|
48
|
+
* Inside a monorepo that nested file would (a) not be hoisted if we
|
|
49
|
+
* only read package.json, regressing the CVE overrides, and (b)
|
|
50
|
+
* declare a nested workspace root that conflicts with the monorepo's
|
|
51
|
+
* own pnpm-workspace.yaml. We hoist its fields into the root and
|
|
52
|
+
* remove the now-redundant nested file.
|
|
52
53
|
*
|
|
53
54
|
* Symlinked sub-projects are skipped entirely: in `--frontend-link` /
|
|
54
55
|
* `--api-link` mode `projects/app` (or `projects/api`) points at the
|
|
@@ -64,14 +65,11 @@ const PNPM11_BUILD_KEY = 'allowBuilds';
|
|
|
64
65
|
function hoistWorkspacePnpmConfig(options) {
|
|
65
66
|
var _a;
|
|
66
67
|
const { filesystem, projectDir, subProjects } = options;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return;
|
|
73
|
-
(_a = rootPkg.pnpm) !== null && _a !== void 0 ? _a : (rootPkg.pnpm = {});
|
|
74
|
-
const rootPnpm = rootPkg.pnpm;
|
|
68
|
+
const rootWsPath = `${projectDir}/pnpm-workspace.yaml`;
|
|
69
|
+
// The root pnpm-workspace.yaml is the destination. It normally exists (the
|
|
70
|
+
// lt-monorepo clone ships one declaring `packages:`); start from it so
|
|
71
|
+
// `packages:` and any root-owned settings are preserved.
|
|
72
|
+
const rootWs = (_a = readYaml(filesystem, rootWsPath)) !== null && _a !== void 0 ? _a : {};
|
|
75
73
|
let rootChanged = false;
|
|
76
74
|
for (const subDir of subProjects) {
|
|
77
75
|
const subPath = `${projectDir}/${subDir}`;
|
|
@@ -81,27 +79,30 @@ function hoistWorkspacePnpmConfig(options) {
|
|
|
81
79
|
// checkout in link mode.
|
|
82
80
|
if (isSymlink(subPath))
|
|
83
81
|
continue;
|
|
84
|
-
if (hoistFromSubPackageJson({ filesystem,
|
|
82
|
+
if (hoistFromSubPackageJson({ filesystem, rootWs, subPath })) {
|
|
85
83
|
rootChanged = true;
|
|
86
84
|
}
|
|
87
|
-
if (hoistFromSubWorkspaceYaml({ filesystem,
|
|
85
|
+
if (hoistFromSubWorkspaceYaml({ filesystem, rootWs, subPath })) {
|
|
88
86
|
rootChanged = true;
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
89
|
if (rootChanged) {
|
|
92
|
-
|
|
90
|
+
// Keep allowBuilds (pnpm 11) and onlyBuiltDependencies (pnpm 10) in sync so
|
|
91
|
+
// the build-script allowlist survives regardless of which key pnpm reads.
|
|
92
|
+
syncBuildAllowlists(rootWs);
|
|
93
|
+
filesystem.write(rootWsPath, (0, js_yaml_1.dump)(rootWs, { lineWidth: -1, sortKeys: false }));
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
/**
|
|
96
|
-
* Move the workspace-scoped pnpm fields from `source` into `
|
|
97
|
+
* Move the workspace-scoped pnpm fields from `source` into `rootWs`,
|
|
97
98
|
* deleting each moved field from `source`. Returns true if anything moved.
|
|
98
99
|
*/
|
|
99
|
-
function hoistFields(
|
|
100
|
+
function hoistFields(rootWs, source) {
|
|
100
101
|
let changed = false;
|
|
101
102
|
for (const field of WORKSPACE_SCOPED_PNPM_FIELDS) {
|
|
102
103
|
if (source[field] === undefined)
|
|
103
104
|
continue;
|
|
104
|
-
|
|
105
|
+
rootWs[field] = mergePnpmFieldValue(field, rootWs[field], source[field]);
|
|
105
106
|
delete source[field];
|
|
106
107
|
changed = true;
|
|
107
108
|
}
|
|
@@ -109,14 +110,14 @@ function hoistFields(rootPnpm, source) {
|
|
|
109
110
|
}
|
|
110
111
|
/** Source 1: the sub-project's package.json `pnpm` block. */
|
|
111
112
|
function hoistFromSubPackageJson(options) {
|
|
112
|
-
const { filesystem,
|
|
113
|
+
const { filesystem, rootWs, subPath } = options;
|
|
113
114
|
const subPkgPath = `${subPath}/package.json`;
|
|
114
115
|
if (!filesystem.exists(subPkgPath))
|
|
115
116
|
return false;
|
|
116
117
|
const subPkg = filesystem.read(subPkgPath, 'json');
|
|
117
118
|
if (!(subPkg === null || subPkg === void 0 ? void 0 : subPkg.pnpm))
|
|
118
119
|
return false;
|
|
119
|
-
if (!hoistFields(
|
|
120
|
+
if (!hoistFields(rootWs, subPkg.pnpm))
|
|
120
121
|
return false;
|
|
121
122
|
// If the sub-project's pnpm section is now empty, drop it entirely.
|
|
122
123
|
if (Object.keys(subPkg.pnpm).length === 0) {
|
|
@@ -125,37 +126,23 @@ function hoistFromSubPackageJson(options) {
|
|
|
125
126
|
filesystem.write(subPkgPath, `${JSON.stringify(subPkg, null, 2)}\n`);
|
|
126
127
|
return true;
|
|
127
128
|
}
|
|
128
|
-
/** Source 2: the sub-project's pnpm-workspace.yaml
|
|
129
|
+
/** Source 2: the sub-project's pnpm-workspace.yaml. */
|
|
129
130
|
function hoistFromSubWorkspaceYaml(options) {
|
|
130
|
-
const { filesystem,
|
|
131
|
+
const { filesystem, rootWs, subPath } = options;
|
|
131
132
|
const subWsPath = `${subPath}/pnpm-workspace.yaml`;
|
|
132
133
|
if (!filesystem.exists(subWsPath))
|
|
133
134
|
return false;
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
135
|
+
const ws = readYaml(filesystem, subWsPath);
|
|
136
|
+
if (!ws)
|
|
136
137
|
return false;
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
parsed = (0, js_yaml_1.load)(raw);
|
|
140
|
-
}
|
|
141
|
-
catch (_a) {
|
|
142
|
-
// Malformed YAML — leave it untouched rather than risk data loss.
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
146
|
-
return false;
|
|
147
|
-
const ws = parsed;
|
|
148
|
-
// Fold the pnpm-11 `allowBuilds` map into `onlyBuiltDependencies` so it is
|
|
149
|
-
// hoisted rather than discarded — even when the array twin is absent.
|
|
150
|
-
normalizeAllowBuilds(ws);
|
|
151
|
-
if (!hoistFields(rootPnpm, ws))
|
|
138
|
+
if (!hoistFields(rootWs, ws))
|
|
152
139
|
return false;
|
|
153
140
|
// A settings-only file (no `packages:`) exists solely to carry these
|
|
154
141
|
// hoisted keys — once emptied it would only declare a nested workspace
|
|
155
142
|
// root, so remove it. A file that declares `packages:` is a real (rare)
|
|
156
143
|
// nested workspace; keep it minus the hoisted keys.
|
|
157
144
|
if (Array.isArray(ws.packages) && ws.packages.length > 0) {
|
|
158
|
-
filesystem.write(subWsPath, (0, js_yaml_1.dump)(ws));
|
|
145
|
+
filesystem.write(subWsPath, (0, js_yaml_1.dump)(ws, { lineWidth: -1, sortKeys: false }));
|
|
159
146
|
}
|
|
160
147
|
else {
|
|
161
148
|
filesystem.remove(subWsPath);
|
|
@@ -174,19 +161,16 @@ function isSymlink(path) {
|
|
|
174
161
|
/**
|
|
175
162
|
* Merge two values for a pnpm workspace-scoped field.
|
|
176
163
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* - `onlyBuiltDependencies` → array of strings
|
|
180
|
-
* - `ignoredOptionalDependencies` → array of strings
|
|
164
|
+
* Arrays (`onlyBuiltDependencies`, `ignoredOptionalDependencies`,
|
|
165
|
+
* `minimumReleaseAgeExclude`): deduplicated, alphabetically sorted union.
|
|
181
166
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* handlebars/minimatch patches).
|
|
167
|
+
* Objects (`overrides`, `allowBuilds`): key-by-key merge where sub-project
|
|
168
|
+
* values take precedence over root (sub-projects like nest-server-starter
|
|
169
|
+
* own the authoritative CVE override list for their transitive deps; the
|
|
170
|
+
* root usually only seeds cross-cutting patches).
|
|
187
171
|
*/
|
|
188
172
|
function mergePnpmFieldValue(field, rootValue, subValue) {
|
|
189
|
-
if (field
|
|
173
|
+
if (isArrayField(field)) {
|
|
190
174
|
const rootArr = Array.isArray(rootValue) ? rootValue : [];
|
|
191
175
|
const subArr = Array.isArray(subValue) ? subValue : [];
|
|
192
176
|
return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
|
|
@@ -198,26 +182,55 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
|
|
|
198
182
|
const merged = Object.assign(Object.assign({}, rootObj), subObj);
|
|
199
183
|
return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
200
184
|
}
|
|
185
|
+
/** Parse a YAML file into a plain object, or null on missing/malformed/non-object. */
|
|
186
|
+
function readYaml(filesystem, path) {
|
|
187
|
+
if (!filesystem.exists(path))
|
|
188
|
+
return null;
|
|
189
|
+
const raw = filesystem.read(path);
|
|
190
|
+
if (!raw)
|
|
191
|
+
return null;
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = (0, js_yaml_1.load)(raw);
|
|
195
|
+
}
|
|
196
|
+
catch (_a) {
|
|
197
|
+
// Malformed YAML — leave it untouched rather than risk data loss.
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
201
|
+
return null;
|
|
202
|
+
return parsed;
|
|
203
|
+
}
|
|
201
204
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* redundant object form does not linger. Mutates `ws` in place.
|
|
205
|
+
* Keep the two build-allowlist forms consistent and complete after hoisting:
|
|
206
|
+
* - `allowBuilds` (pnpm 11) is a `{ pkg: boolean }` map.
|
|
207
|
+
* - `onlyBuiltDependencies` (pnpm 10) is a `string[]` of allowed packages.
|
|
206
208
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
+
* Sub-templates carry one or both (nest-server-starter only `allowBuilds`,
|
|
210
|
+
* nuxt-base-template both). After merging we derive the full set of allowed
|
|
211
|
+
* packages from BOTH forms and write back canonical, sorted twins so the
|
|
212
|
+
* allowlist survives whichever key the active pnpm version reads. Explicit
|
|
213
|
+
* `false` entries in `allowBuilds` are preserved (and excluded from the
|
|
214
|
+
* array). No-op when neither key is present. Mutates `ws` in place.
|
|
209
215
|
*/
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
|
|
216
|
+
function syncBuildAllowlists(ws) {
|
|
217
|
+
const arr = Array.isArray(ws.onlyBuiltDependencies) ? ws.onlyBuiltDependencies : [];
|
|
218
|
+
const obj = ws.allowBuilds && typeof ws.allowBuilds === 'object' && !Array.isArray(ws.allowBuilds)
|
|
219
|
+
? ws.allowBuilds
|
|
220
|
+
: {};
|
|
221
|
+
if (arr.length === 0 && Object.keys(obj).length === 0)
|
|
213
222
|
return;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
// Every array entry implies allowBuilds[entry] = true unless already set.
|
|
224
|
+
const map = {};
|
|
225
|
+
for (const [pkg, enabled] of Object.entries(obj))
|
|
226
|
+
map[pkg] = enabled === true;
|
|
227
|
+
for (const pkg of arr)
|
|
228
|
+
if (map[pkg] === undefined)
|
|
229
|
+
map[pkg] = true;
|
|
230
|
+
const allowed = Object.entries(map)
|
|
231
|
+
.filter(([, enabled]) => enabled)
|
|
232
|
+
.map(([pkg]) => pkg)
|
|
233
|
+
.sort((a, b) => a.localeCompare(b));
|
|
234
|
+
ws.allowBuilds = Object.fromEntries(Object.entries(map).sort(([a], [b]) => a.localeCompare(b)));
|
|
235
|
+
ws.onlyBuiltDependencies = allowed;
|
|
223
236
|
}
|
|
@@ -210,9 +210,14 @@ function upsertVendorBlock(content, marker, newBlock) {
|
|
|
210
210
|
}
|
|
211
211
|
return content.replace(blockRegex(marker), newBlock);
|
|
212
212
|
}
|
|
213
|
-
/** Join block lines into the canonical `marker … --- ` shape (ends with `---\n`). */
|
|
213
|
+
/** Join block lines into the canonical `marker … --- ` shape (ends with `---\n\n`). */
|
|
214
214
|
function block(lines) {
|
|
215
|
-
|
|
215
|
+
// Blank line AFTER the `---` too, so the prepended block is separated from
|
|
216
|
+
// the following template heading by an empty line — oxfmt requires a blank
|
|
217
|
+
// line between a thematic break and a heading, otherwise `format:check`
|
|
218
|
+
// fails on a freshly vendored CLAUDE.md. blockRegex's `---\s*\n?` absorbs
|
|
219
|
+
// the extra newline, so removeVendorBlock stays round-trip-safe.
|
|
220
|
+
return [...lines, '', '---', '', ''].join('\n');
|
|
216
221
|
}
|
|
217
222
|
/** Build the regex that matches an existing block from its marker to the first `---`. */
|
|
218
223
|
function blockRegex(marker) {
|