@fission-ai/openspec
Version:
AI-native system for spec-driven development
108 lines • 4.12 kB
JavaScript
/**
* Migration Utilities
*
* One-time migration logic for existing projects when profile system is introduced.
* Called by both init and update commands before profile resolution.
*/
import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig } from './global-config.js';
import { CommandAdapterRegistry } from './command-generation/index.js';
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
import { ALL_WORKFLOWS } from './profiles.js';
import path from 'path';
import * as fs from 'fs';
function scanInstalledWorkflowArtifacts(projectPath, tools) {
const installed = new Set();
let hasSkills = false;
let hasCommands = false;
for (const tool of tools) {
if (!tool.skillsDir)
continue;
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
for (const workflowId of ALL_WORKFLOWS) {
const skillDirName = WORKFLOW_TO_SKILL_DIR[workflowId];
const skillFile = path.join(skillsDir, skillDirName, 'SKILL.md');
if (fs.existsSync(skillFile)) {
installed.add(workflowId);
hasSkills = true;
}
}
const adapter = CommandAdapterRegistry.get(tool.value);
if (!adapter)
continue;
for (const workflowId of ALL_WORKFLOWS) {
const commandPath = adapter.getFilePath(workflowId);
const fullPath = path.isAbsolute(commandPath)
? commandPath
: path.join(projectPath, commandPath);
if (fs.existsSync(fullPath)) {
installed.add(workflowId);
hasCommands = true;
}
}
}
return {
workflows: ALL_WORKFLOWS.filter((workflowId) => installed.has(workflowId)),
hasSkills,
hasCommands,
};
}
/**
* Scans installed workflow files across all detected tools and returns
* the union of installed workflow IDs.
*/
export function scanInstalledWorkflows(projectPath, tools) {
return scanInstalledWorkflowArtifacts(projectPath, tools).workflows;
}
function inferDelivery(artifacts) {
if (artifacts.hasSkills && artifacts.hasCommands) {
return 'both';
}
if (artifacts.hasCommands) {
return 'commands';
}
return 'skills';
}
/**
* Performs one-time migration if the global config does not yet have a profile field.
* Called by both init and update before profile resolution.
*
* - If no profile field exists and workflows are installed: sets profile to 'custom'
* with the detected workflows, preserving the user's existing setup.
* - If no profile field exists and no workflows are installed: no-op (defaults apply).
* - If profile field already exists: no-op.
*/
export function migrateIfNeeded(projectPath, tools) {
const config = getGlobalConfig();
// Check raw config file for profile field presence
const configPath = getGlobalConfigPath();
let rawConfig = {};
try {
if (fs.existsSync(configPath)) {
rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
}
catch {
return; // Can't read config, skip migration
}
// If profile is already explicitly set, no migration needed
if (rawConfig.profile !== undefined) {
return;
}
// Scan for installed workflows
const artifacts = scanInstalledWorkflowArtifacts(projectPath, tools);
const installedWorkflows = artifacts.workflows;
if (installedWorkflows.length === 0) {
// No workflows installed, new user — defaults will apply
return;
}
// Migrate: set profile to custom with detected workflows
config.profile = 'custom';
config.workflows = installedWorkflows;
if (rawConfig.delivery === undefined) {
config.delivery = inferDelivery(artifacts);
}
saveGlobalConfig(config);
console.log(`Migrated: custom profile with ${installedWorkflows.length} workflows`);
console.log("New in this version: /opsx:propose. Try 'openspec config profile core' for the streamlined experience.");
}
//# sourceMappingURL=migration.js.map