@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 +15 -0
- package/README.md +10 -0
- package/coding-agent-sandbox/scaffoldfy.json +159 -0
- package/monorepo-generate-packages-section/generate-packages-section.js +6 -3
- package/package.json +3 -3
- package/test/workspace-generator.test.ts +96 -9
- package/workspace-generator/scripts/setup-workspace.cjs +296 -6
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
49
|
-
' const
|
|
50
|
-
'
|
|
51
|
-
"
|
|
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
|
-
|
|
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(
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|