Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/getmetadir-walk-up.md
Original file line number Diff line number Diff line change
@@ -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).
34 changes: 31 additions & 3 deletions packages/intent/src/commands/support.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -28,8 +28,36 @@ 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
// 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')
// 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
}
return join(startDir, '..', '..', 'meta')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export function getCheckSkillsWorkflowAdvisories(root: string): Array<string> {
Expand Down
102 changes: 102 additions & 0 deletions packages/intent/tests/support.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
symlinkSync,
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 <pkg>/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 <pkg>/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)
})

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', () => {
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)
})
})
Loading