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 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.2] - 2026-03-01
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
 
@@ -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
- const content = fs.readFileSync(srcFile, 'utf8');
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
- const content = fs.readFileSync(srcFile, 'utf8');
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
- console.log(`✅ Adapter installed: ${env} ${adapter.dest}/ (${adapter.ext})`);
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
- const localPath = path.join(cwd, '.magic', relPath);
490
- if (fs.existsSync(localPath)) {
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) in .magic/:`);
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 only");
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 (.magic only)...' : '🪄 Initializing 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 && !isUpdate) {
761
+ if (envValues.length === 0) {
717
762
  const detected = detectEnvironments(ADAPTERS);
718
763
  if (detected.length > 0) {
719
- // If only one detected, we can suggest it. If multiple, we need user choice or explicit flags.
720
- if (detected.length === 1) {
721
- const env = detected[0];
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
- console.log(`\n💡 Multiple environments detected: ${detected.join(', ')}`);
734
- console.log(` Use --env <name> or --<name> to target specific one.`);
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
- if (!isUpdate) {
767
- if (envValues.length > 0) {
768
- for (const env of envValues) {
769
- installAdapter(sourceRoot, env, ADAPTERS);
770
- }
771
- } else if (selectedEnvResolved) {
772
- installAdapter(sourceRoot, selectedEnvResolved, ADAPTERS);
773
- } else {
774
- // Default install
775
- const srcEng = path.join(sourceRoot, AGENT_DIR);
776
- const destEng = path.join(cwd, AGENT_DIR);
777
- fs.mkdirSync(destEng, { recursive: true });
778
- fs.mkdirSync(path.join(destEng, WORKFLOWS_DIR), { recursive: true });
779
-
780
- for (const wfName of WORKFLOWS) {
781
- const file = wfName + DEFAULT_EXT;
782
- const srcWf = path.join(srcEng, WORKFLOWS_DIR, file);
783
- if (fs.existsSync(srcWf)) {
784
- fs.copyFileSync(srcWf, path.join(destEng, WORKFLOWS_DIR, file));
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
- // Copy other files in .agent if any (not workflows subfolder which we handled selectively)
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(path.join(srcEng, item.name), path.join(destEng, item.name));
851
+ copyDir(srcItem, destItem);
852
+ Object.assign(adapterChecksums, getDirectoryChecksums(destItem, cwd));
794
853
  } else {
795
- fs.copyFileSync(path.join(srcEng, item.name), path.join(destEng, item.name));
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-spec",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Magic Specification-Driven Development (SDD) Workflow",
5
5
  "author": "Oleg Alexandrov <alexandrovoleg.ru@gmail.com>",
6
6
  "license": "MIT",