@lenne.tech/cli 1.47.0 → 1.49.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.
Files changed (34) hide show
  1. package/build/commands/dev/test.js +5 -3
  2. package/build/commands/fullstack/init.js +12 -3
  3. package/build/commands/git/force-pull.js +11 -2
  4. package/build/commands/git/rebase.js +11 -1
  5. package/build/commands/git/reset.js +36 -4
  6. package/build/commands/git/squash.js +4 -1
  7. package/build/commands/git/update.js +42 -6
  8. package/build/extensions/api-mode.js +101 -8
  9. package/build/extensions/frontend-helper.js +11 -12
  10. package/build/extensions/git.js +23 -25
  11. package/build/extensions/server.js +66 -49
  12. package/build/lib/check-template-sync.js +62 -2
  13. package/build/lib/config-env-graphql.js +106 -0
  14. package/build/lib/cross-env.js +94 -0
  15. package/build/lib/dev-api-launch.js +92 -14
  16. package/build/lib/dev-package-manager.js +13 -9
  17. package/build/lib/dev-process.js +66 -30
  18. package/build/lib/dev-state.js +13 -2
  19. package/build/lib/dev-test-session.js +166 -55
  20. package/build/lib/fail-run.js +11 -0
  21. package/build/lib/git-env.js +46 -0
  22. package/build/lib/heal-oxlintrc.js +20 -3
  23. package/build/lib/heal-vendor-migrate-store.js +4 -3
  24. package/build/lib/migrate-scripts.js +69 -0
  25. package/build/lib/module-specifiers.js +144 -0
  26. package/build/lib/spinner-lifetime.js +245 -0
  27. package/build/lib/strip-vendor-schema-augmentation.js +13 -2
  28. package/build/lib/vendor-claude-md.js +9 -6
  29. package/build/lib/workspace-integration.js +12 -2
  30. package/build/templates/check/check.mjs +59 -9
  31. package/build/templates/check/lt-monorepo-pin.json +4 -4
  32. package/docs/commands.md +16 -12
  33. package/package.json +1 -1
  34. package/build/lib/strip-comments.js +0 -78
@@ -23,8 +23,10 @@ const dev_ticket_1 = require("../../lib/dev-ticket");
23
23
  * App mode (default) runs the Playwright suite against a fully ISOLATED test
24
24
  * stack (own URLs / ports / Caddy block / dedicated `<…>-test` database) that
25
25
  * runs parallel to — and never touches — the developer's `lt dev up` session.
26
- * Playwright's global-setup resets that dedicated DB once before the first test.
27
- * The stack is torn down automatically when the run finishes (residue-free).
26
+ * That DB outlives the run, so pending migrations are applied to it before the
27
+ * API starts; a project's Playwright global-setup may reset it on top (the CLI
28
+ * does not). The stack is torn down automatically when the run finishes
29
+ * (residue-free).
28
30
  *
29
31
  * API mode (`--api`) runs the standalone API test suite (`pnpm test:e2e` in the
30
32
  * API), which already isolates itself on its own DB — no stack is brought up.
@@ -225,7 +227,7 @@ const TestCommand = {
225
227
  try {
226
228
  const ctx = yield (0, dev_test_session_1.bringUpTestSession)(layout, identity, log, { devDbName });
227
229
  const env = Object.assign(Object.assign(Object.assign(Object.assign({}, process.env), readBridgeEnv(layout.root)), {
228
- // Playwright global-setup resets THIS db (allow-listed) before the suite.
230
+ // THIS db, for a project whose global-setup resets or cleans it (allow-listed).
229
231
  MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` }), (ctx.apiLogPath ? { NEST_SERVER_LOG: ctx.apiLogPath } : {}));
230
232
  if (debug) {
231
233
  env.PWDEBUG = '1';
@@ -488,7 +488,7 @@ const NewCommand = {
488
488
  yield system.run(`cd ${projectDir} && git init --initial-branch=dev`);
489
489
  }
490
490
  catch (err) {
491
- error(`Failed to initialize git: ${err.message}`);
491
+ ngBaseSpinner.fail(`Failed to initialize git: ${err.message}`);
492
492
  (0, fail_run_1.failRun)(toolbox);
493
493
  return;
494
494
  }
@@ -498,7 +498,7 @@ const NewCommand = {
498
498
  yield system.run(`cd ${projectDir} && git remote add origin ${gitLink}`);
499
499
  }
500
500
  catch (err) {
501
- error(`Failed to add remote: ${err.message}`);
501
+ ngBaseSpinner.fail(`Failed to add remote: ${err.message}`);
502
502
  (0, fail_run_1.failRun)(toolbox);
503
503
  return;
504
504
  }
@@ -526,7 +526,7 @@ const NewCommand = {
526
526
  });
527
527
  }
528
528
  if (!frontendResult.success) {
529
- error(`Failed to set up ${frontend} frontend: ${frontendResult.path}`);
529
+ ngBaseSpinner.fail(`Failed to set up ${frontend} frontend: ${frontendResult.path}`);
530
530
  (0, fail_run_1.failRun)(toolbox);
531
531
  return;
532
532
  }
@@ -774,6 +774,15 @@ const NewCommand = {
774
774
  // For tests
775
775
  return `new workspace ${projectDir} with ${name}`;
776
776
  }
777
+ else {
778
+ // The frontend setup reported success but left no `projects/app`. Without this
779
+ // branch the command fell off the end here: no message, no failing exit code,
780
+ // and `ngBaseSpinner` still spinning — which keeps the event loop alive (see
781
+ // the spinner note in CLAUDE.md), so the process never returned at all.
782
+ ngBaseSpinner.fail(`${frontend} was set up but "${projectDir}/projects/app" does not exist — cannot continue`);
783
+ (0, fail_run_1.failRun)(toolbox);
784
+ return;
785
+ }
777
786
  }),
778
787
  };
779
788
  exports.default = NewCommand;
@@ -55,8 +55,17 @@ const NewCommand = {
55
55
  else {
56
56
  info('No local changes to discard.');
57
57
  }
58
- // Show commits that would be lost
59
- const localCommits = yield run(`git log origin/${branch}..HEAD --oneline 2>/dev/null || echo ""`);
58
+ // Show commits that would be lost (none listed when the upstream ref is unknown locally).
59
+ // `2>/dev/null || echo ""` used to swallow the failure, but cmd.exe on Windows has no
60
+ // /dev/null: the redirect failed first and `echo ""` then reported success, so the preview
61
+ // silently claimed there was nothing ahead. try/catch behaves the same on every platform.
62
+ let localCommits = '';
63
+ try {
64
+ localCommits = yield run(`git log origin/${branch}..HEAD --oneline`);
65
+ }
66
+ catch (_d) {
67
+ // ignore - no upstream ref, so nothing ahead to report
68
+ }
60
69
  if (localCommits === null || localCommits === void 0 ? void 0 : localCommits.trim()) {
61
70
  info('');
62
71
  info('Local commits that would be lost:');
@@ -54,7 +54,17 @@ const NewCommand = {
54
54
  warning('DRY-RUN MODE - No changes will be made');
55
55
  info('');
56
56
  // Show commits that would be rebased
57
- const commitsToRebase = yield run(`git log ${baseBranchPreview}..HEAD --oneline 2>/dev/null || echo ""`);
57
+ // try/catch, not `2>/dev/null || echo ""`: on Windows that `echo` succeeds
58
+ // and the git log never runs, so a dry-run would report zero commits to
59
+ // rebase for a branch that has some — a preview that quietly understates
60
+ // what the real command will do.
61
+ let commitsToRebase = '';
62
+ try {
63
+ commitsToRebase = yield run(`git log ${baseBranchPreview}..HEAD --oneline`);
64
+ }
65
+ catch (_k) {
66
+ // Unknown base ref — nothing to preview, which the check below handles.
67
+ }
58
68
  if (commitsToRebase === null || commitsToRebase === void 0 ? void 0 : commitsToRebase.trim()) {
59
69
  const commitCount = commitsToRebase.trim().split('\n').length;
60
70
  info(`Would rebase ${commitCount} commit(s) from branch "${branch}" onto "${baseBranchPreview}":`);
@@ -10,6 +10,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const path_1 = require("path");
13
+ const git_env_1 = require("../../lib/git-env");
14
+ /**
15
+ * Environment that keeps the `git ls-remote` below non-interactive.
16
+ *
17
+ * This used to be a POSIX prefix on the command string
18
+ * (`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-…}" git …`).
19
+ * `system.run` shells out through `cmd.exe` on Windows, which reads
20
+ * `VAR=value cmd` as a command name, not as an assignment. Paired with the
21
+ * trailing `|| true` the call then "succeeded" with empty output, so every
22
+ * `lt git reset` on Windows aborted with "No remote branch … found!" — the
23
+ * ls-remote had never run. Handing the variables to the child as its
24
+ * environment works on every platform.
25
+ *
26
+ * The `||` keeps the semantics of the shell's `:-` default: a caller who
27
+ * configured ssh deliberately (a user with a custom agent, or a test harness
28
+ * pinning the behaviour) still wins. See `git.ts#gitInstalled` for why the
29
+ * assignment must never be unconditional.
30
+ */
13
31
  /**
14
32
  * Reset current branch
15
33
  */
@@ -46,7 +64,13 @@ const NewCommand = {
46
64
  return;
47
65
  }
48
66
  // Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
49
- const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="\${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git ls-remote --heads origin ${branch} 2>/dev/null || true`);
67
+ let remoteBranch = '';
68
+ try {
69
+ remoteBranch = yield system.run(`git ls-remote --heads origin ${branch}`, { env: (0, git_env_1.nonInteractiveGitEnv)() });
70
+ }
71
+ catch (_d) {
72
+ // ignore - unreachable remote is reported as "no remote branch" below
73
+ }
50
74
  if (!remoteBranch) {
51
75
  error(`No remote branch ${branch} found!`);
52
76
  return;
@@ -68,8 +92,14 @@ const NewCommand = {
68
92
  else {
69
93
  info('No local changes to discard.');
70
94
  }
71
- // Show commits that would be lost
72
- const localCommits = yield system.run(`git log origin/${branch}..HEAD --oneline 2>/dev/null || echo ""`);
95
+ // Show commits that would be lost (none listed when the upstream ref is unknown locally)
96
+ let localCommits = '';
97
+ try {
98
+ localCommits = yield system.run(`git log origin/${branch}..HEAD --oneline`);
99
+ }
100
+ catch (_e) {
101
+ // ignore - no upstream ref, so nothing ahead to report
102
+ }
73
103
  if (localCommits === null || localCommits === void 0 ? void 0 : localCommits.trim()) {
74
104
  info('');
75
105
  info('Local commits that would be lost:');
@@ -109,7 +139,9 @@ const NewCommand = {
109
139
  const projectDir = (0, path_1.dirname)(pkgPath);
110
140
  const detectedPm = toolbox.pm.detect(projectDir);
111
141
  const installSpin = spin(`Install packages using ${detectedPm}`);
112
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)}`);
142
+ // `cwd` instead of `cd <dir> &&`: cmd.exe's `cd` does not switch drives,
143
+ // and it needs no quoting for paths with spaces.
144
+ yield system.run(toolbox.pm.install(detectedPm), { cwd: projectDir });
113
145
  installSpin.succeed();
114
146
  }
115
147
  // Success info
@@ -129,7 +129,10 @@ const NewCommand = {
129
129
  const mergeBaseSpin = spin(`Get merge ${base}`);
130
130
  const mergeBase = yield git.getMergeBase(base);
131
131
  if (!mergeBase) {
132
- error('No merge base found!');
132
+ // `fail` rather than `error` + `return`: a running spinner holds the event
133
+ // loop open (ora resumes stdin and keeps a render interval), so the old
134
+ // shape did not just print and leave — the command never exited at all.
135
+ mergeBaseSpin.fail(`No merge base found with ${base}!`);
133
136
  return;
134
137
  }
135
138
  mergeBaseSpin.succeed();
@@ -10,6 +10,22 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const path_1 = require("path");
13
+ const git_env_1 = require("../../lib/git-env");
14
+ /**
15
+ * Environment that keeps the `git` calls below non-interactive.
16
+ *
17
+ * This used to be a POSIX prefix on the command string
18
+ * (`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-…}" git …`).
19
+ * `system.run` shells out through `cmd.exe` on Windows, which does not read
20
+ * `VAR=value cmd` as an assignment but as a command name — so every `lt git
21
+ * update` failed there with "'GIT_TERMINAL_PROMPT' is not recognized". Handing
22
+ * the variables to the child as its environment works on every platform.
23
+ *
24
+ * The `||` keeps the semantics of the shell's `:-` default: a caller who
25
+ * configured ssh deliberately (a user with a custom agent, or a test harness
26
+ * pinning the behaviour) still wins. See `git.ts#gitInstalled` for why the
27
+ * assignment must never be unconditional.
28
+ */
13
29
  /**
14
30
  * Update branch
15
31
  */
@@ -47,10 +63,22 @@ const NewCommand = {
47
63
  info('');
48
64
  info(`Current branch: ${branch}`);
49
65
  info('');
50
- // Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline)
51
- yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true');
52
- // Check for incoming commits
53
- const incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline 2>/dev/null || echo ""`);
66
+ // Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline).
67
+ // Best effort: offline or without credentials the preview falls back to the local refs.
68
+ try {
69
+ yield run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
70
+ }
71
+ catch (_d) {
72
+ // ignore - a failed fetch only makes the preview less current
73
+ }
74
+ // Check for incoming commits (none yet when the upstream ref is unknown locally)
75
+ let incomingCommits = '';
76
+ try {
77
+ incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline`);
78
+ }
79
+ catch (_e) {
80
+ // ignore - no upstream ref, so nothing incoming to report
81
+ }
54
82
  const commits = (incomingCommits === null || incomingCommits === void 0 ? void 0 : incomingCommits.trim().split('\n').filter((c) => c)) || [];
55
83
  if (commits.length > 0) {
56
84
  info(`Incoming commits (${commits.length}):`);
@@ -78,7 +106,13 @@ const NewCommand = {
78
106
  const timer = startTimer();
79
107
  // Update
80
108
  const updateSpin = spin(`Update branch ${branch}`);
81
- yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git pull --rebase');
109
+ try {
110
+ yield run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
111
+ }
112
+ catch (_f) {
113
+ // ignore - `git pull --rebase` below fetches again and reports the real failure
114
+ }
115
+ yield run('git pull --rebase', { env: (0, git_env_1.nonInteractiveGitEnv)() });
82
116
  updateSpin.succeed();
83
117
  // Install packages (unless skipped) with correctly detected package manager (supports monorepo lockfiles)
84
118
  if (!skipInstall) {
@@ -87,7 +121,9 @@ const NewCommand = {
87
121
  const projectDir = (0, path_1.dirname)(pkgPath);
88
122
  const detectedPm = toolbox.pm.detect(projectDir);
89
123
  const installSpin = spin(`Install packages using ${detectedPm}`);
90
- yield run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)}`);
124
+ // `cwd` instead of `cd <dir> &&`: cmd.exe's `cd` does not switch drives,
125
+ // and it needs no quoting for paths with spaces.
126
+ yield run(toolbox.pm.install(detectedPm), { cwd: projectDir });
91
127
  installSpin.succeed();
92
128
  }
93
129
  }
@@ -10,7 +10,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ApiMode = void 0;
13
+ const glob_1 = require("glob");
13
14
  const path_1 = require("path");
15
+ const config_env_graphql_1 = require("../lib/config-env-graphql");
14
16
  /**
15
17
  * API Mode processing for nest-server-starter template
16
18
  *
@@ -45,11 +47,18 @@ class ApiMode {
45
47
  // REST mode: remove graphql regions, keep rest regions
46
48
  yield this.removeMode(projectPath, manifest, 'graphql', 'rest');
47
49
  yield this.modifyConfigEnvForRest(projectPath);
50
+ this.assertGraphQlDisabled(projectPath);
48
51
  }
49
52
  else if (mode === 'GraphQL') {
50
53
  // GraphQL mode: remove rest regions, keep graphql regions
51
54
  yield this.removeMode(projectPath, manifest, 'rest', 'graphql');
52
55
  }
56
+ // Both conversions above must leave no marker behind. Verified rather than
57
+ // assumed: a strip that quietly does nothing reports success just as loudly as
58
+ // one that worked (see assertNoMarkersRemain).
59
+ if (mode !== 'Both') {
60
+ this.assertNoMarkersRemain(projectPath, mode === 'Rest' ? 'graphql' : 'rest');
61
+ }
53
62
  // Remove manifest and strip-markers script
54
63
  this.filesystem.remove(manifestPath);
55
64
  this.filesystem.remove((0, path_1.join)(projectPath, 'scripts', 'strip-api-mode-markers.mjs'));
@@ -114,10 +123,7 @@ class ApiMode {
114
123
  // 1. Delete files matching filePatterns
115
124
  if (modeConfig.filePatterns) {
116
125
  for (const pattern of modeConfig.filePatterns) {
117
- const matches = this.filesystem.find(projectPath, {
118
- matching: pattern,
119
- });
120
- for (const file of matches) {
126
+ for (const file of this.globFiles(projectPath, pattern)) {
121
127
  this.filesystem.remove(file);
122
128
  }
123
129
  }
@@ -169,6 +175,90 @@ class ApiMode {
169
175
  this.cleanOrphanImports(projectPath);
170
176
  });
171
177
  }
178
+ /**
179
+ * Final check of the REST conversion: every environment block in `config.env.ts`
180
+ * must disable GraphQL.
181
+ *
182
+ * `CoreModule.forRoot` reads a missing `graphQl` as ENABLED, so a block without it
183
+ * yields a project that fails at START time with `Cannot determine a GraphQL output
184
+ * type for the "arguments"` — long after the generator reported success. Every step
185
+ * before this one REPLACES an existing property; none of them adds a missing one, so
186
+ * nothing guaranteed the outcome.
187
+ *
188
+ * Repairs what it can (insert the switch) and throws when a block still lacks it,
189
+ * rather than handing over a project that cannot boot.
190
+ */
191
+ assertGraphQlDisabled(projectPath) {
192
+ const configPath = (0, path_1.join)(projectPath, 'src', 'config.env.ts');
193
+ const content = this.filesystem.read(configPath);
194
+ if (!content) {
195
+ return;
196
+ }
197
+ const { added, content: repaired } = (0, config_env_graphql_1.disableGraphQlInEveryEnvBlock)(content);
198
+ if (added.length > 0) {
199
+ this.filesystem.write(configPath, repaired);
200
+ }
201
+ const offenders = (0, config_env_graphql_1.envBlocksWithoutGraphQlDisabled)(repaired);
202
+ if (offenders.length > 0) {
203
+ throw new Error(`REST conversion incomplete: src/config.env.ts has no \`graphQl: false\` in ${offenders.join(', ')}. ` +
204
+ 'The framework treats a missing switch as GraphQL ENABLED, and the server would fail to start. ' +
205
+ 'Add it to those environment blocks and re-run.');
206
+ }
207
+ }
208
+ /**
209
+ * Refuse to hand over a project that still carries markers of the removed mode.
210
+ *
211
+ * A leftover `// #region graphql` announces a mode the project does not have — and,
212
+ * worse, means the region CONTENT is still there: resolvers, graphql-only wiring,
213
+ * e2e specs. That is what a silent no-op looks like from the outside, and the strip
214
+ * used to be exactly that on Windows (fixed in the glob helper above): it reported
215
+ * success while touching nothing.
216
+ *
217
+ * Cheap, and it fails at generation time instead of in the user's editor.
218
+ */
219
+ assertNoMarkersRemain(projectPath, removedMarker) {
220
+ const marked = [];
221
+ for (const dir of ['src', 'tests']) {
222
+ const base = (0, path_1.join)(projectPath, dir);
223
+ if (!this.filesystem.exists(base)) {
224
+ continue;
225
+ }
226
+ for (const file of this.globFiles(base, '**/*.ts')) {
227
+ const content = this.filesystem.read(file);
228
+ if ((content === null || content === void 0 ? void 0 : content.includes(`// #region ${removedMarker}`)) || (content === null || content === void 0 ? void 0 : content.includes(`// #endregion ${removedMarker}`))) {
229
+ marked.push(`${dir}/${(0, path_1.basename)(file)}`);
230
+ }
231
+ }
232
+ }
233
+ if (marked.length > 0) {
234
+ throw new Error(`API-mode conversion incomplete: ${marked.length} file(s) still carry \`// #region ${removedMarker}\` ` +
235
+ `markers (${marked.slice(0, 5).join(', ')}${marked.length > 5 ? ', …' : ''}). ` +
236
+ 'The region content was not removed, so this project still contains code of a mode it does not have.');
237
+ }
238
+ }
239
+ /**
240
+ * Resolve a POSIX-style glob pattern against `baseDir` and return the
241
+ * matching files as absolute, OS-native paths.
242
+ *
243
+ * Deliberately NOT `filesystem.find()`: gluegun/fs-jetpack builds its
244
+ * matcher by string-concatenating the *resolved* base path in front of
245
+ * the pattern (fs-jetpack/lib/utils/matcher.js). On Windows that base
246
+ * path is backslash-separated, and minimatch reads a backslash inside a
247
+ * pattern as an escape character — so a base of `C:\project\src`
248
+ * collapses to the literal `C:projectsrc` and nothing ever matches.
249
+ * Effect: on Windows no file was deleted and no region was stripped,
250
+ * silently. `glob` keeps pattern and base directory apart and
251
+ * normalises separators itself, so it behaves the same on every
252
+ * platform.
253
+ */
254
+ globFiles(baseDir, pattern) {
255
+ return (0, glob_1.globSync)(pattern, {
256
+ absolute: true,
257
+ cwd: baseDir,
258
+ dot: true,
259
+ nodir: true,
260
+ });
261
+ }
172
262
  /**
173
263
  * Strip region markers from all .ts files
174
264
  *
@@ -181,7 +271,7 @@ class ApiMode {
181
271
  if (!this.filesystem.exists(dir)) {
182
272
  continue;
183
273
  }
184
- const files = this.filesystem.find(dir, { matching: '**/*.ts' });
274
+ const files = this.globFiles(dir, '**/*.ts');
185
275
  for (const file of files) {
186
276
  const content = this.filesystem.read(file);
187
277
  if (!content) {
@@ -196,7 +286,10 @@ class ApiMode {
196
286
  // graphql` block with an explicit `graphQl: false,` so GraphQL
197
287
  // is cleanly disabled.
198
288
  let processed;
199
- if (removeMarker === 'graphql' && file.endsWith('/config.env.ts')) {
289
+ // `basename` instead of `endsWith('/config.env.ts')`: on Windows the
290
+ // path separator is a backslash, so the POSIX-only suffix check never
291
+ // matched and config.env.ts was processed like any other file.
292
+ if (removeMarker === 'graphql' && (0, path_1.basename)(file) === 'config.env.ts') {
200
293
  processed = this.replaceGraphqlRegionsWithDisabled(content, keepMarker);
201
294
  }
202
295
  else {
@@ -299,7 +392,7 @@ class ApiMode {
299
392
  if (!this.filesystem.exists(dir)) {
300
393
  continue;
301
394
  }
302
- const files = this.filesystem.find(dir, { matching: '**/*.ts' });
395
+ const files = this.globFiles(dir, '**/*.ts');
303
396
  for (const file of files) {
304
397
  const content = this.filesystem.read(file);
305
398
  if (!content) {
@@ -444,7 +537,7 @@ class ApiMode {
444
537
  if (!this.filesystem.exists(dir)) {
445
538
  continue;
446
539
  }
447
- const files = this.filesystem.find(dir, { matching: '**/*.ts' });
540
+ const files = this.globFiles(dir, '**/*.ts');
448
541
  for (const file of files) {
449
542
  const content = this.filesystem.read(file);
450
543
  if (!content) {
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FrontendHelper = void 0;
13
13
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
14
14
  const markdown_table_1 = require("../lib/markdown-table");
15
- const strip_comments_1 = require("../lib/strip-comments");
15
+ const module_specifiers_1 = require("../lib/module-specifiers");
16
16
  const strip_vendor_schema_augmentation_1 = require("../lib/strip-vendor-schema-augmentation");
17
17
  const vendor_claude_md_1 = require("../lib/vendor-claude-md");
18
18
  /**
@@ -399,7 +399,7 @@ class FrontendHelper {
399
399
  }
400
400
  }
401
401
  // ── Post-conversion verification ─────────────────────────────────────
402
- const stale = this.findStaleFrontendImports(dest, /from\s+['"]\..*\/core['"]/);
402
+ const stale = this.findStaleFrontendImports(dest, module_specifiers_1.isRelativeCoreImport);
403
403
  if (stale.length > 0) {
404
404
  const { print } = this.toolbox;
405
405
  print.warning(`${stale.length} file(s) still contain relative core imports after npm conversion:`);
@@ -716,8 +716,7 @@ class FrontendHelper {
716
716
  ].join('\n'));
717
717
  }
718
718
  // ── Post-conversion verification ─────────────────────────────────────
719
- // Only match actual import/from statements, not comments or strings
720
- const staleImports = this.findStaleFrontendImports(dest, /(?:^|\s)(?:import|from)\s+['"][^'"]*@lenne\.tech\/nuxt-extensions/m, 'app/core/');
719
+ const staleImports = this.findStaleFrontendImports(dest, (specifier) => (0, module_specifiers_1.isPackageImport)(specifier, '@lenne.tech/nuxt-extensions'), 'app/core/');
721
720
  if (staleImports.length > 0) {
722
721
  const { print } = this.toolbox;
723
722
  print.warning(`${staleImports.length} file(s) still contain '@lenne.tech/nuxt-extensions' imports after vendor conversion:`);
@@ -826,20 +825,20 @@ class FrontendHelper {
826
825
  /**
827
826
  * Scan consumer files for stale imports matching a pattern.
828
827
  */
829
- findStaleFrontendImports(appDir, needle, skipPathContaining) {
828
+ findStaleFrontendImports(appDir, matches, skipPathContaining) {
830
829
  const { filesystem } = this.toolbox;
831
830
  const allFiles = this.walkConsumerFiles(appDir);
832
831
  const stale = [];
833
832
  for (const absFile of allFiles) {
834
833
  if (skipPathContaining && absFile.includes(skipPathContaining))
835
834
  continue;
836
- // Strip comments first — a docblock that DOCUMENTS the conversion legitimately quotes the
837
- // very import syntax this looks for, and would otherwise be reported as a file the user has
838
- // to fix by hand. Same false-positive class that hit the backend detector on
839
- // nest-server-starter's bootstrap-diagnostics.spec.ts.
840
- const content = (0, strip_comments_1.stripComments)(filesystem.read(absFile) || '');
841
- const matches = typeof needle === 'string' ? content.includes(needle) : needle.test(content);
842
- if (matches) {
835
+ // Ask the parser for the file's module specifiers instead of searching its text.
836
+ // A docblock that DOCUMENTS the conversion quotes the very import syntax a text
837
+ // search looks for, and blanking comments first does not hold either — see
838
+ // src/lib/module-specifiers.ts. A comment is not part of the AST, so the whole
839
+ // false-positive class is gone rather than worked around.
840
+ const specifiers = (0, module_specifiers_1.fileImportedSpecifiers)(absFile, (path) => filesystem.read(path) || undefined);
841
+ if (specifiers.some(matches)) {
843
842
  stale.push(absFile.replace(`${appDir}/`, ''));
844
843
  }
845
844
  }
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.Git = void 0;
13
+ const git_env_1 = require("../lib/git-env");
13
14
  /**
14
15
  * Git functions
15
16
  */
@@ -119,8 +120,17 @@ class Git {
119
120
  const result = [];
120
121
  // Toolbox features
121
122
  const { system } = this.toolbox;
122
- // Get branches (use short SSH timeout so fetch doesn't hang in offline environments)
123
- const branches = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null; git show-branch --list');
123
+ // Refresh first, but never let a failed fetch cost us the branch list: the
124
+ // two commands used to be chained with `;`, which cmd.exe does not honour as
125
+ // a separator at all, so on Windows this ran neither reliably. They are two
126
+ // calls now — the fetch is best-effort, the listing is the actual result.
127
+ try {
128
+ yield system.run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
129
+ }
130
+ catch (_a) {
131
+ // Offline or no remote — `git show-branch` still lists what is local.
132
+ }
133
+ const branches = yield system.run('git show-branch --list');
124
134
  branches.split('\n').forEach((item) => {
125
135
  const matches = item.match(/\[(.*?)]/);
126
136
  if (matches) {
@@ -197,27 +207,6 @@ class Git {
197
207
  /**
198
208
  * Check if git is installed (cached for performance)
199
209
  */
200
- /**
201
- * Why the git calls in this repo spell `GIT_SSH_COMMAND="\${GIT_SSH_COMMAND:-…}"` (shell default)
202
- * rather than assigning it outright.
203
- *
204
- * These commands do a best-effort `git fetch` to see whether the branch is
205
- * behind. They set `BatchMode=yes` so ssh fails instead of PROMPTING — but an
206
- * agent that stalls is not a prompt. On a 1Password-backed machine the agent is
207
- * reachable and every signature needs an interactive approval; unattended it
208
- * waits and then reports `communication with agent failed`. Measured: **61 s per
209
- * fetch**, so `lt git update --dry-run` took 62 s and `lt git create --dry-run`
210
- * 123 s (two fetches). `ConnectTimeout` does not bound it — that covers the TCP
211
- * connect, not the agent.
212
- *
213
- * Waiting for a human to approve a key is legitimate for an interactive command,
214
- * so the default is unchanged. What was wrong is that the assignment was
215
- * UNCONDITIONAL: it overrode a caller who had deliberately configured ssh,
216
- * including a test harness trying to make the behaviour deterministic. The
217
- * `:-` default respects an existing value and keeps the old behaviour when there
218
- * is none. `IdentityAgent=none` in the caller's env then drops the same fetch to
219
- * ~1 s with a clean `Permission denied (publickey)`.
220
- */
221
210
  gitInstalled() {
222
211
  return __awaiter(this, void 0, void 0, function* () {
223
212
  // Return cached result if available
@@ -272,8 +261,17 @@ class Git {
272
261
  if (opts.spin) {
273
262
  searchSpin = spin(opts.spinText);
274
263
  }
275
- // Update infos (use short SSH timeout so fetch doesn't hang in offline environments)
276
- const fetch = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true');
264
+ // Update infos. Best-effort, but expressed as try/catch rather than
265
+ // `2>/dev/null || true`: on Windows `true` resolves to Git-for-Windows'
266
+ // `true.exe`, so the pipeline succeeded with empty output and the fetch
267
+ // never ran — a silent no-op that read as a successful refresh.
268
+ let fetch = '';
269
+ try {
270
+ fetch = yield system.run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
271
+ }
272
+ catch (_b) {
273
+ // Offline or no remote — the branch search below still works on local refs.
274
+ }
277
275
  if (fetch.length && !fetch.startsWith('remote')) {
278
276
  info(`Could not update infos ${fetch.length}`);
279
277
  }