@lenne.tech/cli 1.50.6 → 1.51.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 (44) hide show
  1. package/build/commands/claude/plugins.js +29 -16
  2. package/build/commands/cli/create.js +11 -3
  3. package/build/commands/cli/rename.js +10 -2
  4. package/build/commands/dev/up.js +9 -0
  5. package/build/commands/directus/docker-setup.js +2 -2
  6. package/build/commands/directus/remove.js +1 -1
  7. package/build/commands/directus/typegen.js +31 -4
  8. package/build/commands/frontend/convert-mode.js +16 -0
  9. package/build/commands/frontend/nuxt.js +1 -1
  10. package/build/commands/fullstack/add-api.js +1 -2
  11. package/build/commands/fullstack/add-app.js +1 -1
  12. package/build/commands/fullstack/convert-mode.js +20 -0
  13. package/build/commands/fullstack/init.js +9 -6
  14. package/build/commands/git/clean.js +3 -2
  15. package/build/commands/git/create.js +7 -2
  16. package/build/commands/git/force-pull.js +4 -2
  17. package/build/commands/git/get.js +19 -6
  18. package/build/commands/git/rebase.js +13 -3
  19. package/build/commands/git/rename.js +5 -4
  20. package/build/commands/git/reset.js +18 -10
  21. package/build/commands/git/squash.js +4 -2
  22. package/build/commands/git/update.js +2 -1
  23. package/build/commands/npm/reinit.js +3 -3
  24. package/build/commands/server/add-property.js +10 -0
  25. package/build/commands/server/convert-mode.js +16 -0
  26. package/build/commands/server/permissions.js +10 -10
  27. package/build/commands/starter/chrome-extension.js +8 -2
  28. package/build/commands/typescript/create.js +3 -2
  29. package/build/extensions/api-mode.js +1 -1
  30. package/build/extensions/frontend-helper.js +25 -13
  31. package/build/extensions/git.js +25 -14
  32. package/build/extensions/server.js +14 -5
  33. package/build/extensions/template.js +8 -5
  34. package/build/extensions/typescript.js +4 -3
  35. package/build/lib/caddy.js +72 -2
  36. package/build/lib/claude-cli.js +17 -2
  37. package/build/lib/exec-args.js +117 -0
  38. package/build/lib/plugin-permissions.js +201 -0
  39. package/build/lib/plugin-utils.js +96 -36
  40. package/build/lib/safe-paths.js +57 -0
  41. package/build/lib/unknown-options.js +53 -0
  42. package/build/lib/workspace-integration.js +4 -2
  43. package/docs/commands.md +12 -0
  44. package/package.json +6 -6
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ const exec_args_1 = require("../../lib/exec-args");
12
13
  /**
13
14
  * Rename branch
14
15
  */
@@ -20,7 +21,7 @@ const NewCommand = {
20
21
  run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
21
22
  var _a, _b, _c;
22
23
  // Retrieve the tools we need
23
- const { config, git, helper, parameters, print: { error, info, spin, success, warning }, prompt: { confirm }, system: { run, startTimer }, } = toolbox;
24
+ const { config, git, helper, parameters, print: { error, info, spin, success, warning }, prompt: { confirm }, system: { startTimer }, } = toolbox;
24
25
  // Load configuration
25
26
  const ltConfig = config.loadConfig();
26
27
  // Parse CLI arguments
@@ -85,10 +86,10 @@ const NewCommand = {
85
86
  const remote = yield git.getBranch(name, { exact: true, remote: true });
86
87
  // Rename branch
87
88
  const renameSpin = spin(`Rename ${branch} into ${name}`);
88
- yield run(`git branch -m ${name}`);
89
+ yield (0, exec_args_1.execArgs)('git', ['branch', '-m', (0, exec_args_1.safeRef)(name)]);
89
90
  // Ask to push branch
90
91
  if (remote && (noConfirm || (yield confirm(`Push ${name} to remote?`)))) {
91
- yield run(`git push origin ${name}`);
92
+ yield (0, exec_args_1.execArgs)('git', ['push', 'origin', (0, exec_args_1.safeRef)(name)]);
92
93
  }
93
94
  renameSpin.succeed();
94
95
  // Save time
@@ -98,7 +99,7 @@ const NewCommand = {
98
99
  (parameters.options.deleteRemote || (!noConfirm && (yield confirm(`Delete remote branch ${branch}?`))))) {
99
100
  timer = startTimer();
100
101
  const deleteSpin = spin(`Delete remote branch ${branch}`);
101
- yield run(`git push origin :${branch}`);
102
+ yield (0, exec_args_1.execArgs)('git', ['push', 'origin', `:${(0, exec_args_1.safeRef)(branch)}`]);
102
103
  deleteSpin.succeed();
103
104
  time += timer();
104
105
  }
@@ -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
  const path_1 = require("path");
13
+ const exec_args_1 = require("../../lib/exec-args");
13
14
  const git_env_1 = require("../../lib/git-env");
14
15
  /**
15
16
  * Environment that keeps the `git ls-remote` below non-interactive.
@@ -66,7 +67,9 @@ const NewCommand = {
66
67
  // Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
67
68
  let remoteBranch = '';
68
69
  try {
69
- remoteBranch = yield system.run(`git ls-remote --heads origin ${branch}`, { env: (0, git_env_1.nonInteractiveGitEnv)() });
70
+ remoteBranch = yield (0, exec_args_1.execArgs)('git', ['ls-remote', '--heads', 'origin', (0, exec_args_1.safeRef)(branch)], {
71
+ env: (0, git_env_1.nonInteractiveGitEnv)(),
72
+ });
70
73
  }
71
74
  catch (_d) {
72
75
  // ignore - unreachable remote is reported as "no remote branch" below
@@ -95,7 +98,7 @@ const NewCommand = {
95
98
  // Show commits that would be lost (none listed when the upstream ref is unknown locally)
96
99
  let localCommits = '';
97
100
  try {
98
- localCommits = yield system.run(`git log origin/${branch}..HEAD --oneline`);
101
+ localCommits = yield (0, exec_args_1.execArgs)('git', ['log', `origin/${(0, exec_args_1.safeRef)(branch)}..HEAD`, '--oneline']);
99
102
  }
100
103
  catch (_e) {
101
104
  // ignore - no upstream ref, so nothing ahead to report
@@ -124,14 +127,19 @@ const NewCommand = {
124
127
  }
125
128
  // Reset
126
129
  const resetSpin = spin(`Reset ${branch}`);
127
- yield system.run('git clean -fd && ' +
128
- 'git reset HEAD --hard && ' +
129
- 'git checkout main && ' +
130
- 'git fetch && ' +
131
- 'git pull && ' +
132
- `git branch -D ${branch} && ` +
133
- `git checkout ${branch} && ` +
134
- 'git pull');
130
+ // Argument lists, no shell: the current branch name can come from a remote (see exec-args.ts).
131
+ for (const args of [
132
+ ['clean', '-fd'],
133
+ ['reset', 'HEAD', '--hard'],
134
+ ['checkout', 'main'],
135
+ ['fetch'],
136
+ ['pull'],
137
+ ['branch', '-D', (0, exec_args_1.safeRef)(branch)],
138
+ ['checkout', (0, exec_args_1.safeRef)(branch)],
139
+ ['pull'],
140
+ ]) {
141
+ yield (0, exec_args_1.execArgs)('git', args);
142
+ }
135
143
  resetSpin.succeed();
136
144
  // Install packages with correctly detected package manager (supports monorepo lockfiles)
137
145
  const { path: pkgPath } = yield npm.getPackageJson();
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ const exec_args_1 = require("../../lib/exec-args");
12
13
  /**
13
14
  * Squash branch
14
15
  */
@@ -74,7 +75,7 @@ const NewCommand = {
74
75
  return `dry-run squash branch ${branch} (no merge base)`;
75
76
  }
76
77
  // Show commits that would be squashed
77
- const commitsToSquash = yield run(`git log ${mergeBase}..HEAD --oneline`);
78
+ const commitsToSquash = yield (0, exec_args_1.execArgs)('git', ['log', `${(0, exec_args_1.safeRef)(mergeBase)}..HEAD`, '--oneline']);
78
79
  if (commitsToSquash === null || commitsToSquash === void 0 ? void 0 : commitsToSquash.trim()) {
79
80
  const commitCount = commitsToSquash.trim().split('\n').length;
80
81
  info(`Would squash ${commitCount} commit(s) on branch "${branch}" into base "${base}":`);
@@ -201,7 +202,8 @@ const NewCommand = {
201
202
  // Start spinner
202
203
  const commitSpin = spin('Commit');
203
204
  // Commit and push
204
- yield run(`git commit -am "${message}" --author="${author}"`);
205
+ // Argument list, no shell: message and author are free text from the user or lt.config.
206
+ yield (0, exec_args_1.execArgs)('git', ['commit', '-am', message, `--author=${author}`]);
205
207
  commitSpin.succeed();
206
208
  if (!noConfirm && !(yield confirm('Push force?'))) {
207
209
  return;
@@ -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
  const path_1 = require("path");
13
+ const exec_args_1 = require("../../lib/exec-args");
13
14
  const git_env_1 = require("../../lib/git-env");
14
15
  /**
15
16
  * Environment that keeps the `git` calls below non-interactive.
@@ -74,7 +75,7 @@ const NewCommand = {
74
75
  // Check for incoming commits (none yet when the upstream ref is unknown locally)
75
76
  let incomingCommits = '';
76
77
  try {
77
- incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline`);
78
+ incomingCommits = yield (0, exec_args_1.execArgs)('git', ['log', `${(0, exec_args_1.safeRef)(branch)}..origin/${branch}`, '--oneline']);
78
79
  }
79
80
  catch (_e) {
80
81
  // ignore - no upstream ref, so nothing incoming to report
@@ -76,7 +76,7 @@ const NewCommand = {
76
76
  const detectedPm = pm.detect((0, path_1.dirname)(path));
77
77
  if (data.scripts && data.scripts.reinit) {
78
78
  const ownReinitSpin = spin('Reinitialize packages');
79
- yield system.run(`cd ${(0, path_1.dirname)(path)} && ${pm.run('reinit', detectedPm)}`);
79
+ yield system.run(pm.run('reinit', detectedPm), { cwd: (0, path_1.dirname)(path) });
80
80
  ownReinitSpin.succeed();
81
81
  }
82
82
  else {
@@ -92,12 +92,12 @@ const NewCommand = {
92
92
  reinitSpin.succeed();
93
93
  if (data.scripts && data.scripts['test:e2e']) {
94
94
  const testE2eSpin = spin('Run tests');
95
- yield system.run(`cd ${(0, path_1.dirname)(path)} && ${pm.run('test:e2e', detectedPm)}`);
95
+ yield system.run(pm.run('test:e2e', detectedPm), { cwd: (0, path_1.dirname)(path) });
96
96
  testE2eSpin.succeed();
97
97
  }
98
98
  else if (data.scripts && data.scripts && data.scripts.test) {
99
99
  const testSpin = spin('Run tests');
100
- yield system.run(`cd ${(0, path_1.dirname)(path)} && ${pm.run('test', detectedPm)}`);
100
+ yield system.run(pm.run('test', detectedPm), { cwd: (0, path_1.dirname)(path) });
101
101
  testSpin.succeed();
102
102
  }
103
103
  }
@@ -15,7 +15,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.help = void 0;
16
16
  const path_1 = require("path");
17
17
  const ts_morph_1 = require("ts-morph");
18
+ const fail_run_1 = require("../../lib/fail-run");
18
19
  const framework_detection_1 = require("../../lib/framework-detection");
20
+ const safe_paths_1 = require("../../lib/safe-paths");
19
21
  const module_1 = __importDefault(require("./module"));
20
22
  const object_1 = __importDefault(require("./object"));
21
23
  /**
@@ -121,6 +123,14 @@ const NewCommand = {
121
123
  error(`No src directory in "${path}".`);
122
124
  return;
123
125
  }
126
+ // One directory name, nothing else: `--element ../../../other-project/src/server/modules/x`
127
+ // passed the existence check below and edited another project.
128
+ if (!(0, safe_paths_1.isSafePathSegment)(String(elementToEdit))) {
129
+ info('');
130
+ error(`"${elementToEdit}" is not a ${String(objectOrModule).toLowerCase()} name: no "/", "\\" or "..".`);
131
+ (0, fail_run_1.failRun)(toolbox);
132
+ return;
133
+ }
124
134
  // Verify the target module/object directory exists
125
135
  const targetDir = objectOrModule === 'Module'
126
136
  ? (0, path_1.join)(path, 'src', 'server', 'modules', elementToEdit)
@@ -10,7 +10,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.help = void 0;
13
+ const fail_run_1 = require("../../lib/fail-run");
13
14
  const framework_detection_1 = require("../../lib/framework-detection");
15
+ const unknown_options_1 = require("../../lib/unknown-options");
16
+ /** The flag names `lt fullstack convert-mode` uses for the same thing, mapped to the real ones for the error message. */
17
+ const RENAMED_FLAGS = {
18
+ 'framework-upstream-branch': 'upstream-branch',
19
+ 'framework-version': 'version',
20
+ };
14
21
  /**
15
22
  * Convert an existing API project between npm mode and vendor mode.
16
23
  *
@@ -67,6 +74,15 @@ const ConvertModeCommand = {
67
74
  if (toolbox.tools.helpJson(exports.help)) {
68
75
  return;
69
76
  }
77
+ // Refuse flags this command does not know: a misspelt --skip-* would otherwise be ignored
78
+ // and the conversion would touch what the caller asked to leave alone (see unknown-options.ts).
79
+ const declaredFlags = exports.help.options.map((option) => option.flag);
80
+ const unknownFlags = (0, unknown_options_1.findUnknownOptions)(parameters.options, declaredFlags);
81
+ if (unknownFlags.length > 0) {
82
+ error((0, unknown_options_1.formatUnknownOptions)(unknownFlags, declaredFlags, RENAMED_FLAGS));
83
+ (0, fail_run_1.failRun)(toolbox);
84
+ return 'convert-mode: unknown option';
85
+ }
70
86
  const targetMode = parameters.options.to;
71
87
  if (!targetMode || !['npm', 'vendor'].includes(targetMode)) {
72
88
  error('Missing or invalid --to flag. Use: --to vendor or --to npm');
@@ -10,10 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.help = void 0;
13
- const child_process_1 = require("child_process");
14
13
  const fs_1 = require("fs");
15
14
  const path_1 = require("path");
16
15
  const framework_detection_1 = require("../../lib/framework-detection");
16
+ const safe_paths_1 = require("../../lib/safe-paths");
17
17
  // ────────────────────────────────────────────────────────────────────────────
18
18
  // Helper functions (alphabetically sorted per ESLint rules)
19
19
  // ────────────────────────────────────────────────────────────────────────────
@@ -132,14 +132,6 @@ function loadScanner(projectPath) {
132
132
  return null;
133
133
  });
134
134
  }
135
- /**
136
- * Open a file in the default browser/application
137
- */
138
- function openFile(filePath) {
139
- const platform = process.platform;
140
- const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
141
- (0, child_process_1.exec)(`${cmd} "${filePath}"`);
142
- }
143
135
  /**
144
136
  * Print console summary
145
137
  */
@@ -313,6 +305,13 @@ const PermissionsCommand = {
313
305
  defaultValue: defaultOutputName,
314
306
  }) || defaultOutputName;
315
307
  const outputPath = (0, path_1.join)(projectPath, outputFile);
308
+ // --output and lt.config's `output` must not write outside the project (`../../x`).
309
+ if (!(0, safe_paths_1.isPathInside)(projectPath, outputPath)) {
310
+ error(`Output ${outputFile} lies outside the project ${projectPath}. Use a path inside the project.`);
311
+ if (!parameters.options.fromGluegunMenu)
312
+ process.exit(1);
313
+ return 'permissions: output outside project';
314
+ }
316
315
  // Load scanner from project's @lenne.tech/nest-server
317
316
  const spinner = spin('Loading permissions scanner...');
318
317
  const scanner = yield loadScanner(projectPath);
@@ -374,7 +373,8 @@ const PermissionsCommand = {
374
373
  }
375
374
  // Open in browser
376
375
  if (shouldOpen && (format === 'html' || format === 'md')) {
377
- openFile(outputPath);
376
+ // No shell: the file name comes from --output or lt.config (see safe-paths.ts).
377
+ (0, safe_paths_1.openFileWithDefaultApp)(outputPath);
378
378
  info(`Report opened in default application`);
379
379
  }
380
380
  // Summary line (always shown)
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ const exec_args_1 = require("../../lib/exec-args");
12
13
  /**
13
14
  * Create a new TypeScript project
14
15
  */
@@ -46,7 +47,12 @@ const NewCommand = {
46
47
  }
47
48
  // Clone git repository
48
49
  const cloneSpinner = spin('Clone https://github.com/lenneTech/chrome-extension-angular-starter.git');
49
- yield system.run(`git clone https://github.com/lenneTech/chrome-extension-angular-starter.git ${projectDir}`);
50
+ yield (0, exec_args_1.execArgs)('git', [
51
+ 'clone',
52
+ '--',
53
+ 'https://github.com/lenneTech/chrome-extension-angular-starter.git',
54
+ projectDir,
55
+ ]);
50
56
  if (filesystem.isDirectory(`./${projectDir}`)) {
51
57
  filesystem.remove(`./${projectDir}/.git`);
52
58
  cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/chrome-extension-angular-starter.git');
@@ -102,7 +108,7 @@ const NewCommand = {
102
108
  prepareSpinner.succeed('Files prepared');
103
109
  // Install packages
104
110
  const installSpinner = spin('Install packages');
105
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(toolbox.pm.detect(projectDir))}`);
111
+ yield system.run(toolbox.pm.install(toolbox.pm.detect(projectDir)), { cwd: projectDir });
106
112
  installSpinner.succeed('Packages installed');
107
113
  // Init git
108
114
  const initGitSpinner = spin('Initialize git');
@@ -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
  const path_1 = require("path");
13
+ const exec_args_1 = require("../../lib/exec-args");
13
14
  /**
14
15
  * Create a new TypeScript project
15
16
  */
@@ -61,7 +62,7 @@ const NewCommand = {
61
62
  // Clone git repository
62
63
  const cloneSpinner = spin('Clone https://github.com/lenneTech/typescript-starter.git');
63
64
  try {
64
- yield system.run(`git clone https://github.com/lenneTech/typescript-starter.git ${projectDir}`);
65
+ yield (0, exec_args_1.execArgs)('git', ['clone', '--', 'https://github.com/lenneTech/typescript-starter.git', projectDir]);
65
66
  if (filesystem.isDirectory(`./${projectDir}`)) {
66
67
  filesystem.remove(`./${projectDir}/.git`);
67
68
  cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/typescript-starter.git');
@@ -153,7 +154,7 @@ const NewCommand = {
153
154
  // Install packages
154
155
  const installSpinner = spin('Install packages');
155
156
  try {
156
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(toolbox.pm.detect(projectDir))}`);
157
+ yield system.run(toolbox.pm.install(toolbox.pm.detect(projectDir)), { cwd: projectDir });
157
158
  installSpinner.succeed('Packages installed');
158
159
  }
159
160
  catch (err) {
@@ -103,7 +103,7 @@ class ApiMode {
103
103
  const { pm, system } = this.toolbox;
104
104
  const runner = (_c = (_b = pm === null || pm === void 0 ? void 0 : pm.run) === null || _b === void 0 ? void 0 : _b.call(pm, formatScript, pm.detect(projectPath))) !== null && _c !== void 0 ? _c : `pnpm run ${formatScript}`;
105
105
  try {
106
- yield system.run(`cd "${projectPath}" && ${runner}`);
106
+ yield system.run(runner, { cwd: projectPath });
107
107
  }
108
108
  catch (_e) {
109
109
  // Non-fatal: the user can run format manually if this misbehaves.
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  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
+ const exec_args_1 = require("../lib/exec-args");
14
15
  const markdown_table_1 = require("../lib/markdown-table");
15
16
  const module_specifiers_1 = require("../lib/module-specifiers");
16
17
  const strip_vendor_schema_augmentation_1 = require("../lib/strip-vendor-schema-augmentation");
@@ -184,7 +185,8 @@ class FrontendHelper {
184
185
  if (!skipInstall && result.method !== 'link') {
185
186
  try {
186
187
  const { pm } = this.toolbox;
187
- yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
188
+ // `cwd` instead of `cd "<dest>" &&`: the directory name must not be shell syntax.
189
+ yield system.run(pm.install(pm.detect(dest)), { cwd: dest });
188
190
  }
189
191
  catch (_a) {
190
192
  return { method: result.method, path: dest, success: false };
@@ -223,7 +225,8 @@ class FrontendHelper {
223
225
  if (!skipInstall) {
224
226
  try {
225
227
  const { pm } = this.toolbox;
226
- yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
228
+ // `cwd` instead of `cd "<dest>" &&`: the directory name must not be shell syntax.
229
+ yield system.run(pm.install(pm.detect(dest)), { cwd: dest });
227
230
  }
228
231
  catch (err) {
229
232
  return { method: result.method, path: dest, success: false };
@@ -232,12 +235,13 @@ class FrontendHelper {
232
235
  // Initialize git
233
236
  if (!skipGitInit) {
234
237
  try {
235
- yield system.run(`cd "${dest}" && git init --initial-branch=main`);
238
+ // Argument lists, no shell: gitLink comes from a CLI flag or lt.config (see exec-args.ts).
239
+ yield (0, exec_args_1.execArgs)('git', ['init', '--initial-branch=main'], { cwd: dest });
236
240
  if (gitLink) {
237
- yield system.run(`cd "${dest}" && git remote add origin ${gitLink}`);
238
- yield system.run(`cd "${dest}" && git add .`);
239
- yield system.run(`cd "${dest}" && git commit -m "Initial commit"`);
240
- yield system.run(`cd "${dest}" && git push -u origin main`);
241
+ yield (0, exec_args_1.execArgs)('git', ['remote', 'add', '--', 'origin', gitLink], { cwd: dest });
242
+ yield (0, exec_args_1.execArgs)('git', ['add', '.'], { cwd: dest });
243
+ yield (0, exec_args_1.execArgs)('git', ['commit', '-m', 'Initial commit'], { cwd: dest });
244
+ yield (0, exec_args_1.execArgs)('git', ['push', '-u', 'origin', 'main'], { cwd: dest });
241
245
  }
242
246
  }
243
247
  catch (err) {
@@ -260,7 +264,7 @@ class FrontendHelper {
260
264
  // Add localize
261
265
  if (localize) {
262
266
  try {
263
- yield system.run(`cd "${dest}" && ng add @angular/localize --skip-confirmation`);
267
+ yield system.run('ng add @angular/localize --skip-confirmation', { cwd: dest });
264
268
  }
265
269
  catch (_a) {
266
270
  // Localize failure is not fatal
@@ -270,7 +274,7 @@ class FrontendHelper {
270
274
  if (!skipInstall) {
271
275
  try {
272
276
  const { pm: pmHelper } = this.toolbox;
273
- yield system.run(`cd "${dest}" && ${pmHelper.run('init', pmHelper.detect(dest))}`);
277
+ yield system.run(pmHelper.run('init', pmHelper.detect(dest)), { cwd: dest });
274
278
  }
275
279
  catch (_b) {
276
280
  // Init script failure is not fatal
@@ -421,13 +425,21 @@ class FrontendHelper {
421
425
  return __awaiter(this, void 0, void 0, function* () {
422
426
  const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nuxt-extensions.git' } = options;
423
427
  const path = require('node:path');
424
- const { filesystem, system } = this.toolbox;
428
+ const { filesystem } = this.toolbox;
425
429
  const coreDir = path.join(dest, 'app', 'core');
426
430
  // ── 1. Clone @lenne.tech/nuxt-extensions into temp dir ──────────────
427
431
  const tmpClone = path.join(require('os').tmpdir(), `lt-vendor-nuxt-ext-${Date.now()}`);
428
- const branchArg = upstreamBranch ? `--branch ${upstreamBranch} ` : '';
429
432
  try {
430
- yield system.run(`git clone --depth 1 ${branchArg}${upstreamRepoUrl} ${tmpClone}`);
433
+ // Argument list, no shell: the branch comes from a CLI flag or lt.config (see exec-args.ts).
434
+ yield (0, exec_args_1.execArgs)('git', [
435
+ 'clone',
436
+ '--depth',
437
+ '1',
438
+ ...(upstreamBranch ? ['--branch', upstreamBranch] : []),
439
+ '--',
440
+ upstreamRepoUrl,
441
+ tmpClone,
442
+ ]);
431
443
  }
432
444
  catch (err) {
433
445
  const raw = err.message || '';
@@ -476,7 +488,7 @@ class FrontendHelper {
476
488
  }
477
489
  let upstreamCommit = '';
478
490
  try {
479
- const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
491
+ const sha = yield (0, exec_args_1.execArgs)('git', ['-C', tmpClone, 'rev-parse', 'HEAD']);
480
492
  upstreamCommit = (sha || '').trim();
481
493
  }
482
494
  catch (_c) {
@@ -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 exec_args_1 = require("../lib/exec-args");
13
14
  const git_env_1 = require("../lib/git-env");
14
15
  /**
15
16
  * Git functions
@@ -96,10 +97,18 @@ class Git {
96
97
  showWarning: false,
97
98
  }, options);
98
99
  // Toolbox features
99
- const { print: { warning }, system, } = this.toolbox;
100
+ const { print: { warning }, } = this.toolbox;
100
101
  // Get diff
101
102
  try {
102
- const diff = yield system.run(`git --no-pager diff --name-only ${branch} ${opts.otherBranch}`);
103
+ // Every git call in this file that carries a value runs as an argument list, no shell:
104
+ // branch names come from the user, from lt.config and from the remote (see exec-args.ts).
105
+ const diff = yield (0, exec_args_1.execArgs)('git', [
106
+ '--no-pager',
107
+ 'diff',
108
+ '--name-only',
109
+ (0, exec_args_1.safeRef)(branch),
110
+ (0, exec_args_1.safeRef)(opts.otherBranch),
111
+ ]);
103
112
  // Return relative file paths as array
104
113
  return diff.split(/\r?\n/).filter((item) => item);
105
114
  }
@@ -147,8 +156,8 @@ class Git {
147
156
  getMergeBase() {
148
157
  return __awaiter(this, arguments, void 0, function* (baseBranch = 'dev') {
149
158
  // Toolbox features
150
- const { helper: { trim }, system: { run }, } = this.toolbox;
151
- return trim(yield run(`git merge-base HEAD ${baseBranch}`));
159
+ const { helper: { trim }, } = this.toolbox;
160
+ return trim(yield (0, exec_args_1.execArgs)('git', ['merge-base', 'HEAD', (0, exec_args_1.safeRef)(baseBranch)]));
152
161
  });
153
162
  }
154
163
  /**
@@ -163,10 +172,9 @@ class Git {
163
172
  throw new Error('Missing branch');
164
173
  }
165
174
  // Toolbox features
166
- const { helper: { trim }, system: { run }, } = this.toolbox;
175
+ const { helper: { trim }, } = this.toolbox;
167
176
  try {
168
- const logCommand = `git log ${baseBranch}..${branch} --oneline`;
169
- const output = yield run(logCommand);
177
+ const output = yield (0, exec_args_1.execArgs)('git', ['log', `${(0, exec_args_1.safeRef)(baseBranch)}..${(0, exec_args_1.safeRef)(branch)}`, '--oneline']);
170
178
  if (!output) {
171
179
  throw new Error('No commits found');
172
180
  }
@@ -277,14 +285,18 @@ class Git {
277
285
  }
278
286
  // Search branch
279
287
  if (opts.exact) {
280
- if (opts.remote) {
281
- if (!(yield system.run(`git ls-remote --heads origin ${branch}`))) {
288
+ if (!(0, exec_args_1.isSafeRef)(branch)) {
289
+ // Cannot be a branch name; report it as not found instead of handing it to git.
290
+ branch = null;
291
+ }
292
+ else if (opts.remote) {
293
+ if (!(yield (0, exec_args_1.execArgs)('git', ['ls-remote', '--heads', 'origin', (0, exec_args_1.safeRef)(branch)]))) {
282
294
  branch = null;
283
295
  }
284
296
  }
285
297
  else {
286
298
  try {
287
- yield system.run(`git rev-parse --verify ${branch}`);
299
+ yield (0, exec_args_1.execArgs)('git', ['rev-parse', '--verify', (0, exec_args_1.safeRef)(branch)]);
288
300
  }
289
301
  catch (e) {
290
302
  branch = null;
@@ -310,7 +322,7 @@ class Git {
310
322
  branch = trim(branch);
311
323
  // Check remote, if not done before
312
324
  if (opts.remote && !opts.exact) {
313
- const remoteBranch = trim(yield system.run(`git ls-remote --heads origin ${branch}`));
325
+ const remoteBranch = trim(yield (0, exec_args_1.execArgs)('git', ['ls-remote', '--heads', 'origin', (0, exec_args_1.safeRef)(branch)]));
314
326
  if (!remoteBranch) {
315
327
  if (opts.spin) {
316
328
  searchSpin.fail();
@@ -323,7 +335,7 @@ class Git {
323
335
  }
324
336
  // Check local
325
337
  if (opts.local) {
326
- const remoteBranch = trim(yield system.run(`git rev-parse --verify --quiet ${branch}`));
338
+ const remoteBranch = trim(yield (0, exec_args_1.execArgs)('git', ['rev-parse', '--verify', '--quiet', (0, exec_args_1.safeRef)(branch)]));
327
339
  if (!remoteBranch) {
328
340
  if (opts.spin) {
329
341
  searchSpin.fail();
@@ -357,8 +369,7 @@ class Git {
357
369
  */
358
370
  reset(mergeBase, soft = false) {
359
371
  // Toolbox features
360
- const { system: { run }, } = this.toolbox;
361
- return run(soft ? `git reset --soft ${mergeBase}` : `git reset ${mergeBase}`);
372
+ return (0, exec_args_1.execArgs)('git', soft ? ['reset', '--soft', (0, exec_args_1.safeRef)(mergeBase)] : ['reset', (0, exec_args_1.safeRef)(mergeBase)]);
362
373
  }
363
374
  /**
364
375
  * Select a branch
@@ -50,6 +50,7 @@ const ts = __importStar(require("typescript"));
50
50
  const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
51
51
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
52
52
  const cross_env_1 = require("../lib/cross-env");
53
+ const exec_args_1 = require("../lib/exec-args");
53
54
  const express_type_imports_1 = require("../lib/express-type-imports");
54
55
  const markdown_table_1 = require("../lib/markdown-table");
55
56
  const migrate_scripts_1 = require("../lib/migrate-scripts");
@@ -762,7 +763,7 @@ class Server {
762
763
  if (!skipInstall && !experimental) {
763
764
  try {
764
765
  const { pm } = this.toolbox;
765
- yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
766
+ yield system.run(pm.install(pm.detect(dest)), { cwd: dest });
766
767
  }
767
768
  catch (err) {
768
769
  return { method: result.method, path: dest, success: false };
@@ -958,7 +959,6 @@ class Server {
958
959
  var _a;
959
960
  const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nest-server.git' } = options;
960
961
  const filesystem = this.filesystem;
961
- const { system } = this.toolbox;
962
962
  const os = require('os');
963
963
  const path = require('path');
964
964
  const { Project, SyntaxKind } = require('ts-morph');
@@ -970,9 +970,18 @@ class Server {
970
970
  // `bin/migrate.js`, and associated meta files. The clone lives in a
971
971
  // throw-away tmp dir that gets cleaned up at the end.
972
972
  const tmpClone = path.join(os.tmpdir(), `lt-vendor-nest-server-${Date.now()}`);
973
- const branchArg = upstreamBranch ? `--branch ${upstreamBranch} ` : '';
974
973
  try {
975
- yield system.run(`git clone --depth 1 ${branchArg}${upstreamRepoUrl} ${tmpClone}`);
974
+ // Argument list, no shell: the branch comes from --framework-upstream-branch or
975
+ // lt.config (see exec-args.ts).
976
+ yield (0, exec_args_1.execArgs)('git', [
977
+ 'clone',
978
+ '--depth',
979
+ '1',
980
+ ...(upstreamBranch ? ['--branch', upstreamBranch] : []),
981
+ '--',
982
+ upstreamRepoUrl,
983
+ tmpClone,
984
+ ]);
976
985
  }
977
986
  catch (err) {
978
987
  // Clone failures usually boil down to one of four causes — network,
@@ -1067,7 +1076,7 @@ class Server {
1067
1076
  // Snapshot the upstream commit SHA for traceability in VENDOR.md.
1068
1077
  let upstreamCommit = '';
1069
1078
  try {
1070
- const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
1079
+ const sha = yield (0, exec_args_1.execArgs)('git', ['-C', tmpClone, 'rev-parse', 'HEAD']);
1071
1080
  upstreamCommit = (sha || '').trim();
1072
1081
  }
1073
1082
  catch (_f) {