Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions plugins/prompt-templates/comment-polish/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { pluginOwnership } from '../ownership.mjs';

export function buildBuiltinCommentPolishTemplate(t) {
const tr = (key, fallback, params = null) => (typeof t === 'function' ? t(key, params) : fallback);
const line1 = tr('plugins.builtin.commentPolish.line1', '轻微收敛以下代码注释');
const timestamp = new Date().toISOString();
return {
id: 'builtin_comment_polish',
name: tr('plugins.builtin.commentPolish.name', '代码注释润色'),
description: tr('plugins.builtin.commentPolish.desc', '轻微收敛以下代码注释 {{code}}'),
template: [
line1,
'',
'{{code}}'
].join('\n'),
createdAt: timestamp,
updatedAt: timestamp,
isBuiltin: true,
createdBy: pluginOwnership.createdBy,
maintainers: pluginOwnership.maintainers
};
}
32 changes: 31 additions & 1 deletion plugins/prompt-templates/computed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ function normalizePromptTemplateEntry(item) {
const updatedAt = typeof safe.updatedAt === 'string' ? safe.updatedAt : '';
const createdAt = typeof safe.createdAt === 'string' ? safe.createdAt : updatedAt;
const isBuiltin = safe.isBuiltin === true;
const createdBy = typeof safe.createdBy === 'string' ? safe.createdBy.trim() : '';
const maintainers = Array.isArray(safe.maintainers)
? safe.maintainers.map((m) => (typeof m === 'string' ? m.trim() : '')).filter(Boolean)
: [];
return {
id,
name,
description,
template,
createdAt,
updatedAt,
isBuiltin
isBuiltin,
createdBy,
maintainers
};
}

Expand Down Expand Up @@ -104,6 +110,30 @@ export function createPluginsComputed() {
return pluginsRegistry.map((entry) => entry && entry.meta).filter(Boolean);
},

pluginsActiveMeta() {
const id = typeof this.pluginsActiveId === 'string' ? this.pluginsActiveId.trim() : '';
const entry = pluginsRegistry.find((item) => item && item.id === id) || null;
return entry && entry.meta ? entry.meta : null;
},

pluginsActiveAttribution() {
const meta = this.pluginsActiveMeta;
if (!meta || typeof meta !== 'object') return '';
const createdBy = typeof meta.createdBy === 'string' ? meta.createdBy.trim() : '';
const maintainers = Array.isArray(meta.maintainers)
? meta.maintainers.map((m) => (typeof m === 'string' ? m.trim() : '')).filter(Boolean).join(', ')
: '';
if (!createdBy && !maintainers) return '';
if (typeof this.t !== 'function') {
if (createdBy && maintainers) return `Created by ${createdBy} · Maintained by ${maintainers}`;
if (createdBy) return `Created by ${createdBy}`;
return `Maintained by ${maintainers}`;
}
if (createdBy && maintainers) return this.t('plugins.meta.attribution', { createdBy, maintainers });
if (createdBy) return this.t('plugins.meta.createdBy', { createdBy });
return this.t('plugins.meta.maintainedBy', { maintainers });
},

promptTemplatesList() {
const list = Array.isArray(this.promptTemplatesListRaw) ? this.promptTemplatesListRaw : [];
return list
Expand Down
14 changes: 13 additions & 1 deletion plugins/prompt-templates/manifest.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
export const pluginMeta = {
import { pluginOwnership } from './ownership.mjs';

const baseMeta = {
id: 'prompt-templates',
title: 'Prompt Templates',
description: 'Standardized, template-driven prompts with variables and copy/export helpers.',
statusLabel: 'standard',
tone: 'configured'
};

if (pluginOwnership && pluginOwnership.pluginId && pluginOwnership.pluginId !== baseMeta.id) {
throw new Error(`ownership.mjs pluginId mismatch: expected ${baseMeta.id}, got ${pluginOwnership.pluginId}`);
}

export const pluginMeta = {
...baseMeta,
createdBy: pluginOwnership && typeof pluginOwnership.createdBy === 'string' ? pluginOwnership.createdBy : '',
maintainers: pluginOwnership && Array.isArray(pluginOwnership.maintainers) ? pluginOwnership.maintainers : []
};
8 changes: 6 additions & 2 deletions plugins/prompt-templates/methods.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ function normalizePromptTemplateDraft(draft) {
template: typeof safe.template === 'string' ? safe.template : '',
createdAt: typeof safe.createdAt === 'string' ? safe.createdAt : '',
updatedAt: typeof safe.updatedAt === 'string' ? safe.updatedAt : '',
isBuiltin: safe.isBuiltin === true
isBuiltin: safe.isBuiltin === true,
createdBy: typeof safe.createdBy === 'string' ? safe.createdBy : '',
maintainers: Array.isArray(safe.maintainers) ? safe.maintainers : []
};
}

Expand Down Expand Up @@ -322,7 +324,9 @@ export function createPluginsMethods() {
template: entry.template,
createdAt: entry.createdAt,
updatedAt: entry.updatedAt,
isBuiltin: entry.isBuiltin === true
isBuiltin: entry.isBuiltin === true,
createdBy: entry.createdBy || '',
maintainers: Array.isArray(entry.maintainers) ? entry.maintainers : []
};
this.promptTemplateVarValuesRaw = {};
},
Expand Down
29 changes: 6 additions & 23 deletions plugins/prompt-templates/overview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,8 @@ import {
persistPromptTemplatesToStorage,
readPromptTemplatesFromStorage
} from './storage.mjs';

function nowIsoPromptTemplatesOverview() {
return new Date().toISOString();
}

function buildBuiltinCommentPolishTemplate(t) {
const tr = (key, fallback, params = null) => (typeof t === 'function' ? t(key, params) : fallback);
const line1 = tr('plugins.builtin.commentPolish.line1', '轻微收敛以下代码注释');
return {
id: 'builtin_comment_polish',
name: tr('plugins.builtin.commentPolish.name', '代码注释润色'),
description: tr('plugins.builtin.commentPolish.desc', '轻微收敛以下代码注释 {{code}}'),
template: [
line1,
'',
'{{code}}'
].join('\n'),
createdAt: nowIsoPromptTemplatesOverview(),
updatedAt: nowIsoPromptTemplatesOverview(),
isBuiltin: true
};
}
import { buildBuiltinCommentPolishTemplate } from './comment-polish/index.mjs';
import { buildBuiltinRuleAckTemplate } from './rule-ack/index.mjs';

function ensureBuiltinTemplates(rawList, builtins) {
const list = Array.isArray(rawList) ? rawList.filter(Boolean) : [];
Expand All @@ -48,7 +28,10 @@ export async function loadPromptTemplatesOverview(ctx, options = {}) {

const t = typeof app.t === 'function' ? app.t : null;
const rawList = readPromptTemplatesFromStorage(localStorage);
const normalized = ensureBuiltinTemplates(rawList, [buildBuiltinCommentPolishTemplate(t)]);
const normalized = ensureBuiltinTemplates(rawList, [
buildBuiltinCommentPolishTemplate(t),
buildBuiltinRuleAckTemplate(t)
]);
app.promptTemplatesListRaw = normalized;
persistPromptTemplatesToStorage(normalized, localStorage);

Expand Down
5 changes: 5 additions & 0 deletions plugins/prompt-templates/ownership.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const pluginOwnership = {
pluginId: 'prompt-templates',
createdBy: 'ymkiux',
maintainers: ['ymkiux']
};
18 changes: 18 additions & 0 deletions plugins/prompt-templates/rule-ack/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { pluginOwnership } from '../ownership.mjs';

export function buildBuiltinRuleAckTemplate(t) {
const tr = (key, fallback, params = null) => (typeof t === 'function' ? t(key, params) : fallback);
const line1 = tr('plugins.builtin.ruleAck.line1', '请根据【{{rule}}】,收到请回复');
const timestamp = new Date().toISOString();
return {
id: 'builtin_rule_ack',
name: tr('plugins.builtin.ruleAck.name', '规则确认回复'),
description: tr('plugins.builtin.ruleAck.desc', '请根据【{{rule}}】,收到请回复'),
template: line1,
createdAt: timestamp,
updatedAt: timestamp,
isBuiltin: true,
createdBy: pluginOwnership.createdBy,
maintainers: pluginOwnership.maintainers
};
}
7 changes: 1 addition & 6 deletions plugins/registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import { pluginMeta as promptTemplatesMeta } from './prompt-templates/manifest.m
import { loadPromptTemplatesOverview } from './prompt-templates/overview.mjs';

export const pluginsRegistry = [
{
id: promptTemplatesMeta.id,
meta: promptTemplatesMeta,
loadOverview: loadPromptTemplatesOverview
}
{ id: promptTemplatesMeta.id, meta: promptTemplatesMeta, loadOverview: loadPromptTemplatesOverview }
];

export function getFirstPluginId() {
Expand All @@ -18,4 +14,3 @@ export function getPluginEntry(id) {
if (!key) return null;
return pluginsRegistry.find((item) => item && item.id === key) || null;
}

50 changes: 50 additions & 0 deletions tests/unit/plugins-ownership-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.join(__dirname, '..', '..');
const pluginsDir = path.join(root, 'plugins');

function listPluginFolders() {
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => !name.startsWith('.'))
.sort((a, b) => a.localeCompare(b, 'en-US'));
}

function isPluginFolder(name) {
const manifestPath = path.join(pluginsDir, name, 'manifest.mjs');
const overviewPath = path.join(pluginsDir, name, 'overview.mjs');
return fs.existsSync(manifestPath) && fs.existsSync(overviewPath);
}

test('each builtin plugin has ownership file matched to plugin id', async () => {
const folders = listPluginFolders().filter((name) => isPluginFolder(name));
assert.ok(folders.length > 0, 'expected at least one builtin plugin folder');

for (const folder of folders) {
const ownershipPath = path.join(pluginsDir, folder, 'ownership.mjs');
assert.ok(fs.existsSync(ownershipPath), `missing ownership.mjs for plugin: ${folder}`);

const manifestUrl = pathToFileURL(path.join(pluginsDir, folder, 'manifest.mjs')).href;
const ownershipUrl = pathToFileURL(ownershipPath).href;
const { pluginMeta } = await import(`${manifestUrl}?t=${Date.now()}`);
const { pluginOwnership } = await import(`${ownershipUrl}?t=${Date.now()}`);

assert.ok(pluginMeta && typeof pluginMeta === 'object', `invalid pluginMeta for plugin: ${folder}`);
assert.strictEqual(pluginMeta.id, folder, `pluginMeta.id must match folder name: ${folder}`);
assert.ok(pluginOwnership && typeof pluginOwnership === 'object', `invalid pluginOwnership for plugin: ${folder}`);
assert.strictEqual(pluginOwnership.pluginId, folder, `ownership pluginId must match folder name: ${folder}`);
assert.ok(typeof pluginOwnership.createdBy === 'string' && pluginOwnership.createdBy.trim(), `ownership createdBy must be a github handle for plugin: ${folder}`);
assert.ok(Array.isArray(pluginOwnership.maintainers) && pluginOwnership.maintainers.length > 0, `ownership maintainers must be non-empty for plugin: ${folder}`);
for (const maintainer of pluginOwnership.maintainers) {
assert.ok(typeof maintainer === 'string' && maintainer.trim(), `ownership maintainer must be a github handle for plugin: ${folder}`);
}
}
});

20 changes: 20 additions & 0 deletions tests/unit/plugins-registry-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);

const root = path.join(__dirname, '..', '..');
const registryPath = path.join(root, 'plugins', 'registry.mjs');
const generator = require(path.join(root, 'tools', 'dev', 'generate-plugins-registry.js'));

test('plugins registry matches generator output', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how npm test runs .test.mjs files and whether a runner globalizes `test`.
fd -a -t f 'package.json' -d 2 | head -5 | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'
echo "---"
fd -a 'plugins-registry-contract.test.mjs'
fd -a 'plugins-ownership-contract.test.mjs'
echo "---"
# Look for a test runner config (mocha, vitest, node --test, etc.)
fd -a -t f '^(\.mocharc|vitest\.config|jest\.config).*' --hidden | head -20
rg -nP --type=json '"test"\s*:' package.json

Repository: SakuraByteCore/codexmate

Length of output: 2204


🏁 Script executed:

cat -n tests/unit/run.mjs

Repository: SakuraByteCore/codexmate

Length of output: 5036


Add these contract test files to the test runner to ensure they are executed.

The test runner (tests/unit/run.mjs) correctly globalizes test on line 8, so the original concern about missing imports is resolved. However, plugins-registry-contract.test.mjs and plugins-ownership-contract.test.mjs are not imported in run.mjs at all—they exist but are being silently skipped, which is the exact risk flagged. Add them to the import list in run.mjs to ensure these contract checks are actually executed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/plugins-registry-contract.test.mjs` at line 15, The two contract
tests plugins-registry-contract.test.mjs and plugins-ownership-contract.test.mjs
are not being imported into the test runner, so add explicit imports for those
files to the tests/unit/run.mjs import list (the module that globalizes test and
orchestrates execution) so they are executed by the test suite; update run.mjs
to include import './plugins-registry-contract.test.mjs' and import
'./plugins-ownership-contract.test.mjs' (or the equivalent relative paths used
by the existing imports) alongside the other test imports.

const actual = fs.readFileSync(registryPath, 'utf8').replace(/^\uFEFF/u, '');
const expected = String(generator.generatePluginsRegistrySource() || '').replace(/^\uFEFF/u, '');
assert.strictEqual(actual, expected);
});

2 changes: 2 additions & 0 deletions tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
'mainTabSubtitle',
'configTemplateDiffHasChanges',
'pluginsCatalog',
'pluginsActiveMeta',
'pluginsActiveAttribution',
'promptTemplatesList',
'filteredPromptTemplates',
'promptTemplateDraft',
Expand Down
97 changes: 97 additions & 0 deletions tools/dev/generate-plugins-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

const root = path.resolve(__dirname, '..', '..');
const pluginsDir = path.join(root, 'plugins');
const registryPath = path.join(pluginsDir, 'registry.mjs');

function toCamelCase(value) {
const raw = String(value || '').trim();
if (!raw) return '';
return raw
.split(/[^a-zA-Z0-9]+/g)
.filter(Boolean)
.map((part, index) => {
const head = part.slice(0, 1);
const tail = part.slice(1);
if (index === 0) return head.toLowerCase() + tail;
return head.toUpperCase() + tail;
})
.join('');
}

function toPascalCase(value) {
const camel = toCamelCase(value);
if (!camel) return '';
return camel.slice(0, 1).toUpperCase() + camel.slice(1);
}

function listPluginFolders() {
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => !name.startsWith('.'))
.sort((a, b) => a.localeCompare(b, 'en-US'));
}

function isPluginFolder(name) {
const manifestPath = path.join(pluginsDir, name, 'manifest.mjs');
const overviewPath = path.join(pluginsDir, name, 'overview.mjs');
return fs.existsSync(manifestPath) && fs.existsSync(overviewPath);
}

function generatePluginsRegistrySource() {
const folders = listPluginFolders().filter((name) => isPluginFolder(name));
const imports = [];
const entries = [];
for (const folder of folders) {
const camel = toCamelCase(folder);
const pascal = toPascalCase(folder);
const metaName = `${camel}Meta`;
const loadName = `load${pascal}Overview`;
imports.push(`import { pluginMeta as ${metaName} } from './${folder}/manifest.mjs';`);
imports.push(`import { ${loadName} } from './${folder}/overview.mjs';`);
entries.push(` { id: ${metaName}.id, meta: ${metaName}, loadOverview: ${loadName} }`);
}
const lines = [
...imports,
'',
'export const pluginsRegistry = [',
...entries.map((line, index) => (index < entries.length - 1 ? `${line},` : line)),
'];',
'',
'export function getFirstPluginId() {',
" return pluginsRegistry.length ? pluginsRegistry[0].id : '';",
'}',
'',
'export function getPluginEntry(id) {',
" const key = typeof id === 'string' ? id.trim() : '';",
' if (!key) return null;',
' return pluginsRegistry.find((item) => item && item.id === key) || null;',
'}',
''
];
return lines.join('\n');
}

function writePluginsRegistry() {
const next = generatePluginsRegistrySource();
fs.writeFileSync(registryPath, next, 'utf8');
return next;
}

function main() {
writePluginsRegistry();
}

if (require.main === module) {
main();
}

module.exports = {
generatePluginsRegistrySource,
writePluginsRegistry
};

Loading
Loading