@hanv89/azure-arch-skill 0.8.0 → 0.9.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.
@@ -408,6 +408,10 @@ async function withFatalReturn(fn) {
408
408
  }
409
409
  }
410
410
  exports.withFatalReturn = withFatalReturn;
411
+ // Persisted at install time so uninstall can iterate the file list without
412
+ // re-fetching the manifest over the network. Hidden filename so it doesn't
413
+ // clutter the user-visible skill folder.
414
+ const PERSISTED_MANIFEST_BASENAME = ".azure-arch-skill-manifest.json";
411
415
  function makeFolderInstallAdapter(cfg) {
412
416
  const defaultTarget = () => path.join(cfg.rootDir(), "skills", exports.SKILL_NAME);
413
417
  const defaultSkillsRoot = () => path.join(cfg.rootDir(), "skills");
@@ -458,6 +462,9 @@ function makeFolderInstallAdapter(cfg) {
458
462
  const body = await fetchText(`${base}/${src}`);
459
463
  await fs.writeFile(path.join(target, dest), body, "utf8");
460
464
  }
465
+ // Persist the manifest so uninstall can iterate the file list without
466
+ // re-fetching from the network.
467
+ await fs.writeFile(path.join(target, PERSISTED_MANIFEST_BASENAME), JSON.stringify(manifest, null, 2) + "\n", "utf8");
461
468
  process.stdout.write(`installed ${exports.SKILL_NAME} to ${target}\n`);
462
469
  return 0;
463
470
  });
@@ -474,23 +481,89 @@ function makeFolderInstallAdapter(cfg) {
474
481
  if (!ours) {
475
482
  throw new Error(`refusing to remove ${target} - not an azure-architecture-diagram skill folder (no matching SKILL.md). Move/rename the directory or remove it manually if intentional.`);
476
483
  }
477
- try {
478
- await fs.rm(target, { recursive: true, force: false });
484
+ // Manifest-scoped removal: read the persisted manifest at install time
485
+ // and remove only its files + the manifest itself. Leaves any
486
+ // user-authored content under the same folder in place (with a note).
487
+ // Fallback: bundles installed before 0.9.0 have no persisted manifest;
488
+ // legacy whole-folder rm preserves the pre-0.9.0 behaviour.
489
+ const persistedPath = path.join(target, PERSISTED_MANIFEST_BASENAME);
490
+ const manifestBody = await fs.readFile(persistedPath, "utf8").catch(() => null);
491
+ if (!manifestBody) {
492
+ // Legacy uninstall: rm -rf whole folder.
493
+ try {
494
+ await fs.rm(target, { recursive: true, force: false });
495
+ }
496
+ catch (err) {
497
+ const stillExists = await fs.stat(target).then(() => true).catch(() => false);
498
+ if (stillExists) {
499
+ const msg = err instanceof Error ? err.message : String(err);
500
+ throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
501
+ }
502
+ throw err;
503
+ }
479
504
  }
480
- catch (err) {
481
- const stillExists = await fs.stat(target).then(() => true).catch(() => false);
482
- if (stillExists) {
505
+ else {
506
+ let persistedManifest;
507
+ try {
508
+ persistedManifest = JSON.parse(manifestBody);
509
+ }
510
+ catch (err) {
483
511
  const msg = err instanceof Error ? err.message : String(err);
484
- throw new Error(`uninstall partially failed at ${target}: ${msg}; manual cleanup may be required`);
512
+ throw new Error(`persisted manifest at ${persistedPath} is not valid JSON: ${msg}. Remove the file manually then retry.`);
513
+ }
514
+ for (const f of persistedManifest.files) {
515
+ await fs.unlink(path.join(target, f.dest)).catch(() => null);
516
+ }
517
+ await fs.unlink(persistedPath).catch(() => null);
518
+ // Recursively prune empty directories under target. Stop at target
519
+ // itself — only rmdir target if no user-authored files remain.
520
+ const pruneEmptyDirs = async (dir) => {
521
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
522
+ for (const e of entries) {
523
+ if (e.isDirectory()) {
524
+ await pruneEmptyDirs(path.join(dir, e.name));
525
+ const subEntries = await fs.readdir(path.join(dir, e.name)).catch(() => []);
526
+ if (subEntries.length === 0) {
527
+ await fs.rmdir(path.join(dir, e.name)).catch(() => null);
528
+ }
529
+ }
530
+ }
531
+ };
532
+ await pruneEmptyDirs(target);
533
+ const remaining = await fs.readdir(target).catch(() => []);
534
+ if (remaining.length === 0) {
535
+ await fs.rmdir(target).catch(() => null);
536
+ }
537
+ else {
538
+ process.stdout.write(`note: ${target} contains files outside the skill manifest; left in place. Remove manually if intentional.\n`);
485
539
  }
486
- throw err;
487
540
  }
488
541
  process.stdout.write(`uninstalled ${exports.SKILL_NAME} from ${target}\n`);
489
542
  return 0;
490
543
  });
491
544
  }
492
545
  async function update(opts) {
493
- return install({ ...opts, overwrite: true });
546
+ return withFatalReturn(async () => {
547
+ const target = await resolve(opts.target ?? defaultTarget());
548
+ const base = baseUrl(opts.version);
549
+ const manifest = await fetchManifest(base);
550
+ if (manifest.name !== exports.SKILL_NAME) {
551
+ throw new Error(`manifest name mismatch: expected '${exports.SKILL_NAME}', got '${manifest.name}'. CLI and bundle are out of sync.`);
552
+ }
553
+ // Already-at-version short-circuit: read the on-disk SKILL.md
554
+ // frontmatter version and compare to manifest. Equal -> no-op.
555
+ const installedSkillMdPath = path.join(target, "SKILL.md");
556
+ const installedBody = await fs.readFile(installedSkillMdPath, "utf8").catch(() => null);
557
+ if (installedBody) {
558
+ const fmInstalled = parseFrontmatter(installedBody);
559
+ if (fmInstalled.version && fmInstalled.version === manifest.version) {
560
+ process.stdout.write(`${exports.SKILL_NAME} already at version ${manifest.version} (no-op)\n`);
561
+ return 0;
562
+ }
563
+ }
564
+ // Otherwise proceed with overwriting install (the existing path).
565
+ return install({ ...opts, overwrite: true });
566
+ });
494
567
  }
495
568
  async function list(opts) {
496
569
  return withFatalReturn(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanv89/azure-arch-skill",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Install the Azure architecture diagram skill into your AI coding agent (Claude Code, Codex CLI, Cursor).",
5
5
  "bin": {
6
6
  "azure-arch-skill": "dist/index.js"