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/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
+ }
@@ -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
+ }