diff --git a/.gitignore b/.gitignore index d327da9..9db1adf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,10 @@ # Dependencies node_modules/ -# Build output +# Build output (prod, dev, and self targets each emit to their own directory) **/dist/ +**/dist-dev/ +**/dist-self/ *.tsbuildinfo .turbo/ diff --git a/README.md b/README.md index add0e03..fc0dc02 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,15 @@ The CLI bakes its own invocation string into the skill, command, and recipe content it installs. Three build targets pick that string, all driven by the `TASKLESS_BUILD_TARGET` env var via Vite `define` (same source files, no edits): -| Command | Baked invocation | Use for | -| ----------------- | --------------------------------------- | ------------------------------------------------------------------------------------ | -| `pnpm build` | `npx @taskless/cli` | Production / published builds (default). | -| `pnpm build:dev` | `node /packages/cli/dist/index.js` | Validating this build from **another** repo (absolute path resolves anywhere). | -| `pnpm build:self` | `node packages/cli/dist/index.js` | Dogfooding **in this repo** (path is repo-root-relative; run the CLI from the root). | +Each target also emits to its own directory so the three never overwrite one +another — prod → `dist/`, dev → `dist-dev/`, self → `dist-self/` (all +gitignored): + +| Command | Output dir | Baked invocation | Use for | +| ----------------- | ------------ | ------------------------------------------- | ------------------------------------------------------------------------------------ | +| `pnpm build` | `dist/` | `npx @taskless/cli` | Production / published builds (default). | +| `pnpm build:dev` | `dist-dev/` | `node /packages/cli/dist-dev/index.js` | Validating this build from **another** repo (absolute path resolves anywhere). | +| `pnpm build:self` | `dist-self/` | `node packages/cli/dist-self/index.js` | Dogfooding **in this repo** (path is repo-root-relative; run the CLI from the root). | `pnpm build:self` builds the CLI with the relative invocation and then runs `taskless init --no-interactive` to install into this repo — so `.claude` gets diff --git a/eslint.config.js b/eslint.config.js index 89512db..9eec913 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,6 +8,8 @@ export default tseslint.config( ignores: [ "node_modules/", "**/dist/", + "**/dist-dev/", + "**/dist-self/", "**/*.config.js", "**/*.config.ts", ".lintstagedrc.js", diff --git a/openspec/specs/infrastructure/spec.md b/openspec/specs/infrastructure/spec.md index 185251e..54966a8 100644 --- a/openspec/specs/infrastructure/spec.md +++ b/openspec/specs/infrastructure/spec.md @@ -102,7 +102,7 @@ The repository SHALL have `turbo` as a root devDependency and a `turbo.json` con ### Requirement: Build pipeline runs across all packages -The root `pnpm build` command SHALL invoke `turbo run build`, which runs the `build` script in every workspace package that defines one. Build outputs (`dist/**`) SHALL be cached. +The root `pnpm build` command SHALL invoke `turbo run build`, which runs the `build` script in every workspace package that defines one. Build outputs (`dist/**`, `dist-dev/**`, `dist-self/**`) SHALL be cached. #### Scenario: Root build command runs CLI build diff --git a/package.json b/package.json index dcf6f6a..362aba4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build:dev": "pnpm --filter @taskless/cli build:dev", "build:self": "run-s build:self:compile build:self:install", "build:self:compile": "pnpm --filter @taskless/cli build:self", - "build:self:install": "node packages/cli/dist/index.js init --no-interactive", + "build:self:install": "node packages/cli/dist-self/index.js init --no-interactive", "bump": "run-s bump:version bump:sync", "bump:sync": "tsx scripts/sync-skill-versions.ts", "bump:version": "changeset version", diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index de75579..d4cdd36 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -8,5 +8,5 @@ "noUncheckedIndexedAccess": true }, "include": ["src", "test", "scripts"], - "exclude": ["node_modules", "dist", "test/fixtures"] + "exclude": ["node_modules", "dist", "dist-dev", "dist-self", "test/fixtures"] } diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts index ab285fe..6161c8f 100644 --- a/packages/cli/vite.config.ts +++ b/packages/cli/vite.config.ts @@ -12,18 +12,37 @@ const pkg = JSON.parse( readFileSync(resolve(import.meta.dirname, "package.json"), "utf8") ) as { version: string }; +// Each build target emits to its own directory so prod, dev, and self builds +// never overwrite one another. Keyed by TASKLESS_BUILD_TARGET; anything other +// than "dev"/"self" is treated as prod. +const OUT_DIRS = { + prod: "dist", + dev: "dist-dev", + self: "dist-self", +} as const; + +function resolveBuildTarget(): keyof typeof OUT_DIRS { + const target = process.env.TASKLESS_BUILD_TARGET; + return target === "dev" || target === "self" ? target : "prod"; +} + +function resolveOutDir(): string { + return OUT_DIRS[resolveBuildTarget()]; +} + // The CLI invocation baked into emitted skill/command/recipe content, chosen // by the TASKLESS_BUILD_TARGET env var (see package.json build:dev/build:self): // - prod (default): the published `npx @taskless/cli` // - dev: an absolute path, for validating this build from another repo // - self: a repo-root-relative path, for dogfooding inside this repo +// The dev/self paths point at their own output directory (dist-dev/dist-self). function resolveCliInvocation(): string { - switch (process.env.TASKLESS_BUILD_TARGET) { + switch (resolveBuildTarget()) { case "self": { - return "node packages/cli/dist/index.js"; + return `node packages/cli/${OUT_DIRS.self}/index.js`; } case "dev": { - return `node ${resolve(import.meta.dirname, "dist/index.js")}`; + return `node ${resolve(import.meta.dirname, OUT_DIRS.dev, "index.js")}`; } default: { return "npx @taskless/cli"; @@ -125,7 +144,7 @@ function shebang(): Plugin { writeBundle(options, bundle) { for (const [fileName, chunk] of Object.entries(bundle)) { if (chunk.type === "chunk" && chunk.isEntry) { - const outPath = resolve(options.dir ?? "dist", fileName); + const outPath = resolve(options.dir ?? resolveOutDir(), fileName); chmodSync(outPath, 0o755); } } @@ -141,6 +160,7 @@ export default defineConfig({ }, plugins: [tsconfigPaths(), assertSkillVersions(), shebang()], build: { + outDir: resolveOutDir(), lib: { entry: resolve(import.meta.dirname, "src/index.ts"), formats: ["es"], diff --git a/tsconfig.json b/tsconfig.json index f1e62a1..e959fe0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,5 +7,5 @@ "noImplicitReturns": true, "noUncheckedIndexedAccess": true }, - "exclude": ["node_modules", "dist", "packages"] + "exclude": ["node_modules", "dist", "dist-dev", "dist-self", "packages"] } diff --git a/turbo.json b/turbo.json index d2c2009..7f81a09 100644 --- a/turbo.json +++ b/turbo.json @@ -2,7 +2,7 @@ "$schema": "https://turbo.build/schema.json", "tasks": { "build": { - "outputs": ["dist/**"], + "outputs": ["dist/**", "dist-dev/**", "dist-self/**"], "env": ["TASKLESS_BUILD_TARGET"] }, "test": {