@lenne.tech/cli 1.46.0 → 1.48.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.
@@ -3,9 +3,13 @@
3
3
  /**
4
4
  * Postinstall script for lt CLI
5
5
  * Generates static completion files on install/update (no runtime overhead)
6
+ *
7
+ * Must never fail the install: npm runs this through cmd.exe on Windows, so the
8
+ * former `2>/dev/null || true` guard in package.json broke `npm i -g` there
9
+ * instead of protecting it. The guard now lives in this file (see bottom).
6
10
  */
7
11
 
8
- const { execSync } = require('child_process');
12
+ const { execFileSync } = require('child_process');
9
13
  const { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
10
14
  const { homedir } = require('os');
11
15
  const { join } = require('path');
@@ -26,11 +30,17 @@ function addSourceLine(configFile, sourceLine) {
26
30
 
27
31
  /**
28
32
  * Detect user's shell
33
+ *
34
+ * Returns null when no supported shell can be identified. On Windows an unset
35
+ * SHELL means PowerShell or cmd.exe, which read none of the rc files below —
36
+ * appending to a ~/.bashrc nothing sources would only leave clutter behind.
37
+ * Git Bash sets SHELL, so it still gets bash completions.
29
38
  */
30
39
  function detectShell() {
31
40
  const shell = process.env.SHELL || '';
32
41
  if (shell.includes('zsh')) return 'zsh';
33
42
  if (shell.includes('fish')) return 'fish';
43
+ if (!shell && process.platform === 'win32') return null;
34
44
  return 'bash';
35
45
  }
36
46
 
@@ -45,8 +55,13 @@ function generateCompletionFile(shell, completionFile) {
45
55
  mkdirSync(dir, { recursive: true });
46
56
  }
47
57
 
48
- // Generate completion script
49
- const script = execSync(`lt completion ${shell}`, { encoding: 'utf-8' });
58
+ // Run this package's own entry point with the current Node binary instead of
59
+ // resolving `lt` from PATH: during a global install the bin link may not exist
60
+ // yet, and on Windows `lt` is a .cmd shim that cannot be spawned without a shell.
61
+ const script = execFileSync(process.execPath, [join(__dirname, 'lt'), 'completion', shell], {
62
+ encoding: 'utf-8',
63
+ stdio: ['ignore', 'pipe', 'ignore'],
64
+ });
50
65
  writeFileSync(completionFile, script);
51
66
  return true;
52
67
  } catch {
@@ -109,6 +124,10 @@ function main() {
109
124
  }
110
125
 
111
126
  const shell = detectShell();
127
+ if (!shell) {
128
+ return; // No supported shell (e.g. PowerShell) - nothing to install
129
+ }
130
+
112
131
  const paths = getCompletionPaths(shell);
113
132
  const isTTY = process.stdout.isTTY;
114
133
 
@@ -152,5 +171,9 @@ function main() {
152
171
  }
153
172
  }
154
173
 
155
- // Run
156
- main();
174
+ // Run - completions are a convenience, so no failure may abort the install
175
+ try {
176
+ main();
177
+ } catch {
178
+ // Silent fail
179
+ }
@@ -169,7 +169,7 @@ const DoctorCommand = {
169
169
  try {
170
170
  const { readFileSync: read } = yield Promise.resolve().then(() => __importStar(require('fs')));
171
171
  const { join: j } = yield Promise.resolve().then(() => __importStar(require('path')));
172
- const { resolveCopySet } = yield Promise.resolve().then(() => __importStar(require('../../lib/heal-check-wrapper')));
172
+ const { keptWrapperReason, resolveCopySet } = yield Promise.resolve().then(() => __importStar(require('../../lib/heal-check-wrapper')));
173
173
  const bundledCheck = j(__dirname, '..', '..', 'templates', 'check', 'check.mjs');
174
174
  if (filesystem.exists(bundledCheck)) {
175
175
  const missing = [];
@@ -183,12 +183,26 @@ const DoctorCommand = {
183
183
  drifted.push(rel);
184
184
  }
185
185
  }
186
- if (missing.length > 0) {
186
+ // A missing module only breaks `check` when the project's wrapper is the
187
+ // canonical one that imports it (or the wrapper itself is missing). An
188
+ // older wrapper does not import the newer modules (e.g. `lib/*.mjs`) and
189
+ // still runs — calling that "cannot start" would be a false alarm.
190
+ const wrapperDrifted = drifted.includes('scripts/check.mjs');
191
+ // A project wrapper that heal refuses to replace (newer release, or the same
192
+ // release with other content) is not drift to fix: pointing at
193
+ // `lt fullstack update` there would recommend a no-op or a downgrade.
194
+ const kept = keptWrapperReason(layout.root, bundledCheck);
195
+ if (kept) {
196
+ line('INFO', colors.cyan, `check wrapper: ${kept}`);
197
+ }
198
+ else if (missing.length > 0 && !wrapperDrifted) {
187
199
  line('ERROR', colors.red, `check wrapper incomplete — missing ${missing.join(', ')}`);
188
200
  line('ERROR', colors.red, ' `pnpm run check` cannot start; run `lt fullstack update` to install it');
189
201
  }
190
202
  else if (drifted.length > 0) {
191
- line('WARN', colors.yellow, `${drifted.join(', ')} differs from the canonical CLI version`);
203
+ const outdated = [...drifted, ...missing.map((rel) => `${rel} (missing)`)];
204
+ const verb = outdated.length === 1 ? 'differs' : 'differ';
205
+ line('WARN', colors.yellow, `${outdated.join(', ')} ${verb} from the canonical CLI version`);
192
206
  line('WARN', colors.yellow, ' run `lt fullstack update` to sync it (skips uncommitted local edits)');
193
207
  }
194
208
  else if (filesystem.exists(j(layout.root, 'scripts', 'check.mjs'))) {
@@ -199,6 +213,34 @@ const DoctorCommand = {
199
213
  catch (_a) {
200
214
  /* best-effort diagnostics */
201
215
  }
216
+ // 7.6 oxlint config filename: oxlint only auto-discovers `.oxlintrc.json`.
217
+ // Apps from the template before the rename carry `oxlint.json`, whose
218
+ // rules then silently never apply — lint passes on code the project's
219
+ // own rules forbid.
220
+ if (layout.appDir) {
221
+ // Behaviour-changing fix flags delete console calls once rules load.
222
+ try {
223
+ const { findDangerousFixFlagUsage } = yield Promise.resolve().then(() => __importStar(require('../../lib/heal-oxlintrc')));
224
+ const workspaceRoot = layout.root !== layout.appDir ? layout.root : undefined;
225
+ const flagged = findDangerousFixFlagUsage(layout.appDir, workspaceRoot);
226
+ if (flagged.length > 0) {
227
+ line('WARN', colors.yellow, `oxlint --fix-suggestions/--fix-dangerously in ${flagged.join(', ')}`);
228
+ line('WARN', colors.yellow, ' they apply behaviour-changing fixes (e.g. delete console calls); run `lt fullstack update`');
229
+ }
230
+ }
231
+ catch (_b) {
232
+ /* best-effort diagnostics */
233
+ }
234
+ const hasLegacy = filesystem.exists(filesystem.path(layout.appDir, 'oxlint.json'));
235
+ const hasCurrent = filesystem.exists(filesystem.path(layout.appDir, '.oxlintrc.json'));
236
+ if (hasLegacy && !hasCurrent) {
237
+ line('WARN', colors.yellow, 'oxlint config not loaded — oxlint only reads .oxlintrc.json, the app has oxlint.json');
238
+ line('WARN', colors.yellow, ' run `lt fullstack update` to rename it');
239
+ }
240
+ else if (hasLegacy) {
241
+ line('WARN', colors.yellow, 'oxlint.json is ignored next to .oxlintrc.json — merge its rules and delete it');
242
+ }
243
+ }
202
244
  // 8. Slug ↔ path: is this project's slug registered to a DIFFERENT checkout?
203
245
  // Two clones of the same project (same package.json "name") share the
204
246
  // slug → Caddy block / ports / DB and collide. Surface it proactively.
@@ -14,6 +14,7 @@ const dev_patches_1 = require("../../lib/dev-patches");
14
14
  const framework_detection_1 = require("../../lib/framework-detection");
15
15
  const frontend_framework_detection_1 = require("../../lib/frontend-framework-detection");
16
16
  const heal_check_wrapper_1 = require("../../lib/heal-check-wrapper");
17
+ const heal_oxlintrc_1 = require("../../lib/heal-oxlintrc");
17
18
  const heal_vendor_migrate_store_1 = require("../../lib/heal-vendor-migrate-store");
18
19
  const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
19
20
  /**
@@ -213,6 +214,36 @@ const NewCommand = {
213
214
  warning(` Check wrapper NOT updated: ${entry}`);
214
215
  }
215
216
  }
217
+ // ── Self-heal: let oxlint actually load the app's config ───────────────
218
+ //
219
+ // oxlint only auto-discovers `.oxlintrc.json`; the app template shipped
220
+ // `oxlint.json`, so its rules never applied. Runs AFTER the check wrapper
221
+ // heal on purpose: the rename is refused while anything still passes
222
+ // `--fix-suggestions`, which would delete console calls once the config loads.
223
+ if (appDir) {
224
+ const workspaceRoot = isWorkspace ? cwd : undefined;
225
+ // 1. Strip the behaviour-changing fix flags first (the root wrapper was
226
+ // handled above). A file it has to skip keeps blocking the rename below.
227
+ const flags = (0, heal_oxlintrc_1.healDangerousOxlintFixFlags)(appDir, workspaceRoot);
228
+ if (flags.changed.length > 0) {
229
+ info('');
230
+ success(` Removed --fix-suggestions/--fix-dangerously from: ${flags.changed.join(', ')}`);
231
+ }
232
+ for (const entry of flags.skipped) {
233
+ warning(` Fix flag NOT removed: ${entry}`);
234
+ }
235
+ // 2. Rename — the gate inside re-checks every file.
236
+ const oxlintrc = (0, heal_oxlintrc_1.healOxlintrcFilename)(appDir, workspaceRoot);
237
+ if (oxlintrc.action === 'renamed') {
238
+ info('');
239
+ success(` Renamed the oxlint config so oxlint loads it: ${oxlintrc.changed.join(', ')}`);
240
+ info(' Its rules apply from now on — expect new lint findings on the next check.');
241
+ }
242
+ else if (oxlintrc.detail) {
243
+ info('');
244
+ warning(` oxlint config NOT renamed: ${oxlintrc.detail}`);
245
+ }
246
+ }
216
247
  // ── Self-heal: keep `.lt-dev/` out of git ──────────────────────────────
217
248
  //
218
249
  // Newer templates ship the entry, but pre-existing projects predate it —
@@ -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
@@ -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,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.ApiMode = void 0;
13
+ const glob_1 = require("glob");
13
14
  const path_1 = require("path");
14
15
  /**
15
16
  * API Mode processing for nest-server-starter template
@@ -114,10 +115,7 @@ class ApiMode {
114
115
  // 1. Delete files matching filePatterns
115
116
  if (modeConfig.filePatterns) {
116
117
  for (const pattern of modeConfig.filePatterns) {
117
- const matches = this.filesystem.find(projectPath, {
118
- matching: pattern,
119
- });
120
- for (const file of matches) {
118
+ for (const file of this.globFiles(projectPath, pattern)) {
121
119
  this.filesystem.remove(file);
122
120
  }
123
121
  }
@@ -169,6 +167,29 @@ class ApiMode {
169
167
  this.cleanOrphanImports(projectPath);
170
168
  });
171
169
  }
170
+ /**
171
+ * Resolve a POSIX-style glob pattern against `baseDir` and return the
172
+ * matching files as absolute, OS-native paths.
173
+ *
174
+ * Deliberately NOT `filesystem.find()`: gluegun/fs-jetpack builds its
175
+ * matcher by string-concatenating the *resolved* base path in front of
176
+ * the pattern (fs-jetpack/lib/utils/matcher.js). On Windows that base
177
+ * path is backslash-separated, and minimatch reads a backslash inside a
178
+ * pattern as an escape character — so a base of `C:\project\src`
179
+ * collapses to the literal `C:projectsrc` and nothing ever matches.
180
+ * Effect: on Windows no file was deleted and no region was stripped,
181
+ * silently. `glob` keeps pattern and base directory apart and
182
+ * normalises separators itself, so it behaves the same on every
183
+ * platform.
184
+ */
185
+ globFiles(baseDir, pattern) {
186
+ return (0, glob_1.globSync)(pattern, {
187
+ absolute: true,
188
+ cwd: baseDir,
189
+ dot: true,
190
+ nodir: true,
191
+ });
192
+ }
172
193
  /**
173
194
  * Strip region markers from all .ts files
174
195
  *
@@ -181,7 +202,7 @@ class ApiMode {
181
202
  if (!this.filesystem.exists(dir)) {
182
203
  continue;
183
204
  }
184
- const files = this.filesystem.find(dir, { matching: '**/*.ts' });
205
+ const files = this.globFiles(dir, '**/*.ts');
185
206
  for (const file of files) {
186
207
  const content = this.filesystem.read(file);
187
208
  if (!content) {
@@ -196,7 +217,10 @@ class ApiMode {
196
217
  // graphql` block with an explicit `graphQl: false,` so GraphQL
197
218
  // is cleanly disabled.
198
219
  let processed;
199
- if (removeMarker === 'graphql' && file.endsWith('/config.env.ts')) {
220
+ // `basename` instead of `endsWith('/config.env.ts')`: on Windows the
221
+ // path separator is a backslash, so the POSIX-only suffix check never
222
+ // matched and config.env.ts was processed like any other file.
223
+ if (removeMarker === 'graphql' && (0, path_1.basename)(file) === 'config.env.ts') {
200
224
  processed = this.replaceGraphqlRegionsWithDisabled(content, keepMarker);
201
225
  }
202
226
  else {
@@ -299,7 +323,7 @@ class ApiMode {
299
323
  if (!this.filesystem.exists(dir)) {
300
324
  continue;
301
325
  }
302
- const files = this.filesystem.find(dir, { matching: '**/*.ts' });
326
+ const files = this.globFiles(dir, '**/*.ts');
303
327
  for (const file of files) {
304
328
  const content = this.filesystem.read(file);
305
329
  if (!content) {
@@ -444,7 +468,7 @@ class ApiMode {
444
468
  if (!this.filesystem.exists(dir)) {
445
469
  continue;
446
470
  }
447
- const files = this.filesystem.find(dir, { matching: '**/*.ts' });
471
+ const files = this.globFiles(dir, '**/*.ts');
448
472
  for (const file of files) {
449
473
  const content = this.filesystem.read(file);
450
474
  if (!content) {
@@ -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
  }
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.Server = void 0;
46
46
  const crypto = __importStar(require("crypto"));
47
+ const glob_1 = require("glob");
47
48
  const path_1 = require("path");
48
49
  const ts = __importStar(require("typescript"));
49
50
  const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
@@ -1232,13 +1233,15 @@ class Server {
1232
1233
  // only as type annotations — any runtime `new Request(...)` calls refer
1233
1234
  // to the global Fetch API Request, not the express one.
1234
1235
  const expressImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"]\s*;?\s*$/gm;
1235
- const vendoredTsFiles = filesystem.find(coreDir, {
1236
- matching: '**/*.ts',
1237
- recursive: true,
1238
- }) || [];
1239
- for (const filePath of vendoredTsFiles) {
1240
- // filesystem.find returns paths relative to jetpack cwd — use absolute resolution
1241
- const absPath = filePath.startsWith('/') ? filePath : require('node:path').resolve(filePath);
1236
+ // `glob`, not `filesystem.find`: jetpack builds its matcher by concatenating the
1237
+ // RESOLVED base path in front of the pattern, and minimatch reads a backslash
1238
+ // inside a PATTERN as an escape. On Windows `C:\…\core` + `**/*.ts` therefore
1239
+ // collapses to a literal that matches nothing, so this loop silently patched
1240
+ // no files at all. (minimatch 3 normalised the pattern's separators; the repo
1241
+ // now resolves the hoisted minimatch 10, which does not — see package.json.)
1242
+ // `glob` keeps base and pattern apart and returns absolute OS-native paths.
1243
+ const vendoredTsFiles = (0, glob_1.globSync)('**/*.ts', { absolute: true, cwd: coreDir, dot: true, nodir: true });
1244
+ for (const absPath of vendoredTsFiles) {
1242
1245
  if (!filesystem.exists(absPath))
1243
1246
  continue;
1244
1247
  const content = filesystem.read(absPath) || '';
@@ -2516,20 +2519,24 @@ class Server {
2516
2519
  * @param pattern Optional regex for more flexible matching
2517
2520
  */
2518
2521
  findStaleImports(dest, needle, pattern) {
2522
+ // Patterns are relative to `dest` and passed to `glob` rather than
2523
+ // `filesystem.find`: jetpack concatenates the resolved base path in front of
2524
+ // the pattern, and minimatch treats a backslash inside a PATTERN as an escape,
2525
+ // so on Windows every slash-bearing pattern below matched nothing. This
2526
+ // function would then report "no stale imports" for a project that has them —
2527
+ // a false clean bill of health on the one check meant to catch a broken
2528
+ // conversion. See the same fix at the vendored-express patch above.
2519
2529
  const globs = [
2520
- `${dest}/src/server/**/*.ts`,
2521
- `${dest}/src/main.ts`,
2522
- `${dest}/src/config.env.ts`,
2523
- `${dest}/tests/**/*.ts`,
2524
- `${dest}/migrations/**/*.ts`,
2525
- `${dest}/scripts/**/*.ts`,
2530
+ 'src/server/**/*.ts',
2531
+ 'src/main.ts',
2532
+ 'src/config.env.ts',
2533
+ 'tests/**/*.ts',
2534
+ 'migrations/**/*.ts',
2535
+ 'scripts/**/*.ts',
2526
2536
  ];
2527
2537
  const stale = [];
2528
2538
  for (const glob of globs) {
2529
- const files = this.filesystem.find(dest, {
2530
- matching: glob.replace(`${dest}/`, ''),
2531
- recursive: true,
2532
- }) || [];
2539
+ const files = (0, glob_1.globSync)(glob, { absolute: true, cwd: dest, dot: true, nodir: true });
2533
2540
  for (const file of files) {
2534
2541
  const raw = this.filesystem.read(file) || '';
2535
2542
  // Strip comments before matching. The keyword-anchored pattern is not enough on its own: