maskweaver 0.10.0 → 0.10.1

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.
@@ -0,0 +1,7 @@
1
+ export interface NpmDistTags {
2
+ latest?: string;
3
+ beta?: string;
4
+ next?: string;
5
+ [tag: string]: string | undefined;
6
+ }
7
+ export declare function fetchNpmDistTags(packageName: string): Promise<NpmDistTags | null>;
@@ -0,0 +1,15 @@
1
+ const NPM_FETCH_TIMEOUT_MS = 5000;
2
+ export async function fetchNpmDistTags(packageName) {
3
+ try {
4
+ const res = await fetch(`https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`, {
5
+ signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
6
+ });
7
+ if (!res.ok)
8
+ return null;
9
+ const data = (await res.json());
10
+ return data;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export declare function getPluginNameWithVersion(currentVersion: string, packageName: string): Promise<string>;
@@ -0,0 +1,21 @@
1
+ import { fetchNpmDistTags } from "./npm-dist-tags.js";
2
+ const PRIORITIZED_TAGS = ["latest", "beta", "next"];
3
+ function getFallbackEntry(version, packageName) {
4
+ const prereleaseMatch = version.match(/-([a-zA-Z][a-zA-Z0-9-]*)(?:\.|$)/);
5
+ if (prereleaseMatch) {
6
+ return `${packageName}@${prereleaseMatch[1]}`;
7
+ }
8
+ return packageName;
9
+ }
10
+ export async function getPluginNameWithVersion(currentVersion, packageName) {
11
+ const distTags = await fetchNpmDistTags(packageName);
12
+ if (distTags) {
13
+ const allTags = new Set([...PRIORITIZED_TAGS, ...Object.keys(distTags)]);
14
+ for (const tag of allTags) {
15
+ if (distTags[tag] === currentVersion) {
16
+ return `${packageName}@${tag}`;
17
+ }
18
+ }
19
+ }
20
+ return getFallbackEntry(currentVersion, packageName);
21
+ }
@@ -0,0 +1,9 @@
1
+ export interface VersionCompatibility {
2
+ canUpgrade: boolean;
3
+ reason?: string;
4
+ isDowngrade: boolean;
5
+ isMajorBump: boolean;
6
+ requiresMigration: boolean;
7
+ }
8
+ export declare function checkVersionCompatibility(currentVersion: string | null, newVersion: string): VersionCompatibility;
9
+ export declare function extractVersionFromPluginEntry(entry: string): string | null;
@@ -0,0 +1,81 @@
1
+ function parseVersion(version) {
2
+ const clean = version.replace(/^v/, "").split("-")[0];
3
+ return clean.split(".").map(Number);
4
+ }
5
+ function compareVersions(a, b) {
6
+ const partsA = parseVersion(a);
7
+ const partsB = parseVersion(b);
8
+ const maxLen = Math.max(partsA.length, partsB.length);
9
+ for (let i = 0; i < maxLen; i++) {
10
+ const numA = partsA[i] ?? 0;
11
+ const numB = partsB[i] ?? 0;
12
+ if (numA !== numB) {
13
+ return numA - numB;
14
+ }
15
+ }
16
+ return 0;
17
+ }
18
+ export function checkVersionCompatibility(currentVersion, newVersion) {
19
+ if (!currentVersion) {
20
+ return {
21
+ canUpgrade: true,
22
+ isDowngrade: false,
23
+ isMajorBump: false,
24
+ requiresMigration: false,
25
+ };
26
+ }
27
+ const cleanCurrent = currentVersion.replace(/^v/, "");
28
+ const cleanNew = newVersion.replace(/^v/, "");
29
+ try {
30
+ const comparison = compareVersions(cleanNew, cleanCurrent);
31
+ if (comparison < 0) {
32
+ return {
33
+ canUpgrade: false,
34
+ reason: `Downgrade from ${currentVersion} to ${newVersion} is not allowed`,
35
+ isDowngrade: true,
36
+ isMajorBump: false,
37
+ requiresMigration: false,
38
+ };
39
+ }
40
+ if (comparison === 0) {
41
+ return {
42
+ canUpgrade: true,
43
+ reason: `Version ${newVersion} is already installed`,
44
+ isDowngrade: false,
45
+ isMajorBump: false,
46
+ requiresMigration: false,
47
+ };
48
+ }
49
+ const currentMajor = cleanCurrent.split(".")[0];
50
+ const newMajor = cleanNew.split(".")[0];
51
+ const isMajorBump = currentMajor !== newMajor;
52
+ if (isMajorBump) {
53
+ return {
54
+ canUpgrade: true,
55
+ reason: `Major version upgrade from ${currentVersion} to ${newVersion} - configuration migration may be required`,
56
+ isDowngrade: false,
57
+ isMajorBump: true,
58
+ requiresMigration: true,
59
+ };
60
+ }
61
+ return {
62
+ canUpgrade: true,
63
+ isDowngrade: false,
64
+ isMajorBump: false,
65
+ requiresMigration: false,
66
+ };
67
+ }
68
+ catch {
69
+ return {
70
+ canUpgrade: true,
71
+ reason: `Unable to compare versions ${currentVersion} and ${newVersion} - proceeding with caution`,
72
+ isDowngrade: false,
73
+ isMajorBump: false,
74
+ requiresMigration: false,
75
+ };
76
+ }
77
+ }
78
+ export function extractVersionFromPluginEntry(entry) {
79
+ const match = entry.match(/@(.+)$/);
80
+ return match ? match[1] : null;
81
+ }
@@ -7,8 +7,10 @@ import { VERSION } from "../version.js";
7
7
  import { runDoctor, printDoctorReport } from "./doctor.js";
8
8
  import { writeDefaultRuntimeConfig, writeDefaultPluginConfig } from "../shared/generate-agents.js";
9
9
  const PLUGIN_NAME = "maskweaver";
10
+ const PLUGIN_LATEST = "maskweaver@latest";
10
11
  const LEGACY_PLUGIN_NAMES = ["maskweaver/plugin", "@maskweaver/plugin"];
11
- const ALL_PLUGIN_NAMES = [PLUGIN_NAME, ...LEGACY_PLUGIN_NAMES];
12
+ const ALL_PLUGIN_NAMES = [PLUGIN_NAME, PLUGIN_LATEST, ...LEGACY_PLUGIN_NAMES];
13
+ const ALL_BARE_OR_VERSIONED = [PLUGIN_NAME, PLUGIN_LATEST];
12
14
  const CONFIG_NAME = "opencode.json";
13
15
  const colors = {
14
16
  reset: "\x1b[0m",
@@ -83,19 +85,31 @@ function getInstalledPluginEntries(config) {
83
85
  return config.plugin.filter((plugin) => ALL_PLUGIN_NAMES.includes(plugin));
84
86
  }
85
87
  function isPluginInstalled(config) {
86
- return getInstalledPluginEntries(config).length > 0;
88
+ if (!config.plugin || !Array.isArray(config.plugin))
89
+ return false;
90
+ return config.plugin.some((p) => ALL_BARE_OR_VERSIONED.some(name => p === name || p.startsWith(`${name}@`)));
87
91
  }
88
92
  function normalizePluginEntries(config) {
89
93
  if (!config.plugin || !Array.isArray(config.plugin)) {
90
94
  return { migratedFrom: [], changed: false };
91
95
  }
92
96
  const migratedFrom = config.plugin.filter((plugin) => LEGACY_PLUGIN_NAMES.includes(plugin));
93
- if (migratedFrom.length === 0) {
97
+ const hasBare = config.plugin.some((p) => p === PLUGIN_NAME);
98
+ const hasVersioned = config.plugin.some((p) => p.startsWith(`${PLUGIN_NAME}@`));
99
+ if (migratedFrom.length === 0 && !hasBare && !hasVersioned) {
100
+ if (config.plugin.some((p) => p === PLUGIN_LATEST)) {
101
+ return { migratedFrom: [], changed: false };
102
+ }
94
103
  return { migratedFrom: [], changed: false };
95
104
  }
96
- const preserved = config.plugin.filter((plugin) => !ALL_PLUGIN_NAMES.includes(plugin));
97
- config.plugin = [...preserved, PLUGIN_NAME];
98
- return { migratedFrom, changed: true };
105
+ const preserved = config.plugin.filter((plugin) => !ALL_PLUGIN_NAMES.includes(plugin) &&
106
+ !plugin.startsWith(`${PLUGIN_NAME}@`));
107
+ const existingVersioned = config.plugin.find((p) => p.startsWith(`${PLUGIN_NAME}@`));
108
+ config.plugin = [...preserved, existingVersioned || PLUGIN_LATEST];
109
+ if (migratedFrom.length > 0 || hasBare) {
110
+ return { migratedFrom: [...migratedFrom, hasBare ? PLUGIN_NAME : ""].filter(Boolean), changed: true };
111
+ }
112
+ return { migratedFrom: [], changed: false };
99
113
  }
100
114
  async function installPlugin(options) {
101
115
  const isLocal = options.local || false;
@@ -120,13 +134,14 @@ async function installPlugin(options) {
120
134
  log("");
121
135
  return;
122
136
  }
123
- if (config.plugin.includes(PLUGIN_NAME)) {
137
+ const alreadyInstalled = config.plugin.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`));
138
+ if (alreadyInstalled) {
124
139
  warning(`플러그인이 이미 설치되어 있습니다.`);
125
- info(`현재 ${scope} 설정에 등록되어 있습니다: ${PLUGIN_NAME}`);
140
+ info(`현재 ${scope} 설정에 등록되어 있습니다: ${PLUGIN_LATEST}`);
126
141
  log("");
127
142
  return;
128
143
  }
129
- config.plugin.push(PLUGIN_NAME);
144
+ config.plugin.push(PLUGIN_LATEST);
130
145
  writeConfig(configPath, config);
131
146
  let createdRuntimeConfig = false;
132
147
  let createdPluginConfig = false;
@@ -150,7 +165,7 @@ async function installPlugin(options) {
150
165
  }
151
166
  success(`플러그인이 성공적으로 설치되었습니다!`);
152
167
  log("");
153
- log(`📦 설치된 플러그인: ${PLUGIN_NAME}`, "bright");
168
+ log(`📦 설치된 플러그인: ${PLUGIN_LATEST}`, "bright");
154
169
  log(`📍 위치: ${configPath}`, "dim");
155
170
  log("");
156
171
  log(`다음 단계:`, "bright");
@@ -190,7 +205,7 @@ async function uninstallPlugin(options) {
190
205
  log("");
191
206
  return;
192
207
  }
193
- config.plugin = config.plugin.filter((p) => !ALL_PLUGIN_NAMES.includes(p));
208
+ config.plugin = config.plugin.filter((p) => !ALL_PLUGIN_NAMES.includes(p) && !p.startsWith(`${PLUGIN_NAME}@`));
194
209
  writeConfig(configPath, config);
195
210
  success(`플러그인이 성공적으로 제거되었습니다.`);
196
211
  log("");
@@ -5,6 +5,8 @@ import * as os from 'node:os';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { VERSION } from '../version.js';
8
+ import { fetchNpmDistTags } from '../cli/config-manager/npm-dist-tags.js';
9
+ import { checkVersionCompatibility } from '../cli/config-manager/version-compatibility.js';
8
10
  import { loadPluginConfig, isMaskEnabled, isToolEnabled, getDefaultMask, isAutoActivateEnabled, isVerboseLoggingEnabled, isCompletionSoundEnabled, validateConfig, } from './config/index.js';
9
11
  import { createMemorySearchTool } from './tools/memorySearch.js';
10
12
  import { createMemoryWriteTool } from './tools/memoryWrite.js';
@@ -595,10 +597,10 @@ function playCompletionSound(config) {
595
597
  }
596
598
  let state = null;
597
599
  const OPENCODE_PACKAGES_DIR = path.join(os.homedir(), '.cache', 'opencode', 'packages');
598
- const MASKWEAVER_PACKAGE_GLOB = 'maskweaver@*';
599
- function getCacheVersion() {
600
- if (!fs.existsSync(OPENCODE_PACKAGES_DIR))
601
- return null;
600
+ function getCachedPackageVersion() {
601
+ if (!fs.existsSync(OPENCODE_PACKAGES_DIR)) {
602
+ return { version: null, pkgDir: null };
603
+ }
602
604
  const entries = fs.readdirSync(OPENCODE_PACKAGES_DIR);
603
605
  for (const entry of entries) {
604
606
  if (!entry.startsWith('maskweaver@'))
@@ -615,51 +617,88 @@ function getCacheVersion() {
615
617
  continue;
616
618
  }
617
619
  }
618
- return null;
620
+ return { version: null, pkgDir: null };
619
621
  }
620
- function getLatestNpmVersion() {
621
- try {
622
- const result = spawnSync('npm', ['view', 'maskweaver', 'version'], {
623
- encoding: 'utf-8',
624
- stdio: ['pipe', 'pipe', 'pipe'],
625
- timeout: 10000,
626
- windowsHide: true,
627
- });
628
- if (result.status === 0 && result.stdout) {
629
- return result.stdout.trim();
630
- }
631
- }
632
- catch { }
633
- return null;
622
+ async function fetchLatestFromRegistry() {
623
+ return fetchNpmDistTags('maskweaver');
634
624
  }
635
- function checkAndInvalidateCache() {
636
- const cached = getCacheVersion();
637
- if (!cached) {
638
- return { invalidated: false, cachedVersion: null, latestVersion: null };
625
+ async function checkForUpdates() {
626
+ const cached = getCachedPackageVersion();
627
+ const installedVersion = cached.version;
628
+ const latestTags = await fetchLatestFromRegistry();
629
+ const latestVersion = latestTags?.latest ?? null;
630
+ if (!installedVersion) {
631
+ return {
632
+ updateAvailable: false,
633
+ installedVersion: null,
634
+ latestVersion,
635
+ isDowngrade: false,
636
+ isMajorBump: false,
637
+ requiresMigration: false,
638
+ message: null,
639
+ };
639
640
  }
640
- if (cached.version === VERSION) {
641
- return { invalidated: false, cachedVersion: cached.version, latestVersion: null };
641
+ if (installedVersion === VERSION) {
642
+ return {
643
+ updateAvailable: false,
644
+ installedVersion,
645
+ latestVersion,
646
+ isDowngrade: false,
647
+ isMajorBump: false,
648
+ requiresMigration: false,
649
+ message: null,
650
+ };
642
651
  }
643
- const latest = getLatestNpmVersion();
644
- if (latest && latest !== cached.version) {
645
- try {
646
- fs.rmSync(cached.pkgDir, { recursive: true, force: true });
647
- return { invalidated: true, cachedVersion: cached.version, latestVersion: latest };
648
- }
649
- catch {
650
- return { invalidated: false, cachedVersion: cached.version, latestVersion: latest };
651
- }
652
+ if (!latestVersion || latestVersion === installedVersion) {
653
+ return {
654
+ updateAvailable: false,
655
+ installedVersion,
656
+ latestVersion,
657
+ isDowngrade: false,
658
+ isMajorBump: false,
659
+ requiresMigration: false,
660
+ message: `Plugin cache v${installedVersion} — current is v${VERSION}.`,
661
+ };
662
+ }
663
+ const compatibility = checkVersionCompatibility(installedVersion, latestVersion);
664
+ const currentIsLatest = installedVersion === latestVersion;
665
+ if (currentIsLatest) {
666
+ return {
667
+ updateAvailable: false,
668
+ installedVersion,
669
+ latestVersion,
670
+ isDowngrade: false,
671
+ isMajorBump: false,
672
+ requiresMigration: false,
673
+ message: `Plugin cache v${installedVersion} — current is v${VERSION}.`,
674
+ };
652
675
  }
653
- return { invalidated: false, cachedVersion: cached.version, latestVersion: latest };
676
+ return {
677
+ updateAvailable: true,
678
+ installedVersion,
679
+ latestVersion,
680
+ isDowngrade: compatibility.isDowngrade,
681
+ isMajorBump: compatibility.isMajorBump,
682
+ requiresMigration: compatibility.requiresMigration,
683
+ message: `Update available: v${installedVersion} → v${latestVersion}${compatibility.requiresMigration ? ' (major upgrade — migration may be required)' : ''}.`,
684
+ };
654
685
  }
655
686
  export const MaskweaverPlugin = async ({ client, directory, project, worktree, $, serverUrl }) => {
656
- const cacheCheck = checkAndInvalidateCache();
657
- if (cacheCheck.invalidated) {
658
- pluginLog(client, 'warn', `Stale plugin cache detected (v${cacheCheck.cachedVersion}). Cleared — v${cacheCheck.latestVersion} will install on next restart.`);
659
- pluginLog(client, 'warn', `Please restart OpenCode to activate maskweaver v${VERSION}.`);
687
+ const updateCheck = await checkForUpdates();
688
+ if (updateCheck.updateAvailable) {
689
+ if (updateCheck.isDowngrade) {
690
+ pluginLog(client, 'warn', `Downgrade detected (v${updateCheck.installedVersion} v${updateCheck.latestVersion}) — not allowed. Please use opencode --upgrade.`);
691
+ }
692
+ else {
693
+ pluginLog(client, 'warn', `Update available: maskweaver v${updateCheck.installedVersion} → v${updateCheck.latestVersion}`);
694
+ if (updateCheck.requiresMigration) {
695
+ pluginLog(client, 'warn', `Major version upgrade — configuration migration may be required.`);
696
+ }
697
+ pluginLog(client, 'info', `Run \`opencode --upgrade\` or \`npm update -g maskweaver\` to update.`);
698
+ }
660
699
  }
661
- else if (cacheCheck.cachedVersion && cacheCheck.cachedVersion !== VERSION) {
662
- pluginLog(client, 'info', `Plugin cache v${cacheCheck.cachedVersion} — current is v${VERSION}. Restart recommended.`);
700
+ else if (updateCheck.message) {
701
+ pluginLog(client, 'info', updateCheck.message);
663
702
  }
664
703
  const pluginConfig = loadPluginConfig(directory, { client, verbose: false });
665
704
  const configErrors = validateConfig(pluginConfig);
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.9.9";
1
+ export declare const VERSION = "0.10.1";
2
2
  export declare function getVersionString(): string;
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = '0.9.9';
1
+ export const VERSION = '0.10.1';
2
2
  export function getVersionString() {
3
3
  return `Maskweaver v${VERSION}`;
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maskweaver",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "AI Expert Persona System - Give your AI coding assistant expert personalities (가면술사)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",