feat(plugins): bundle project scaffold marketplace plugin
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# Task: Publish scaffold Skill through official Marketplace
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20260904-marketplace-scaffold-client-6c4e8a21
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20260904-integrate-project-scaffold-b6d3e8a1-integrate-project-scaffold
|
||||||
|
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-integrate-project-scaffold-b6d3e8a1
|
||||||
|
- Base commit: 22078060386e38dd5f12af4717dc6c6e3378c2df
|
||||||
|
- Owner: codex
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Publish `makelore.project-scaffold` as a code-owned bundled Marketplace plugin.
|
||||||
|
- Move the existing project-scaffold Skill runtime into `resources/coding-plugins/project-scaffold` so it ships with the signed MakeLore application.
|
||||||
|
- Register the bundled release in the client plugin registry and preserve the acquire, project-enable, and Agent-assignment lifecycle.
|
||||||
|
- Cover the official-script boundary with focused manifest, resolver, scaffold, and artifact-package tests.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- The Skill owns creation of the fixed interactive AI application starter and static publish-readiness checks; it does not build, upload, submit, or approve a project.
|
||||||
|
- Official executable resources may be loaded only from the code-owned bundled plugin root. Downloadable Marketplace artifacts keep the existing text/image-only P0 asset policy, including rejection of `.mjs`.
|
||||||
|
- Keep one runtime copy of the Skill rather than a Device Package copy plus a bundled copy.
|
||||||
|
- Use plugin ID `makelore.project-scaffold`, Skill ID `makelore-project-scaffold`, release version `1.0.0`, and minimum client version `2.0.0`.
|
||||||
|
- Preserve non-overwrite scaffolding and explicit user invocation of the Skill.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Moved the existing scaffold Skill, templates, release reference, and `.mjs` runner into the signed client resource tree at `resources/coding-plugins/project-scaffold`; no second runtime copy remains.
|
||||||
|
- Added schema-v2 MakeLore capability metadata and retained the portable `.codex-plugin/plugin.json`, both identifying `makelore.project-scaffold` version `1.0.0` and Skill `makelore-project-scaffold`.
|
||||||
|
- Registered the plugin as a fixed, code-owned, `skill_only` bundled release with ID `00000000-0000-4000-8000-000000000303` and the existing acquire, project-enable, and Agent-assignment lifecycle.
|
||||||
|
- Narrowed executable-script authority to the fixed bundled root. Downloaded Marketplace ZIPs still reject `.mjs`; they do not install this plugin's runtime.
|
||||||
|
- Preserved the existing non-overwrite scaffold behavior, legacy `mini_game` / `mini_program` read compatibility, canonical `interactive_ai_app` output, and static publication-readiness guidance.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `pnpm exec vitest run tests/unit/coding-plugin-manifest.test.ts tests/unit/coding-plugin-marketplace-client.test.ts tests/unit/project-scaffold-plugin.test.ts --maxWorkers=2`: 54 passed.
|
||||||
|
- `node --test tests/project-scaffold/scaffold.test.mjs`: 12 passed.
|
||||||
|
- `pnpm exec vitest run tests/unit/plugin-workspace-model.test.ts tests/unit/coding-plugin-effective-resolver.test.ts tests/unit/coding-composition-paths.test.ts --maxWorkers=2`: 33 passed.
|
||||||
|
- ESLint on the changed TypeScript files: passed.
|
||||||
|
- `pnpm run typecheck`: passed.
|
||||||
|
- Plugin manifest validation: passed. Skill validation: passed with UTF-8 mode and the repository Python environment that provides PyYAML.
|
||||||
|
- `pnpm test`: 224 test files and 1,879 tests passed, 2 skipped; one unrelated real-process timing assertion exceeded two seconds under full parallel load. Re-running `tests/unit/pi-agent-server-process-real.test.ts` alone with one worker passed all 6 tests, so no unrelated timeout change was made.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Run a packaged-app smoke test before the first public client release to prove the signed resource layout and `ELECTRON_RUN_AS_NODE` execution path outside the repository environment.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- Update ADR-008 after integration: project scaffolding is distributed as a code-owned bundled Marketplace plugin, while downloadable artifacts remain non-executable in P0.
|
||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
CODE_OWNED_PLUGIN_PERMISSION_IDS,
|
CODE_OWNED_PLUGIN_PERMISSION_IDS,
|
||||||
GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
||||||
GAME_RESOURCE_PLUGIN_ID,
|
GAME_RESOURCE_PLUGIN_ID,
|
||||||
|
PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
DATA_SERVICE_CAPABILITY_IDS,
|
DATA_SERVICE_CAPABILITY_IDS,
|
||||||
DATA_SERVICE_OPERATION_DEFINITIONS,
|
DATA_SERVICE_OPERATION_DEFINITIONS,
|
||||||
DATA_SERVICE_PLUGIN_ID,
|
DATA_SERVICE_PLUGIN_ID,
|
||||||
@@ -35,6 +37,7 @@ import {
|
|||||||
export const BUNDLED_CODING_PLUGIN_ROOTS = Object.freeze([
|
export const BUNDLED_CODING_PLUGIN_ROOTS = Object.freeze([
|
||||||
'data-service',
|
'data-service',
|
||||||
'game-resource',
|
'game-resource',
|
||||||
|
'project-scaffold',
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const BUNDLED_CODING_PLUGIN_METADATA = Object.freeze({
|
const BUNDLED_CODING_PLUGIN_METADATA = Object.freeze({
|
||||||
@@ -52,6 +55,13 @@ const BUNDLED_CODING_PLUGIN_METADATA = Object.freeze({
|
|||||||
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
||||||
bundledV2: true,
|
bundledV2: true,
|
||||||
}),
|
}),
|
||||||
|
'project-scaffold': Object.freeze({
|
||||||
|
pluginId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
|
runtimeKind: 'skill_only' as const,
|
||||||
|
acquisitionMode: 'user_acquired' as const,
|
||||||
|
releaseId: PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
bundledV2: true,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CAPABILITY_MANIFEST_RELATIVE_PATH = 'com.makelore/capability.json';
|
const CAPABILITY_MANIFEST_RELATIVE_PATH = 'com.makelore/capability.json';
|
||||||
@@ -722,8 +732,11 @@ function trustedMetadata(
|
|||||||
if (options.bundledV2 && !bundledV2) {
|
if (options.bundledV2 && !bundledV2) {
|
||||||
fail(filePath, 'trusted metadata.bundledV2', 'is valid only for schema 2 packages');
|
fail(filePath, 'trusted metadata.bundledV2', 'is valid only for schema 2 packages');
|
||||||
}
|
}
|
||||||
if (bundledV2 && (runtimeKind !== 'platform_hosted' || releaseId === null)) {
|
if (bundledV2 && (runtimeKind !== 'platform_hosted' && runtimeKind !== 'skill_only')) {
|
||||||
fail(filePath, 'trusted metadata.bundledV2', 'requires a hosted runtime and fixed Release ID');
|
fail(filePath, 'trusted metadata.bundledV2', 'requires a distributed runtime');
|
||||||
|
}
|
||||||
|
if (bundledV2 && releaseId === null) {
|
||||||
|
fail(filePath, 'trusted metadata.bundledV2', 'requires a fixed Release ID');
|
||||||
}
|
}
|
||||||
const defaultProvenance: CodingPluginPackageProvenance = {
|
const defaultProvenance: CodingPluginPackageProvenance = {
|
||||||
source: schemaVersion === 1 || bundledV2 ? 'bundled' : 'marketplace',
|
source: schemaVersion === 1 || bundledV2 ? 'bundled' : 'marketplace',
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "makelore-project-scaffold",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"test": "node --test tests/scaffold.test.mjs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "makelore-project-scaffold",
|
"name": "makelore.project-scaffold",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"description": "为 MakeLore 交互式 AI 应用生成工程骨架并检查一键提交要求。",
|
"description": "为 MakeLore 交互式 AI 应用生成工程骨架并检查一键提交要求。",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "MakeLore Team"
|
"name": "MakeLore Team"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"displayName": "MakeLore 项目初始化与发布检查",
|
"displayName": "MakeLore 项目初始化与发布检查",
|
||||||
"shortDescription": "初始化工程骨架并检查发布要求。",
|
"shortDescription": "初始化工程骨架并检查发布要求。",
|
||||||
"longDescription": "读取当前 MakeLore 项目类型,生成固定、可构建的交互式 AI 应用工程骨架,并按平台的一键提交合同区分必需项、直接阻断项、打包排除项和运行期检查。",
|
"longDescription": "读取当前 MakeLore 项目类型,生成固定、可构建的交互式 AI 应用工程骨架,并按平台的一键提交合同区分必需项、直接阻断项、打包排除项和运行期检查。",
|
||||||
"developerName": "MakeLore Team",
|
"developerName": "MakeLore",
|
||||||
"category": "Developer Tools",
|
"category": "Developer Tools",
|
||||||
"capabilities": ["Write"],
|
"capabilities": ["Write"],
|
||||||
"defaultPrompt": "使用 $makelore-project-scaffold 初始化当前 MakeLore 交互式 AI 应用项目,并检查是否满足一键提交要求。"
|
"defaultPrompt": "使用 $makelore-project-scaffold 初始化当前 MakeLore 交互式 AI 应用项目,并检查是否满足一键提交要求。"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"pluginId": "makelore.project-scaffold",
|
||||||
|
"contractVersion": 1,
|
||||||
|
"scope": "project",
|
||||||
|
"runtime": {
|
||||||
|
"kind": "skill_only"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"id": "makelore-project-scaffold",
|
||||||
|
"entry": "../skills/makelore-project-scaffold/SKILL.md",
|
||||||
|
"grants": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": []
|
||||||
|
}
|
||||||
6
resources/coding-plugins/project-scaffold/package.json
Normal file
6
resources/coding-plugins/project-scaffold/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "makelore.project-scaffold",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
14
resources/coding-plugins/project-scaffold/plugin.json
Normal file
14
resources/coding-plugins/project-scaffold/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||||
|
"name": "makelore.project-scaffold",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "为 MakeLore 交互式 AI 应用生成固定工程骨架并检查发布与审批提交要求。",
|
||||||
|
"author": {
|
||||||
|
"name": "MakeLore"
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"com.makelore": {
|
||||||
|
"capabilityManifest": "./com.makelore/capability.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,8 @@ export const DATA_SERVICE_PREVIEW_RUNTIME_SURFACE = 'data-service-v1' as const;
|
|||||||
|
|
||||||
export const GAME_RESOURCE_PLUGIN_ID = 'makelore.game-resource' as const;
|
export const GAME_RESOURCE_PLUGIN_ID = 'makelore.game-resource' as const;
|
||||||
export const GAME_RESOURCE_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000105' as const;
|
export const GAME_RESOURCE_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000105' as const;
|
||||||
|
export const PROJECT_SCAFFOLD_PLUGIN_ID = 'makelore.project-scaffold' as const;
|
||||||
|
export const PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000303' as const;
|
||||||
|
|
||||||
export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
||||||
[GAME_RESOURCE_PLUGIN_ID]: Object.freeze({
|
[GAME_RESOURCE_PLUGIN_ID]: Object.freeze({
|
||||||
@@ -110,6 +112,11 @@ export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
channel: 'stable' as const,
|
channel: 'stable' as const,
|
||||||
}),
|
}),
|
||||||
|
[PROJECT_SCAFFOLD_PLUGIN_ID]: Object.freeze({
|
||||||
|
releaseId: PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
version: '1.0.0',
|
||||||
|
channel: 'stable' as const,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function isCodeOwnedOptionalBundledPluginId(pluginId: string): boolean {
|
export function isCodeOwnedOptionalBundledPluginId(pluginId: string): boolean {
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import { spawnSync } from 'node:child_process';
|
|||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
const pluginRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
const pluginRoot = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
'../../resources/coding-plugins/project-scaffold',
|
||||||
|
);
|
||||||
const scaffoldScript = path.join(
|
const scaffoldScript = path.join(
|
||||||
pluginRoot,
|
pluginRoot,
|
||||||
'skills',
|
'skills',
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
|
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
|
||||||
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
|
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
|
||||||
|
const PROJECT_SCAFFOLD_ROOT = path.resolve('resources/coding-plugins/project-scaffold');
|
||||||
|
|
||||||
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
|
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
|
||||||
return {
|
return {
|
||||||
@@ -33,12 +34,17 @@ describe('bundled coding plugin manifests', () => {
|
|||||||
it('loads the fixed Data Service package and immutable declarations', async () => {
|
it('loads the fixed Data Service package and immutable declarations', async () => {
|
||||||
const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'));
|
const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'));
|
||||||
const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'));
|
const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'));
|
||||||
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource']);
|
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual([
|
||||||
|
'data-service',
|
||||||
|
'game-resource',
|
||||||
|
'project-scaffold',
|
||||||
|
]);
|
||||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
||||||
PACKAGE_ROOT,
|
PACKAGE_ROOT,
|
||||||
GAME_RESOURCE_ROOT,
|
GAME_RESOURCE_ROOT,
|
||||||
|
PROJECT_SCAFFOLD_ROOT,
|
||||||
]);
|
]);
|
||||||
expect(definitions).toHaveLength(2);
|
expect(definitions).toHaveLength(3);
|
||||||
expect(definitions[0]).toMatchObject({
|
expect(definitions[0]).toMatchObject({
|
||||||
id: 'makelore.data-service',
|
id: 'makelore.data-service',
|
||||||
adapterId: 'data-service',
|
adapterId: 'data-service',
|
||||||
@@ -54,6 +60,17 @@ describe('bundled coding plugin manifests', () => {
|
|||||||
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
||||||
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
|
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'makelore.project-scaffold', version: '1.0.0', runtimeKind: 'skill_only',
|
||||||
|
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000303',
|
||||||
|
provenance: { source: 'bundled', packageRoot: 'project-scaffold' },
|
||||||
|
skills: [{
|
||||||
|
id: 'makelore-project-scaffold',
|
||||||
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
||||||
|
grants: [],
|
||||||
|
}],
|
||||||
|
tools: [],
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
expect(startupDefinitions).toEqual(definitions);
|
expect(startupDefinitions).toEqual(definitions);
|
||||||
expect(Object.isFrozen(startupDefinitions)).toBe(true);
|
expect(Object.isFrozen(startupDefinitions)).toBe(true);
|
||||||
@@ -128,6 +145,7 @@ describe('bundled coding plugin manifests', () => {
|
|||||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
|
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
|
||||||
path.resolve('tmp/data-service'),
|
path.resolve('tmp/data-service'),
|
||||||
path.resolve('tmp/game-resource'),
|
path.resolve('tmp/game-resource'),
|
||||||
|
path.resolve('tmp/project-scaffold'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1419,8 +1419,8 @@ describe('PluginPackageStore', () => {
|
|||||||
});
|
});
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
name: 'unsupported asset extension',
|
name: 'executable Skill script in a downloadable artifact',
|
||||||
archive: buildSkillOnlyArchive({ 'skills/example-skill/assets/run.exe': Buffer.from('not executable') }),
|
archive: buildSkillOnlyArchive({ 'skills/example-skill/scripts/run.mjs': Buffer.from('export {};') }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'non-UTF-8 Skill text',
|
name: 'non-UTF-8 Skill text',
|
||||||
|
|||||||
@@ -261,40 +261,45 @@ describe('buildPluginWorkspaceProjection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps bundled and downloadable official command sets inside their delivery boundaries', () => {
|
it('keeps bundled and downloadable official command sets inside their delivery boundaries', () => {
|
||||||
const bundledCatalog = {
|
for (const [pluginId, title, runtimeKind] of [
|
||||||
...catalog.items[0]!,
|
['makelore.game-resource', 'Game Resource', 'platform_hosted'],
|
||||||
pluginId: 'makelore.game-resource',
|
['makelore.project-scaffold', 'Project Scaffold', 'skill_only'],
|
||||||
title: 'Game Resource',
|
] as const) {
|
||||||
runtimeKind: 'bundled_typed' as const,
|
const bundledCatalog = {
|
||||||
};
|
...catalog.items[0]!,
|
||||||
const bundledLibrary = {
|
pluginId,
|
||||||
...library.items[0]!,
|
title,
|
||||||
pluginId: bundledCatalog.pluginId,
|
runtimeKind,
|
||||||
title: bundledCatalog.title,
|
};
|
||||||
};
|
const bundledLibrary = {
|
||||||
const bundledProject = {
|
...library.items[0]!,
|
||||||
...project,
|
pluginId: bundledCatalog.pluginId,
|
||||||
items: [{
|
title: bundledCatalog.title,
|
||||||
...project.items[0]!,
|
};
|
||||||
id: bundledCatalog.pluginId,
|
const bundledProject = {
|
||||||
displayName: bundledCatalog.title,
|
...project,
|
||||||
enabled: false,
|
items: [{
|
||||||
state: 'disabled' as const,
|
...project.items[0]!,
|
||||||
}],
|
id: bundledCatalog.pluginId,
|
||||||
};
|
displayName: bundledCatalog.title,
|
||||||
|
enabled: false,
|
||||||
|
state: 'disabled' as const,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
expect(commandKinds({
|
expect(commandKinds({
|
||||||
catalog: { ...catalog, items: [bundledCatalog] },
|
catalog: { ...catalog, items: [bundledCatalog] },
|
||||||
library: { ...library, items: [] },
|
library: { ...library, items: [] },
|
||||||
marketplaceInstallations: {},
|
marketplaceInstallations: {},
|
||||||
project: null,
|
project: null,
|
||||||
})).toEqual(['acquire']);
|
})).toEqual(['acquire']);
|
||||||
expect(commandKinds({
|
expect(commandKinds({
|
||||||
catalog: { ...catalog, items: [bundledCatalog] },
|
catalog: { ...catalog, items: [bundledCatalog] },
|
||||||
library: { ...library, items: [bundledLibrary] },
|
library: { ...library, items: [bundledLibrary] },
|
||||||
marketplaceInstallations: {},
|
marketplaceInstallations: {},
|
||||||
project: bundledProject,
|
project: bundledProject,
|
||||||
})).toEqual(['remove_from_library', 'enable_project', 'open_agent_assignment']);
|
})).toEqual(['remove_from_library', 'enable_project', 'open_agent_assignment']);
|
||||||
|
}
|
||||||
|
|
||||||
expect(commandKinds({ marketplaceInstallations: {} })).toEqual([
|
expect(commandKinds({ marketplaceInstallations: {} })).toEqual([
|
||||||
'remove_from_library',
|
'remove_from_library',
|
||||||
|
|||||||
@@ -3,18 +3,22 @@
|
|||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
devicePackageKind,
|
devicePackageKind,
|
||||||
inspectDevicePackage,
|
inspectDevicePackage,
|
||||||
} from '@electron/coding-packages/device-package-format';
|
} from '@electron/coding-packages/device-package-format';
|
||||||
import { DevicePackageManager } from '@electron/coding-packages/device-package-manager';
|
import { loadBundledCodingPluginDefinitions } from '@electron/coding-plugins/manifest';
|
||||||
import { createCodingProjectMetadata } from '@electron/coding-projects/project-config';
|
import { createCodingProjectMetadata } from '@electron/coding-projects/project-config';
|
||||||
import { createStaticProjectPackage } from '@electron/services/project-packager';
|
import { createStaticProjectPackage } from '@electron/services/project-packager';
|
||||||
import type { ProjectType } from '../../shared/project-config';
|
import {
|
||||||
|
PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
|
isCodeOwnedOptionalBundledPluginId,
|
||||||
|
} from '../../shared/coding-plugins';
|
||||||
|
|
||||||
const pluginRoot = resolve('plugins/makelore-project-scaffold');
|
const pluginRoot = resolve('resources/coding-plugins/project-scaffold');
|
||||||
const scaffoldScript = join(
|
const scaffoldScript = join(
|
||||||
pluginRoot,
|
pluginRoot,
|
||||||
'skills',
|
'skills',
|
||||||
@@ -42,23 +46,30 @@ afterEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('MakeLore project scaffold plugin', () => {
|
describe('MakeLore project scaffold plugin', () => {
|
||||||
it('is discovered as a skill-only package with executable Skill scripts', async () => {
|
it('loads as a fixed code-owned bundled Skill while retaining portable plugin metadata', async () => {
|
||||||
const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8'));
|
const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8'));
|
||||||
const pluginManifest = JSON.parse(await readFile(
|
const portableManifest = JSON.parse(await readFile(
|
||||||
join(pluginRoot, '.codex-plugin', 'plugin.json'),
|
join(pluginRoot, '.codex-plugin', 'plugin.json'),
|
||||||
'utf8',
|
'utf8',
|
||||||
));
|
));
|
||||||
|
const marketplaceManifest = JSON.parse(await readFile(join(pluginRoot, 'plugin.json'), 'utf8'));
|
||||||
const inspected = await inspectDevicePackage(pluginRoot);
|
const inspected = await inspectDevicePackage(pluginRoot);
|
||||||
|
const definitions = await loadBundledCodingPluginDefinitions(resolve('resources/coding-plugins'));
|
||||||
|
const definition = definitions.find(({ id }) => id === PROJECT_SCAFFOLD_PLUGIN_ID);
|
||||||
|
|
||||||
expect({ name: packageJson.name, version: packageJson.version }).toEqual({
|
expect({ name: packageJson.name, version: packageJson.version }).toEqual({
|
||||||
name: pluginManifest.name,
|
name: marketplaceManifest.name,
|
||||||
version: pluginManifest.version,
|
version: marketplaceManifest.version,
|
||||||
});
|
});
|
||||||
expect(pluginManifest.skills).toBe('./skills/');
|
expect({ name: portableManifest.name, version: portableManifest.version }).toEqual({
|
||||||
|
name: marketplaceManifest.name,
|
||||||
|
version: marketplaceManifest.version,
|
||||||
|
});
|
||||||
|
expect(portableManifest.skills).toBe('./skills/');
|
||||||
expect(devicePackageKind(inspected)).toBe('skill-only');
|
expect(devicePackageKind(inspected)).toBe('skill-only');
|
||||||
expect(inspected).toMatchObject({
|
expect(inspected).toMatchObject({
|
||||||
packageId: 'makelore-project-scaffold',
|
packageId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
resolvedVersion: '0.1.0',
|
resolvedVersion: '1.0.0',
|
||||||
extensionEntries: [],
|
extensionEntries: [],
|
||||||
hasSkillScripts: true,
|
hasSkillScripts: true,
|
||||||
ignoredLifecycleScripts: [],
|
ignoredLifecycleScripts: [],
|
||||||
@@ -67,30 +78,25 @@ describe('MakeLore project scaffold plugin', () => {
|
|||||||
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
expect(definition).toMatchObject({
|
||||||
|
id: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||||
|
version: '1.0.0',
|
||||||
|
runtimeKind: 'skill_only',
|
||||||
|
acquisitionMode: 'user_acquired',
|
||||||
|
releaseId: PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||||
|
provenance: { source: 'bundled', packageRoot: 'project-scaffold' },
|
||||||
|
skills: [{
|
||||||
|
id: 'makelore-project-scaffold',
|
||||||
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
||||||
|
grants: [],
|
||||||
|
}],
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
expect(isCodeOwnedOptionalBundledPluginId(PROJECT_SCAFFOLD_PLUGIN_ID)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prepares, confirms, and installs the real package with every Skill resource', async () => {
|
it('ships the executable scaffold and every Skill resource in the bundled root', async () => {
|
||||||
const root = await mkdtemp(join(tmpdir(), 'makelore-scaffold-device-package-'));
|
const skillRoot = join(pluginRoot, 'skills', 'makelore-project-scaffold');
|
||||||
tempDirectories.push(root);
|
|
||||||
const manager = new DevicePackageManager({
|
|
||||||
rootDir: join(root, 'store'),
|
|
||||||
now: () => Date.parse('2026-09-04T00:00:00.000Z'),
|
|
||||||
createId: () => 'project-scaffold-plan',
|
|
||||||
});
|
|
||||||
|
|
||||||
const preview = await manager.prepare(pluginRoot, 'prepare-turn');
|
|
||||||
expect(preview).toMatchObject({
|
|
||||||
kind: 'skill-only',
|
|
||||||
includesExecutableCode: true,
|
|
||||||
skillEntries: [{ id: 'makelore-project-scaffold' }],
|
|
||||||
extensionEntries: [],
|
|
||||||
});
|
|
||||||
expect(preview.warnings.join(' ')).toContain('可执行 Skill 脚本');
|
|
||||||
await manager.commit(preview.planId, true, 'confirm-turn');
|
|
||||||
|
|
||||||
const [resource] = (await manager.resolveEnabledResources()).skillEntries;
|
|
||||||
expect(resource?.id).toBe('makelore-project-scaffold');
|
|
||||||
const skillRoot = dirname(join(resource!.packageRoot, resource!.entryPath));
|
|
||||||
expect(await readFile(join(skillRoot, 'scripts', 'scaffold.mjs'), 'utf8'))
|
expect(await readFile(join(skillRoot, 'scripts', 'scaffold.mjs'), 'utf8'))
|
||||||
.toContain('schemaVersion: 1');
|
.toContain('schemaVersion: 1');
|
||||||
expect(await readFile(join(skillRoot, 'assets', 'templates', 'common', 'package-lock.json'), 'utf8'))
|
expect(await readFile(join(skillRoot, 'assets', 'templates', 'common', 'package-lock.json'), 'utf8'))
|
||||||
@@ -164,7 +170,7 @@ describe('MakeLore project scaffold plugin', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('creates an interactive AI application accepted by the existing source packager', async () => {
|
it('creates an interactive AI application accepted by the existing source packager', async () => {
|
||||||
const projectType: ProjectType = 'interactive_ai_app';
|
const projectType = 'interactive_ai_app' as const;
|
||||||
const projectPath = await mkdtemp(join(tmpdir(), `makelore-${projectType}-`));
|
const projectPath = await mkdtemp(join(tmpdir(), `makelore-${projectType}-`));
|
||||||
const outputPath = await mkdtemp(join(tmpdir(), 'makelore-scaffold-package-'));
|
const outputPath = await mkdtemp(join(tmpdir(), 'makelore-scaffold-package-'));
|
||||||
tempDirectories.push(projectPath, outputPath);
|
tempDirectories.push(projectPath, outputPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user