Skip to content

Commit e885566

Browse files
fix(intent): walk up to meta/ instead of hardcoded depth (#194)
* fix(intent): resolve meta/ by walking up instead of hardcoded depth 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. * 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.
1 parent 3620cca commit e885566

3 files changed

Lines changed: 138 additions & 3 deletions

File tree

.changeset/getmetadir-walk-up.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
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).

packages/intent/src/commands/support.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync } from 'node:fs'
1+
import { existsSync, readFileSync, statSync } from 'node:fs'
22
import { dirname, join, relative, resolve } from 'node:path'
33
import { fileURLToPath } from 'node:url'
44
import { fail } from '../shared/cli-error.js'
@@ -28,8 +28,36 @@ export interface StaleTargetResult {
2828
export const INTENT_CHECK_SKILLS_WORKFLOW_VERSION = 3
2929

3030
export function getMetaDir(): string {
31-
const thisDir = dirname(fileURLToPath(import.meta.url))
32-
return join(thisDir, '..', '..', 'meta')
31+
return findMetaDir(dirname(fileURLToPath(import.meta.url)))
32+
}
33+
34+
/**
35+
* Resolve the package `meta/` directory by walking up from `startDir`.
36+
*
37+
* The CLI module sits at `src/commands/` in source but is bundled flat into
38+
* `dist/` in the published package, so the depth from the module to the
39+
* package root differs between the two layouts. A hardcoded `..` count is
40+
* correct for only one of them and breaks `setup` / `meta` / `scaffold` in the
41+
* other. Walking up to the first `meta/` directory handles both layouts (and
42+
* symlinked installs such as pnpm) without depending on the build output
43+
* depth. Falls back to the historical `../../meta` resolution if no `meta/`
44+
* is found, so behaviour is never worse than before.
45+
*/
46+
export function findMetaDir(startDir: string): string {
47+
let dir = startDir
48+
// Sanity cap: a package is never this many dirs deep; the root check below
49+
// also stops the walk at the filesystem root.
50+
for (let limit = 0; limit < 10; limit++) {
51+
const candidate = join(dir, 'meta')
52+
// Check it's a directory, not just that something named `meta` exists — a
53+
// stray file named `meta` in an ancestor must not short-circuit the walk.
54+
if (existsSync(candidate) && statSync(candidate).isDirectory())
55+
return candidate
56+
const parent = dirname(dir)
57+
if (parent === dir) break
58+
dir = parent
59+
}
60+
return join(startDir, '..', '..', 'meta')
3361
}
3462

3563
export function getCheckSkillsWorkflowAdvisories(root: string): Array<string> {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
mkdtempSync,
5+
rmSync,
6+
symlinkSync,
7+
writeFileSync,
8+
} from 'node:fs'
9+
import { join } from 'node:path'
10+
import { tmpdir } from 'node:os'
11+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
12+
import { findMetaDir, getMetaDir } from '../src/commands/support.js'
13+
14+
let root: string
15+
16+
beforeEach(() => {
17+
root = mkdtempSync(join(tmpdir(), 'support-test-'))
18+
})
19+
20+
afterEach(() => {
21+
rmSync(root, { recursive: true, force: true })
22+
})
23+
24+
function seedMeta(pkgDir: string): string {
25+
const metaDir = join(pkgDir, 'meta', 'templates', 'workflows')
26+
mkdirSync(metaDir, { recursive: true })
27+
writeFileSync(
28+
join(metaDir, 'check-skills.yml'),
29+
'# intent-workflow-version: 3\n',
30+
)
31+
return join(pkgDir, 'meta')
32+
}
33+
34+
describe('findMetaDir', () => {
35+
it('resolves from the flat dist/ layout used by the published package', () => {
36+
const pkgDir = join(root, 'pkg')
37+
const metaDir = seedMeta(pkgDir)
38+
// Bundled module lives flat at <pkg>/dist/support-*.mjs.
39+
const startDir = join(pkgDir, 'dist')
40+
mkdirSync(startDir, { recursive: true })
41+
expect(findMetaDir(startDir)).toBe(metaDir)
42+
})
43+
44+
it('resolves from the deep src/commands/ layout used in source', () => {
45+
const pkgDir = join(root, 'pkg')
46+
const metaDir = seedMeta(pkgDir)
47+
// Source module lives at <pkg>/src/commands/support.ts (two levels deep).
48+
const startDir = join(pkgDir, 'src', 'commands')
49+
mkdirSync(startDir, { recursive: true })
50+
expect(findMetaDir(startDir)).toBe(metaDir)
51+
})
52+
53+
it('falls back to the historical ../../meta when no meta/ is found', () => {
54+
const startDir = join(root, 'pkg', 'dist')
55+
mkdirSync(startDir, { recursive: true })
56+
expect(findMetaDir(startDir)).toBe(join(startDir, '..', '..', 'meta'))
57+
expect(existsSync(findMetaDir(startDir))).toBe(false)
58+
})
59+
60+
it('skips a stray file named meta and keeps walking', () => {
61+
const pkgDir = join(root, 'pkg')
62+
const metaDir = seedMeta(pkgDir)
63+
const startDir = join(pkgDir, 'dist')
64+
mkdirSync(startDir, { recursive: true })
65+
// A file (not a directory) named `meta` next to startDir must not short-circuit.
66+
writeFileSync(join(startDir, 'meta'), '')
67+
expect(findMetaDir(startDir)).toBe(metaDir)
68+
})
69+
70+
it('resolves through a symlinked install path (pnpm-style)', () => {
71+
// Real package lives in a pnpm-style store; the consumer reaches it via a symlink.
72+
const storePkg = join(
73+
root,
74+
'store',
75+
'@tanstack+intent',
76+
'node_modules',
77+
'@tanstack',
78+
'intent',
79+
)
80+
seedMeta(storePkg)
81+
mkdirSync(join(storePkg, 'dist'), { recursive: true })
82+
83+
const consumerTanstack = join(root, 'consumer', 'node_modules', '@tanstack')
84+
mkdirSync(consumerTanstack, { recursive: true })
85+
symlinkSync(storePkg, join(consumerTanstack, 'intent'), 'dir')
86+
87+
const startDir = join(consumerTanstack, 'intent', 'dist')
88+
const resolved = findMetaDir(startDir)
89+
expect(resolved).toBe(join(consumerTanstack, 'intent', 'meta'))
90+
expect(existsSync(resolved)).toBe(true)
91+
})
92+
})
93+
94+
describe('getMetaDir', () => {
95+
it('resolves to a meta/ directory that ships the workflow template', () => {
96+
const metaDir = getMetaDir()
97+
expect(existsSync(metaDir)).toBe(true)
98+
expect(
99+
existsSync(join(metaDir, 'templates', 'workflows', 'check-skills.yml')),
100+
).toBe(true)
101+
})
102+
})

0 commit comments

Comments
 (0)