diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index b985e61..1ecd330 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -24,7 +24,6 @@ import { import { manageIgnoreFiles } from './ignore-files.js' import { contentHash, getConfigPath, getLockPath, readLock, upsertLockEntry } from './io.js' import { getReader } from './manifest/index.js' -import { migrateLegacyWorkspace } from './migrate-legacy.js' import { fetchRegistryEntry, parseDocSpec, parseEcosystem, resolveFromRegistry } from './registry.js' import { getResolver } from './resolvers/index.js' import { generateSkill, removeSkill } from './skill.js' @@ -255,7 +254,6 @@ const addCmd = defineCommand({ }, async run({ args }) { const projectDir = process.cwd() - migrateLegacyWorkspace(projectDir) let effectiveSpec = args.spec const parsed = parseDocSpec(effectiveSpec) const { spec: cleanSpec } = parseEcosystem(effectiveSpec) @@ -538,7 +536,6 @@ export async function runSync( projectDir: string, options: RunSyncOptions = {}, ): Promise<{ drifted: number, unchanged: number, failed: number }> { - migrateLegacyWorkspace(projectDir) const config = loadConfig(projectDir) const lock = readLock(projectDir) @@ -602,7 +599,6 @@ const listCmd = defineCommand({ meta: { name: 'list', description: 'List downloaded documentation' }, run() { const projectDir = process.cwd() - migrateLegacyWorkspace(projectDir) const entries = listDocs(projectDir) if (entries.length === 0) { @@ -624,7 +620,6 @@ const removeCmd = defineCommand({ }, run({ args }) { const projectDir = process.cwd() - migrateLegacyWorkspace(projectDir) const { name, version } = parseSpec(args.spec) const hasExplicitVersion = args.spec.lastIndexOf('@') > 0 const ver = hasExplicitVersion ? version : undefined diff --git a/packages/cli/src/migrate-legacy.ts b/packages/cli/src/migrate-legacy.ts deleted file mode 100644 index 242be52..0000000 --- a/packages/cli/src/migrate-legacy.ts +++ /dev/null @@ -1,103 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { consola } from 'consola' -import { getAskDir, getConfigPath, writeConfig } from './io.js' -import { ConfigSchema } from './schemas.js' - -const LEGACY_DIR = '.please' - -/** - * One-shot migration from `.please/` (used by ASK before April 2026) to - * `.ask/`. Triggered exactly once on the first CLI invocation in a project - * that still has the legacy layout. - * - * Idempotency sentinel: the presence of `.ask/config.json` (NOT just `.ask/`) - * marks the migration as complete. A bare `.ask/` directory may exist for - * other reasons (created by another tool, leftover from a partial run), so - * we require the config file specifically. - * - * Order of operations is critical for safety: - * 1. Parse and validate the legacy config FIRST (in memory). - * 2. Write the new config to .ask/config.json — atomic-ish, no destructive - * side effects yet. - * 3. Move .please/docs/* into .ask/docs/. - * 4. Remove the legacy config file. - * - * If step 1 or 2 throws, nothing on disk has changed and the next run can - * retry. If step 3 throws partway through, we leave the partial state and - * throw — the user must intervene rather than silently losing entries on - * the next run. - */ -export function migrateLegacyWorkspace(projectDir: string): void { - const newDir = getAskDir(projectDir) - const newConfig = getConfigPath(projectDir) - - if (fs.existsSync(newConfig)) { - return - } - - const legacyDir = path.join(projectDir, LEGACY_DIR) - const legacyDocs = path.join(legacyDir, 'docs') - const legacyConfig = path.join(legacyDir, 'config.json') - const hasLegacyDocs = fs.existsSync(legacyDocs) - const hasLegacyConfig = fs.existsSync(legacyConfig) - - if (!hasLegacyDocs && !hasLegacyConfig) { - return - } - - consola.warn( - 'ASK legacy layout detected (.please/). Migrating to .ask/. ' - + 'This is a one-time operation; future runs will not log this message.', - ) - - let migratedConfig: ReturnType | null = null - if (hasLegacyConfig) { - try { - const raw = fs.readFileSync(legacyConfig, 'utf-8') - const legacy = JSON.parse(raw) as { docs?: unknown[] } - migratedConfig = ConfigSchema.parse({ - schemaVersion: 1, - docs: legacy.docs ?? [], - }) - } - catch (err) { - throw new Error( - `Failed to parse legacy .please/config.json: ${err instanceof Error ? err.message : err}. ` - + 'The legacy file is left intact for manual recovery — fix or delete it and re-run.', - ) - } - } - - fs.mkdirSync(newDir, { recursive: true }) - writeConfig(projectDir, migratedConfig ?? { schemaVersion: 1, docs: [] }) - - if (hasLegacyDocs) { - const newDocs = path.join(newDir, 'docs') - fs.mkdirSync(newDocs, { recursive: true }) - try { - for (const entry of fs.readdirSync(legacyDocs)) { - fs.renameSync( - path.join(legacyDocs, entry), - path.join(newDocs, entry), - ) - } - fs.rmSync(legacyDocs, { recursive: true, force: true }) - } - catch (err) { - throw new Error( - `Failed to move legacy docs from .please/docs to .ask/docs: ${err instanceof Error ? err.message : err}. ` - + 'Workspace is now in a partial state — please move any remaining entries manually.', - ) - } - } - - if (hasLegacyConfig) { - try { - fs.rmSync(legacyConfig, { force: true }) - } - catch { - // best-effort: the new config is the sentinel, not the absence of the old one - } - } -} diff --git a/packages/cli/test/migrate-legacy.test.ts b/packages/cli/test/migrate-legacy.test.ts deleted file mode 100644 index 6711102..0000000 --- a/packages/cli/test/migrate-legacy.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'bun:test' -import { readConfig } from '../src/io.js' -import { migrateLegacyWorkspace } from '../src/migrate-legacy.js' - -let tmpDir: string - -beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ask-migrate-test-')) -}) - -afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }) -}) - -function seedLegacy(legacyConfig: object, libs: Record): void { - const please = path.join(tmpDir, '.please') - fs.mkdirSync(path.join(please, 'docs'), { recursive: true }) - fs.writeFileSync( - path.join(please, 'config.json'), - `${JSON.stringify(legacyConfig, null, 2)}\n`, - ) - for (const [dirName, content] of Object.entries(libs)) { - const libDir = path.join(please, 'docs', dirName) - fs.mkdirSync(libDir, { recursive: true }) - fs.writeFileSync(path.join(libDir, 'README.md'), content) - } -} - -describe('migrateLegacyWorkspace', () => { - it('is a no-op when neither .please nor .ask exists', () => { - migrateLegacyWorkspace(tmpDir) - expect(fs.existsSync(path.join(tmpDir, '.ask'))).toBe(false) - expect(fs.existsSync(path.join(tmpDir, '.please'))).toBe(false) - }) - - it('is a no-op when .ask/config.json already exists (sentinel)', () => { - // The sentinel is .ask/config.json specifically — a bare .ask/ directory - // could exist for unrelated reasons, so the migration only short-circuits - // when the new config file is in place. - fs.mkdirSync(path.join(tmpDir, '.ask'), { recursive: true }) - fs.writeFileSync( - path.join(tmpDir, '.ask', 'config.json'), - '{"schemaVersion":1,"docs":[]}\n', - ) - fs.mkdirSync(path.join(tmpDir, '.please', 'docs', 'foo@1.0.0'), { recursive: true }) - fs.writeFileSync(path.join(tmpDir, '.please', 'docs', 'foo@1.0.0', 'README.md'), 'old') - migrateLegacyWorkspace(tmpDir) - // .please/docs should still be there because migration was skipped - expect(fs.existsSync(path.join(tmpDir, '.please', 'docs', 'foo@1.0.0'))).toBe(true) - // .ask/docs not created because migration was skipped - expect(fs.existsSync(path.join(tmpDir, '.ask', 'docs'))).toBe(false) - }) - - it('moves .please/docs/* into .ask/docs/', () => { - seedLegacy( - { - docs: [ - { source: 'github', name: 'foo', version: '1.0.0', repo: 'a/foo', tag: 'v1.0.0' }, - ], - }, - { 'foo@1.0.0': '# foo' }, - ) - migrateLegacyWorkspace(tmpDir) - expect(fs.existsSync(path.join(tmpDir, '.ask', 'docs', 'foo@1.0.0', 'README.md'))).toBe(true) - expect(fs.existsSync(path.join(tmpDir, '.please', 'docs', 'foo@1.0.0'))).toBe(false) - }) - - it('parses legacy config and rewrites it under .ask/ with schemaVersion 1', () => { - seedLegacy( - { - docs: [ - { source: 'github', name: 'foo', version: '1.0.0', repo: 'a/foo', tag: 'v1.0.0' }, - { source: 'npm', name: 'bar', version: '2.0.0' }, - ], - }, - { 'foo@1.0.0': '# foo', 'bar@2.0.0': '# bar' }, - ) - migrateLegacyWorkspace(tmpDir) - const cfg = readConfig(tmpDir) - expect(cfg.schemaVersion).toBe(1) - expect(cfg.docs).toHaveLength(2) - // docs[] is sorted by name on write — bar before foo - expect(cfg.docs[0].name).toBe('bar') - expect(cfg.docs[1].name).toBe('foo') - expect(fs.existsSync(path.join(tmpDir, '.please', 'config.json'))).toBe(false) - }) - - it('runs exactly once — second invocation is a no-op', () => { - seedLegacy( - { docs: [{ source: 'npm', name: 'foo', version: '1.0.0' }] }, - { 'foo@1.0.0': '# foo' }, - ) - migrateLegacyWorkspace(tmpDir) - const firstReadme = fs.readFileSync( - path.join(tmpDir, '.ask', 'docs', 'foo@1.0.0', 'README.md'), - 'utf-8', - ) - // Tamper with the migrated file to verify second call doesn't re-migrate - fs.writeFileSync( - path.join(tmpDir, '.ask', 'docs', 'foo@1.0.0', 'README.md'), - 'tampered', - ) - migrateLegacyWorkspace(tmpDir) - const secondReadme = fs.readFileSync( - path.join(tmpDir, '.ask', 'docs', 'foo@1.0.0', 'README.md'), - 'utf-8', - ) - expect(secondReadme).toBe('tampered') - expect(firstReadme).toBe('# foo') - }) -})