@pixpilot/scaffoldfy-configs 0.34.0 → 0.35.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @pixpilot/scaffoldfy-configs
2
2
 
3
+ ## 0.35.0
4
+
5
+ ### Minor Changes
6
+
7
+ - add coding-agent-sandbox initial scaffoldfy config
8
+ - improve workspace generator
9
+ - d52ed00: Add the `coding-agent-sandbox` Scaffoldfy config for running Claude Code, OpenAI Codex, or GitHub Copilot CLI in Docker against a dedicated Git worktree.
10
+
11
+ ### Patch Changes
12
+
13
+ - generate-packages-section.js: fix the issue with package section generation
14
+ - 65b34e8: fix config release
15
+ - 5a5d17b: fixes release
16
+ - 022bded: test ci release
17
+
3
18
  ## 0.34.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -14,6 +14,16 @@ This package provides the following scaffoldfy templates:
14
14
 
15
15
  <!-- scaffoldfy-templates:start -->
16
16
 
17
+ ### coding-agent-sandbox
18
+
19
+ Run an AI coding agent (Claude Code, OpenAI Codex, GitHub Copilot CLI) in Docker against a dedicated Git worktree
20
+
21
+ Usage:
22
+
23
+ ```sh
24
+ npx @pixpilot/scaffoldfy@latest --config https://unpkg.com/@pixpilot/scaffoldfy-configs@latest/coding-agent-sandbox/scaffoldfy.json
25
+ ```
26
+
17
27
  ### license-file
18
28
 
19
29
  Generate a LICENSE file with common open-source licenses
@@ -0,0 +1,159 @@
1
+ {
2
+ "$schema": "../node_modules/@pixpilot/scaffoldfy/schema/scaffoldfy.schema.json",
3
+ "name": "coding-agent-sandbox",
4
+ "description": "Run an AI coding agent (Claude Code, OpenAI Codex, GitHub Copilot CLI) in Docker against a dedicated Git worktree",
5
+ "prompts": [
6
+ {
7
+ "id": "sandboxAction",
8
+ "type": "select",
9
+ "message": "What would you like to do?",
10
+ "choices": [
11
+ {
12
+ "name": "Start a session with internet access",
13
+ "value": "online"
14
+ },
15
+ {
16
+ "name": "Start offline (installed CLI/image required; cloud agents cannot connect)",
17
+ "value": "offline"
18
+ },
19
+ {
20
+ "name": "Prune unused dependency/cache volumes (asks before deleting)",
21
+ "value": "prune"
22
+ }
23
+ ],
24
+ "default": "online",
25
+ "required": true
26
+ },
27
+ {
28
+ "id": "agent",
29
+ "type": "select",
30
+ "message": "Which coding agent should run this task?",
31
+ "choices": [
32
+ {
33
+ "name": "Claude Code",
34
+ "value": "claude"
35
+ },
36
+ {
37
+ "name": "OpenAI Codex",
38
+ "value": "codex"
39
+ },
40
+ {
41
+ "name": "GitHub Copilot CLI",
42
+ "value": "copilot"
43
+ }
44
+ ],
45
+ "default": "claude",
46
+ "required": true,
47
+ "enabled": {
48
+ "type": "condition",
49
+ "value": "sandboxAction !== 'prune'"
50
+ }
51
+ },
52
+ {
53
+ "id": "repoPath",
54
+ "type": "input",
55
+ "message": "Main Git repository path",
56
+ "default": {
57
+ "type": "exec",
58
+ "value": "node -e \"const{execSync}=require('child_process');let p=process.cwd();try{p=execSync('git rev-parse --show-toplevel',{stdio:['ignore','pipe','ignore']}).toString().trim()}catch{}process.stdout.write(p)\""
59
+ },
60
+ "required": true,
61
+ "enabled": {
62
+ "type": "condition",
63
+ "value": "sandboxAction !== 'prune'"
64
+ }
65
+ },
66
+ {
67
+ "id": "taskName",
68
+ "type": "input",
69
+ "message": "Task name (used for the ai/<agent>/<task> branch and the worktree)",
70
+ "required": true,
71
+ "enabled": {
72
+ "type": "condition",
73
+ "value": "sandboxAction !== 'prune'"
74
+ }
75
+ },
76
+ {
77
+ "id": "skillsDir",
78
+ "type": "input",
79
+ "message": "Centralized skills/prompts directory",
80
+ "default": "Z:\\github\\ccpu\\skills",
81
+ "required": true,
82
+ "enabled": {
83
+ "type": "condition",
84
+ "value": "sandboxAction === 'online'"
85
+ }
86
+ },
87
+ {
88
+ "id": "fullAccess",
89
+ "type": "confirm",
90
+ "message": "Let the agent act without approval prompts? (mounted data remains writable)",
91
+ "default": true,
92
+ "enabled": {
93
+ "type": "condition",
94
+ "value": "sandboxAction !== 'prune'"
95
+ }
96
+ },
97
+ {
98
+ "id": "gitMountFlag",
99
+ "type": "select",
100
+ "message": "Allow writes to shared Git metadata (refs, hooks and config)?",
101
+ "choices": [
102
+ {
103
+ "name": "Allow Git commits and history in the container",
104
+ "value": ""
105
+ },
106
+ {
107
+ "name": "Withhold Git metadata (Git will not work in the container)",
108
+ "value": "--no-git-mount"
109
+ }
110
+ ],
111
+ "default": "",
112
+ "enabled": {
113
+ "type": "condition",
114
+ "value": "sandboxAction !== 'prune'"
115
+ }
116
+ }
117
+ ],
118
+ "tasks": [
119
+ {
120
+ "id": "start-sandbox",
121
+ "name": "Start the coding agent sandbox",
122
+ "description": "Creates the agent worktree, starts Docker and hands the terminal to the agent",
123
+ "type": "exec",
124
+ "config": {
125
+ "command": "npx -y @pixpilot/coding-agent-sandbox@latest --agent {{agent}} --repo \"{{repoPath}}\" --task \"{{taskName}}\" --skills-dir \"{{skillsDir}}\" --full-access {{fullAccess}} {{gitMountFlag}}"
126
+ },
127
+ "enabled": {
128
+ "type": "condition",
129
+ "value": "sandboxAction === 'online'"
130
+ }
131
+ },
132
+ {
133
+ "id": "start-offline-sandbox",
134
+ "name": "Start the offline coding agent sandbox",
135
+ "description": "Uses the installed CLI and cached image without package downloads or provisioning",
136
+ "type": "exec",
137
+ "enabled": {
138
+ "type": "condition",
139
+ "value": "sandboxAction === 'offline'"
140
+ },
141
+ "config": {
142
+ "command": "coding-agent-sandbox --offline --agent {{agent}} --repo \"{{repoPath}}\" --task \"{{taskName}}\" --full-access {{fullAccess}} {{gitMountFlag}}"
143
+ }
144
+ },
145
+ {
146
+ "id": "prune-sandbox-volumes",
147
+ "name": "Prune unused sandbox caches",
148
+ "description": "Lists unused labeled dependency/cache volumes and asks before permanent deletion; preserves auth",
149
+ "type": "exec",
150
+ "enabled": {
151
+ "type": "condition",
152
+ "value": "sandboxAction === 'prune'"
153
+ },
154
+ "config": {
155
+ "command": "npx -y @pixpilot/coding-agent-sandbox@latest prune"
156
+ }
157
+ }
158
+ ]
159
+ }
@@ -60,6 +60,9 @@ const after = readme.substring(endIndex);
60
60
 
61
61
  const newReadme = `${before}\n\n${markdown}${after}`;
62
62
 
63
- fs.writeFileSync(readmePath, newReadme);
64
-
65
- console.log('README.md updated with packages.');
63
+ if (newReadme !== readme) {
64
+ fs.writeFileSync(readmePath, newReadme);
65
+ console.log('README.md updated with packages.');
66
+ } else {
67
+ console.log('README.md package section is already up to date.');
68
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pixpilot/scaffoldfy-configs",
3
3
  "type": "module",
4
- "version": "0.34.0",
4
+ "version": "0.35.0",
5
5
  "author": "PixPilot <m.doaie@hotmail.com>",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -26,11 +26,11 @@
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.18.11",
28
28
  "eslint": "^9.38.0",
29
+ "@internal/eslint-config": "0.3.0",
29
30
  "@internal/prettier-config": "0.0.1",
30
31
  "@internal/tsdown-config": "0.1.0",
31
32
  "@internal/vitest-config": "0.1.0",
32
- "@pixpilot/scaffoldfy": "0.52.0",
33
- "@internal/eslint-config": "0.3.0"
33
+ "@pixpilot/scaffoldfy": "0.53.0"
34
34
  },
35
35
  "prettier": "@internal/prettier-config",
36
36
  "publishConfig": {
@@ -27,6 +27,10 @@ function createFakeGit(testDirectory: string): string {
27
27
  binDirectory,
28
28
  process.platform === 'win32' ? 'git.exe' : 'git',
29
29
  );
30
+ const pnpmCommandPath = path.join(
31
+ binDirectory,
32
+ process.platform === 'win32' ? 'pnpm.exe' : 'pnpm',
33
+ );
30
34
 
31
35
  fs.mkdirSync(binDirectory, { recursive: true });
32
36
  fs.writeFileSync(
@@ -41,22 +45,37 @@ function createFakeGit(testDirectory: string): string {
41
45
  ' const destination = args.at(-1);',
42
46
  " if (destination === '.') {",
43
47
  " fs.writeFileSync(path.join(process.cwd(), 'base-template.txt'), 'base');",
48
+ " fs.writeFileSync(path.join(process.cwd(), 'package.json'), JSON.stringify({ scripts: { lint: 'base-lint' }, devDependencies: { 'base-dependency': '1.0.0' } }));",
49
+ " fs.writeFileSync(path.join(process.cwd(), 'pnpm-workspace.yaml'), \"packages:\\n - 'packages/**'\\n - 'tooling/**'\\ncatalogs:\\n dev:\\n tsdown: ^1.0.0\\n typescript-eslint: ^1.0.0\\n\");",
44
50
  ' } else {',
45
51
  ' fs.mkdirSync(destination, { recursive: true });',
46
52
  ' }',
53
+ " } else if (command === 'pnpm' || command === 'install') {",
54
+ " fs.writeFileSync(path.join(process.cwd(), 'pnpm-lock.yaml'), 'lock');",
47
55
  " } else if (command === 'sparse-checkout') {",
48
- ' const sourceDirectory = args.at(-1);',
49
- ' const templateDirectory = path.join(process.cwd(), sourceDirectory);',
50
- ' fs.mkdirSync(templateDirectory, { recursive: true });',
51
- " fs.writeFileSync(path.join(templateDirectory, 'template.txt'), sourceDirectory);",
56
+ " const sourceDirectories = args.filter((arg) => !arg.startsWith('-') && arg !== 'set');",
57
+ ' for (const sourceDirectory of sourceDirectories) {',
58
+ " if (sourceDirectory === 'package.json') {",
59
+ " fs.writeFileSync(path.join(process.cwd(), sourceDirectory), JSON.stringify({ scripts: { 'web:dev': 'source-web-dev', 'web:build': 'source-web-build', dev: 'source-dev', native: 'source-native', 'expo:clean:install': 'source-expo-clean-install', 'expo:clean:run': 'source-expo-clean-run', 'android:dev': 'source-android-dev', 'android:clean:dev': 'source-android-clean-dev', 'ios:dev': 'source-ios-dev', 'ios:clean:dev': 'source-ios-clean-dev' }, devDependencies: { 'template-dependency': '1.0.0' } }));",
60
+ " } else if (sourceDirectory === 'pnpm-workspace.yaml') {",
61
+ ' fs.writeFileSync(path.join(process.cwd(), sourceDirectory), "packages:\\n - apps/*\\n - packages/*\\n - tooling/*\\ncatalogs:\\n dev:\\n template-dependency: ^1.0.0\\n \\"typescript-eslint\\": ^2.0.0\\n");',
62
+ " } else if (sourceDirectory === '.github/workflows') {",
63
+ ' fs.mkdirSync(path.join(process.cwd(), sourceDirectory), { recursive: true });',
64
+ " fs.writeFileSync(path.join(process.cwd(), sourceDirectory, 'ci.yml'), 'ci');",
65
+ ' } else {',
66
+ ' const templateDirectory = path.join(process.cwd(), sourceDirectory);',
67
+ ' fs.mkdirSync(templateDirectory, { recursive: true });',
68
+ " fs.writeFileSync(path.join(templateDirectory, 'template.txt'), sourceDirectory);",
69
+ ' }',
70
+ ' }',
52
71
  '}',
53
72
  '}',
54
73
  "if (process.env.SCAFFOLDFY_FAKE_GIT_MODE === 'preload') {",
55
74
  ' const resolveFilename = Module._resolveFilename;',
56
75
  ' Module._resolveFilename = function resolveFakeGit(request, ...args) {',
57
76
  ' const command = path.basename(request);',
58
- " if (command === 'clone' || command === 'sparse-checkout') {",
59
- ' applyGitOperation(command, process.argv.slice(2));',
77
+ " if (command === 'clone' || command === 'install' || command === 'pnpm' || command === 'sparse-checkout') {",
78
+ " applyGitOperation(command === 'install' ? 'pnpm' : command, process.argv.slice(2));",
60
79
  " return path.join(__dirname, 'noop.cjs');",
61
80
  ' }',
62
81
  ' return resolveFilename.call(this, request, ...args);',
@@ -71,12 +90,18 @@ function createFakeGit(testDirectory: string): string {
71
90
  if (process.platform === 'win32') {
72
91
  fs.writeFileSync(path.join(binDirectory, 'noop.cjs'), "'use strict';\n");
73
92
  fs.copyFileSync(process.execPath, gitCommandPath);
93
+ fs.copyFileSync(process.execPath, pnpmCommandPath);
74
94
  } else {
75
95
  fs.writeFileSync(
76
96
  gitCommandPath,
77
97
  `#!${process.execPath}\nrequire('./fake-git.cjs');\n`,
78
98
  );
79
99
  fs.chmodSync(gitCommandPath, 0o755);
100
+ fs.writeFileSync(
101
+ pnpmCommandPath,
102
+ `#!${process.execPath}\nrequire('./fake-git.cjs');\n`,
103
+ );
104
+ fs.chmodSync(pnpmCommandPath, 0o755);
80
105
  }
81
106
 
82
107
  return binDirectory;
@@ -118,14 +143,67 @@ describe('workspace-generator setup script', () => {
118
143
  expect(fs.existsSync(path.join(workspaceDirectory, 'apps'))).toBe(false);
119
144
  });
120
145
 
146
+ it('should add the selected app and its required workspace files', () => {
147
+ const workspaceDirectory = runSetup('nextjs-cloudflare');
148
+
149
+ expect(
150
+ fs.readFileSync(path.join(workspaceDirectory, 'base-template.txt'), 'utf8'),
151
+ ).toBe('base');
152
+ expect(
153
+ fs.readFileSync(
154
+ path.join(workspaceDirectory, 'apps', 'web', 'template.txt'),
155
+ 'utf8',
156
+ ),
157
+ ).toBe('apps/web');
158
+ expect(
159
+ fs.readFileSync(path.join(workspaceDirectory, 'packages', 'template.txt'), 'utf8'),
160
+ ).toBe('packages');
161
+ expect(
162
+ fs.readFileSync(path.join(workspaceDirectory, 'tooling', 'template.txt'), 'utf8'),
163
+ ).toBe('tooling');
164
+ expect(
165
+ fs.readFileSync(
166
+ path.join(workspaceDirectory, '.github', 'workflows', 'ci.yml'),
167
+ 'utf8',
168
+ ),
169
+ ).toBe('ci');
170
+
171
+ const packageJson = JSON.parse(
172
+ fs.readFileSync(path.join(workspaceDirectory, 'package.json'), 'utf8'),
173
+ );
174
+ expect(packageJson.scripts).toMatchObject({
175
+ lint: 'base-lint',
176
+ 'web:dev': 'source-web-dev',
177
+ });
178
+ expect(packageJson.devDependencies).toMatchObject({
179
+ 'base-dependency': '1.0.0',
180
+ 'template-dependency': '1.0.0',
181
+ });
182
+ expect(
183
+ fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
184
+ ).toContain(' - apps/*');
185
+ expect(
186
+ fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
187
+ ).not.toContain('apps/**');
188
+ expect(
189
+ fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
190
+ ).toContain(' tsdown: ^1.0.0');
191
+ expect(
192
+ fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
193
+ ).toContain(' template-dependency: ^1.0.0');
194
+ expect(
195
+ fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
196
+ ).toContain(' "typescript-eslint": ^2.0.0');
197
+ expect(fs.readFileSync(path.join(workspaceDirectory, 'pnpm-lock.yaml'), 'utf8')).toBe(
198
+ 'lock',
199
+ );
200
+ });
201
+
121
202
  it('should add every selected app to the base workspace', () => {
122
203
  const workspaceDirectory = runSetup(
123
204
  'library,nextjs-cloudflare,chrome-extension,expo',
124
205
  );
125
206
 
126
- expect(
127
- fs.readFileSync(path.join(workspaceDirectory, 'base-template.txt'), 'utf8'),
128
- ).toBe('base');
129
207
  expect(
130
208
  fs.readFileSync(
131
209
  path.join(workspaceDirectory, 'apps', 'web', 'template.txt'),
@@ -144,6 +222,15 @@ describe('workspace-generator setup script', () => {
144
222
  'utf8',
145
223
  ),
146
224
  ).toBe('apps/expo');
225
+
226
+ const packageJson = JSON.parse(
227
+ fs.readFileSync(path.join(workspaceDirectory, 'package.json'), 'utf8'),
228
+ );
229
+ expect(packageJson.scripts).toMatchObject({
230
+ dev: 'source-dev',
231
+ native: 'source-native',
232
+ 'web:dev': 'source-web-dev',
233
+ });
147
234
  });
148
235
 
149
236
  it('should fail when no workspace app is selected', () => {
@@ -15,23 +15,53 @@ const APP_TEMPLATES = {
15
15
  repository: 'https://github.com/ccpu/nextjs-cloudflare-monorepo-template.git',
16
16
  sourceDirectory: 'apps/web',
17
17
  destinationDirectory: 'apps/web',
18
+ rootScripts: ['web:dev', 'web:build'],
18
19
  },
19
20
  'chrome-extension': {
20
21
  repository: 'https://github.com/ccpu/chrome-extension-monorepo-template.git',
21
22
  sourceDirectory: 'apps/chrome-extension',
22
23
  destinationDirectory: 'apps/chrome-extension',
24
+ rootScripts: ['dev'],
23
25
  },
24
26
  expo: {
25
27
  repository: 'https://github.com/ccpu/full-stack-starter.git',
26
28
  sourceDirectory: 'apps/expo',
27
29
  destinationDirectory: 'apps/expo',
30
+ rootScripts: [
31
+ 'native',
32
+ 'expo:clean:install',
33
+ 'expo:clean:run',
34
+ 'android:dev',
35
+ 'android:clean:dev',
36
+ 'ios:dev',
37
+ 'ios:clean:dev',
38
+ ],
28
39
  },
29
40
  };
30
41
 
42
+ const TEMPLATE_SOURCE_PATHS = [
43
+ 'package.json',
44
+ 'pnpm-workspace.yaml',
45
+ 'packages',
46
+ 'tooling',
47
+ '.github/workflows',
48
+ ];
49
+ const JSON_INDENTATION = 2;
50
+ const LINE_BREAK_PATTERN = /\r?\n/u;
51
+ const TOP_LEVEL_SECTION_PATTERN = /^[^\s#][^:]*:/u;
52
+ const INDENTED_LIST_ENTRY_PATTERN = /^ {2}- /u;
53
+
31
54
  function runGit(args, cwd) {
32
55
  execFileSync('git', args, { cwd, stdio: 'inherit' });
33
56
  }
34
57
 
58
+ function refreshLockfile(cwd) {
59
+ execFileSync('pnpm', ['install', '--lockfile-only', '--ignore-scripts'], {
60
+ cwd,
61
+ stdio: 'inherit',
62
+ });
63
+ }
64
+
35
65
  function getSelectedApps() {
36
66
  return new Set(
37
67
  (process.env.WORKSPACE_APPS ?? '')
@@ -57,6 +87,234 @@ function copyDirectoryContents(sourceDirectory, destinationDirectory) {
57
87
  }
58
88
  }
59
89
 
90
+ function overlayDirectoryContents(sourceDirectory, destinationDirectory) {
91
+ if (!fs.existsSync(sourceDirectory)) {
92
+ return;
93
+ }
94
+
95
+ fs.mkdirSync(destinationDirectory, { recursive: true });
96
+
97
+ for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
98
+ fs.cpSync(
99
+ path.join(sourceDirectory, entry.name),
100
+ path.join(destinationDirectory, entry.name),
101
+ { recursive: entry.isDirectory(), force: true },
102
+ );
103
+ }
104
+ }
105
+
106
+ function mergeDependencySections(basePackage, templatePackage) {
107
+ return [
108
+ 'dependencies',
109
+ 'devDependencies',
110
+ 'optionalDependencies',
111
+ 'peerDependencies',
112
+ ].reduce(
113
+ (mergedPackage, section) =>
114
+ templatePackage[section]
115
+ ? {
116
+ ...mergedPackage,
117
+ [section]: {
118
+ ...templatePackage[section],
119
+ ...mergedPackage[section],
120
+ },
121
+ }
122
+ : mergedPackage,
123
+ basePackage,
124
+ );
125
+ }
126
+
127
+ function mergeRootPackageManifest(template, repositoryDirectory, workspaceDirectory) {
128
+ const packageJsonPath = path.join(workspaceDirectory, 'package.json');
129
+ const templatePackageJsonPath = path.join(repositoryDirectory, 'package.json');
130
+ const basePackage = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
131
+ const templatePackage = JSON.parse(fs.readFileSync(templatePackageJsonPath, 'utf8'));
132
+ const packageWithDependencies = mergeDependencySections(basePackage, templatePackage);
133
+ const templateScripts = Object.fromEntries(
134
+ template.rootScripts.flatMap((scriptName) => {
135
+ const script = templatePackage.scripts?.[scriptName];
136
+
137
+ return script ? [[scriptName, script]] : [];
138
+ }),
139
+ );
140
+
141
+ const mergedPackage = {
142
+ ...packageWithDependencies,
143
+ scripts: {
144
+ ...templateScripts,
145
+ ...packageWithDependencies.scripts,
146
+ },
147
+ };
148
+
149
+ fs.writeFileSync(
150
+ packageJsonPath,
151
+ `${JSON.stringify(mergedPackage, null, JSON_INDENTATION)}\n`,
152
+ );
153
+ }
154
+
155
+ function escapeRegularExpression(value) {
156
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
157
+ }
158
+
159
+ function normalizeYamlKey(value) {
160
+ return value.replace(/^['"]|['"]$/gu, '');
161
+ }
162
+
163
+ function getCatalogEntries(workspaceManifest) {
164
+ const catalogEntries = new Map();
165
+ let isInCatalogsSection = false;
166
+ let catalogName;
167
+
168
+ for (const line of workspaceManifest.split(LINE_BREAK_PATTERN)) {
169
+ if (line === 'catalogs:') {
170
+ isInCatalogsSection = true;
171
+ } else if (isInCatalogsSection && TOP_LEVEL_SECTION_PATTERN.test(line)) {
172
+ isInCatalogsSection = false;
173
+ } else if (isInCatalogsSection) {
174
+ const catalogMatch = line.match(/^ {2}(?<catalogName>[^:\r\n]+):[\t ]*$/u);
175
+ const entryMatch = line.match(/^ {4}(?<key>['"][^'"]+['"]|[^:]+):/u);
176
+
177
+ if (catalogMatch) {
178
+ const { catalogName: nextCatalogName } = catalogMatch.groups;
179
+ catalogName = nextCatalogName;
180
+ catalogEntries.set(catalogName, []);
181
+ } else if (catalogName && entryMatch) {
182
+ const { key } = entryMatch.groups;
183
+ catalogEntries.get(catalogName).push({ key, line });
184
+ }
185
+ }
186
+ }
187
+
188
+ return catalogEntries;
189
+ }
190
+
191
+ function getYamlListEntries(workspaceManifest, sectionName) {
192
+ const entries = [];
193
+ let isInSection = false;
194
+
195
+ for (const line of workspaceManifest.split(LINE_BREAK_PATTERN)) {
196
+ if (line === `${sectionName}:`) {
197
+ isInSection = true;
198
+ } else if (isInSection && TOP_LEVEL_SECTION_PATTERN.test(line)) {
199
+ isInSection = false;
200
+ } else if (isInSection && INDENTED_LIST_ENTRY_PATTERN.test(line)) {
201
+ entries.push(line);
202
+ }
203
+ }
204
+
205
+ return entries;
206
+ }
207
+
208
+ function mergeCatalogEntries(baseWorkspaceManifest, templateWorkspaceManifest) {
209
+ let mergedWorkspaceManifest = baseWorkspaceManifest;
210
+ const lineEnding = baseWorkspaceManifest.includes('\r\n') ? '\r\n' : '\n';
211
+
212
+ for (const [catalogName, entries] of getCatalogEntries(templateWorkspaceManifest)) {
213
+ const catalogHeader = new RegExp(
214
+ `^ {2}${escapeRegularExpression(catalogName)}:[\\t ]*(?:\\r?\\n|$)`,
215
+ 'mu',
216
+ );
217
+
218
+ if (!catalogHeader.test(mergedWorkspaceManifest)) {
219
+ mergedWorkspaceManifest = mergedWorkspaceManifest.replace(
220
+ /^catalogs:[\t ]*(?:\r?\n|$)/mu,
221
+ (header) =>
222
+ `${header} ${catalogName}:${lineEnding}${entries.map((entry) => entry.line).join(lineEnding)}${lineEnding}`,
223
+ );
224
+ } else {
225
+ for (const entry of entries) {
226
+ const entryPattern = new RegExp(
227
+ `^ {4}['"]?${escapeRegularExpression(normalizeYamlKey(entry.key))}['"]?:[^\\r\\n]*`,
228
+ 'mu',
229
+ );
230
+
231
+ if (entryPattern.test(mergedWorkspaceManifest)) {
232
+ mergedWorkspaceManifest = mergedWorkspaceManifest.replace(
233
+ entryPattern,
234
+ entry.line,
235
+ );
236
+ } else {
237
+ const catalogMatch = catalogHeader.exec(mergedWorkspaceManifest);
238
+ const catalogContents = mergedWorkspaceManifest.slice(
239
+ catalogMatch.index + catalogMatch[0].length,
240
+ );
241
+ const nextCatalogMatch = catalogContents.match(
242
+ /^(?: {2})?[^\s#][^:]*:[\t ]*(?:\r?\n|$)/mu,
243
+ );
244
+ const insertionIndex = nextCatalogMatch
245
+ ? catalogMatch.index + catalogMatch[0].length + nextCatalogMatch.index
246
+ : mergedWorkspaceManifest.length;
247
+
248
+ mergedWorkspaceManifest = `${mergedWorkspaceManifest.slice(0, insertionIndex)}${entry.line}${lineEnding}${mergedWorkspaceManifest.slice(insertionIndex)}`;
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ return mergedWorkspaceManifest;
255
+ }
256
+
257
+ function mergeYamlListEntries(
258
+ baseWorkspaceManifest,
259
+ templateWorkspaceManifest,
260
+ sectionName,
261
+ ) {
262
+ const templateEntries = getYamlListEntries(templateWorkspaceManifest, sectionName);
263
+ if (templateEntries.length === 0) {
264
+ return baseWorkspaceManifest;
265
+ }
266
+
267
+ const lineEnding = baseWorkspaceManifest.includes('\r\n') ? '\r\n' : '\n';
268
+ const lines = baseWorkspaceManifest.split(LINE_BREAK_PATTERN);
269
+ const sectionIndex = lines.indexOf(`${sectionName}:`);
270
+
271
+ if (sectionIndex === -1) {
272
+ return `${baseWorkspaceManifest.trimEnd()}${lineEnding}${sectionName}:${lineEnding}${templateEntries.join(lineEnding)}${lineEnding}`;
273
+ }
274
+
275
+ let sectionEndIndex = sectionIndex + 1;
276
+ while (
277
+ sectionEndIndex < lines.length &&
278
+ !TOP_LEVEL_SECTION_PATTERN.test(lines[sectionEndIndex])
279
+ ) {
280
+ sectionEndIndex += 1;
281
+ }
282
+
283
+ const existingEntries = new Set(
284
+ lines
285
+ .slice(sectionIndex + 1, sectionEndIndex)
286
+ .filter((line) => INDENTED_LIST_ENTRY_PATTERN.test(line)),
287
+ );
288
+ const missingEntries = templateEntries.filter((entry) => !existingEntries.has(entry));
289
+
290
+ lines.splice(sectionEndIndex, 0, ...missingEntries);
291
+ return lines.join(lineEnding);
292
+ }
293
+
294
+ function mergeWorkspaceManifest(templateWorkspaceManifestPath, workspaceManifestPath) {
295
+ const baseWorkspaceManifest = fs.readFileSync(workspaceManifestPath, 'utf8');
296
+ const templateWorkspaceManifest = fs.readFileSync(
297
+ templateWorkspaceManifestPath,
298
+ 'utf8',
299
+ );
300
+ const catalogMergedWorkspaceManifest = mergeCatalogEntries(
301
+ baseWorkspaceManifest,
302
+ templateWorkspaceManifest,
303
+ );
304
+ const mergedWorkspaceManifest = mergeYamlListEntries(
305
+ catalogMergedWorkspaceManifest,
306
+ templateWorkspaceManifest,
307
+ 'packages',
308
+ );
309
+ const workspaceManifestWithTemplatePackages = mergeYamlListEntries(
310
+ mergedWorkspaceManifest,
311
+ templateWorkspaceManifest,
312
+ 'onlyBuiltDependencies',
313
+ );
314
+
315
+ fs.writeFileSync(workspaceManifestPath, workspaceManifestWithTemplatePackages);
316
+ }
317
+
60
318
  function addAppTemplate(template, workspaceDirectory) {
61
319
  const destinationDirectory = path.join(
62
320
  workspaceDirectory,
@@ -84,7 +342,16 @@ function addAppTemplate(template, workspaceDirectory) {
84
342
  ],
85
343
  workspaceDirectory,
86
344
  );
87
- runGit(['sparse-checkout', 'set', template.sourceDirectory], repositoryDirectory);
345
+ runGit(
346
+ [
347
+ 'sparse-checkout',
348
+ 'set',
349
+ '--skip-checks',
350
+ template.sourceDirectory,
351
+ ...TEMPLATE_SOURCE_PATHS,
352
+ ],
353
+ repositoryDirectory,
354
+ );
88
355
 
89
356
  const sourceDirectory = path.join(repositoryDirectory, template.sourceDirectory);
90
357
  if (!fs.existsSync(sourceDirectory)) {
@@ -94,6 +361,27 @@ function addAppTemplate(template, workspaceDirectory) {
94
361
  }
95
362
 
96
363
  copyDirectoryContents(sourceDirectory, destinationDirectory);
364
+ overlayDirectoryContents(
365
+ path.join(repositoryDirectory, 'packages'),
366
+ path.join(workspaceDirectory, 'packages'),
367
+ );
368
+ overlayDirectoryContents(
369
+ path.join(repositoryDirectory, 'tooling'),
370
+ path.join(workspaceDirectory, 'tooling'),
371
+ );
372
+ overlayDirectoryContents(
373
+ path.join(repositoryDirectory, '.github', 'workflows'),
374
+ path.join(workspaceDirectory, '.github', 'workflows'),
375
+ );
376
+ mergeRootPackageManifest(template, repositoryDirectory, workspaceDirectory);
377
+
378
+ const templateWorkspaceManifestPath = path.join(
379
+ repositoryDirectory,
380
+ 'pnpm-workspace.yaml',
381
+ );
382
+ const workspaceManifestPath = path.join(workspaceDirectory, 'pnpm-workspace.yaml');
383
+
384
+ mergeWorkspaceManifest(templateWorkspaceManifestPath, workspaceManifestPath);
97
385
  } finally {
98
386
  fs.rmSync(temporaryDirectory, { recursive: true, force: true });
99
387
  }
@@ -102,6 +390,7 @@ function addAppTemplate(template, workspaceDirectory) {
102
390
  function main() {
103
391
  const workspaceDirectory = process.cwd();
104
392
  const selectedApps = getSelectedApps();
393
+ const selectedTemplates = [...selectedApps].filter((app) => APP_TEMPLATES[app]);
105
394
 
106
395
  if (selectedApps.size === 0) {
107
396
  throw new Error('Select at least one workspace app.');
@@ -110,11 +399,12 @@ function main() {
110
399
  ensureEmptyDirectory(workspaceDirectory);
111
400
  runGit(['clone', '--depth', '1', BASE_TEMPLATE_REPOSITORY, '.'], workspaceDirectory);
112
401
 
113
- for (const app of selectedApps) {
114
- const template = APP_TEMPLATES[app];
115
- if (template) {
116
- addAppTemplate(template, workspaceDirectory);
117
- }
402
+ for (const app of selectedTemplates) {
403
+ addAppTemplate(APP_TEMPLATES[app], workspaceDirectory);
404
+ }
405
+
406
+ if (selectedTemplates.length > 0) {
407
+ refreshLockfile(workspaceDirectory);
118
408
  }
119
409
  }
120
410