@lenne.tech/cli 1.50.6 → 1.51.1

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 +37 -18
  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
@@ -56,25 +56,38 @@ function installPlugin(plugin, cli, toolbox) {
56
56
  info('');
57
57
  info(`Warning: No skills or commands found for ${plugin.pluginName}. Plugin may not be installed correctly.`);
58
58
  }
59
- // Step 3: Setup permissions
60
- if (pluginContents.permissions.length > 0) {
61
- const permSpinner = spin(`Configuring permissions for ${plugin.pluginName}`);
62
- const permResult = (0, plugin_utils_1.setupPermissions)(pluginContents.permissions, error);
63
- if (permResult.success) {
64
- if (permResult.added.length > 0) {
65
- permSpinner.succeed(`Permissions configured for ${plugin.pluginName} (${permResult.added.length} added)`);
66
- }
67
- else {
68
- permSpinner.succeed(`Permissions already configured for ${plugin.pluginName}`);
69
- }
70
- }
71
- else {
72
- permSpinner.fail(`Failed to configure permissions for ${plugin.pluginName}`);
59
+ // Step 3: Sync permissions. Also for a plugin that ships no rules (any more): rules an
60
+ // earlier run wrote for it, and the ones it names in formerPatterns, still have to go.
61
+ const permSpinner = spin(`Configuring permissions for ${plugin.pluginName}`);
62
+ const permResult = (0, plugin_utils_1.setupPermissions)(plugin, pluginContents, error);
63
+ if (!permResult.success) {
64
+ permSpinner.fail(`Failed to configure permissions for ${plugin.pluginName}`);
65
+ if (permResult.written.length > 0) {
73
66
  info('');
74
- info('Add manually to ~/.claude/settings.json:');
75
- info(JSON.stringify({ permissions: { allow: pluginContents.permissions } }, null, 2));
67
+ info(`Add manually to ${permResult.settingsPath}:`);
68
+ info(JSON.stringify({ permissions: { allow: permResult.written } }, null, 2));
76
69
  }
77
70
  }
71
+ else if (permResult.added.length > 0 || permResult.removed.length > 0) {
72
+ const changes = [
73
+ permResult.added.length > 0 ? `${permResult.added.length} added` : '',
74
+ permResult.removed.length > 0 ? `${permResult.removed.length} removed` : '',
75
+ ].filter(Boolean);
76
+ permSpinner.succeed(`Permissions configured for ${plugin.pluginName} (${changes.join(', ')})`);
77
+ for (const rule of permResult.removed) {
78
+ info(` removed: ${rule}`);
79
+ }
80
+ }
81
+ else if (permResult.written.length > 0) {
82
+ permSpinner.succeed(`Permissions already configured for ${plugin.pluginName}`);
83
+ }
84
+ else {
85
+ permSpinner.stop();
86
+ }
87
+ if (permResult.success && permResult.skipped.length > 0) {
88
+ // ${CLAUDE_PLUGIN_ROOT} cannot be scoped safely (see plugin-permissions.ts); Claude asks instead.
89
+ info(` not written (uses \${CLAUDE_PLUGIN_ROOT}, Claude will ask): ${permResult.skipped.length} rule(s)`);
90
+ }
78
91
  // Step 4: Process post-installation requirements (for LSP plugins, etc.)
79
92
  const postInstallResult = (0, plugin_utils_1.processPostInstall)(plugin.pluginName, toolbox);
80
93
  return { action: pluginAction, contents: pluginContents, postInstall: postInstallResult, success: true };
@@ -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 CLI
14
15
  */
@@ -105,7 +106,7 @@ const NewCommand = {
105
106
  // Clone git repository
106
107
  const cloneSpinner = spin('Clone https://github.com/lenneTech/cli-starter.git');
107
108
  try {
108
- yield system.run(`git clone https://github.com/lenneTech/cli-starter.git ${projectDir}`);
109
+ yield (0, exec_args_1.execArgs)('git', ['clone', '--', 'https://github.com/lenneTech/cli-starter.git', projectDir]);
109
110
  if (filesystem.isDirectory(`./${projectDir}`)) {
110
111
  filesystem.remove(`./${projectDir}/.git`);
111
112
  cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/cli-starter.git');
@@ -123,7 +124,7 @@ const NewCommand = {
123
124
  const detectedPm = toolbox.pm.detect(projectDir);
124
125
  const installSpinner = spin('Install packages');
125
126
  try {
126
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)}`);
127
+ yield system.run(toolbox.pm.install(detectedPm), { cwd: projectDir });
127
128
  installSpinner.succeed('Packages installed');
128
129
  }
129
130
  catch (err) {
@@ -133,7 +134,14 @@ const NewCommand = {
133
134
  // Rename files and data
134
135
  const renameSpinner = spin(`Rename files & data ${link ? ' and link' : ''}`);
135
136
  try {
136
- yield system.run(`cd ${projectDir} && ${toolbox.pm.run('rename', detectedPm)} -- "${name}" --author "${author}" --${link ? 'link' : 'nolink'}`);
137
+ // Argument list, no shell: name and author come from flags and lt.config (see exec-args.ts).
138
+ if ([name, author].some((value) => (0, exec_args_1.hasCmdShimHazard)(String(value)))) {
139
+ throw new Error('name and author must not contain ", % or line breaks on Windows');
140
+ }
141
+ const [runner, ...runArgs] = toolbox.pm.run('rename', detectedPm).split(' ');
142
+ yield (0, exec_args_1.spawnArgs)(runner, [...runArgs, '--', String(name), '--author', String(author), `--${link ? 'link' : 'nolink'}`], {
143
+ cwd: projectDir,
144
+ });
137
145
  renameSpinner.succeed(`Files & data renamed${link ? ' and linked' : ''}`);
138
146
  }
139
147
  catch (err) {
@@ -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
  * Rename current CLI
15
16
  */
@@ -20,7 +21,7 @@ const NewCommand = {
20
21
  name: 'rename',
21
22
  run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
22
23
  // Retrieve the tools we need
23
- const { npm, parameters, print: { error, info }, system, } = toolbox;
24
+ const { npm, parameters, print: { error, info }, } = toolbox;
24
25
  // Info
25
26
  info('Rename current CLI');
26
27
  // Get root path
@@ -35,7 +36,14 @@ const NewCommand = {
35
36
  return;
36
37
  }
37
38
  // Run rename script
38
- yield system.run(`cd ${rootPath} && ${toolbox.pm.run('rename', toolbox.pm.detect(rootPath))} -- ${parameters.string}`);
39
+ // Argument list, no shell: the arguments are passed through from the command line (see exec-args.ts).
40
+ const renameArgs = (parameters.array || []).map(String);
41
+ if (renameArgs.some((value) => (0, exec_args_1.hasCmdShimHazard)(value))) {
42
+ error('Arguments must not contain ", % or line breaks on Windows.');
43
+ return;
44
+ }
45
+ const [runner, ...runArgs] = toolbox.pm.run('rename', toolbox.pm.detect(rootPath)).split(' ');
46
+ yield (0, exec_args_1.spawnArgs)(runner, [...runArgs, '--', ...renameArgs], { cwd: rootPath });
39
47
  // Exit if not running from menu
40
48
  if (!toolbox.parameters.options.fromGluegunMenu) {
41
49
  process.exit();
@@ -313,6 +313,15 @@ const UpCommand = {
313
313
  // that nothing listens on.
314
314
  dbName: identity.subdomains.api ? ((_e = existingEntry === null || existingEntry === void 0 ? void 0 : existingEntry.dbName) !== null && _e !== void 0 ? _e : dbName) : undefined,
315
315
  });
316
+ // A Caddyfile from before the loopback bind would keep this running stack on the LAN until
317
+ // its next restart, because the upsert below is never reached on this path.
318
+ if ((0, caddy_1.healLoopbackBind)()) {
319
+ const reload = yield (0, caddy_1.reloadCaddy)();
320
+ if (reload.ok)
321
+ info('Caddy now listens on this machine only (default_bind 127.0.0.1 [::1]).');
322
+ else
323
+ warning(`caddy reload failed:\n${reload.stderr}`);
324
+ }
316
325
  info('Run `lt dev down` first to force a full restart.');
317
326
  if (!parameters.options.fromGluegunMenu)
318
327
  process.exit();
@@ -431,7 +431,7 @@ const NewCommand = {
431
431
  if (instanceExists) {
432
432
  const stopSpin = spin('Stopping existing containers');
433
433
  try {
434
- yield system.run(`cd ${directusDir} && docker-compose down`);
434
+ yield system.run('docker-compose down', { cwd: directusDir });
435
435
  stopSpin.succeed();
436
436
  }
437
437
  catch (stopError) {
@@ -444,7 +444,7 @@ const NewCommand = {
444
444
  // Start Directus with docker-compose
445
445
  const startSpin = spin('Starting Directus instance');
446
446
  try {
447
- yield system.run(`cd ${directusDir} && docker-compose up -d`);
447
+ yield system.run('docker-compose up -d', { cwd: directusDir });
448
448
  startSpin.succeed();
449
449
  }
450
450
  catch (startError) {
@@ -93,7 +93,7 @@ const NewCommand = {
93
93
  if (filesystem.exists(composePath)) {
94
94
  const stopSpin = spin('Stopping and removing containers');
95
95
  try {
96
- yield system.run(`cd ${instanceDir} && docker-compose down -v`);
96
+ yield system.run('docker-compose down -v', { cwd: instanceDir });
97
97
  stopSpin.succeed();
98
98
  }
99
99
  catch (stopError) {
@@ -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
  * Generate TypeScript types from Directus collections
14
15
  */
@@ -134,13 +135,39 @@ const NewCommand = {
134
135
  }
135
136
  }
136
137
  }
138
+ // url, token and output come from flags or lt.config, which a cloned repository can supply,
139
+ // and they used to sit in double quotes in a shell string, where `$(…)` ran. They are now an
140
+ // argument list (see exec-args.ts). On Windows the runner is a `.cmd` shim that re-parses its
141
+ // arguments through cmd.exe, so values that could break out there are refused first.
142
+ let directusUrl;
143
+ try {
144
+ const parsed = new URL(url);
145
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
146
+ throw new Error(parsed.protocol);
147
+ }
148
+ // The value as given: `href` would append a "/" and change the URL the generator sees.
149
+ directusUrl = url;
150
+ }
151
+ catch (_m) {
152
+ error(`Invalid Directus URL "${url}": expected http:// or https://.`);
153
+ return;
154
+ }
155
+ if (!/^[\w.~+/=-]+$/.test(token)) {
156
+ error('Invalid Directus token: only letters, digits and . _ ~ + / = - are allowed.');
157
+ return;
158
+ }
159
+ if ([directusUrl, output].some((value) => (0, exec_args_1.hasCmdShimHazard)(value))) {
160
+ error('URL and output path must not contain ", % or line breaks on Windows.');
161
+ return;
162
+ }
163
+ const [runner, ...runnerArgs] = useGlobalTypegen
164
+ ? ['directus-sdk-typegen']
165
+ : toolbox.pm.exec('directus-sdk-typegen').split(' ');
137
166
  // Generate types
138
167
  const generateSpin = spin('Generating TypeScript types from Directus');
139
- const command = useGlobalTypegen
140
- ? `directus-sdk-typegen -u "${url}" -t "${token}" -o "${output}"`
141
- : `${toolbox.pm.exec(`directus-sdk-typegen -u "${url}" -t "${token}" -o "${output}"`)}`;
142
168
  try {
143
- yield system.run(command);
169
+ // Asynchronous: a synchronous spawn under the spinner would block Ctrl+C until it exits.
170
+ yield (0, exec_args_1.spawnArgs)(runner, [...runnerArgs, '-u', directusUrl, '-t', token, '-o', output]);
144
171
  generateSpin.succeed();
145
172
  success(`TypeScript types generated successfully at ${output}`);
146
173
  }
@@ -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 frontend_framework_detection_1 = require("../../lib/frontend-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
+ 'frontend-framework-upstream-branch': 'upstream-branch',
19
+ 'frontend-framework-version': 'version',
20
+ };
14
21
  /**
15
22
  * Convert an existing frontend 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');
@@ -225,7 +225,7 @@ const NewCommand = {
225
225
  // leaving pnpm-lock.yaml stale. Resync it — otherwise the first
226
226
  // `pnpm install --frozen-lockfile` (CI, Docker build) fails with
227
227
  // ERR_PNPM_OUTDATED_LOCKFILE and the project cannot be built/deployed.
228
- yield toolbox.system.run(`cd "./${projectDir}" && pnpm install --no-frozen-lockfile`);
228
+ yield toolbox.system.run('pnpm install --no-frozen-lockfile', { cwd: projectDir });
229
229
  vendorSpinner.succeed('Frontend converted to vendor mode (app/core/)');
230
230
  }
231
231
  catch (err) {
@@ -300,7 +300,6 @@ const NewCommand = {
300
300
  apiDir: apiDest,
301
301
  patching,
302
302
  projectDir,
303
- system,
304
303
  });
305
304
  if (renameResult.error) {
306
305
  renameSpinner.warn(`Auto-rename failed (${renameResult.error.message}). Run \`bun run rename ${projectDir}\` manually inside projects/api.`);
@@ -341,7 +340,7 @@ const NewCommand = {
341
340
  const installSpinner = spin('Install workspace packages');
342
341
  try {
343
342
  const detectedPm = toolbox.pm.detect(workspaceDir);
344
- yield system.run(`cd ${workspaceDir} && ${toolbox.pm.install(detectedPm)}`);
343
+ yield system.run(toolbox.pm.install(detectedPm), { cwd: workspaceDir });
345
344
  installSpinner.succeed('Successfully installed workspace packages');
346
345
  }
347
346
  catch (err) {
@@ -268,7 +268,7 @@ const NewCommand = {
268
268
  const installSpinner = spin('Install workspace packages');
269
269
  try {
270
270
  const detectedPm = toolbox.pm.detect(workspaceDir);
271
- yield system.run(`cd ${workspaceDir} && ${toolbox.pm.install(detectedPm)}`);
271
+ yield system.run(toolbox.pm.install(detectedPm), { cwd: workspaceDir });
272
272
  installSpinner.succeed('Successfully installed workspace packages');
273
273
  }
274
274
  catch (err) {
@@ -11,9 +11,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.help = void 0;
13
13
  const path_1 = require("path");
14
+ const fail_run_1 = require("../../lib/fail-run");
14
15
  const framework_detection_1 = require("../../lib/framework-detection");
15
16
  const frontend_framework_detection_1 = require("../../lib/frontend-framework-detection");
17
+ const unknown_options_1 = require("../../lib/unknown-options");
16
18
  const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
19
+ /** The names older lenne.tech plugin docs used for this command's flags, mapped to the real ones for the error message. */
20
+ const RENAMED_FLAGS = {
21
+ 'api-ref': 'framework-upstream-branch',
22
+ 'api-version': 'framework-version',
23
+ 'app-ref': 'frontend-framework-upstream-branch',
24
+ 'app-version': 'frontend-framework-version',
25
+ 'skip-api': 'skip-backend',
26
+ 'skip-app': 'skip-frontend',
27
+ };
17
28
  /**
18
29
  * Convert both backend and frontend of a fullstack monorepo between
19
30
  * npm mode and vendor mode in a single command.
@@ -103,6 +114,15 @@ const ConvertModeCommand = {
103
114
  if (toolbox.tools.helpJson(exports.help)) {
104
115
  return;
105
116
  }
117
+ // Refuse flags this command does not know: a misspelt --skip-* would otherwise be ignored
118
+ // and the conversion would touch what the caller asked to leave alone (see unknown-options.ts).
119
+ const declaredFlags = exports.help.options.map((option) => option.flag);
120
+ const unknownFlags = (0, unknown_options_1.findUnknownOptions)(parameters.options, declaredFlags);
121
+ if (unknownFlags.length > 0) {
122
+ error((0, unknown_options_1.formatUnknownOptions)(unknownFlags, declaredFlags, RENAMED_FLAGS));
123
+ (0, fail_run_1.failRun)(toolbox);
124
+ return 'convert-mode: unknown option';
125
+ }
106
126
  const targetMode = parameters.options.to;
107
127
  if (!targetMode || !['npm', 'vendor'].includes(targetMode)) {
108
128
  error('Missing or invalid --to flag. Use: --to vendor or --to npm');
@@ -16,6 +16,7 @@ const gluegun_1 = require("gluegun");
16
16
  const caddy_1 = require("../../lib/caddy");
17
17
  const dev_migrate_helper_1 = require("../../lib/dev-migrate-helper");
18
18
  const dev_project_1 = require("../../lib/dev-project");
19
+ const exec_args_1 = require("../../lib/exec-args");
19
20
  const fail_run_1 = require("../../lib/fail-run");
20
21
  const package_name_1 = require("../../lib/package-name");
21
22
  const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
@@ -419,7 +420,7 @@ const NewCommand = {
419
420
  const workspaceSpinner = spin(`Create fullstack workspace with ${frontend} in ${projectDir} with ${name} app`);
420
421
  // Clone monorepo
421
422
  try {
422
- yield system.run(`git clone https://github.com/lenneTech/lt-monorepo.git ${projectDir}`);
423
+ yield (0, exec_args_1.execArgs)('git', ['clone', '--', 'https://github.com/lenneTech/lt-monorepo.git', projectDir]);
423
424
  }
424
425
  catch (err) {
425
426
  workspaceSpinner.fail(`Failed to clone monorepo: ${err.message}`);
@@ -485,7 +486,8 @@ const NewCommand = {
485
486
  (0, package_name_1.setPackageName)({ filesystem, name: projectDir, packageJsonPath: `${projectDir}/package.json` });
486
487
  // Always initialize git
487
488
  try {
488
- yield system.run(`cd ${projectDir} && git init --initial-branch=dev`);
489
+ // Argument lists, no shell: projectDir comes from --name, gitLink from a flag or lt.config.
490
+ yield (0, exec_args_1.execArgs)('git', ['init', '--initial-branch=dev'], { cwd: projectDir });
489
491
  }
490
492
  catch (err) {
491
493
  ngBaseSpinner.fail(`Failed to initialize git: ${err.message}`);
@@ -495,7 +497,7 @@ const NewCommand = {
495
497
  // Add remote if push is configured
496
498
  if (pushToRemote && gitLink) {
497
499
  try {
498
- yield system.run(`cd ${projectDir} && git remote add origin ${gitLink}`);
500
+ yield (0, exec_args_1.execArgs)('git', ['remote', 'add', '--', 'origin', gitLink], { cwd: projectDir });
499
501
  }
500
502
  catch (err) {
501
503
  ngBaseSpinner.fail(`Failed to add remote: ${err.message}`);
@@ -600,7 +602,7 @@ const NewCommand = {
600
602
  }
601
603
  const renameSpinner = spin(`Rename nest-base → ${projectDir}`);
602
604
  try {
603
- yield system.run(`cd ${apiDest} && bun run rename ${projectDir}`);
605
+ yield (0, exec_args_1.spawnArgs)('bun', ['run', 'rename', projectDir], { cwd: apiDest });
604
606
  renameSpinner.succeed(`Renamed nest-base → ${projectDir} in projects/api`);
605
607
  }
606
608
  catch (err) {
@@ -701,7 +703,8 @@ const NewCommand = {
701
703
  });
702
704
  // Create initial commit after everything is set up
703
705
  try {
704
- yield system.run(`cd ${projectDir} && git add . && git commit -m "Initial commit"`);
706
+ yield (0, exec_args_1.execArgs)('git', ['add', '.'], { cwd: projectDir });
707
+ yield (0, exec_args_1.execArgs)('git', ['commit', '-m', 'Initial commit'], { cwd: projectDir });
705
708
  }
706
709
  catch (err) {
707
710
  error(`Failed to create initial commit: ${err.message}`);
@@ -711,7 +714,7 @@ const NewCommand = {
711
714
  // Push to remote if configured
712
715
  if (pushToRemote) {
713
716
  try {
714
- yield system.run(`cd ${projectDir} && git push -u origin dev`);
717
+ yield (0, exec_args_1.execArgs)('git', ['push', '-u', 'origin', 'dev'], { cwd: projectDir });
715
718
  }
716
719
  catch (err) {
717
720
  error(`Failed to push to remote: ${err.message}`);
@@ -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 gluegun_1 = require("gluegun");
13
+ const exec_args_1 = require("../../lib/exec-args");
13
14
  /**
14
15
  * Removed local merged branches
15
16
  */
@@ -89,7 +90,7 @@ const NewCommand = {
89
90
  spin: true,
90
91
  });
91
92
  }
92
- yield run(`git checkout ${branch}`);
93
+ yield (0, exec_args_1.execArgs)('git', ['checkout', (0, exec_args_1.safeRef)(branch)]);
93
94
  info(`Changed Branch to ${branch}`);
94
95
  }
95
96
  // Start timer
@@ -114,7 +115,7 @@ const NewCommand = {
114
115
  info(`Deleting branches: ${branches.join(', ')}`);
115
116
  // Delete branches
116
117
  for (const branchToDelete of branches) {
117
- yield gluegun_1.system.run(`git branch -d ${branchToDelete}`);
118
+ yield (0, exec_args_1.execArgs)('git', ['branch', '-d', (0, exec_args_1.safeRef)(branchToDelete)]);
118
119
  success(`Deleted branch: ${branchToDelete}`);
119
120
  }
120
121
  undoSpinner.succeed();
@@ -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 branch
15
16
  */
@@ -139,7 +140,11 @@ const NewCommand = {
139
140
  const timer = system.startTimer();
140
141
  // Checkout
141
142
  const createSpin = spin(`Create ${branch}`);
142
- yield system.run('git fetch &&' + `git checkout ${baseBranch} &&` + 'git pull && ' + `git checkout -b ${branch}`);
143
+ // Argument lists, no shell: both branch names come from the user or lt.config (see exec-args.ts).
144
+ yield (0, exec_args_1.execArgs)('git', ['fetch']);
145
+ yield (0, exec_args_1.execArgs)('git', ['checkout', (0, exec_args_1.safeRef)(baseBranch)]);
146
+ yield (0, exec_args_1.execArgs)('git', ['pull']);
147
+ yield (0, exec_args_1.execArgs)('git', ['checkout', '-b', (0, exec_args_1.safeRef)(branch)]);
143
148
  createSpin.succeed();
144
149
  // Install packages with correctly detected package manager (supports monorepo lockfiles)
145
150
  const { path: pkgPath } = yield npm.getPackageJson();
@@ -147,7 +152,7 @@ const NewCommand = {
147
152
  const projectDir = (0, path_1.dirname)(pkgPath);
148
153
  const detectedPm = toolbox.pm.detect(projectDir);
149
154
  const installSpin = spin(`Install packages using ${detectedPm}`);
150
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)}`);
155
+ yield system.run(toolbox.pm.install(detectedPm), { cwd: projectDir });
151
156
  installSpin.succeed();
152
157
  }
153
158
  // Success info
@@ -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
  * Pull branch with losing changes
14
15
  */
@@ -61,7 +62,7 @@ const NewCommand = {
61
62
  // silently claimed there was nothing ahead. try/catch behaves the same on every platform.
62
63
  let localCommits = '';
63
64
  try {
64
- localCommits = yield run(`git log origin/${branch}..HEAD --oneline`);
65
+ localCommits = yield (0, exec_args_1.execArgs)('git', ['log', `origin/${(0, exec_args_1.safeRef)(branch)}..HEAD`, '--oneline']);
65
66
  }
66
67
  catch (_d) {
67
68
  // ignore - no upstream ref, so nothing ahead to report
@@ -84,7 +85,8 @@ const NewCommand = {
84
85
  const timer = startTimer();
85
86
  // Reset soft
86
87
  const pullSpinner = spin(`Fetch and pull ${branch}`);
87
- yield run(`git fetch && git reset origin/${branch} --hard`);
88
+ yield (0, exec_args_1.execArgs)('git', ['fetch']);
89
+ yield (0, exec_args_1.execArgs)('git', ['reset', `origin/${(0, exec_args_1.safeRef)(branch)}`, '--hard']);
88
90
  pullSpinner.succeed();
89
91
  // Success
90
92
  success(`Force pulled ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`);
@@ -10,6 +10,18 @@ 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");
14
+ /**
15
+ * Run git steps one after another, each without a shell; stops at the first failure like the
16
+ * former `a && b && c` chain did.
17
+ */
18
+ function gitSteps(...steps) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ for (const args of steps) {
21
+ yield (0, exec_args_1.execArgs)('git', args);
22
+ }
23
+ });
24
+ }
13
25
  /**
14
26
  * Checkout git branch
15
27
  */
@@ -74,7 +86,8 @@ const NewCommand = {
74
86
  success(`Branch ${branchName} found`);
75
87
  }
76
88
  // Checkout branch
77
- yield system.run(`git checkout ${(yield git.getDefaultBranch()) || 'main'}`);
89
+ // Argument lists, no shell: branch names come from the user and from the remote (see exec-args.ts).
90
+ yield (0, exec_args_1.execArgs)('git', ['checkout', (0, exec_args_1.safeRef)((yield git.getDefaultBranch()) || 'main')]);
78
91
  let checkoutSpin;
79
92
  // Handling for remote
80
93
  if (remoteBranch) {
@@ -99,7 +112,7 @@ const NewCommand = {
99
112
  }
100
113
  if (mode === 'hard') {
101
114
  const prepareSpin = spin(`Refresh ${branch}`);
102
- yield system.run(`git branch -D ${branch}`);
115
+ yield (0, exec_args_1.execArgs)('git', ['branch', '-D', (0, exec_args_1.safeRef)(branch)]);
103
116
  removed = true;
104
117
  prepareSpin.succeed();
105
118
  }
@@ -111,17 +124,17 @@ const NewCommand = {
111
124
  checkoutSpin = spin(`Checkout ${branch}`);
112
125
  // Checkout remote if local branch not exists
113
126
  if (removed || !(yield git.getBranch(branch, { local: true }))) {
114
- yield system.run(`git fetch && git checkout --track origin/${branch} && git reset --hard && git clean -fd && git pull`);
127
+ yield gitSteps(['fetch'], ['checkout', '--track', `origin/${(0, exec_args_1.safeRef)(branch)}`], ['reset', '--hard'], ['clean', '-fd'], ['pull']);
115
128
  // Checkout local branch
116
129
  }
117
130
  else {
118
- yield system.run(`git fetch && git checkout ${branch} && git reset --hard && git clean -fd && git pull`);
131
+ yield gitSteps(['fetch'], ['checkout', (0, exec_args_1.safeRef)(branch)], ['reset', '--hard'], ['clean', '-fd'], ['pull']);
119
132
  }
120
133
  // Handling for local only
121
134
  }
122
135
  else if (branch) {
123
136
  checkoutSpin = spin(`Checkout ${branch}`);
124
- yield system.run(`git fetch && git checkout ${branch} && git reset --hard && git clean -fd`);
137
+ yield gitSteps(['fetch'], ['checkout', (0, exec_args_1.safeRef)(branch)], ['reset', '--hard'], ['clean', '-fd']);
125
138
  // No branch found
126
139
  }
127
140
  else {
@@ -136,7 +149,7 @@ const NewCommand = {
136
149
  const projectDir = (0, path_1.dirname)(pkgPath);
137
150
  const detectedPm = toolbox.pm.detect(projectDir);
138
151
  const installSpin = spin(`Install packages using ${detectedPm}`);
139
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)}`);
152
+ yield system.run(toolbox.pm.install(detectedPm), { cwd: projectDir });
140
153
  installSpin.succeed();
141
154
  }
142
155
  // Init lerna projects
@@ -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
  * Rebase 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, _d, _e, _f, _g, _h, _j;
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
  const configBaseBranch = (_d = (_c = (_b = (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.git) === null || _b === void 0 ? void 0 : _b.rebase) === null || _c === void 0 ? void 0 : _c.base) !== null && _d !== void 0 ? _d : (_f = (_e = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _e === void 0 ? void 0 : _e.git) === null || _f === void 0 ? void 0 : _f.baseBranch;
@@ -60,7 +61,7 @@ const NewCommand = {
60
61
  // what the real command will do.
61
62
  let commitsToRebase = '';
62
63
  try {
63
- commitsToRebase = yield run(`git log ${baseBranchPreview}..HEAD --oneline`);
64
+ commitsToRebase = yield (0, exec_args_1.execArgs)('git', ['log', `${(0, exec_args_1.safeRef)(baseBranchPreview)}..HEAD`, '--oneline']);
64
65
  }
65
66
  catch (_k) {
66
67
  // Unknown base ref — nothing to preview, which the check below handles.
@@ -139,7 +140,16 @@ const NewCommand = {
139
140
  const timer = startTimer();
140
141
  // Rebase
141
142
  const rebaseSpin = spin(`Set ${baseBranch} as base of ${branch}`);
142
- yield run(`git fetch && git checkout ${baseBranch} && git pull && git checkout ${branch} && git rebase ${baseBranch}`);
143
+ // Argument lists, no shell: both names come from flags, lt.config and the remote (see exec-args.ts).
144
+ for (const args of [
145
+ ['fetch'],
146
+ ['checkout', (0, exec_args_1.safeRef)(baseBranch)],
147
+ ['pull'],
148
+ ['checkout', (0, exec_args_1.safeRef)(branch)],
149
+ ['rebase', (0, exec_args_1.safeRef)(baseBranch)],
150
+ ]) {
151
+ yield (0, exec_args_1.execArgs)('git', args);
152
+ }
143
153
  rebaseSpin.succeed();
144
154
  // Success
145
155
  success(`Rebased ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`);