From fc42994158ccbb027feec250931d58ddfa6c9c8f Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Fri, 25 Sep 2026 01:56:08 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feature/Inside-verdict-ignores-gitignore=20?= =?UTF-8?q?=C2=B7=20L-260924-258487=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initializer read any destination inside another repository's work tree as part of that repository, so a project created under a path the enclosing repository ignores got no repository and was under no version control at all. It now asks git whether the enclosing repository ignores the destination, read as the directory it will be, and gives such a project a repository of its own with the pristine commit, while a directory that stays visible keeps the no-nested-repository outcome and says so when every file in it is ignored. Closes L-260924-258487 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014jwcxdPLFmuWiwMg7PKWsH --- ## Summary by cubic Fixes the initializer so a project created at a path another repository ignores gets a repository of its own instead of no version control at all. **Behavior** - `readGit` now asks git whether the enclosing repository ignores the destination, read as the directory it will be (a missing destination is made briefly for the question). - An ignored destination gets a repository on `main` with the pristine commit, and the git line names the repository that ignores it; the identity probe runs at the destination there since the enclosing repository's own configuration does not reach the new one. - A directory the enclosing repository does not ignore still gets no repository even when every file in it is ignored; the git line now says the project is under no version control, and only when nothing under it is tracked. Closes L-260924-258487. Written for commit 5a19c1becf9e5e5ec7cc37c2dc188f5a26b42acc. Summary will update on new commits. Review in cubic --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 6 ++ docs/family.md | 2 +- initializers/cases.json | 79 ++++++++++++++- initializers/js/README.md | 5 +- initializers/js/lib/git.mjs | 148 +++++++++++++++++++++------- initializers/js/lib/main.mjs | 40 +++++--- initializers/js/test/cases.test.mjs | 40 +++++++- initializers/js/test/git.test.mjs | 114 +++++++++++++++++++++ initializers/js/test/run.test.mjs | 56 +++++++++++ 9 files changed, 438 insertions(+), 52 deletions(-) create mode 100644 initializers/js/test/git.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 504e129..a58dea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- **`create-method-app` at a path another repository ignores**: a destination inside another repository's work tree that the repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, now gets a repository of its own on `main` with the pristine commit, and the `git:` line names the repository that ignores it. The initializer used to report such a project as new files in the enclosing repository and make no repository, which left it under no version control at all; a project made that way earlier can be given one with `git init` and a first commit. A directory the repository does not ignore still gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, and the `git:` line now says the project is under no version control instead of calling it new files of that repository. + ## [v0.5.3] - 2026-09-24 ### Changed diff --git a/docs/family.md b/docs/family.md index 44ecc18..5a7ed79 100644 --- a/docs/family.md +++ b/docs/family.md @@ -67,7 +67,7 @@ make -C my-app serve **Each initializer's table of templates is `templates.json` at its root**, which says which templates it serves, the extras each one's `make create` takes, and the command of the other ecosystem's initializer, named when a person asks one for the other's template (`refused: other-ecosystem`). JSON, so the root reads every initializer's table whatever its language: `scripts/initializers.test.mjs` fails when a template of `TEMPLATES` is served by no initializer or by more than one, and when a table's variables are not the `make create` contract's. -**`initializers/cases.json` is the table of cases every initializer executes**: destinations (missing, empty, a lone `.git` with and without history, a lone `.git` with a staged file and no commit, a `.DS_Store`, a file of the user's, spelled `.`, a name with a space), git states (none, inside another work tree, inside a template's checkout, `--no-git`), flags, and the verdict each must print. The npm initializer's suite runs it now, and the Python one will, so the two print the same verdicts for the same situations. +**`initializers/cases.json` is the table of cases every initializer executes**: destinations (missing, empty, a lone `.git` with and without history, a lone `.git` with a staged file and no commit, a `.DS_Store`, a file of the user's, spelled `.`, a name with a space), git states (none, inside another work tree, under a path that work tree ignores or re-includes, in one that ignores every file but no directory, inside a template's checkout, an identity only the enclosing repository holds, `--no-git`), flags, and the verdict each must print. The npm initializer's suite runs it now, and the Python one will, so the two print the same verdicts for the same situations. ## The version and the release diff --git a/initializers/cases.json b/initializers/cases.json index b805478..2fd5b68 100644 --- a/initializers/cases.json +++ b/initializers/cases.json @@ -3,16 +3,16 @@ "legend": { "destination": "What stands at the destination before the run. missing: nothing; empty: an empty directory; git-without-commit: a directory holding only the .git of git init; git-with-history: a directory holding only the .git of a repository with a commit, its files deleted; git-with-staged-file: a directory holding only the .git of git init whose index holds notes.txt, added and then deleted from the directory; ds-store: a directory holding .DS_Store; user-file: a directory holding notes.txt; beneath-a-file: the destination is typed as notes.txt/my-app, notes.txt being a file, so nothing can stand there.", "spelled": "How the destination is typed. name: my-app, from its parent; space: my app, from its parent; dot: ., from inside it (the destination must exist); absent: no directory at all.", - "within": "Where the destination's parent is. nothing: a directory no repository tracks; work-tree: inside the work tree of a repository with a commit and an origin of its own; template-checkout: inside the work tree of a repository whose origin is https://github.com/Pipelex/pipelex-method-apps.git.", + "within": "Where the destination's parent is. nothing: a directory no repository tracks; work-tree: inside the work tree of a repository with a commit and an origin of its own; template-checkout: inside the work tree of a repository whose origin is https://github.com/Pipelex/pipelex-method-apps.git; ignored-parent: under the tmp/ directory of a repository as in work-tree, whose committed .gitignore holds tmp/; ignored-by-name: at the root of a repository as in work-tree, whose committed .gitignore holds the directory-only pattern my-app/, which matches the destination only when it is read as a directory; re-included: under the apps/ directory of a repository as in work-tree, whose committed .gitignore holds apps/* and then !apps/my-app/, so that the destination itself is not ignored when it is read as a directory; ignores-everything: at the root of a repository as in work-tree, whose committed .gitignore holds *, as a home directory kept as a repository often does; ignores-files-only: at the root of a repository as in work-tree, whose committed .gitignore holds * and then !*/, which ignores every file but no directory; contents-ignored: at the root of a repository as in work-tree, whose committed .gitignore holds my-app/* and then !my-app/package.json, which ignores what the destination holds but not the destination itself.", "args": "The arguments after the destination. {other-ecosystem} stands for a template the other ecosystem's initializer serves.", "method": "Whether --method mt_case is passed. Defaults to true.", "key": "Whether PIPELEX_API_KEY is set. Defaults to true.", "tools": "Which of make and git are on the PATH. Defaults to both.", - "git_identity": "Whether git has an identity to commit with. true: in its global configuration; false: none, and git refuses to guess one; under-parent: only through an includeIf \"gitdir:/\" section, git refusing to guess one elsewhere. Defaults to true.", + "git_identity": "Whether git has an identity to commit with. true: in its global configuration; false: none, and git refuses to guess one; under-parent: only through an includeIf \"gitdir:/\" section, git refusing to guess one elsewhere; enclosing-only: only in the enclosing repository's own .git/config, git refusing to guess one elsewhere. Defaults to true.", "make_exit": "The stub make's exit status. Defaults to 0.", "expect.verdict": "The verdict: the last line's first word, or its first two for refused: and failed:.", "expect.written": "Whether the template was written. When false, the destination holds exactly what it held before the run.", - "expect.git": "initialized: the destination is a repository on main with one commit, the pristine one; first-commit: the repository that was there has one commit, the pristine one; none: the destination holds no .git, and the enclosing repository's HEAD did not move; untouched: the repository that was there did not change.", + "expect.git": "initialized: the destination is a repository on main with one commit, the pristine one, and an enclosing repository's HEAD did not move and its git status --porcelain --untracked-files=all prints nothing; first-commit: the repository that was there has one commit, the pristine one; none: the destination holds no .git, and the enclosing repository's HEAD did not move; untouched: the repository that was there did not change.", "expect.make": "Whether make ran.", "expect.forwarded": "Arguments make's argument vector must contain." }, @@ -104,6 +104,79 @@ "git_identity": false, "expect": { "verdict": "created", "written": true, "git": "none", "make": true } }, + { + "name": "under a path the enclosing repository ignores, the project gets a repository of its own", + "destination": "missing", + "within": "ignored-parent", + "expect": { "verdict": "created", "written": true, "git": "initialized", "make": true } + }, + { + "name": "an empty directory under an ignored path", + "destination": "empty", + "within": "ignored-parent", + "expect": { "verdict": "created", "written": true, "git": "initialized", "make": true } + }, + { + "name": "a destination a directory-only pattern ignores is read as a directory before it exists", + "destination": "missing", + "within": "ignored-by-name", + "expect": { "verdict": "created", "written": true, "git": "initialized", "make": true } + }, + { + "name": "an ignored destination spelled .", + "destination": "empty", + "within": "ignored-by-name", + "spelled": "dot", + "expect": { "verdict": "created", "written": true, "git": "initialized", "make": true } + }, + { + "name": "a destination the enclosing repository re-includes is new files in that repository", + "destination": "missing", + "within": "re-included", + "expect": { "verdict": "created", "written": true, "git": "none", "make": true } + }, + { + "name": "a repository that ignores everything gives the project a repository of its own", + "destination": "missing", + "within": "ignores-everything", + "expect": { "verdict": "created", "written": true, "git": "initialized", "make": true } + }, + { + "name": "a directory whose files alone are ignored gets no repository before it exists", + "destination": "missing", + "within": "ignores-files-only", + "expect": { "verdict": "created", "written": true, "git": "none", "make": true } + }, + { + "name": "an empty directory whose files alone are ignored gets no repository", + "destination": "empty", + "within": "ignores-files-only", + "expect": { "verdict": "created", "written": true, "git": "none", "make": true } + }, + { + "name": "a destination whose contents alone are ignored gets no repository before it exists", + "destination": "missing", + "within": "contents-ignored", + "expect": { "verdict": "created", "written": true, "git": "none", "make": true } + }, + { + "name": "an empty destination whose contents alone are ignored gets no repository", + "destination": "empty", + "within": "contents-ignored", + "expect": { "verdict": "created", "written": true, "git": "none", "make": true } + }, + { + "name": "under an ignored path, an identity only the enclosing repository holds is not the new repository's", + "destination": "missing", + "within": "ignored-parent", + "git_identity": "enclosing-only", + "expect": { + "verdict": "refused: no-git-identity", + "written": false, + "git": "none", + "make": false + } + }, { "name": "a template's own checkout is refused", "destination": "missing", diff --git a/initializers/js/README.md b/initializers/js/README.md index 934ef5b..a76aaaf 100644 --- a/initializers/js/README.md +++ b/initializers/js/README.md @@ -46,10 +46,13 @@ The initializer reads git before it writes anything: | A repository with no commit yet (a `git init` by hand, or a clone of an empty GitHub repository) | The pristine commit, as its first | | A repository with no commit whose index already holds a file, added and then deleted | Refused: the commit would record that file beside the template | | A repository with commits, holding only `.git` | Refused: every tracked file would show as deleted, and the commit would record that | +| Inside another repository's work tree, at a path it ignores | A new repository on `main`, then the pristine commit: that repository does not see the project | | Inside another repository's work tree | No repository and no commit: the project is new files in that repository. `--no-create` lets you commit the template there first | | Inside a checkout of `pipelex-method-apps` or of a starter | Refused, `--no-git` included whenever git is on the PATH | -A commit needs a git identity, and a missing one is refused before anything is written. When git shows none outside a repository, the initializer asks again inside a throwaway repository at the destination, which it removes before going on, so an identity given only by an `includeIf "gitdir:…"` section is found. +A path the enclosing repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, is one it does not version, so a project there gets a repository of its own rather than no version control at all. Every source git reads counts: the enclosing repository's `.gitignore` files, its `.git/info/exclude`, and your `core.excludesFile`. What counts is the directory itself, which a missing destination is made for a moment to let git read: a directory the enclosing repository does not ignore gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, since a repository there would still show in the enclosing one's `git status`, and the `git:` line then says the project is under no version control. + +A commit needs a git identity, and a missing one is refused before anything is written. When git shows none outside a repository, the initializer asks again inside a throwaway repository at the destination, which it removes before going on, so an identity given only by an `includeIf "gitdir:…"` section is found. At a path another repository ignores, it always asks inside that throwaway repository, since an identity set in the enclosing repository's own configuration does not reach the new one. The pristine commit reads `Start from Pipelex/pipelex-method-apps/webapp-js ()`, so `make create`'s changes are a diff you can read before committing them. `make create` itself commits nothing. diff --git a/initializers/js/lib/git.mjs b/initializers/js/lib/git.mjs index 63b3727..f015b37 100644 --- a/initializers/js/lib/git.mjs +++ b/initializers/js/lib/git.mjs @@ -5,6 +5,7 @@ * the root of a repository with no commit → the pristine commit, its first * … whose index already holds an entry → refused: repository-has-staged-files * the root of a repository with history → refused: repository-has-history + * where another repository ignores it → a new repository on main, then the pristine commit * inside another repository's work tree → no repository and no commit * inside a checkout of a template → refused: inside-template-checkout * @@ -16,7 +17,15 @@ * inside another one's work tree makes the enclosing one fail `git add -A` * until the nested one has a commit, and then see an embedded repository, so * the project becomes new files in the enclosing repository instead, as - * `create-next-app` does. + * `create-next-app` does. A path the enclosing repository ignores is the + * exception: a repository nested there is as invisible to it as plain files + * would be, and without one the project would be under no version control at + * all, so it gets a repository of its own, as outside every work tree. What + * counts is the directory: one the enclosing repository does not ignore gets + * no repository even when every file in it is ignored, as under a `*` that a + * later pattern lifts from every directory, since a repository there would + * still show in the enclosing one's status, and the report then says the + * project is under no version control. */ import { spawnSync } from "node:child_process"; @@ -66,6 +75,7 @@ export function git(args, { cwd, env }) { * { kind: "outside" } * { kind: "template-checkout", origin } * { kind: "root", history: boolean, staged: string[] } `staged` is read only when there is no history + * { kind: "ignored", toplevel } the repository whose work tree it is in ignores it * { kind: "inside", toplevel } * { kind: "unreadable-git" } the destination holds a `.git` git does not read as its repository */ @@ -88,61 +98,82 @@ export function readGit({ dest, from, destExists, destHasGit, env }) { } if (destHasGit) return { kind: "unreadable-git" }; } + if (ignores({ dest, from, env })) return { kind: "ignored", toplevel: top.stdout }; return { kind: "inside", toplevel: top.stdout }; } -/** The `origin` of the repository `from` stands in when it is a template's own, or null. */ -export function templateOrigin({ from, env }) { - const origin = git(["remote", "get-url", "origin"], { cwd: from, env }); - return origin.status === 0 && TEMPLATE_ORIGINS.test(origin.stdout) ? origin.stdout : null; +/** + * Whether the repository `from` stands in ignores `dest` as a directory, by + * every source git reads: its `.gitignore` files, its `info/exclude` and the + * user's `core.excludesFile`. That is exactly when a repository nested there is + * invisible to it. + * + * Git reads a path as a directory only when one stands there, so a missing + * destination is made for the question and removed after it. A trailing slash + * is no substitute: it makes git test an empty last name as well, which `*` and + * `my-app/*` match, so a `*` that a later pattern lifts from every directory + * would read a directory it re-includes as ignored. For the same reason the + * directory is named from its parent, with no slash, since asked from inside + * it as `./` git tests it with one. The name starts with `./` because git + * reads a leading `:` as pathspec magic, and `--literal-pathspecs` is refused + * by `check-ignore`. Any answer but a plain yes, a directory that cannot be + * made included, reads as not ignored, since mistaking a tracked destination + * for an ignored one would plant a repository in the user's tracked tree. A + * directory that holds a tracked path reads as not ignored whatever the + * patterns say, but a destination the preflight accepts holds none. + */ +function ignores({ dest, from, env }) { + return withDirectories({ dest, from }, false, () => { + const name = `./${path.basename(dest)}`; + return git(["check-ignore", "-q", "--", name], { cwd: path.dirname(dest), env }).status === 0; + }); } -/** Whether git can name an author and a committer, as a commit needs. */ -export function hasIdentity({ cwd, env }) { - return ( - git(["var", "GIT_AUTHOR_IDENT"], { cwd, env }).status === 0 && - git(["var", "GIT_COMMITTER_IDENT"], { cwd, env }).status === 0 - ); +/** + * Whether the repository whose work tree holds `dest` shows none of the files + * written there, every one of them being ignored, though the directory is not. + * An empty status does not say so alone: a project the enclosing repository + * tracks, deleted and then written again as it was, shows nothing either, so + * its index must hold nothing under `dest` as well. + */ +export function ignoresEveryFile({ dest, env }) { + const status = git(["status", "--porcelain", "--untracked-files=all", "--", "."], { + cwd: dest, + env, + }); + if (status.status !== 0 || status.stdout !== "") return false; + const tracked = git(["ls-files", "--", "."], { cwd: dest, env }); + return tracked.status === 0 && tracked.stdout === ""; } /** - * Whether git can name an author and a committer for the commit in the - * repository about to be made at `dest`, which `from`, the destination or its - * nearest existing ancestor, stands outside of. Outside a repository git reads - * no `includeIf "gitdir:…"` section, so an identity given only to the - * repositories under a directory is invisible from `from`. When that reading - * finds none, the question is asked again inside a throwaway repository made - * at `dest`, and what the probe made is removed before the answer is returned. + * Answer with every missing directory from `from`, the destination's nearest + * existing ancestor, down to `dest` made for the call and removed after it. + * Returns what `answer` returns, or `fallback` when a directory cannot be made + * or `answer` throws. * - * It removes only what it made, as the write does. Each missing directory and - * the `.git` are made without `recursive`, so a path that stands there already, - * a dangling symlink the reading took for missing, or one another process - * creates meanwhile fails the probe instead of being taken over and removed. - * The directories go deepest first and only while empty, so what another - * process writes into one meanwhile stays. A probe that cannot be made - * answers no. + * It removes only what it made, as the write does. Each missing directory is + * made without `recursive`, so a path that stands there already, a dangling + * symlink the reading took for missing, or one another process creates + * meanwhile fails the call instead of being taken over and removed. The + * directories go deepest first and only while empty, so what another process + * writes into one meanwhile stays. */ -export function hasIdentityForInit({ dest, from, env }) { - if (hasIdentity({ cwd: from, env })) return true; +function withDirectories({ dest, from }, fallback, answer) { const missing = []; for (let at = dest; at !== from && path.dirname(at) !== at; at = path.dirname(at)) { missing.unshift(at); } - const probe = path.join(dest, ".git"); const made = []; - let probeMade = false; try { for (const dir of missing) { fs.mkdirSync(dir); made.push(dir); } - fs.mkdirSync(probe); - probeMade = true; - return git(["init", "-q"], { cwd: dest, env }).status === 0 && hasIdentity({ cwd: dest, env }); + return answer(); } catch { - return false; + return fallback; } finally { - if (probeMade) fs.rmSync(probe, { recursive: true, force: true }); for (const dir of made.reverse()) { try { fs.rmdirSync(dir); @@ -153,6 +184,55 @@ export function hasIdentityForInit({ dest, from, env }) { } } +/** The `origin` of the repository `from` stands in when it is a template's own, or null. */ +export function templateOrigin({ from, env }) { + const origin = git(["remote", "get-url", "origin"], { cwd: from, env }); + return origin.status === 0 && TEMPLATE_ORIGINS.test(origin.stdout) ? origin.stdout : null; +} + +/** Whether git can name an author and a committer, as a commit needs. */ +export function hasIdentity({ cwd, env }) { + return ( + git(["var", "GIT_AUTHOR_IDENT"], { cwd, env }).status === 0 && + git(["var", "GIT_COMMITTER_IDENT"], { cwd, env }).status === 0 + ); +} + +/** + * Whether git can name an author and a committer for the commit in the + * repository about to be made at `dest`. `from` is the destination or its + * nearest existing ancestor, and `enclosed` says whether it lies in the work + * tree of a repository that ignores the destination. + * + * Outside every repository, an identity git shows from `from` is one the new + * repository will see too, but git reads no `includeIf "gitdir:…"` section + * there, so an identity given only to the repositories under a directory is + * invisible from `from`. When that reading finds none, and always when `from` + * is enclosed, since it would then read the enclosing repository's own + * configuration, which the new repository does not inherit, the question is + * asked inside a throwaway repository made at `dest`, and what the probe made + * is removed before the answer is returned. + * + * The missing directories are made and removed as `withDirectories` says, and + * the `.git` is made without `recursive` too, so one that stands there already + * fails the probe instead of being taken over and removed. A probe that cannot + * be made answers no. + */ +export function hasIdentityForInit({ dest, from, enclosed, env }) { + if (!enclosed && hasIdentity({ cwd: from, env })) return true; + return withDirectories({ dest, from }, false, () => { + const probe = path.join(dest, ".git"); + fs.mkdirSync(probe); + try { + return ( + git(["init", "-q"], { cwd: dest, env }).status === 0 && hasIdentity({ cwd: dest, env }) + ); + } finally { + fs.rmSync(probe, { recursive: true, force: true }); + } + }); +} + /** The pristine commit's message, in the scaffold skill's format. */ export function pristineMessage({ template, version, source }) { return `Start from Pipelex/pipelex-method-apps/${template} ${version} (${source})`; diff --git a/initializers/js/lib/main.mjs b/initializers/js/lib/main.mjs index e4cd80f..a06df00 100644 --- a/initializers/js/lib/main.mjs +++ b/initializers/js/lib/main.mjs @@ -10,14 +10,17 @@ * `git` unless `--no-git`; `--method` and `PIPELEX_API_KEY` unless * `--no-create`, the key tested for presence and never printed; the * destination rule; git's reading of the destination, and an identity when - * a commit will be made. It writes nothing but the throwaway repository in - * which a new repository's identity is read when none shows outside one, - * and removes that before going on. Each failure is a `refused:` verdict - * naming the fix. + * a commit will be made. It writes nothing but a missing destination inside + * another repository's work tree, made so that git reads it as the + * directory it will be, and the throwaway repository in which a new + * repository's identity is read when none shows outside one, or always + * under a path another repository ignores, and removes each before going + * on. Each failure is a `refused:` verdict naming the fix. * 2. **The write**, exclusive, removing what it created when it cannot finish. - * 3. **Git**: a new repository on `main` outside any work tree, then the pristine - * commit; the commit alone at the root of a repository with no commit yet; - * nothing inside another repository's work tree, or with `--no-git`. + * 3. **Git**: a new repository on `main` outside any work tree or where the + * enclosing repository ignores the destination, then the pristine commit; + * the commit alone at the root of a repository with no commit yet; nothing + * inside another repository's work tree, or with `--no-git`. * 4. **`make create`**, unless `--no-create`, streamed or logged. * 5. **The report**: the gesture's warnings, the git outcome, and the verdict * line, last. @@ -36,6 +39,7 @@ import { commitPristine, hasIdentity, hasIdentityForInit, + ignoresEveryFile, pristineByHand, pristineMessage, readGit, @@ -215,13 +219,19 @@ function planGit(args, dest, found, env) { return { commit: false, init: false, + inside: reading.toplevel, line: `git: ${dest} is inside the work tree of ${reading.toplevel}, so no repository was made and nothing was committed; the project is new files in that repository.`, }; + case "ignored": + // The enclosing repository does not version the destination, so the + // project gets a repository of its own, as outside every work tree. + plan = { commit: true, init: true, ignoredBy: reading.toplevel }; + break; default: plan = { commit: true, init: true }; } const identity = plan.init - ? hasIdentityForInit({ dest, from, env }) + ? hasIdentityForInit({ dest, from, enclosed: reading.kind === "ignored", env }) : hasIdentity({ cwd: from, env }); if (!identity) { throw Verdict.refused( @@ -333,6 +343,9 @@ async function create(argv, d, say, state) { // ── Git ── state.phase = "commit"; let gitLine = git.line; + if (git.inside !== undefined && ignoresEveryFile({ dest, env: d.env })) { + gitLine = `git: ${dest} is inside the work tree of ${git.inside}, which ignores every file of the project but not its directory, so no repository was made and the project is under no version control.`; + } if (git.commit) { const message = pristineMessage(pack); try { @@ -343,9 +356,14 @@ async function create(argv, d, say, state) { message, env: d.env, }); - gitLine = git.init - ? `git: made a repository on main and committed the template as ${sha.slice(0, 12)}, "${message}".` - : `git: committed the template as the repository's first commit, ${sha.slice(0, 12)}, "${message}".`; + const committed = `${sha.slice(0, 12)}, "${message}"`; + if (git.ignoredBy !== undefined) { + gitLine = `git: ${git.ignoredBy} ignores ${dest}, so the project got a repository of its own: made on main, with the template committed as ${committed}.`; + } else if (git.init) { + gitLine = `git: made a repository on main and committed the template as ${committed}.`; + } else { + gitLine = `git: committed the template as the repository's first commit, ${committed}.`; + } } catch (error) { for (const line of error.message.split("\n")) say(line); throw Verdict.failed( diff --git a/initializers/js/test/cases.test.mjs b/initializers/js/test/cases.test.mjs index af8bb6b..468eaaa 100644 --- a/initializers/js/test/cases.test.mjs +++ b/initializers/js/test/cases.test.mjs @@ -26,6 +26,22 @@ const TABLE = JSON.parse(fs.readFileSync(new URL("../../cases.json", import.meta /** A template the other ecosystem's initializer serves. */ const OTHER_ECOSYSTEM = "cli-python"; +/** + * For each `within` inside an enclosing repository: what its committed + * `.gitignore` holds, and the directory under its root the destination's + * parent is. + */ +const ENCLOSING = { + "work-tree": { ignores: [], under: "" }, + "template-checkout": { ignores: [], under: "" }, + "ignored-parent": { ignores: ["tmp/"], under: "tmp" }, + "ignored-by-name": { ignores: ["my-app/"], under: "" }, + "re-included": { ignores: ["apps/*", "!apps/my-app/"], under: "apps" }, + "ignores-everything": { ignores: ["*"], under: "" }, + "ignores-files-only": { ignores: ["*", "!*/"], under: "" }, + "contents-ignored": { ignores: ["my-app/*", "!my-app/package.json"], under: "" }, +}; + /** The destination's parent, inside what the case says. */ function within(root, kind) { const env = setupEnv(root); @@ -34,18 +50,24 @@ function within(root, kind) { fs.mkdirSync(parent); return { parent, repo: null }; } + if (!Object.hasOwn(ENCLOSING, kind)) throw new Error(`cases.json: unknown within ${kind}`); + const { ignores, under } = ENCLOSING[kind]; const repo = path.join(root, "theirs"); fs.mkdirSync(repo); git(repo, ["init", "-q", "-b", "main"], env); fs.writeFileSync(path.join(repo, "README.md"), "theirs\n"); - git(repo, ["add", "README.md"], env); + if (ignores.length > 0) + fs.writeFileSync(path.join(repo, ".gitignore"), `${ignores.join("\n")}\n`); + git(repo, ["add", "--force", "-A"], env); git(repo, ["commit", "-q", "-m", "Their first commit"], env); const origin = kind === "template-checkout" ? "https://github.com/Pipelex/pipelex-method-apps.git" : "https://github.com/someone/theirs.git"; git(repo, ["remote", "add", "origin", origin], env); - return { parent: repo, repo }; + const parent = path.join(repo, under); + fs.mkdirSync(parent, { recursive: true }); + return { parent, repo }; } /** Put what the case says at the destination. */ @@ -141,6 +163,12 @@ describe("initializers/cases.json", () => { makeExit: c.make_exit ?? 0, }); if (identity === "under-parent") scopeIdentity(runsIn, parent); + if (identity === "enclosing-only") { + if (repo === null) + throw new Error("cases.json: enclosing-only needs an enclosing repository"); + git(repo, ["config", "user.name", "Enclosing Runner"], env); + git(repo, ["config", "user.email", "enclosing@example.com"], env); + } const { code, output, verdict } = await runInitializer(argv, { cwd: c.spelled === "dot" ? dest : parent, env: runsIn, @@ -181,6 +209,14 @@ describe("initializers/cases.json", () => { "", "the commit holds the whole copy", ); + if (repo !== null) { + assert.equal(headOf(repo, env), enclosingHead, "the enclosing repository did not move"); + assert.equal( + git(repo, ["status", "--porcelain", "--untracked-files=all"], env), + "", + "the enclosing repository sees nothing of the project", + ); + } break; case "first-commit": assert.equal(git(dest, ["rev-list", "--count", "HEAD"], env), "1"); diff --git a/initializers/js/test/git.test.mjs b/initializers/js/test/git.test.mjs new file mode 100644 index 0000000..38b10fd --- /dev/null +++ b/initializers/js/test/git.test.mjs @@ -0,0 +1,114 @@ +// Git's reading of a destination inside another repository's work tree: which +// of its ignore sources count, and the patterns and paths `check-ignore` could +// misread. The family's table of cases covers a `.gitignore`; these cover what +// it does not vary. + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { describe, it } from "node:test"; + +import { nearestExisting } from "../lib/destination.mjs"; +import { readGit } from "../lib/git.mjs"; +import { git, isolatedEnv, setupEnv, tempRoot } from "./support.mjs"; + +/** A repository with a commit, and what the enclosing `.gitignore` holds. */ +function enclosing(gitignore = []) { + const root = tempRoot("create-method-app-git-"); + const env = setupEnv(root); + const repo = path.join(root, "theirs"); + fs.mkdirSync(repo); + git(repo, ["init", "-q", "-b", "main"], env); + fs.writeFileSync(path.join(repo, "README.md"), "theirs\n"); + if (gitignore.length > 0) + fs.writeFileSync(path.join(repo, ".gitignore"), `${gitignore.join("\n")}\n`); + git(repo, ["add", "--force", "-A"], env); + git(repo, ["commit", "-q", "-m", "Their first commit"], env); + return { root, repo, env: isolatedEnv(root) }; +} + +/** Read git at a destination that does not exist yet, as the preflight does. */ +function readMissing(dest, env) { + return readGit({ + dest, + from: nearestExisting(dest), + destExists: false, + destHasGit: false, + env, + }); +} + +/** Read git at an existing empty destination, as the preflight does. */ +function readExisting(dest, env) { + return readGit({ dest, from: dest, destExists: true, destHasGit: false, env }); +} + +describe("readGit inside another repository's work tree", () => { + it("reads a destination ignored only through the repository's info/exclude", () => { + const { repo, env } = enclosing(); + fs.appendFileSync(path.join(repo, ".git", "info", "exclude"), "play/\n"); + fs.mkdirSync(path.join(repo, "play")); + assert.deepEqual(readMissing(path.join(repo, "play", "my-app"), env), { + kind: "ignored", + toplevel: repo, + }); + }); + + it("reads a destination ignored only through the user's core.excludesFile", () => { + const { root, repo, env } = enclosing(); + const excludes = path.join(root, "global-excludes"); + fs.writeFileSync(excludes, "scratch/\n"); + const config = path.join(root, "gitconfig"); + fs.writeFileSync(config, `[core]\n\texcludesFile = ${excludes}\n`); + const reading = readMissing(path.join(repo, "scratch", "deep", "my-app"), { + ...env, + GIT_CONFIG_GLOBAL: config, + }); + assert.deepEqual(reading, { kind: "ignored", toplevel: repo }); + }); + + it("reads a destination whose name starts with a colon literally", () => { + const { repo, env } = enclosing(["/:odd/"]); + assert.deepEqual(readMissing(path.join(repo, ":odd"), env), { + kind: "ignored", + toplevel: repo, + }); + }); + + it("reads an existing empty directory the repository ignores", () => { + const { repo, env } = enclosing(["my-app/"]); + const dest = path.join(repo, "my-app"); + fs.mkdirSync(dest); + assert.deepEqual(readExisting(dest, env), { kind: "ignored", toplevel: repo }); + }); + + it("reads a directory ignored with everything else by *", () => { + const { repo, env } = enclosing(["*"]); + assert.deepEqual(readMissing(path.join(repo, "work", "my-app"), env), { + kind: "ignored", + toplevel: repo, + }); + }); + + for (const [what, patterns] of [ + ["* then !*/, which ignores its files alone", ["*", "!*/"]], + ["my-app/*, which ignores what it holds alone", ["my-app/*", "!my-app/package.json"]], + ]) { + it(`reads a directory under ${what} as inside, whether or not it exists`, () => { + const { repo, env } = enclosing(patterns); + const dest = path.join(repo, "my-app"); + assert.deepEqual(readMissing(dest, env), { kind: "inside", toplevel: repo }); + assert.equal(fs.existsSync(dest), false, "the directory made for the reading is removed"); + fs.mkdirSync(dest); + assert.deepEqual(readExisting(dest, env), { kind: "inside", toplevel: repo }); + }); + } + + it("reads a destination under a tracked path as inside", () => { + const { repo, env } = enclosing(["*.log"]); + assert.deepEqual(readMissing(path.join(repo, "src", "my-app"), env), { + kind: "inside", + toplevel: repo, + }); + }); +}); diff --git a/initializers/js/test/run.test.mjs b/initializers/js/test/run.test.mjs index f9fc605..f7465ab 100644 --- a/initializers/js/test/run.test.mjs +++ b/initializers/js/test/run.test.mjs @@ -357,6 +357,62 @@ describe("a run", () => { ); }); + it("names the repository that ignores the destination in its git outcome", async () => { + const { root, work } = workspace(); + const env = runEnv(root); + git(work, ["init", "-q"], env); + fs.writeFileSync(path.join(work, ".gitignore"), "tmp/\n"); + fs.mkdirSync(path.join(work, "tmp")); + const { output, verdict } = await runInitializer(["tmp/app", "--no-create"], { + cwd: work, + env, + }); + assert.equal(verdict, "copied", output); + const said = output.trimEnd().split("\n").at(-2); + const expected = `git: ${work} ignores ${path.join(work, "tmp", "app")}, so the project got a repository of its own: made on main, with the template committed as `; + assert.ok(said.startsWith(expected), said); + assert.match( + said, + /as [0-9a-f]{12}, "Start from Pipelex\/pipelex-method-apps\/webapp-js .*"\.$/, + ); + }); + + it("says a project is under no version control when its enclosing repository ignores every file of it", async () => { + const { root, work } = workspace(); + const env = runEnv(root); + git(work, ["init", "-q"], env); + fs.writeFileSync(path.join(work, ".gitignore"), "*\n!*/\n"); + const { output, verdict } = await runInitializer(["app", "--no-create"], { cwd: work, env }); + assert.equal(verdict, "copied", output); + const dest = path.join(work, "app"); + assert.equal( + output.trimEnd().split("\n").at(-2), + `git: ${dest} is inside the work tree of ${work}, which ignores every file of the project but not its directory, so no repository was made and the project is under no version control.`, + ); + assert.equal(fs.existsSync(path.join(dest, ".git")), false); + }); + + it("does not say a project is under no version control when its enclosing repository tracks it", async () => { + const { root, work } = workspace(); + const env = runEnv(root); + git(work, ["init", "-q"], env); + const first = await runInitializer(["app", "--no-create"], { cwd: work, env }); + assert.equal(first.verdict, "copied", first.output); + git(work, ["add", "-A"], env); + git(work, ["commit", "-q", "-m", "Their copy of the project"], env); + const dest = path.join(work, "app"); + fs.rmSync(dest, { recursive: true }); + const { output, verdict } = await runInitializer(["app", "--no-create"], { cwd: work, env }); + assert.equal(verdict, "copied", output); + assert.equal(git(work, ["status", "--porcelain"], env), "", "the copy is what was tracked"); + const said = output.trimEnd().split("\n").at(-2); + assert.ok( + said.startsWith(`git: ${dest} is inside the work tree of ${work}, so no repository was made`), + said, + ); + assert.doesNotMatch(said, /under no version control/); + }); + it("names the copy and the next make create after --no-create", async () => { const { root, work } = workspace(); const { output } = await runInitializer(["my app", "--no-create", "--title", "Bob's app"], { From da4edad38edffbdad53d17724e0034bc7591559e Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Fri, 25 Sep 2026 02:06:37 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feature/Symlinked-destination-wrong-repo=20?= =?UTF-8?q?=C2=B7=20L-260924-c4e3df=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initializer asked the repository a symlinked destination sits in whether it ignores it, while every other reading followed the link into the repository its target is in, so the first one's ignores could plant a repository in the second one's tracked tree. It now asks git about the destination where it physically is, and a test pairs a link ignored where it sits with a target that is not ignored. Closes L-260924-c4e3df 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014jwcxdPLFmuWiwMg7PKWsH --- ## Summary by cubic Fixes ignore detection for symlinked destinations during initialization. The initializer asked the repository a symlinked destination sits in whether it ignores it, while every other reading followed the link into the repository its target is in, letting the first one's ignores plant a repository in the second one's tracked tree. It now reads the link's target and asks the repository it points into, and a test pairs a link ignored where it sits with a target that is not. Closes L-260924-c4e3df. Written for commit 6a4ceb41d3d347416a5a8d795019272cc20d70f6. Summary will update on new commits. Review in cubic --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 2 +- initializers/js/README.md | 2 +- initializers/js/lib/git.mjs | 19 ++++++++++++------- initializers/js/test/git.test.mjs | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a58dea2..b34911f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- **`create-method-app` at a path another repository ignores**: a destination inside another repository's work tree that the repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, now gets a repository of its own on `main` with the pristine commit, and the `git:` line names the repository that ignores it. The initializer used to report such a project as new files in the enclosing repository and make no repository, which left it under no version control at all; a project made that way earlier can be given one with `git init` and a first commit. A directory the repository does not ignore still gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, and the `git:` line now says the project is under no version control instead of calling it new files of that repository. +- **`create-method-app` at a path another repository ignores**: a destination inside another repository's work tree that the repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, now gets a repository of its own on `main` with the pristine commit, and the `git:` line names the repository that ignores it. The initializer used to report such a project as new files in the enclosing repository and make no repository, which left it under no version control at all; a project made that way earlier can be given one with `git init` and a first commit. A directory the repository does not ignore still gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, and the `git:` line now says the project is under no version control instead of calling it new files of that repository. A destination that is a symlink is judged by the repository its target is in, not the one the link sits in. ## [v0.5.3] - 2026-09-24 diff --git a/initializers/js/README.md b/initializers/js/README.md index a76aaaf..5fb485c 100644 --- a/initializers/js/README.md +++ b/initializers/js/README.md @@ -50,7 +50,7 @@ The initializer reads git before it writes anything: | Inside another repository's work tree | No repository and no commit: the project is new files in that repository. `--no-create` lets you commit the template there first | | Inside a checkout of `pipelex-method-apps` or of a starter | Refused, `--no-git` included whenever git is on the PATH | -A path the enclosing repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, is one it does not version, so a project there gets a repository of its own rather than no version control at all. Every source git reads counts: the enclosing repository's `.gitignore` files, its `.git/info/exclude`, and your `core.excludesFile`. What counts is the directory itself, which a missing destination is made for a moment to let git read: a directory the enclosing repository does not ignore gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, since a repository there would still show in the enclosing one's `git status`, and the `git:` line then says the project is under no version control. +A path the enclosing repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, is one it does not version, so a project there gets a repository of its own rather than no version control at all. Every source git reads counts: the enclosing repository's `.gitignore` files, its `.git/info/exclude`, and your `core.excludesFile`. What counts is the directory itself, which a missing destination is made for a moment to let git read: a directory the enclosing repository does not ignore gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, since a repository there would still show in the enclosing one's `git status`, and the `git:` line then says the project is under no version control. A destination that is a symlink is read where it points: the repository whose work tree holds its target is the one whose ignores count. A commit needs a git identity, and a missing one is refused before anything is written. When git shows none outside a repository, the initializer asks again inside a throwaway repository at the destination, which it removes before going on, so an identity given only by an `includeIf "gitdir:…"` section is found. At a path another repository ignores, it always asks inside that throwaway repository, since an identity set in the enclosing repository's own configuration does not reach the new one. diff --git a/initializers/js/lib/git.mjs b/initializers/js/lib/git.mjs index f015b37..10335b6 100644 --- a/initializers/js/lib/git.mjs +++ b/initializers/js/lib/git.mjs @@ -116,16 +116,21 @@ export function readGit({ dest, from, destExists, destHasGit, env }) { * directory is named from its parent, with no slash, since asked from inside * it as `./` git tests it with one. The name starts with `./` because git * reads a leading `:` as pathspec magic, and `--literal-pathspecs` is refused - * by `check-ignore`. Any answer but a plain yes, a directory that cannot be - * made included, reads as not ignored, since mistaking a tracked destination - * for an ignored one would plant a repository in the user's tracked tree. A - * directory that holds a tracked path reads as not ignored whatever the - * patterns say, but a destination the preflight accepts holds none. + * by `check-ignore`. The directory is named where it physically is: a + * destination that is a symlink sits in one repository's work tree and points + * into another's, the one every other reading of the destination sees, and + * that one is the repository whose ignores count. Any answer but a plain yes, a + * directory that cannot be made or resolved included, reads as not ignored, + * since mistaking a tracked destination for an ignored one would plant a + * repository in the user's tracked tree. A directory that holds a tracked path + * reads as not ignored whatever the patterns say, whether or not its files are + * still on disk. */ function ignores({ dest, from, env }) { return withDirectories({ dest, from }, false, () => { - const name = `./${path.basename(dest)}`; - return git(["check-ignore", "-q", "--", name], { cwd: path.dirname(dest), env }).status === 0; + const real = fs.realpathSync(dest); + const name = `./${path.basename(real)}`; + return git(["check-ignore", "-q", "--", name], { cwd: path.dirname(real), env }).status === 0; }); } diff --git a/initializers/js/test/git.test.mjs b/initializers/js/test/git.test.mjs index 38b10fd..b795e5f 100644 --- a/initializers/js/test/git.test.mjs +++ b/initializers/js/test/git.test.mjs @@ -104,6 +104,24 @@ describe("readGit inside another repository's work tree", () => { }); } + it("reads a symlinked destination by the ignores of the repository it points into", () => { + const { root, repo, env } = enclosing(["link"]); + const setup = setupEnv(root); + const theirs = path.join(root, "other"); + fs.mkdirSync(theirs); + git(theirs, ["init", "-q", "-b", "main"], setup); + fs.writeFileSync(path.join(theirs, "README.md"), "other\n"); + git(theirs, ["add", "-A"], setup); + git(theirs, ["commit", "-q", "-m", "Other first commit"], setup); + const target = path.join(theirs, "apps", "new"); + fs.mkdirSync(target, { recursive: true }); + const link = path.join(repo, "link"); + fs.symlinkSync(target, link); + assert.deepEqual(readExisting(link, env), { kind: "inside", toplevel: theirs }); + fs.writeFileSync(path.join(theirs, ".gitignore"), "apps/new/\n"); + assert.deepEqual(readExisting(link, env), { kind: "ignored", toplevel: theirs }); + }); + it("reads a destination under a tracked path as inside", () => { const { repo, env } = enclosing(["*.log"]); assert.deepEqual(readMissing(path.join(repo, "src", "my-app"), env), { From 8c7b3559647d21fe8af88cf9c956cf5d38314a62 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Fri, 25 Sep 2026 03:08:30 +0200 Subject: [PATCH 3/4] =?UTF-8?q?feature/Bump-sdk-0251=20=C2=B7=20L-260925-8?= =?UTF-8?q?37eac=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the web app template from \`@pipelex/sdk\` ^0.24.0 to ^0.25.1 and re-locks it, with the root changelog entry. The one breaking entry in between, 0.25.0's removal of the Pipelex Gateway key calls, reaches neither the template nor any action the scaffold writes, and the hook fix and the documentation fix in the same range reach nothing here either. \`make all\` passed, and one \`/rev\` pass at profile 1 was clean. Closes L-260925-837eac 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014jwcxdPLFmuWiwMg7PKWsH --- ## Summary by cubic Bumps the webapp template's `@pipelex/sdk` dependency to 0.25.1 and re-locks it. The 0.25.0 breaking change (removing the Pipelex Gateway key calls) doesn't affect the template or anything the scaffold writes, so projects behave unchanged. `make all` passes. Written for commit 43393a1951abedcf33de26edc1e4a32233c13378. Summary will update on new commits. Review in cubic Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ webapp-js/package-lock.json | 8 ++++---- webapp-js/package.json | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b34911f..65092ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- **The web app template runs on `@pipelex/sdk` 0.25.1**: the SDK no longer offers the Pipelex Gateway key calls (`createGatewayApiKey`, `getGatewayApiKey`), which neither the template nor any action the scaffold writes ever made, so nothing changes for a project. The app keeps calling the hosted API with a Pipelex API key, which this release leaves untouched. + ### Fixed - **`create-method-app` at a path another repository ignores**: a destination inside another repository's work tree that the repository ignores, such as a `tmp/` its `.gitignore` lists or anywhere under a home directory kept as a repository that ignores `*`, now gets a repository of its own on `main` with the pristine commit, and the `git:` line names the repository that ignores it. The initializer used to report such a project as new files in the enclosing repository and make no repository, which left it under no version control at all; a project made that way earlier can be given one with `git init` and a first commit. A directory the repository does not ignore still gets no repository even when every file in it is ignored, as under `*` followed by `!*/`, and the `git:` line now says the project is under no version control instead of calling it new files of that repository. A destination that is a symlink is judged by the repository its target is in, not the one the link sits in. diff --git a/webapp-js/package-lock.json b/webapp-js/package-lock.json index 2591c08..05cdcdb 100644 --- a/webapp-js/package-lock.json +++ b/webapp-js/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.3", "dependencies": { "@pipelex/mthds-form": "^0.10.0", - "@pipelex/sdk": "^0.24.0", + "@pipelex/sdk": "^0.25.1", "next": "^16.3.5", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -1928,9 +1928,9 @@ "license": "MIT" }, "node_modules/@pipelex/sdk": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@pipelex/sdk/-/sdk-0.24.0.tgz", - "integrity": "sha512-NvZlly7NpmMHWXEW2nTIUN8RR4D5mTgT7YtJaEzxnenh9BeFANelK7eunCxEdnJ9zJ8EcN3gjJw1pxHrPJiHqQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@pipelex/sdk/-/sdk-0.25.1.tgz", + "integrity": "sha512-QjSx2/UAjrowvrspcNUhMTmOmiHkWqlB0duF/Xwl4fZynV+KbTX+Dub1W53zC9SmbEAEIOqwLvABxoxJTvCq6A==", "license": "MIT", "dependencies": { "mthds": "^0.25.0", diff --git a/webapp-js/package.json b/webapp-js/package.json index 1703bb8..6c19087 100644 --- a/webapp-js/package.json +++ b/webapp-js/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@pipelex/mthds-form": "^0.10.0", - "@pipelex/sdk": "^0.24.0", + "@pipelex/sdk": "^0.25.1", "next": "^16.3.5", "react": "^19.2.4", "react-dom": "^19.2.4", From daf3ca1d5e205f1471ddba3cbfa03ea0d46fab12 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Fri, 25 Sep 2026 03:17:33 +0200 Subject: [PATCH 4/4] Release v0.5.4 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014jwcxdPLFmuWiwMg7PKWsH --- CHANGELOG.md | 2 +- VERSION | 2 +- initializers/js/package.json | 2 +- webapp-js/package-lock.json | 4 ++-- webapp-js/package.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65092ea..92cfb4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [v0.5.4] - 2026-09-25 ### Changed diff --git a/VERSION b/VERSION index be14282..7d85683 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.3 +0.5.4 diff --git a/initializers/js/package.json b/initializers/js/package.json index 27ee0a6..bf73bca 100644 --- a/initializers/js/package.json +++ b/initializers/js/package.json @@ -1,6 +1,6 @@ { "name": "@pipelex/create-method-app", - "version": "0.5.3", + "version": "0.5.4", "description": "Start a Pipelex method app: npm create @pipelex/method-app@latest my-app -- --method ", "keywords": [ "pipelex", diff --git a/webapp-js/package-lock.json b/webapp-js/package-lock.json index 05cdcdb..361e2c8 100644 --- a/webapp-js/package-lock.json +++ b/webapp-js/package-lock.json @@ -1,12 +1,12 @@ { "name": "pipelex-method-webapp-js", - "version": "0.5.3", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pipelex-method-webapp-js", - "version": "0.5.3", + "version": "0.5.4", "dependencies": { "@pipelex/mthds-form": "^0.10.0", "@pipelex/sdk": "^0.25.1", diff --git a/webapp-js/package.json b/webapp-js/package.json index 6c19087..848d0b4 100644 --- a/webapp-js/package.json +++ b/webapp-js/package.json @@ -1,6 +1,6 @@ { "name": "pipelex-method-webapp-js", - "version": "0.5.3", + "version": "0.5.4", "private": true, "description": "A Next.js app that runs MTHDS methods through the Pipelex API, with each method's input form and result view generated from its own contract.", "scripts": {