create-nx-workspace 23.1.0-beta.5 → 23.1.0-beta.7

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.
@@ -65,6 +65,8 @@ interface UnknownStackArguments extends BaseArguments {
65
65
  }
66
66
  type Arguments = NoneArguments | ReactArguments | AngularArguments | VueArguments | NodeArguments | UnknownStackArguments;
67
67
  export declare const commandsObject: yargs.Argv<Arguments>;
68
+ /** Workspace names must start with a letter. */
69
+ export declare function isValidWorkspaceName(name: string): boolean;
68
70
  export declare function validateWorkspaceName(name: string): void;
69
71
  /**
70
72
  * Resolves special folder name patterns (`.`, `./`, absolute paths) into a
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.commandsObject = void 0;
4
+ exports.isValidWorkspaceName = isValidWorkspaceName;
4
5
  exports.validateWorkspaceName = validateWorkspaceName;
5
6
  exports.resolveSpecialFolderName = resolveSpecialFolderName;
6
7
  exports.determineFolder = determineFolder;
@@ -620,9 +621,12 @@ function invariant(predicate, errorCode, message) {
620
621
  throw new error_utils_1.CnwError(errorCode, message);
621
622
  }
622
623
  }
624
+ /** Workspace names must start with a letter. */
625
+ function isValidWorkspaceName(name) {
626
+ return /^[a-zA-Z]/.test(name);
627
+ }
623
628
  function validateWorkspaceName(name) {
624
- const pattern = /^[a-zA-Z]/;
625
- if (!pattern.test(name)) {
629
+ if (!isValidWorkspaceName(name)) {
626
630
  throw new error_utils_1.CnwError('INVALID_WORKSPACE_NAME', `The workspace name "${name}" is invalid. Workspace names must start with a letter. Examples of valid names: myapp, MyApp, my-app, my_app`);
627
631
  }
628
632
  }
@@ -641,12 +645,9 @@ function isCurrentDirReference(folderName) {
641
645
  * input is a regular name that needs no special handling.
642
646
  */
643
647
  function resolveSpecialFolderName(folderName) {
644
- // User wants to init in the current directory
648
+ // User wants to scaffold in the current directory.
645
649
  if (isCurrentDirReference(folderName)) {
646
650
  const cwd = (0, path_1.resolve)(process.cwd());
647
- if ((0, fs_1.readdirSync)(cwd).length > 0) {
648
- throw new error_utils_1.CnwError('DIRECTORY_EXISTS', `The current directory is not empty. Use "nx init" to add Nx to an existing project.`);
649
- }
650
651
  return { name: (0, path_1.basename)(cwd), workingDir: (0, path_1.dirname)(cwd) };
651
652
  }
652
653
  // Handle absolute paths like /tmp/acme
@@ -677,11 +678,20 @@ async function determineFolder(parsedArgs) {
677
678
  parsedArgs.workingDir = resolved.workingDir;
678
679
  }
679
680
  validateWorkspaceName(folderName);
680
- // When input is "." or "./", resolveSpecialFolderName already validated
681
- // the directory is empty. The target always "exists" because it IS the
682
- // current working directory, so skip the existsSync check and default
683
- // the workspace name to the directory name.
681
+ // When input is "." or "./", scaffold into the current directory. The
682
+ // target always "exists" because it IS the cwd, so skip the existsSync
683
+ // check and use the directory name as the workspace name.
684
684
  if (isCurrentDirReference(rawFolderName)) {
685
+ // Interactively confirm before scaffolding into the current directory;
686
+ // non-interactive (CI/AI) proceeds without prompting.
687
+ if (parsedArgs.interactive && !(0, is_ci_1.isCI)()) {
688
+ if (!(await promptCreateInCurrentDir(folderName))) {
689
+ // Declined - fall back to creating a named subfolder under the cwd.
690
+ parsedArgs.workingDir = undefined;
691
+ return promptForFolder(parsedArgs);
692
+ }
693
+ }
694
+ parsedArgs.useCurrentDir = true;
685
695
  return folderName;
686
696
  }
687
697
  // If directory exists, either re-prompt (interactive) or error (non-interactive)
@@ -710,6 +720,18 @@ async function determineFolder(parsedArgs) {
710
720
  }
711
721
  return promptForFolder(parsedArgs);
712
722
  }
723
+ async function promptCreateInCurrentDir(dirName) {
724
+ const { useCurrentDir } = await enquirer_1.default.prompt([
725
+ {
726
+ name: 'useCurrentDir',
727
+ message: `Create workspace in the current directory (${dirName})? Existing files may be overwritten.`,
728
+ type: 'autocomplete',
729
+ choices: [{ name: 'Yes' }, { name: 'No' }],
730
+ initial: 0,
731
+ },
732
+ ]);
733
+ return useCurrentDir === 'Yes';
734
+ }
713
735
  async function promptForFolder(parsedArgs) {
714
736
  const reply = await enquirer_1.default.prompt([
715
737
  {
@@ -722,7 +744,7 @@ async function promptForFolder(parsedArgs) {
722
744
  if (!value) {
723
745
  return 'Folder name cannot be empty';
724
746
  }
725
- if (!/^[a-zA-Z]/.test(value)) {
747
+ if (!isValidWorkspaceName(value)) {
726
748
  return 'Workspace name must start with a letter';
727
749
  }
728
750
  if ((0, fs_1.existsSync)(value)) {
@@ -27,10 +27,13 @@ async function createEmptyWorkspace(tmpDir, name, packageManager, options) {
27
27
  // Even though --skipInstall is not an option to create-nx-workspace, we pass through extra options to presets.
28
28
  // See: https://github.com/nrwl/nx/issues/31834
29
29
  delete options.skipInstall;
30
- // workingDir is consumed by CNW itself, not passed to `nx new`
31
- const { workingDir: _workingDir, ...nxNewOptions } = options;
30
+ // workingDir and useCurrentDir are consumed by CNW itself, not passed to `nx new`
31
+ const { workingDir: _workingDir, useCurrentDir, ...nxNewOptions } = options;
32
32
  const args = (0, unparse_1.unparse)({
33
33
  ...nxNewOptions,
34
+ // Scaffolding into the current directory: relax the generator's
35
+ // empty-directory guard so it can write into a non-empty cwd.
36
+ ...(useCurrentDir ? { skipEmptyDirCheck: true } : {}),
34
37
  }).join(' ');
35
38
  const pmc = (0, package_manager_1.getPackageManagerCommand)(packageManager);
36
39
  const command = `new ${args}`;
@@ -9,6 +9,12 @@ export interface CreateWorkspaceOptions {
9
9
  * Used when the user provides "." or an absolute path as the workspace name.
10
10
  */
11
11
  workingDir?: string;
12
+ /**
13
+ * @description Scaffold into the current directory in place. Set when the
14
+ * user passes "." / "./". Relaxes the empty-directory guard, so existing
15
+ * files in the cwd that collide with generated files are overwritten.
16
+ */
17
+ useCurrentDir?: boolean;
12
18
  packageManager: PackageManager;
13
19
  nxCloud: NxCloud;
14
20
  useGitHub?: boolean;
@@ -16,7 +16,7 @@ const nx_cloud_1 = require("./utils/nx/nx-cloud");
16
16
  const output_1 = require("./utils/output");
17
17
  const get_third_party_preset_1 = require("./utils/preset/get-third-party-preset");
18
18
  const preset_1 = require("./utils/preset/preset");
19
- const clone_template_1 = require("./utils/template/clone-template");
19
+ const download_template_1 = require("./utils/template/download-template");
20
20
  const update_readme_1 = require("./utils/template/update-readme");
21
21
  const child_process_utils_1 = require("./utils/child-process-utils");
22
22
  const package_manager_1 = require("./utils/package-manager");
@@ -40,21 +40,27 @@ async function createWorkspace(preset, options, rawArgs) {
40
40
  options.template = resolveTemplateShorthand(options.template);
41
41
  if (!options.template.startsWith('nrwl/'))
42
42
  throw new Error(`Invalid template. Only templates from the 'nrwl' GitHub org are supported.`);
43
- const templateUrl = `https://github.com/${options.template}`;
44
43
  const workingDir = (options.workingDir ?? process.cwd()).replace(/\\/g, '/');
45
44
  directory = (0, path_1.join)(workingDir, name);
45
+ // downloadTemplate extracts into `directory`, creating it and overwriting
46
+ // files. That is intended only when scaffolding into the current directory.
47
+ // Otherwise refuse to write over an existing path (the CLI already guards
48
+ // this in determineFolder; this protects direct createWorkspace() callers).
49
+ if (!options.useCurrentDir && (0, node_fs_1.existsSync)(directory)) {
50
+ throw new error_utils_2.CnwError('DIRECTORY_EXISTS', `The directory '${directory}' already exists. Choose a different name or remove the existing directory.`);
51
+ }
46
52
  const aiMode = (0, ai_output_1.isAiAgent)();
47
53
  // Use spinner for human mode, progress logs for AI mode
48
54
  let workspaceSetupSpinner;
49
55
  if (aiMode) {
50
- (0, ai_output_1.logProgress)('cloning', `Cloning template ${options.template}...`);
56
+ (0, ai_output_1.logProgress)('downloading', `Downloading template ${options.template}...`);
51
57
  }
52
58
  else {
53
59
  const ora = require('ora');
54
60
  workspaceSetupSpinner = ora(`Creating workspace from template`).start();
55
61
  }
56
62
  try {
57
- await (0, clone_template_1.cloneTemplate)(templateUrl, name, workingDir);
63
+ await (0, download_template_1.downloadTemplate)(options.template, directory);
58
64
  // Remove npm lockfile from template since we'll generate the correct one
59
65
  const npmLockPath = (0, path_1.join)(directory, 'package-lock.json');
60
66
  if ((0, node_fs_1.existsSync)(npmLockPath)) {
@@ -14,7 +14,6 @@ const enquirer_1 = tslib_1.__importDefault(require("enquirer"));
14
14
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
15
15
  const ab_testing_1 = require("../utils/nx/ab-testing");
16
16
  const default_base_1 = require("../utils/git/default-base");
17
- const git_1 = require("../utils/git/git");
18
17
  const package_manager_1 = require("../utils/package-manager");
19
18
  const string_utils_1 = require("../utils/string-utils");
20
19
  const is_ci_1 = require("../utils/ci/is-ci");
@@ -102,9 +101,6 @@ async function determineTemplate(parsedArgs) {
102
101
  // Docs generation needs preset flow to document all presets
103
102
  if (process.env.NX_GENERATE_DOCS_PROCESS === 'true')
104
103
  return 'custom';
105
- // Template flow requires git for cloning - fall back to custom preset if git is not available
106
- if (!(0, git_1.isGitAvailable)())
107
- return 'custom';
108
104
  const { template } = await enquirer_1.default.prompt([
109
105
  {
110
106
  name: 'template',
@@ -16,7 +16,7 @@ export declare function isReplitAi(): boolean;
16
16
  export declare function isCursorAi(): boolean;
17
17
  export declare function isGeminiCli(): boolean;
18
18
  export declare function detectAiAgentName(): string | null;
19
- export type ProgressStage = 'starting' | 'cloning' | 'installing' | 'configuring' | 'initializing' | 'complete' | 'error';
19
+ export type ProgressStage = 'starting' | 'downloading' | 'installing' | 'configuring' | 'initializing' | 'complete' | 'error';
20
20
  export interface ProgressMessage {
21
21
  stage: ProgressStage;
22
22
  message: string;
@@ -9,11 +9,6 @@ export declare class GitHubPushError extends Error {
9
9
  constructor(message: string, reason: 'gh-not-installed' | 'gh-auth-failed' | 'push-timeout' | 'push-failed' | 'env-skip');
10
10
  }
11
11
  export declare function checkGitVersion(): Promise<string | null | undefined>;
12
- /**
13
- * Synchronously checks if git is available on the system.
14
- * Returns true if git command can be executed, false otherwise.
15
- */
16
- export declare function isGitAvailable(): boolean;
17
12
  /**
18
13
  * Synchronously checks if GitHub CLI (gh) is available on the system.
19
14
  * Returns true if gh command can be executed within 2 seconds, false otherwise.
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GitHubPushError = exports.VcsPushStatus = void 0;
4
4
  exports.checkGitVersion = checkGitVersion;
5
- exports.isGitAvailable = isGitAvailable;
6
5
  exports.isGhCliAvailable = isGhCliAvailable;
7
6
  exports.initializeGitRepo = initializeGitRepo;
8
7
  exports.pushToGitHub = pushToGitHub;
@@ -37,19 +36,6 @@ async function checkGitVersion() {
37
36
  return null;
38
37
  }
39
38
  }
40
- /**
41
- * Synchronously checks if git is available on the system.
42
- * Returns true if git command can be executed, false otherwise.
43
- */
44
- function isGitAvailable() {
45
- try {
46
- (0, child_process_1.execSync)('git --version', { stdio: 'ignore', windowsHide: true });
47
- return true;
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
39
  // 1 second timeout for gh CLI pre-flight checks (version, auth). If gh is
54
40
  // wrapped by 1Password, a credential manager, or corporate SSO the call can
55
41
  // hang indefinitely. Better to skip the push than freeze the CLI.
@@ -155,9 +141,11 @@ async function initializeGitRepo(directory, options) {
155
141
  }
156
142
  const insideRepo = await (0, child_process_utils_1.execAndWait)('git rev-parse --is-inside-work-tree', directory, true).then(() => true, () => false);
157
143
  if (insideRepo) {
158
- output_1.output.log({
159
- title: 'Directory is already under version control. Skipping initialization of git.',
160
- });
144
+ if (process.env.NX_VERBOSE_LOGGING === 'true') {
145
+ output_1.output.log({
146
+ title: 'Directory is already under version control. Skipping initialization of git.',
147
+ });
148
+ }
161
149
  return;
162
150
  }
163
151
  const defaultBase = options.defaultBase || (0, default_base_1.deduceDefaultBase)();
@@ -2,10 +2,10 @@ import type { BannerVariant, CompletionMessageKey } from './messages';
2
2
  export declare const NX_CLOUD_URL = "https://nx.dev/nx-cloud";
3
3
  /**
4
4
  * Clickable Nx Cloud marketing link for cloud prompt footers. Every
5
- * create-nx-workspace prompt reports the same medium, so the link is a single
6
- * baked constant embedded directly in the footers. Visible text stays the clean
7
- * `NX_CLOUD_URL` while clicks carry UTM attribution; terminals without OSC 8
8
- * support just render the bare URL (CLOUD-4642).
5
+ * create-nx-workspace prompt reports the same content tag, so the link is a
6
+ * single baked constant embedded directly in the footers. Visible text stays
7
+ * the clean `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
8
+ * without OSC 8 support just render the bare URL (CLOUD-4642).
9
9
  */
10
10
  export declare const NX_CLOUD_HYPERLINK: string;
11
11
  /**
@@ -17,12 +17,12 @@ const terminal_link_1 = require("../terminal-link");
17
17
  exports.NX_CLOUD_URL = 'https://nx.dev/nx-cloud';
18
18
  /**
19
19
  * Clickable Nx Cloud marketing link for cloud prompt footers. Every
20
- * create-nx-workspace prompt reports the same medium, so the link is a single
21
- * baked constant embedded directly in the footers. Visible text stays the clean
22
- * `NX_CLOUD_URL` while clicks carry UTM attribution; terminals without OSC 8
23
- * support just render the bare URL (CLOUD-4642).
20
+ * create-nx-workspace prompt reports the same content tag, so the link is a
21
+ * single baked constant embedded directly in the footers. Visible text stays
22
+ * the clean `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
23
+ * without OSC 8 support just render the bare URL (CLOUD-4642).
24
24
  */
25
- exports.NX_CLOUD_HYPERLINK = (0, terminal_link_1.terminalLink)(exports.NX_CLOUD_URL, `${exports.NX_CLOUD_URL}?utm_source=nx-cli&utm_medium=create-nx-workspace`);
25
+ exports.NX_CLOUD_HYPERLINK = (0, terminal_link_1.terminalLink)(exports.NX_CLOUD_URL, `${exports.NX_CLOUD_URL}?utm_source=nx-cli&utm_medium=cli&utm_campaign=nx-cloud-connect&utm_content=create-nx-workspace`);
26
26
  // Flow variant controls both tracking and banner display (CLOUD-4235)
27
27
  // Variants: 0 = control, 1 = updated prompt, 2 = no prompt (auto-connect)
28
28
  const FLOW_VARIANT_CACHE_FILE = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'nx-cnw-flow-variant');
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Download an nrwl template repository and extract it into `directory`.
3
+ *
4
+ * git is not required (fresh machines / CI / AI agents may have none), and an
5
+ * existing `.git` in `directory` is left intact, so this can scaffold into the
6
+ * current directory. Existing files (e.g. README) are overwritten.
7
+ *
8
+ * @param template GitHub repo slug, e.g. `nrwl/react-template`.
9
+ * @param directory Absolute path to extract into.
10
+ */
11
+ export declare function downloadTemplate(template: string, directory: string): Promise<void>;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downloadTemplate = downloadTemplate;
4
+ const tslib_1 = require("tslib");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const node_stream_1 = require("node:stream");
8
+ const promises_1 = require("node:stream/promises");
9
+ const node_zlib_1 = require("node:zlib");
10
+ const tar = tslib_1.__importStar(require("tar-stream"));
11
+ const error_utils_1 = require("../error-utils");
12
+ // Branches to try, in order. GitHub serves a gzipped tarball of a repo at
13
+ // https://github.com/<org>/<repo>/archive/refs/heads/<branch>.tar.gz.
14
+ const DEFAULT_BRANCHES = ['main', 'master'];
15
+ /**
16
+ * Download an nrwl template repository and extract it into `directory`.
17
+ *
18
+ * git is not required (fresh machines / CI / AI agents may have none), and an
19
+ * existing `.git` in `directory` is left intact, so this can scaffold into the
20
+ * current directory. Existing files (e.g. README) are overwritten.
21
+ *
22
+ * @param template GitHub repo slug, e.g. `nrwl/react-template`.
23
+ * @param directory Absolute path to extract into.
24
+ */
25
+ async function downloadTemplate(template, directory) {
26
+ let body;
27
+ const attempts = [];
28
+ // A thrown fetch is a connectivity problem; a non-ok response is a missing
29
+ // branch/repo. Distinguish them so the error code (and its hints) are right.
30
+ let networkError = false;
31
+ for (const branch of DEFAULT_BRANCHES) {
32
+ const url = `https://github.com/${template}/archive/refs/heads/${branch}.tar.gz`;
33
+ try {
34
+ const res = await fetch(url);
35
+ if (res.ok && res.body) {
36
+ body = res.body;
37
+ break;
38
+ }
39
+ attempts.push(`${branch}: HTTP ${res.status}`);
40
+ }
41
+ catch (e) {
42
+ networkError = true;
43
+ attempts.push(`${branch}: ${e instanceof Error ? e.message : String(e)}`);
44
+ }
45
+ }
46
+ if (!body) {
47
+ throw new error_utils_1.CnwError(networkError ? 'NETWORK_ERROR' : 'TEMPLATE_CLONE_FAILED', `Failed to download template '${template}' (${attempts.join('; ')})`);
48
+ }
49
+ // Only remove the directory on failure if we created it - never delete a
50
+ // pre-existing dir (e.g. the user's current directory).
51
+ const dirPreexisted = (0, node_fs_1.existsSync)(directory);
52
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
53
+ try {
54
+ await extractTarball(body, directory);
55
+ }
56
+ catch (e) {
57
+ if (!dirPreexisted) {
58
+ (0, node_fs_1.rmSync)(directory, { recursive: true, force: true });
59
+ }
60
+ const message = e instanceof Error ? e.message : String(e);
61
+ throw new error_utils_1.CnwError('TEMPLATE_CLONE_FAILED', `Failed to create starter workspace: ${message}`);
62
+ }
63
+ }
64
+ async function extractTarball(body, directory) {
65
+ const extract = tar.extract();
66
+ extract.on('entry', (header, stream, next) => {
67
+ // Drain the entry and move to the next one without writing anything.
68
+ const skip = () => {
69
+ stream.on('end', next);
70
+ stream.resume();
71
+ };
72
+ try {
73
+ // GitHub wraps everything in a top-level `<repo>-<branch>/` directory.
74
+ // Strip that first segment so files land directly in `directory`.
75
+ const relativePath = header.name.split('/').slice(1).join('/');
76
+ // Top-level dir entry or unsupported type (symlink, pax header).
77
+ if (!relativePath ||
78
+ (header.type !== 'file' && header.type !== 'directory')) {
79
+ return skip();
80
+ }
81
+ const destPath = (0, node_path_1.join)(directory, relativePath);
82
+ // Defense-in-depth against a malicious tarball escaping the target dir
83
+ // (zip-slip) via `..` entries.
84
+ const rel = (0, node_path_1.relative)(directory, destPath);
85
+ if (rel.startsWith('..') || (0, node_path_1.isAbsolute)(rel)) {
86
+ return skip();
87
+ }
88
+ if (header.type === 'directory') {
89
+ (0, node_fs_1.mkdirSync)(destPath, { recursive: true });
90
+ return skip();
91
+ }
92
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destPath), { recursive: true });
93
+ const writeStream = (0, node_fs_1.createWriteStream)(destPath, { mode: header.mode });
94
+ // Surface a write failure to the pipeline below so it rejects and tears
95
+ // down every stream.
96
+ writeStream.on('error', (err) => extract.destroy(err));
97
+ writeStream.on('close', next);
98
+ stream.pipe(writeStream);
99
+ }
100
+ catch (err) {
101
+ // A synchronous failure here (e.g. mkdirSync hitting EACCES/ENOSPC, or a
102
+ // path component that is a file) would otherwise escape the awaited
103
+ // pipeline; route it so the operation rejects instead of crashing.
104
+ extract.destroy(err);
105
+ }
106
+ });
107
+ // pipeline (unlike a manual .pipe() chain) forwards errors from every stage -
108
+ // a network drop on the source or a corrupt/truncated gzip rejects here and
109
+ // destroys all streams, so the caller can wrap it in a CnwError.
110
+ // Cast: the global (DOM) ReadableStream and node:stream/web differ structurally.
111
+ await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(body), (0, node_zlib_1.createGunzip)(), extract);
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nx-workspace",
3
- "version": "23.1.0-beta.5",
3
+ "version": "23.1.0-beta.7",
4
4
  "private": false,
5
5
  "description": "Smart Monorepos · Fast Builds",
6
6
  "repository": {
@@ -43,6 +43,7 @@
43
43
  "flat": "^5.0.2",
44
44
  "open": "^8.4.0",
45
45
  "ora": "^5.3.0",
46
+ "tar-stream": "~2.2.0",
46
47
  "tmp": "~0.2.7",
47
48
  "tslib": "^2.3.0",
48
49
  "yargs": "^17.6.2"
@@ -1 +0,0 @@
1
- export declare function cloneTemplate(templateUrl: string, targetDirectory: string, workingDir?: string): Promise<void>;
@@ -1,25 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.cloneTemplate = cloneTemplate;
4
- const child_process_utils_1 = require("../child-process-utils");
5
- const fs_1 = require("fs");
6
- const promises_1 = require("fs/promises");
7
- const path_1 = require("path");
8
- const error_utils_1 = require("../error-utils");
9
- async function cloneTemplate(templateUrl, targetDirectory, workingDir) {
10
- if ((0, fs_1.existsSync)(targetDirectory)) {
11
- throw new error_utils_1.CnwError('DIRECTORY_EXISTS', `The directory '${targetDirectory}' already exists. Choose a different name or remove the existing directory.`);
12
- }
13
- try {
14
- await (0, child_process_utils_1.execAndWait)(`git clone --depth 1 "${templateUrl}" "${targetDirectory}"`, workingDir ?? process.cwd());
15
- // Ensure clean history
16
- const gitDir = (0, path_1.join)(targetDirectory, '.git');
17
- if ((0, fs_1.existsSync)(gitDir)) {
18
- await (0, promises_1.rm)(gitDir, { recursive: true, force: true });
19
- }
20
- }
21
- catch (e) {
22
- const message = e instanceof Error ? e.message : String(e);
23
- throw new error_utils_1.CnwError('TEMPLATE_CLONE_FAILED', `Failed to create starter workspace: ${message}`);
24
- }
25
- }