magic-spec 1.4.2 → 1.4.3
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 +13 -1
- package/installers/node/index.js +133 -55
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [1.4.
|
|
8
|
+
## [1.4.3] - 2026-03-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Smart Adapter Updates**: Both Python and Node.js installers now seamlessly verify and update adapter `.mdc`/`.toml` wrapper files during `--update`.
|
|
13
|
+
- **Smart Update Conflict Resolution**: When users manually modify `.cursor/rules/*` wrapper files in their project, the `--update` command now detects the modifications. Users can intelligently choose to skip updates for specific conflicting adapter files, preserving their changes while updating the core `.magic` engine logic.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Updater Integrity**: Modified files that are bypassed during an update via `[s] Skip modified files` now persist their original hashes so they don't get silently overwritten in subsequent updates.
|
|
18
|
+
- **Testing Logic**: Fixed an issue in the `run_tests.py` exhaustive test suite section where failures during adapter testing cycles were mistakenly swallowed, reporting a false "All tests completed successfully".
|
|
19
|
+
|
|
20
|
+
## [1.4.3] - 2026-03-01
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
11
23
|
|
package/installers/node/index.js
CHANGED
|
@@ -190,14 +190,15 @@ function convertToMdc(content, description) {
|
|
|
190
190
|
return `---\ndescription: ${description || ''}\nglobs: \n---\n${content}`;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function installAdapter(sourceRoot, env, adapters) {
|
|
193
|
+
function installAdapter(sourceRoot, env, adapters, conflictsToSkip = []) {
|
|
194
|
+
const installedChecksums = {};
|
|
194
195
|
const adapter = adapters[env];
|
|
195
196
|
if (!adapter) {
|
|
196
197
|
console.warn(`⚠️ Unknown --env value: "${env}".`);
|
|
197
198
|
console.warn(` Valid values: ${Object.keys(adapters).join(', ')}`);
|
|
198
199
|
console.warn(` Falling back to default ${AGENT_DIR}/`);
|
|
199
200
|
copyDir(path.join(sourceRoot, AGENT_DIR), path.join(cwd, AGENT_DIR));
|
|
200
|
-
return;
|
|
201
|
+
return installedChecksums;
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
const srcDir = path.join(sourceRoot, AGENT_DIR, WORKFLOWS_DIR);
|
|
@@ -205,9 +206,12 @@ function installAdapter(sourceRoot, env, adapters) {
|
|
|
205
206
|
|
|
206
207
|
if (!fs.existsSync(srcDir)) {
|
|
207
208
|
console.warn(`⚠️ Source ${AGENT_DIR}/${WORKFLOWS_DIR}/ not found.`);
|
|
208
|
-
return;
|
|
209
|
+
return installedChecksums;
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
if (adapter.marker) {
|
|
213
|
+
fs.mkdirSync(path.join(cwd, adapter.marker), { recursive: true });
|
|
214
|
+
}
|
|
211
215
|
fs.mkdirSync(destDir, { recursive: true });
|
|
212
216
|
|
|
213
217
|
for (const wfName of WORKFLOWS) {
|
|
@@ -222,22 +226,56 @@ function installAdapter(sourceRoot, env, adapters) {
|
|
|
222
226
|
destName = destName.slice(removePrefix.length);
|
|
223
227
|
}
|
|
224
228
|
}
|
|
229
|
+
|
|
225
230
|
const destFile = path.join(destDir, destName);
|
|
231
|
+
const relTarget = path.relative(cwd, destFile).replace(/\\/g, '/');
|
|
232
|
+
|
|
233
|
+
if (conflictsToSkip.includes(relTarget)) continue;
|
|
234
|
+
|
|
235
|
+
const content = fs.readFileSync(srcFile, 'utf8');
|
|
236
|
+
let finalContent = content;
|
|
226
237
|
|
|
227
238
|
if (adapter.format === 'toml' || adapter.ext === '.toml') {
|
|
228
|
-
|
|
229
|
-
const description = `Magic SDD Workflow: ${destName}`;
|
|
230
|
-
fs.writeFileSync(destFile, convertToToml(content, description), 'utf8');
|
|
239
|
+
finalContent = convertToToml(content, `Magic SDD Workflow: ${destName}`);
|
|
231
240
|
} else if (adapter.format === 'mdc' || adapter.ext === '.mdc') {
|
|
232
|
-
|
|
233
|
-
const description = `Magic SDD Workflow: ${destName}`;
|
|
234
|
-
fs.writeFileSync(destFile, convertToMdc(content, description), 'utf8');
|
|
235
|
-
} else {
|
|
236
|
-
fs.copyFileSync(srcFile, destFile);
|
|
241
|
+
finalContent = convertToMdc(content, `Magic SDD Workflow: ${destName}`);
|
|
237
242
|
}
|
|
243
|
+
|
|
244
|
+
fs.writeFileSync(destFile, finalContent, 'utf8');
|
|
245
|
+
installedChecksums[relTarget] = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
|
|
248
|
+
// Copy other files in .agent if any
|
|
249
|
+
const srcEng = path.join(sourceRoot, AGENT_DIR);
|
|
250
|
+
if (fs.existsSync(srcEng)) {
|
|
251
|
+
const items = fs.readdirSync(srcEng, { withFileTypes: true });
|
|
252
|
+
for (const item of items) {
|
|
253
|
+
if (item.name === WORKFLOWS_DIR) continue;
|
|
254
|
+
|
|
255
|
+
const srcItem = path.join(srcEng, item.name);
|
|
256
|
+
const destItem = path.join(cwd, AGENT_DIR, item.name);
|
|
257
|
+
const relTarget = path.relative(cwd, destItem).replace(/\\/g, '/');
|
|
258
|
+
|
|
259
|
+
if (item.isDirectory()) {
|
|
260
|
+
copyDir(srcItem, destItem);
|
|
261
|
+
const dirHash = getDirectoryChecksums(destItem, cwd);
|
|
262
|
+
Object.assign(installedChecksums, dirHash);
|
|
263
|
+
} else {
|
|
264
|
+
if (conflictsToSkip.includes(relTarget)) continue;
|
|
265
|
+
fs.mkdirSync(path.join(cwd, AGENT_DIR), { recursive: true });
|
|
266
|
+
fs.copyFileSync(srcItem, destItem);
|
|
267
|
+
installedChecksums[relTarget] = getFileChecksum(destItem);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (conflictsToSkip.length > 0 || isUpdate) {
|
|
273
|
+
console.log(`✅ Adapter updated: ${env} → ${adapter.dest}/ (${adapter.ext})`);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`✅ Adapter installed: ${env} → ${adapter.dest}/ (${adapter.ext})`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return installedChecksums;
|
|
241
279
|
}
|
|
242
280
|
|
|
243
281
|
function runDoctor() {
|
|
@@ -486,8 +524,15 @@ async function handleConflicts(cwd) {
|
|
|
486
524
|
|
|
487
525
|
const conflicts = [];
|
|
488
526
|
for (const [relPath, storedHash] of Object.entries(storedChecksums)) {
|
|
489
|
-
|
|
490
|
-
|
|
527
|
+
if (relPath === '.checksums' || relPath === '.version') continue;
|
|
528
|
+
|
|
529
|
+
// Backward compatibility: try .magic first, then project root
|
|
530
|
+
let localPath = path.join(cwd, '.magic', relPath);
|
|
531
|
+
if (!fs.existsSync(localPath)) {
|
|
532
|
+
localPath = path.join(cwd, relPath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
|
|
491
536
|
const currentHash = getFileChecksum(localPath);
|
|
492
537
|
if (currentHash !== storedHash) {
|
|
493
538
|
conflicts.push(relPath);
|
|
@@ -497,7 +542,7 @@ async function handleConflicts(cwd) {
|
|
|
497
542
|
|
|
498
543
|
if (conflicts.length === 0) return;
|
|
499
544
|
|
|
500
|
-
console.log(`\n⚠️ Local changes detected in ${conflicts.length} file(s)
|
|
545
|
+
console.log(`\n⚠️ Local changes detected in ${conflicts.length} file(s):`);
|
|
501
546
|
conflicts.slice(0, 5).forEach(f => console.log(` - ${f}`));
|
|
502
547
|
if (conflicts.length > 5) console.log(` ... and ${conflicts.length - 5} more.`);
|
|
503
548
|
|
|
@@ -665,14 +710,14 @@ async function main() {
|
|
|
665
710
|
console.log("\nOptions:");
|
|
666
711
|
console.log(" --env <adapter> Specify environment adapter");
|
|
667
712
|
console.log(" --<adapter> Shortcut for --env <adapter> (e.g. --cursor)");
|
|
668
|
-
console.log(" --update Update engine files
|
|
713
|
+
console.log(" --update Update engine and adapter files");
|
|
669
714
|
console.log(" --local Use local project files instead of GitHub");
|
|
670
715
|
console.log(" --fallback-main Pull payload from main branch");
|
|
671
716
|
console.log(" --yes, -y Auto-accept prompts");
|
|
672
717
|
process.exit(0);
|
|
673
718
|
}
|
|
674
719
|
|
|
675
|
-
console.log(isUpdate ? '🪄 Updating magic-spec
|
|
720
|
+
console.log(isUpdate ? '🪄 Updating magic-spec...' : '🪄 Initializing magic-spec...');
|
|
676
721
|
|
|
677
722
|
if (isUpdate) {
|
|
678
723
|
createBackup();
|
|
@@ -713,28 +758,35 @@ async function main() {
|
|
|
713
758
|
selectedEnvResolved = envValues[0];
|
|
714
759
|
}
|
|
715
760
|
|
|
716
|
-
if (envValues.length === 0
|
|
761
|
+
if (envValues.length === 0) {
|
|
717
762
|
const detected = detectEnvironments(ADAPTERS);
|
|
718
763
|
if (detected.length > 0) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const adapterName = ADAPTERS[env].description || env;
|
|
723
|
-
console.log(`\n💡 Detected ${adapterName} (${ADAPTERS[env].marker}/ directory found).`);
|
|
724
|
-
let shouldAdopt = autoAccept;
|
|
725
|
-
if (!shouldAdopt) {
|
|
726
|
-
const answer = await askQuestion(` Install ${env} adapter instead of default? (y/N): `);
|
|
727
|
-
shouldAdopt = answer.toLowerCase() === 'y';
|
|
728
|
-
}
|
|
729
|
-
if (shouldAdopt) {
|
|
730
|
-
selectedEnvResolved = env;
|
|
764
|
+
if (isUpdate) {
|
|
765
|
+
for (const env of detected) {
|
|
766
|
+
if (!envValues.includes(env)) envValues.push(env);
|
|
731
767
|
}
|
|
768
|
+
selectedEnvResolved = envValues[0];
|
|
732
769
|
} else {
|
|
733
|
-
|
|
734
|
-
|
|
770
|
+
if (detected.length === 1) {
|
|
771
|
+
const env = detected[0];
|
|
772
|
+
const adapterName = ADAPTERS[env].description || env;
|
|
773
|
+
console.log(`\n💡 Detected ${adapterName} (${ADAPTERS[env].marker}/ directory found).`);
|
|
774
|
+
let shouldAdopt = autoAccept;
|
|
775
|
+
if (!shouldAdopt) {
|
|
776
|
+
const answer = await askQuestion(` Install ${env} adapter instead of default? (y/N): `);
|
|
777
|
+
shouldAdopt = answer.toLowerCase() === 'y';
|
|
778
|
+
}
|
|
779
|
+
if (shouldAdopt) {
|
|
780
|
+
selectedEnvResolved = env;
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
console.log(`\n💡 Multiple environments detected: ${detected.join(', ')}`);
|
|
784
|
+
console.log(` Use --env <name> or --<name> to target specific one.`);
|
|
785
|
+
}
|
|
735
786
|
}
|
|
736
787
|
}
|
|
737
788
|
}
|
|
789
|
+
|
|
738
790
|
let conflictsToSkip = [];
|
|
739
791
|
if (isUpdate) {
|
|
740
792
|
const conflictResult = await handleConflicts(cwd);
|
|
@@ -763,36 +815,45 @@ async function main() {
|
|
|
763
815
|
}
|
|
764
816
|
|
|
765
817
|
// 2. Adapters
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
818
|
+
let adapterChecksums = {};
|
|
819
|
+
if (envValues.length > 0) {
|
|
820
|
+
for (const env of envValues) {
|
|
821
|
+
Object.assign(adapterChecksums, installAdapter(sourceRoot, env, ADAPTERS, conflictsToSkip));
|
|
822
|
+
}
|
|
823
|
+
} else if (selectedEnvResolved) {
|
|
824
|
+
Object.assign(adapterChecksums, installAdapter(sourceRoot, selectedEnvResolved, ADAPTERS, conflictsToSkip));
|
|
825
|
+
} else if (!isUpdate) {
|
|
826
|
+
// Default install
|
|
827
|
+
const srcEng = path.join(sourceRoot, AGENT_DIR);
|
|
828
|
+
const destEng = path.join(cwd, AGENT_DIR);
|
|
829
|
+
fs.mkdirSync(destEng, { recursive: true });
|
|
830
|
+
fs.mkdirSync(path.join(destEng, WORKFLOWS_DIR), { recursive: true });
|
|
831
|
+
|
|
832
|
+
for (const wfName of WORKFLOWS) {
|
|
833
|
+
const file = wfName + DEFAULT_EXT;
|
|
834
|
+
const srcWf = path.join(srcEng, WORKFLOWS_DIR, file);
|
|
835
|
+
if (fs.existsSync(srcWf)) {
|
|
836
|
+
const destWf = path.join(destEng, WORKFLOWS_DIR, file);
|
|
837
|
+
fs.copyFileSync(srcWf, destWf);
|
|
838
|
+
const relTarget = path.relative(cwd, destWf).replace(/\\/g, '/');
|
|
839
|
+
adapterChecksums[relTarget] = getFileChecksum(destWf);
|
|
786
840
|
}
|
|
841
|
+
}
|
|
787
842
|
|
|
788
|
-
|
|
843
|
+
// Copy other files in .agent if any (not workflows subfolder which we handled selectively)
|
|
844
|
+
if (fs.existsSync(srcEng)) {
|
|
789
845
|
const items = fs.readdirSync(srcEng, { withFileTypes: true });
|
|
790
846
|
for (const item of items) {
|
|
791
847
|
if (item.name === WORKFLOWS_DIR) continue;
|
|
848
|
+
const srcItem = path.join(srcEng, item.name);
|
|
849
|
+
const destItem = path.join(destEng, item.name);
|
|
792
850
|
if (item.isDirectory()) {
|
|
793
|
-
copyDir(
|
|
851
|
+
copyDir(srcItem, destItem);
|
|
852
|
+
Object.assign(adapterChecksums, getDirectoryChecksums(destItem, cwd));
|
|
794
853
|
} else {
|
|
795
|
-
fs.copyFileSync(
|
|
854
|
+
fs.copyFileSync(srcItem, destItem);
|
|
855
|
+
const relTarget = path.relative(cwd, destItem).replace(/\\/g, '/');
|
|
856
|
+
adapterChecksums[relTarget] = getFileChecksum(destItem);
|
|
796
857
|
}
|
|
797
858
|
}
|
|
798
859
|
}
|
|
@@ -852,6 +913,23 @@ async function main() {
|
|
|
852
913
|
// 6. Save checksums - [T-2C03]
|
|
853
914
|
try {
|
|
854
915
|
const currentChecksums = getDirectoryChecksums(path.join(cwd, '.magic'));
|
|
916
|
+
|
|
917
|
+
if (isUpdate) {
|
|
918
|
+
const checksumsFile = path.join(cwd, '.magic', '.checksums');
|
|
919
|
+
if (fs.existsSync(checksumsFile)) {
|
|
920
|
+
try {
|
|
921
|
+
const oldChecksums = JSON.parse(fs.readFileSync(checksumsFile, 'utf8'));
|
|
922
|
+
for (const skipped of conflictsToSkip) {
|
|
923
|
+
if (oldChecksums[skipped]) {
|
|
924
|
+
currentChecksums[skipped] = oldChecksums[skipped];
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} catch (e) { }
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
Object.assign(currentChecksums, adapterChecksums);
|
|
932
|
+
|
|
855
933
|
fs.writeFileSync(path.join(cwd, '.magic', '.checksums'), JSON.stringify(currentChecksums, null, 2), 'utf8');
|
|
856
934
|
} catch (cErr) {
|
|
857
935
|
console.warn(`⚠️ Failed to save checksums: ${cErr.message}`);
|