From 17059ec9c5060118bda36fcb2fb3e5f571a41f63 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 3 Jul 2026 18:22:46 +0300 Subject: [PATCH 1/2] fix(intent): resolve meta/ by walking up instead of hardcoded depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getMetaDir hardcoded ../../meta, which is correct for the src/commands/ source layout but overshoots to @tanstack/meta from the flat dist/ layout the build ships — so setup, setup-github-actions, meta, and scaffold fail with "No templates directory found" / "Meta-skills directory not found" in the published package. Walk up to the first meta/ directory so resolution works in both layouts and symlinked installs. --- .changeset/getmetadir-walk-up.md | 5 ++ packages/intent/src/commands/support.ts | 27 +++++++++- packages/intent/tests/support.test.ts | 68 +++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .changeset/getmetadir-walk-up.md create mode 100644 packages/intent/tests/support.test.ts diff --git a/.changeset/getmetadir-walk-up.md b/.changeset/getmetadir-walk-up.md new file mode 100644 index 0000000..7cd1932 --- /dev/null +++ b/.changeset/getmetadir-walk-up.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Fix `setup`, `setup-github-actions`, `meta`, and `scaffold` failing with "No templates directory found" / "Meta-skills directory not found" in the published package. `getMetaDir` hardcoded `../../meta`, which is correct for the `src/commands/` source layout but overshoots to `@tanstack/meta` from the flat `dist/` layout the build ships. Walk up to the first `meta/` directory instead so resolution works in both layouts (and symlinked installs). diff --git a/packages/intent/src/commands/support.ts b/packages/intent/src/commands/support.ts index 5304d8c..f300af6 100644 --- a/packages/intent/src/commands/support.ts +++ b/packages/intent/src/commands/support.ts @@ -28,8 +28,31 @@ export interface StaleTargetResult { export const INTENT_CHECK_SKILLS_WORKFLOW_VERSION = 3 export function getMetaDir(): string { - const thisDir = dirname(fileURLToPath(import.meta.url)) - return join(thisDir, '..', '..', 'meta') + return findMetaDir(dirname(fileURLToPath(import.meta.url))) +} + +/** + * Resolve the package `meta/` directory by walking up from `startDir`. + * + * The CLI module sits at `src/commands/` in source but is bundled flat into + * `dist/` in the published package, so the depth from the module to the + * package root differs between the two layouts. A hardcoded `..` count is + * correct for only one of them and breaks `setup` / `meta` / `scaffold` in the + * other. Walking up to the first `meta/` directory handles both layouts (and + * symlinked installs such as pnpm) without depending on the build output + * depth. Falls back to the historical `../../meta` resolution if no `meta/` + * is found, so behaviour is never worse than before. + */ +export function findMetaDir(startDir: string): string { + let dir = startDir + for (let limit = 0; limit < 10; limit++) { + const candidate = join(dir, 'meta') + if (existsSync(candidate)) return candidate + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + return join(startDir, '..', '..', 'meta') } export function getCheckSkillsWorkflowAdvisories(root: string): Array { diff --git a/packages/intent/tests/support.test.ts b/packages/intent/tests/support.test.ts new file mode 100644 index 0000000..bbcca8d --- /dev/null +++ b/packages/intent/tests/support.test.ts @@ -0,0 +1,68 @@ +import { + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { findMetaDir, getMetaDir } from '../src/commands/support.js' + +let root: string + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'support-test-')) +}) + +afterEach(() => { + rmSync(root, { recursive: true, force: true }) +}) + +function seedMeta(pkgDir: string): string { + const metaDir = join(pkgDir, 'meta', 'templates', 'workflows') + mkdirSync(metaDir, { recursive: true }) + writeFileSync( + join(metaDir, 'check-skills.yml'), + '# intent-workflow-version: 3\n', + ) + return join(pkgDir, 'meta') +} + +describe('findMetaDir', () => { + it('resolves from the flat dist/ layout used by the published package', () => { + const pkgDir = join(root, 'pkg') + const metaDir = seedMeta(pkgDir) + // Bundled module lives flat at /dist/support-*.mjs. + const startDir = join(pkgDir, 'dist') + mkdirSync(startDir, { recursive: true }) + expect(findMetaDir(startDir)).toBe(metaDir) + }) + + it('resolves from the deep src/commands/ layout used in source', () => { + const pkgDir = join(root, 'pkg') + const metaDir = seedMeta(pkgDir) + // Source module lives at /src/commands/support.ts (two levels deep). + const startDir = join(pkgDir, 'src', 'commands') + mkdirSync(startDir, { recursive: true }) + expect(findMetaDir(startDir)).toBe(metaDir) + }) + + it('falls back to the historical ../../meta when no meta/ is found', () => { + const startDir = join(root, 'pkg', 'dist') + mkdirSync(startDir, { recursive: true }) + expect(findMetaDir(startDir)).toBe(join(startDir, '..', '..', 'meta')) + expect(existsSync(findMetaDir(startDir))).toBe(false) + }) +}) + +describe('getMetaDir', () => { + it('resolves to a meta/ directory that ships the workflow template', () => { + const metaDir = getMetaDir() + expect(existsSync(metaDir)).toBe(true) + expect( + existsSync(join(metaDir, 'templates', 'workflows', 'check-skills.yml')), + ).toBe(true) + }) +}) From 2891205b9fce321af79045801737b420eff2e12f Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 3 Jul 2026 18:34:07 +0300 Subject: [PATCH 2/2] fix(intent): verify meta candidate is a directory; cover symlink + stray-file cases Address review: statSync(candidate).isDirectory() so a stray file named `meta` can't short-circuit the walk-up; add a comment on the iteration cap; add tests for the stray-file skip and a pnpm-style symlinked install path. --- packages/intent/src/commands/support.ts | 9 +++++-- packages/intent/tests/support.test.ts | 34 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/intent/src/commands/support.ts b/packages/intent/src/commands/support.ts index f300af6..d6ea381 100644 --- a/packages/intent/src/commands/support.ts +++ b/packages/intent/src/commands/support.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs' +import { existsSync, readFileSync, statSync } from 'node:fs' import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { fail } from '../shared/cli-error.js' @@ -45,9 +45,14 @@ export function getMetaDir(): string { */ export function findMetaDir(startDir: string): string { let dir = startDir + // Sanity cap: a package is never this many dirs deep; the root check below + // also stops the walk at the filesystem root. for (let limit = 0; limit < 10; limit++) { const candidate = join(dir, 'meta') - if (existsSync(candidate)) return candidate + // Check it's a directory, not just that something named `meta` exists — a + // stray file named `meta` in an ancestor must not short-circuit the walk. + if (existsSync(candidate) && statSync(candidate).isDirectory()) + return candidate const parent = dirname(dir) if (parent === dir) break dir = parent diff --git a/packages/intent/tests/support.test.ts b/packages/intent/tests/support.test.ts index bbcca8d..f604257 100644 --- a/packages/intent/tests/support.test.ts +++ b/packages/intent/tests/support.test.ts @@ -3,6 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, + symlinkSync, writeFileSync, } from 'node:fs' import { join } from 'node:path' @@ -55,6 +56,39 @@ describe('findMetaDir', () => { expect(findMetaDir(startDir)).toBe(join(startDir, '..', '..', 'meta')) expect(existsSync(findMetaDir(startDir))).toBe(false) }) + + it('skips a stray file named meta and keeps walking', () => { + const pkgDir = join(root, 'pkg') + const metaDir = seedMeta(pkgDir) + const startDir = join(pkgDir, 'dist') + mkdirSync(startDir, { recursive: true }) + // A file (not a directory) named `meta` next to startDir must not short-circuit. + writeFileSync(join(startDir, 'meta'), '') + expect(findMetaDir(startDir)).toBe(metaDir) + }) + + it('resolves through a symlinked install path (pnpm-style)', () => { + // Real package lives in a pnpm-style store; the consumer reaches it via a symlink. + const storePkg = join( + root, + 'store', + '@tanstack+intent', + 'node_modules', + '@tanstack', + 'intent', + ) + seedMeta(storePkg) + mkdirSync(join(storePkg, 'dist'), { recursive: true }) + + const consumerTanstack = join(root, 'consumer', 'node_modules', '@tanstack') + mkdirSync(consumerTanstack, { recursive: true }) + symlinkSync(storePkg, join(consumerTanstack, 'intent'), 'dir') + + const startDir = join(consumerTanstack, 'intent', 'dist') + const resolved = findMetaDir(startDir) + expect(resolved).toBe(join(consumerTanstack, 'intent', 'meta')) + expect(existsSync(resolved)).toBe(true) + }) }) describe('getMetaDir', () => {