ai-ide-config 0.1.0 → 0.1.2
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/README.md +117 -117
- package/bin/cli.mjs +186 -186
- package/package.json +3 -3
- package/src/init.mjs +195 -195
- package/src/package-manager.mjs +95 -95
- package/src/paths.mjs +20 -20
- package/src/prompt.mjs +49 -49
- package/src/skills.mjs +46 -46
- package/src/stacks.mjs +20 -20
- package/templates/angular/AGENTS.md +50 -50
- package/templates/angular/pnpm-workspace.yaml +5 -5
- package/templates/shared/.cursor/commands/generate-pr-description.md +70 -70
- package/templates/shared/.cursor/rules/conventional-commits.mdc +63 -63
package/src/init.mjs
CHANGED
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { sharedTemplatesDir, stackTemplatesDir } from './paths.mjs';
|
|
4
|
-
import { ensureExactPnpmEngines } from './package-manager.mjs';
|
|
5
|
-
|
|
6
|
-
const GITIGNORE = '.gitignore';
|
|
7
|
-
const GITIGNORE_ENTRIES = ['.cursor/', '.agents/'];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Recursively collect relative file paths under dir.
|
|
11
|
-
* @param {string} dir
|
|
12
|
-
* @param {string} [base]
|
|
13
|
-
* @returns {string[]}
|
|
14
|
-
*/
|
|
15
|
-
function listFiles(dir, base = dir) {
|
|
16
|
-
if (!fs.existsSync(dir)) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
-
const files = [];
|
|
22
|
-
|
|
23
|
-
for (const entry of entries) {
|
|
24
|
-
const absolute = path.join(dir, entry.name);
|
|
25
|
-
if (entry.isDirectory()) {
|
|
26
|
-
files.push(...listFiles(absolute, base));
|
|
27
|
-
} else if (entry.isFile()) {
|
|
28
|
-
files.push(path.relative(base, absolute));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return files;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @typedef {{ created: string[], skipped: string[], overwritten: string[], updated: string[] }} CopyReport
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Whether .gitignore already ignores a directory entry (with or without trailing slash).
|
|
41
|
-
* @param {string} content
|
|
42
|
-
* @param {string} entry e.g. ".cursor/" or ".agents/"
|
|
43
|
-
*/
|
|
44
|
-
function hasIgnoreEntry(content, entry) {
|
|
45
|
-
const name = entry.replace(/\/$/, '').replace(/^\./, '\\.');
|
|
46
|
-
return new RegExp(`(?:^|\\n)\\s*${name}\\/?\\s*(?:\\n|$)`).test(content);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Create .gitignore with required entries, or append any that are missing.
|
|
51
|
-
* @param {string} targetDir
|
|
52
|
-
* @param {{ dryRun?: boolean }} options
|
|
53
|
-
* @param {CopyReport} report
|
|
54
|
-
*/
|
|
55
|
-
function ensureGitignore(targetDir, options, report) {
|
|
56
|
-
const filePath = path.join(targetDir, GITIGNORE);
|
|
57
|
-
const exists = fs.existsSync(filePath);
|
|
58
|
-
|
|
59
|
-
if (!exists) {
|
|
60
|
-
if (options.dryRun) {
|
|
61
|
-
report.created.push(`${GITIGNORE} (${GITIGNORE_ENTRIES.join(', ')})`);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
fs.writeFileSync(filePath, `${GITIGNORE_ENTRIES.join('\n')}\n`, 'utf8');
|
|
65
|
-
report.created.push(GITIGNORE);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
70
|
-
const missing = GITIGNORE_ENTRIES.filter(
|
|
71
|
-
(entry) => !hasIgnoreEntry(content, entry),
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (missing.length === 0) {
|
|
75
|
-
report.skipped.push(
|
|
76
|
-
`${GITIGNORE} (${GITIGNORE_ENTRIES.join(', ')} already listed)`,
|
|
77
|
-
);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (options.dryRun) {
|
|
82
|
-
report.updated.push(`${GITIGNORE} (append ${missing.join(', ')})`);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
|
|
87
|
-
fs.writeFileSync(
|
|
88
|
-
filePath,
|
|
89
|
-
`${content}${separator}${missing.join('\n')}\n`,
|
|
90
|
-
'utf8',
|
|
91
|
-
);
|
|
92
|
-
report.updated.push(`${GITIGNORE} (appended ${missing.join(', ')})`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Copy all files from sourceRoot into targetDir.
|
|
97
|
-
* @param {string} sourceRoot
|
|
98
|
-
* @param {string} targetDir
|
|
99
|
-
* @param {{ force?: boolean, dryRun?: boolean }} options
|
|
100
|
-
* @param {CopyReport} report
|
|
101
|
-
*/
|
|
102
|
-
function copyTree(sourceRoot, targetDir, options, report) {
|
|
103
|
-
const relativeFiles = listFiles(sourceRoot);
|
|
104
|
-
|
|
105
|
-
for (const relative of relativeFiles) {
|
|
106
|
-
const from = path.join(sourceRoot, relative);
|
|
107
|
-
const to = path.join(targetDir, relative);
|
|
108
|
-
const exists = fs.existsSync(to);
|
|
109
|
-
|
|
110
|
-
if (exists && !options.force) {
|
|
111
|
-
report.skipped.push(relative);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (options.dryRun) {
|
|
116
|
-
if (exists) {
|
|
117
|
-
report.overwritten.push(relative);
|
|
118
|
-
} else {
|
|
119
|
-
report.created.push(relative);
|
|
120
|
-
}
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
125
|
-
fs.copyFileSync(from, to);
|
|
126
|
-
|
|
127
|
-
if (exists) {
|
|
128
|
-
report.overwritten.push(relative);
|
|
129
|
-
} else {
|
|
130
|
-
report.created.push(relative);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Scaffold shared .cursor config + stack templates into targetDir.
|
|
137
|
-
* @param {{ stack: { id: string, agentsDir: string }, targetDir: string, force?: boolean, dryRun?: boolean }} opts
|
|
138
|
-
* @returns {CopyReport}
|
|
139
|
-
*/
|
|
140
|
-
export function initStack(opts) {
|
|
141
|
-
const { stack, targetDir, force = false, dryRun = false } = opts;
|
|
142
|
-
const options = { force, dryRun };
|
|
143
|
-
/** @type {CopyReport} */
|
|
144
|
-
const report = {
|
|
145
|
-
created: [],
|
|
146
|
-
skipped: [],
|
|
147
|
-
overwritten: [],
|
|
148
|
-
updated: [],
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const shared = sharedTemplatesDir();
|
|
152
|
-
const stackDir = stackTemplatesDir(stack.agentsDir);
|
|
153
|
-
|
|
154
|
-
if (!fs.existsSync(shared)) {
|
|
155
|
-
throw new Error(`Shared templates missing: ${shared}`);
|
|
156
|
-
}
|
|
157
|
-
if (!fs.existsSync(stackDir)) {
|
|
158
|
-
throw new Error(`Stack templates missing for "${stack.id}": ${stackDir}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
copyTree(shared, targetDir, options, report);
|
|
162
|
-
copyTree(stackDir, targetDir, options, report);
|
|
163
|
-
ensureGitignore(targetDir, options, report);
|
|
164
|
-
ensureExactPnpmEngines(targetDir, options, report);
|
|
165
|
-
|
|
166
|
-
return report;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function printReport(report, { dryRun = false } = {}) {
|
|
170
|
-
const prefix = dryRun ? '[dry-run] ' : '';
|
|
171
|
-
|
|
172
|
-
const sections = [
|
|
173
|
-
['Created', report.created],
|
|
174
|
-
['Updated', report.updated],
|
|
175
|
-
['Overwritten', report.overwritten],
|
|
176
|
-
['Skipped (already exists)', report.skipped],
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
for (const [label, files] of sections) {
|
|
180
|
-
if (!files || files.length === 0) continue;
|
|
181
|
-
console.log(`${prefix}${label}:`);
|
|
182
|
-
for (const file of files) {
|
|
183
|
-
console.log(` ${file}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
report.created.length === 0 &&
|
|
189
|
-
report.overwritten.length === 0 &&
|
|
190
|
-
report.skipped.length === 0 &&
|
|
191
|
-
(report.updated?.length ?? 0) === 0
|
|
192
|
-
) {
|
|
193
|
-
console.log(`${prefix}Nothing to do.`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sharedTemplatesDir, stackTemplatesDir } from './paths.mjs';
|
|
4
|
+
import { ensureExactPnpmEngines } from './package-manager.mjs';
|
|
5
|
+
|
|
6
|
+
const GITIGNORE = '.gitignore';
|
|
7
|
+
const GITIGNORE_ENTRIES = ['.cursor/', '.agents/'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively collect relative file paths under dir.
|
|
11
|
+
* @param {string} dir
|
|
12
|
+
* @param {string} [base]
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
function listFiles(dir, base = dir) {
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
const files = [];
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const absolute = path.join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
files.push(...listFiles(absolute, base));
|
|
27
|
+
} else if (entry.isFile()) {
|
|
28
|
+
files.push(path.relative(base, absolute));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {{ created: string[], skipped: string[], overwritten: string[], updated: string[] }} CopyReport
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether .gitignore already ignores a directory entry (with or without trailing slash).
|
|
41
|
+
* @param {string} content
|
|
42
|
+
* @param {string} entry e.g. ".cursor/" or ".agents/"
|
|
43
|
+
*/
|
|
44
|
+
function hasIgnoreEntry(content, entry) {
|
|
45
|
+
const name = entry.replace(/\/$/, '').replace(/^\./, '\\.');
|
|
46
|
+
return new RegExp(`(?:^|\\n)\\s*${name}\\/?\\s*(?:\\n|$)`).test(content);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create .gitignore with required entries, or append any that are missing.
|
|
51
|
+
* @param {string} targetDir
|
|
52
|
+
* @param {{ dryRun?: boolean }} options
|
|
53
|
+
* @param {CopyReport} report
|
|
54
|
+
*/
|
|
55
|
+
function ensureGitignore(targetDir, options, report) {
|
|
56
|
+
const filePath = path.join(targetDir, GITIGNORE);
|
|
57
|
+
const exists = fs.existsSync(filePath);
|
|
58
|
+
|
|
59
|
+
if (!exists) {
|
|
60
|
+
if (options.dryRun) {
|
|
61
|
+
report.created.push(`${GITIGNORE} (${GITIGNORE_ENTRIES.join(', ')})`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(filePath, `${GITIGNORE_ENTRIES.join('\n')}\n`, 'utf8');
|
|
65
|
+
report.created.push(GITIGNORE);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
70
|
+
const missing = GITIGNORE_ENTRIES.filter(
|
|
71
|
+
(entry) => !hasIgnoreEntry(content, entry),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (missing.length === 0) {
|
|
75
|
+
report.skipped.push(
|
|
76
|
+
`${GITIGNORE} (${GITIGNORE_ENTRIES.join(', ')} already listed)`,
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (options.dryRun) {
|
|
82
|
+
report.updated.push(`${GITIGNORE} (append ${missing.join(', ')})`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
filePath,
|
|
89
|
+
`${content}${separator}${missing.join('\n')}\n`,
|
|
90
|
+
'utf8',
|
|
91
|
+
);
|
|
92
|
+
report.updated.push(`${GITIGNORE} (appended ${missing.join(', ')})`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Copy all files from sourceRoot into targetDir.
|
|
97
|
+
* @param {string} sourceRoot
|
|
98
|
+
* @param {string} targetDir
|
|
99
|
+
* @param {{ force?: boolean, dryRun?: boolean }} options
|
|
100
|
+
* @param {CopyReport} report
|
|
101
|
+
*/
|
|
102
|
+
function copyTree(sourceRoot, targetDir, options, report) {
|
|
103
|
+
const relativeFiles = listFiles(sourceRoot);
|
|
104
|
+
|
|
105
|
+
for (const relative of relativeFiles) {
|
|
106
|
+
const from = path.join(sourceRoot, relative);
|
|
107
|
+
const to = path.join(targetDir, relative);
|
|
108
|
+
const exists = fs.existsSync(to);
|
|
109
|
+
|
|
110
|
+
if (exists && !options.force) {
|
|
111
|
+
report.skipped.push(relative);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.dryRun) {
|
|
116
|
+
if (exists) {
|
|
117
|
+
report.overwritten.push(relative);
|
|
118
|
+
} else {
|
|
119
|
+
report.created.push(relative);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
125
|
+
fs.copyFileSync(from, to);
|
|
126
|
+
|
|
127
|
+
if (exists) {
|
|
128
|
+
report.overwritten.push(relative);
|
|
129
|
+
} else {
|
|
130
|
+
report.created.push(relative);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scaffold shared .cursor config + stack templates into targetDir.
|
|
137
|
+
* @param {{ stack: { id: string, agentsDir: string }, targetDir: string, force?: boolean, dryRun?: boolean }} opts
|
|
138
|
+
* @returns {CopyReport}
|
|
139
|
+
*/
|
|
140
|
+
export function initStack(opts) {
|
|
141
|
+
const { stack, targetDir, force = false, dryRun = false } = opts;
|
|
142
|
+
const options = { force, dryRun };
|
|
143
|
+
/** @type {CopyReport} */
|
|
144
|
+
const report = {
|
|
145
|
+
created: [],
|
|
146
|
+
skipped: [],
|
|
147
|
+
overwritten: [],
|
|
148
|
+
updated: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const shared = sharedTemplatesDir();
|
|
152
|
+
const stackDir = stackTemplatesDir(stack.agentsDir);
|
|
153
|
+
|
|
154
|
+
if (!fs.existsSync(shared)) {
|
|
155
|
+
throw new Error(`Shared templates missing: ${shared}`);
|
|
156
|
+
}
|
|
157
|
+
if (!fs.existsSync(stackDir)) {
|
|
158
|
+
throw new Error(`Stack templates missing for "${stack.id}": ${stackDir}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
copyTree(shared, targetDir, options, report);
|
|
162
|
+
copyTree(stackDir, targetDir, options, report);
|
|
163
|
+
ensureGitignore(targetDir, options, report);
|
|
164
|
+
ensureExactPnpmEngines(targetDir, options, report);
|
|
165
|
+
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function printReport(report, { dryRun = false } = {}) {
|
|
170
|
+
const prefix = dryRun ? '[dry-run] ' : '';
|
|
171
|
+
|
|
172
|
+
const sections = [
|
|
173
|
+
['Created', report.created],
|
|
174
|
+
['Updated', report.updated],
|
|
175
|
+
['Overwritten', report.overwritten],
|
|
176
|
+
['Skipped (already exists)', report.skipped],
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
for (const [label, files] of sections) {
|
|
180
|
+
if (!files || files.length === 0) continue;
|
|
181
|
+
console.log(`${prefix}${label}:`);
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
console.log(` ${file}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
report.created.length === 0 &&
|
|
189
|
+
report.overwritten.length === 0 &&
|
|
190
|
+
report.skipped.length === 0 &&
|
|
191
|
+
(report.updated?.length ?? 0) === 0
|
|
192
|
+
) {
|
|
193
|
+
console.log(`${prefix}Nothing to do.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/package-manager.mjs
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Resolve pnpm version without using the target project as cwd.
|
|
8
|
-
* Running inside a project with `devEngines.packageManager.version: "^…"`
|
|
9
|
-
* makes Corepack/pnpm refuse to start.
|
|
10
|
-
* @returns {string | null} installed pnpm version (e.g. "11.13.0")
|
|
11
|
-
*/
|
|
12
|
-
export function detectPnpmVersion() {
|
|
13
|
-
const result = spawnSync('pnpm --version', {
|
|
14
|
-
cwd: os.tmpdir(),
|
|
15
|
-
encoding: 'utf8',
|
|
16
|
-
shell: true,
|
|
17
|
-
env: process.env,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
if (result.status !== 0) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const version = (result.stdout ?? '').trim().split(/\s+/)[0];
|
|
25
|
-
return /^\d+\.\d+\.\d+/.test(version) ? version : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* True if version is not an exact x.y.z (e.g. ^11.13.0, >=11).
|
|
30
|
-
* @param {string | undefined} version
|
|
31
|
-
*/
|
|
32
|
-
function isRangeOrInvalid(version) {
|
|
33
|
-
if (!version || typeof version !== 'string') return true;
|
|
34
|
-
return !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version.trim());
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Pin packageManager / devEngines to an exact pnpm version so Corepack/pnpm
|
|
39
|
-
* do not fail on ranges like "^11.13.0" (from `pnpm init`).
|
|
40
|
-
*
|
|
41
|
-
* @param {string} targetDir
|
|
42
|
-
* @param {{ dryRun?: boolean }} options
|
|
43
|
-
* @param {{ created: string[], updated: string[], skipped: string[] }} report
|
|
44
|
-
*/
|
|
45
|
-
export function ensureExactPnpmEngines(targetDir, options, report) {
|
|
46
|
-
const pkgPath = path.join(targetDir, 'package.json');
|
|
47
|
-
|
|
48
|
-
if (!fs.existsSync(pkgPath)) {
|
|
49
|
-
report.skipped.push('package.json (missing — skipped packageManager pin)');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const version = detectPnpmVersion();
|
|
54
|
-
if (!version) {
|
|
55
|
-
report.skipped.push('package.json (pnpm not found — skipped packageManager pin)');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
60
|
-
const expectedPackageManager = `pnpm@${version}`;
|
|
61
|
-
const currentDevVersion = pkg.devEngines?.packageManager?.version;
|
|
62
|
-
const needsPackageManager = pkg.packageManager !== expectedPackageManager;
|
|
63
|
-
const needsDevEngines =
|
|
64
|
-
!pkg.devEngines?.packageManager ||
|
|
65
|
-
pkg.devEngines.packageManager.name !== 'pnpm' ||
|
|
66
|
-
isRangeOrInvalid(currentDevVersion) ||
|
|
67
|
-
currentDevVersion !== version;
|
|
68
|
-
|
|
69
|
-
if (!needsPackageManager && !needsDevEngines) {
|
|
70
|
-
report.skipped.push('package.json (packageManager already exact)');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (options.dryRun) {
|
|
75
|
-
report.updated.push(
|
|
76
|
-
`package.json (pin packageManager to ${expectedPackageManager})`,
|
|
77
|
-
);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
pkg.packageManager = expectedPackageManager;
|
|
82
|
-
pkg.devEngines = {
|
|
83
|
-
...pkg.devEngines,
|
|
84
|
-
packageManager: {
|
|
85
|
-
name: 'pnpm',
|
|
86
|
-
version,
|
|
87
|
-
onFail: 'download',
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
92
|
-
report.updated.push(
|
|
93
|
-
`package.json (pinned packageManager to ${expectedPackageManager})`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve pnpm version without using the target project as cwd.
|
|
8
|
+
* Running inside a project with `devEngines.packageManager.version: "^…"`
|
|
9
|
+
* makes Corepack/pnpm refuse to start.
|
|
10
|
+
* @returns {string | null} installed pnpm version (e.g. "11.13.0")
|
|
11
|
+
*/
|
|
12
|
+
export function detectPnpmVersion() {
|
|
13
|
+
const result = spawnSync('pnpm --version', {
|
|
14
|
+
cwd: os.tmpdir(),
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
shell: true,
|
|
17
|
+
env: process.env,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (result.status !== 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const version = (result.stdout ?? '').trim().split(/\s+/)[0];
|
|
25
|
+
return /^\d+\.\d+\.\d+/.test(version) ? version : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True if version is not an exact x.y.z (e.g. ^11.13.0, >=11).
|
|
30
|
+
* @param {string | undefined} version
|
|
31
|
+
*/
|
|
32
|
+
function isRangeOrInvalid(version) {
|
|
33
|
+
if (!version || typeof version !== 'string') return true;
|
|
34
|
+
return !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pin packageManager / devEngines to an exact pnpm version so Corepack/pnpm
|
|
39
|
+
* do not fail on ranges like "^11.13.0" (from `pnpm init`).
|
|
40
|
+
*
|
|
41
|
+
* @param {string} targetDir
|
|
42
|
+
* @param {{ dryRun?: boolean }} options
|
|
43
|
+
* @param {{ created: string[], updated: string[], skipped: string[] }} report
|
|
44
|
+
*/
|
|
45
|
+
export function ensureExactPnpmEngines(targetDir, options, report) {
|
|
46
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(pkgPath)) {
|
|
49
|
+
report.skipped.push('package.json (missing — skipped packageManager pin)');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const version = detectPnpmVersion();
|
|
54
|
+
if (!version) {
|
|
55
|
+
report.skipped.push('package.json (pnpm not found — skipped packageManager pin)');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
60
|
+
const expectedPackageManager = `pnpm@${version}`;
|
|
61
|
+
const currentDevVersion = pkg.devEngines?.packageManager?.version;
|
|
62
|
+
const needsPackageManager = pkg.packageManager !== expectedPackageManager;
|
|
63
|
+
const needsDevEngines =
|
|
64
|
+
!pkg.devEngines?.packageManager ||
|
|
65
|
+
pkg.devEngines.packageManager.name !== 'pnpm' ||
|
|
66
|
+
isRangeOrInvalid(currentDevVersion) ||
|
|
67
|
+
currentDevVersion !== version;
|
|
68
|
+
|
|
69
|
+
if (!needsPackageManager && !needsDevEngines) {
|
|
70
|
+
report.skipped.push('package.json (packageManager already exact)');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.dryRun) {
|
|
75
|
+
report.updated.push(
|
|
76
|
+
`package.json (pin packageManager to ${expectedPackageManager})`,
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pkg.packageManager = expectedPackageManager;
|
|
82
|
+
pkg.devEngines = {
|
|
83
|
+
...pkg.devEngines,
|
|
84
|
+
packageManager: {
|
|
85
|
+
name: 'pnpm',
|
|
86
|
+
version,
|
|
87
|
+
onFail: 'download',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
92
|
+
report.updated.push(
|
|
93
|
+
`package.json (pinned packageManager to ${expectedPackageManager})`,
|
|
94
|
+
);
|
|
95
|
+
}
|
package/src/paths.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
|
|
6
|
-
export function packageRoot() {
|
|
7
|
-
return path.resolve(__dirname, '..');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function templatesRoot() {
|
|
11
|
-
return path.join(packageRoot(), 'templates');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function sharedTemplatesDir() {
|
|
15
|
-
return path.join(templatesRoot(), 'shared');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function stackTemplatesDir(agentsDir) {
|
|
19
|
-
return path.join(templatesRoot(), agentsDir);
|
|
20
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
export function packageRoot() {
|
|
7
|
+
return path.resolve(__dirname, '..');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function templatesRoot() {
|
|
11
|
+
return path.join(packageRoot(), 'templates');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sharedTemplatesDir() {
|
|
15
|
+
return path.join(templatesRoot(), 'shared');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stackTemplatesDir(agentsDir) {
|
|
19
|
+
return path.join(templatesRoot(), agentsDir);
|
|
20
|
+
}
|