craftar 0.28.0 → 0.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/dist/cli.js +5 -9
- package/dist/cli.js.map +1 -1
- package/dist/core/cache.js +25 -4
- package/dist/core/cache.js.map +1 -1
- package/dist/core/doctor.js +5 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/init-flow.js +8 -2
- package/dist/core/init-flow.js.map +1 -1
- package/dist/core/proc.js +31 -8
- package/dist/core/proc.js.map +1 -1
- package/dist/core/registry.js +28 -4
- package/dist/core/registry.js.map +1 -1
- package/dist/core/remote.js +37 -9
- package/dist/core/remote.js.map +1 -1
- package/dist/core/services.js +18 -11
- package/dist/core/services.js.map +1 -1
- package/dist/core/sync.js +3 -3
- package/dist/core/sync.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -372,7 +372,7 @@ They are generated files like any other: `status`, `diff`, `sync --check` and `-
|
|
|
372
372
|
|
|
373
373
|
**The plan fails** when a fragment is not a YAML mapping, declares no service, declares a top-level `name` (the project name is craftar's) or `include` (every file of the stack is in the lock) — also when they arrive through a YAML merge key (`<<`): a fragment is checked as Compose reads it; when a service's `extends` names a `file` (it may extend a service of the same fragment), or when two fragments declare the same key under `services`, `volumes`, `networks`, `configs` or `secrets` — Compose would keep one silently. A file not listed in `binary` must be UTF-8 text. **It warns** when two fragments publish the same literal host port, and when a rendered file holds a secret-like value (the kind and the line, never the value): keep values out of the Forge and use `${NAME}`.
|
|
374
374
|
|
|
375
|
-
**Running it.** `craftar services up` runs `docker compose up -d` on the stack, `services down` stops it and `services status` shows it (see *Commands*). They need Docker with its Compose plugin, found on `PATH
|
|
375
|
+
**Running it.** `craftar services up` runs `docker compose up -d` on the stack, `services down` stops it and `services status` shows it (see *Commands*). They need Docker with its Compose plugin, found on `PATH` by the lookup that finds `dotnet` and `git` (see *Project templates*); `up` needs Compose **2.20.0 or later** (the first with `include`), while `down` and `status` work on an older one. `sync` needs no Docker. `up` reads the lock, not the Forge: it comes up on what the last `sync` wrote, and refuses a file that is missing or edited rather than run something the lock does not record. Nothing runs Docker implicitly — not `sync`, not `init`, and `doctor` only asks for the version. craftar adds no policy over what a fragment declares: a service runs with your Docker rights, as the hooks and scripts of a Forge run with yours.
|
|
376
376
|
|
|
377
377
|
- **The project** is `craftar-<slug>-<hash>`: the workspace directory's name, lowercased, every character outside `a-z 0-9 _ -` replaced by `-`, cut at 40, then the first 8 hex digits of the SHA-256 of the workspace's real path. It is computed, never written, so the files under `.craftar/` are the same on every machine and two checkouts are two projects. `services status` prints it. Moving a workspace changes it: stop the old one with `docker compose -p <old name> down`. A name that leaves nothing but dashes gives `craftar--<hash>`.
|
|
378
378
|
- **`${CRAFTAR_WORKSPACE}`** is set by `craftar services` to the workspace's real path, so a fragment can mount a repository of the workspace.
|
|
@@ -453,7 +453,7 @@ A `kind` is a lowercase name (`dotnet-new`); a package is a NuGet ID, optionally
|
|
|
453
453
|
- **The store is the user's** — one per machine user, shared by every workspace; the last install of a package ID wins. Remove one with `dotnet new uninstall <package>`.
|
|
454
454
|
- **Feeds are additive** — a declared `source` is added to the feeds `dotnet` already uses, so an ID nobody reserved on a public feed can be served from there. Pin it with the team's own `NuGet.Config` at the workspace root (craftar looks there only, and generates none). `install` and `doctor` warn when an entry declares a `source` and the root has no `NuGet.Config`.
|
|
455
455
|
- **Private feeds** — authentication is NuGet's (`NuGet.Config`, a credential provider, `--interactive`); craftar holds no token, and a `source` carrying a user or a password is refused and never printed. A `source` is printed as written by `list`, `--dry-run` and `install`: craftar refuses a user or a password in it, not a token in its path or query — keep those out of the profile.
|
|
456
|
-
- **`dotnet` comes from `PATH`** — craftar looks for it itself, in the absolute entries of `PATH` only (on Windows, a `dotnet.exe` or `dotnet.com` in an entry with a drive or a UNC host), and never in the workspace root unless `PATH` names it: a `dotnet` committed in a repository is not run by `install` or by `doctor`'s probe, which both run there. An empty or relative entry, an unset `PATH` and a `.cmd` / `.bat` shim are not used. The same lookup finds `git`.
|
|
456
|
+
- **`dotnet` comes from `PATH`** — craftar looks for it itself, in the absolute entries of `PATH` only (on Windows, a `dotnet.exe` or `dotnet.com` in an entry with a drive or a UNC host), and never in the workspace root unless `PATH` names it: a `dotnet` committed in a repository is not run by `install` or by `doctor`'s probe, which both run there. An empty or relative entry, an unset `PATH` and a `.cmd` / `.bat` shim are not used, and neither is a `node_modules/.bin` of the directory craftar is started in or of a directory above it — the entries `npx` and `npm` put first. The same lookup finds `git` and `docker`.
|
|
457
457
|
- **Unknown kind** — an entry of a kind this craftar cannot install (`npm-create`, `nx`) is listed, skipped and warned; `install` then exits 1 (`--dry-run` still exits 0).
|
|
458
458
|
|
|
459
459
|
## Roadmap (from the functional spec)
|
|
@@ -468,6 +468,21 @@ Later: rulesync bridge for Codex/Kimi/Cursor specifics; `craftar docs validate`;
|
|
|
468
468
|
|
|
469
469
|
## Upgrading
|
|
470
470
|
|
|
471
|
+
### to 0.28.2
|
|
472
|
+
|
|
473
|
+
- **A program is no longer looked for in a `node_modules/.bin` of the current directory or of a directory above it.** `npx craftar` — and an `npm` script — puts those entries first on `PATH`, as absolute ones, so a `git`, a `dotnet` or a `docker` committed there was the program craftar ran. They are now skipped, in the lookup and in the `PATH` the program gets, so what it starts in turn (`ssh`, a credential helper) is not found there either. The current directory is the one craftar is started in, whatever `--workspace` names. The entries are compared as written — `.`, `..` and repeated separators resolved, no link followed, without case on Windows. A `node_modules/.bin` anywhere else on `PATH` (npx's own cache, a global prefix) is looked in as before.
|
|
474
|
+
- **`docker` is now found as `git` and `dotnet` are** (see *to 0.27.2*): in the absolute entries of `PATH` only, never in the directory craftar runs in; on Windows only `docker.exe` / `docker.com`, in an entry with a drive or a UNC host — a `docker.cmd` shim is not a program. `docker` gets the same reduced `PATH` (what `docker` then does with it when it starts a credential helper was not measured). This holds for `craftar services up | down | status` and for the `docker` check of `craftar doctor`.
|
|
475
|
+
- **A program reachable only that way is no longer found**, and craftar then behaves as on a machine without it: for `git`, what *to 0.27.2* describes — a path Forge whose `git` is no longer found is read without its commit, and its next writing `sync` rewrites `craftar.lock` once with `forge.commit: null`; for `dotnet`, `craftar templates install` and `craftar doctor` say the .NET SDK is not available; for `docker`, `craftar services up` and `down` stop with `docker not found — install Docker with the Compose plugin 2.20.0 or later`, `services status` warns the same, shows every declared service as `not created` and exits 1 (`docker.available` is `false` under `--json`), and `doctor` warns `docker not found`. A toolchain that lives only in such a `node_modules/.bin` has to be installed in another directory that `PATH` names: an entry written by hand for that same `node_modules/.bin` is skipped like the one `npx` adds.
|
|
476
|
+
- **No generated file changes**, and no lock changes for a workspace whose programs are found as before.
|
|
477
|
+
|
|
478
|
+
### to 0.28.1
|
|
479
|
+
|
|
480
|
+
- **A Forge URL with credentials that an earlier release recorded is no longer printed from the registry.** A release before `0.27.1` could record such a URL (see *to 0.27.1*). For a `missing` or an `error` row whose recorded URL holds credentials, `craftar workspaces --json` now gives `(a URL with credentials — not shown)` as `forge.source` and `null` as `forge.key`. `craftar cache prune` — its lines and, under `--json`, `kept[].path`, `removed[].path` and `warnings` — and the `cache` check of `craftar doctor` put the same label where the name of the cache entry such a row names would be: `path` then reads `forges/(a URL with credentials — not shown)`, `forges/(a URL with credentials — not shown)/trees/<commit>` or `forges/~removing-(a URL with credentials — not shown)-<pid>`, which is not a path, and two such entries read the same; a failure either command reports with a path inside such an entry names it the same way. No `--json` key is added, removed or renamed, and no type changes: `forge.key` could already be `null` (a path Forge that cannot be resolved); a `remote` row can now carry it too. A row that loads gives what `craftar.yaml` holds, as before.
|
|
481
|
+
- **A cache entry that no registered workspace names any more is still printed by its name.** Once every workspace that named the URL has synced again without the credentials, the registry no longer records it, and nothing in the cache says which entry was named after a refused URL. `craftar doctor` (`<name> — nothing names it`, `<name> — a fetch never completed`) and `craftar cache prune` (the line of the entry it removes or keeps, of a tree of one it keeps, and a warning about it) then print that name, which holds the user and the password. With `CRAFTAR_NO_REGISTRY` set or a registry that cannot be read, `cache prune` names every entry that way, and `doctor` every one whose fetch never completed. Run `craftar cache prune` right after the re-sync, in a terminal rather than where the output is kept (a CI log, a file) — and rotate the credential either way.
|
|
482
|
+
- **A `forge:` that holds credentials behind a `\` in place of the `//` is refused** — `https:\\user:token@host\…`, or any run of `/` and `\` after the scheme that holds a `\`. It was read as a path, and `Forge not found at …` printed it whole. It is now refused wherever a URL with credentials is (`… › forge holds credentials in the URL — remove them and let git authenticate`): `craftar.yaml` and `craftar.local.yaml`, `init --forge` and its questions, `import --write-config`, a remote of a Forge clone. As with `://`, what counts is any user on `http(s)` and a `user:password` on another scheme, and `file:` is left alone. A drive path (`C:\…`) is read as before — a name there cannot hold the `:` of a `user:password` — and so is such a spelling with no user and no password.
|
|
483
|
+
- **A `ref:` that holds a URL with credentials is not quoted.** The messages that name a ref — `ref "…" is ignored: the Forge is a path`, `ref "…" not found in <url>`, `ref "…" is both a branch and a tag` (which then gives no `write refs/heads/…` hint) and `… the cached copy cannot resolve …` — read `ref (a URL with credentials — not shown)` for such a value, as the whole of it or inside it. The interactive `craftar init` says `Ref: (a URL with credentials — not shown) (from craftar.local.yaml)` for one it reads there, and refuses one typed as an answer (`the answer holds credentials in a URL — a ref is a branch, a tag or a full SHA`), where it offered it back as the next default. Every other ref is printed as before.
|
|
484
|
+
- **No byte change**: no emitter changed, no lock changes and no workspace sees `update`. No exit code changes for a workspace whose `forge:` is still accepted. In one whose `forge:` is spelled with the `\` and holds credentials, the commands that load it failed before (`Forge not found at …`) and fail now with the refusal; `craftar import --write-config`, which replaced the value, now exits 1 with the Forge untouched, where it exited 0; `craftar cache prune` run there, or with `--workspace` naming it, keeps every whole entry (`unchecked`: the checked `craftar.yaml` does not read) where it pruned; and the `cache` check of `craftar doctor` there no longer reports an orphan — its line, or lines, end `(orphans not checked: the checked craftar.yaml does not read)`.
|
|
485
|
+
|
|
471
486
|
### to 0.28.0
|
|
472
487
|
|
|
473
488
|
- **A Forge can declare `schema: 4`**, which includes 3 and 2. Every craftar before `0.28.0` refuses such a Forge at `craftar.forge.yaml` — `0.27.0` to `0.27.2`, which read a Forge at `schema: 3`, included — so none can write a templated server with its braces. Upgrade every machine and CI that reads the Forge before setting it. Nothing changes for a Forge at 1, 2 or 3.
|
|
@@ -479,15 +494,15 @@ Later: rulesync bridge for Codex/Kimi/Cursor specifics; `craftar docs validate`;
|
|
|
479
494
|
|
|
480
495
|
### to 0.27.2
|
|
481
496
|
|
|
482
|
-
- **`git` is looked for in the absolute entries of `PATH` only** — never in the directory craftar runs in, which is a workspace or a Forge, unless `PATH` names that directory by an absolute entry: a `git` committed in a repository is not run through an empty or a relative entry. `npx craftar` is the case to know: npm puts `<cwd>/node_modules/.bin`, and every ancestor's, first on `PATH` as absolute entries, so a `git` committed there is still run, as before. An empty entry (a leading or a trailing `:`, `::`), a relative one (`.`, `bin`, an unexpanded `~/bin`) and an unset `PATH` are not looked in. On Windows only `git.exe` / `git.com` in an entry with a drive or a UNC host count.
|
|
497
|
+
- **`git` is looked for in the absolute entries of `PATH` only** — never in the directory craftar runs in, which is a workspace or a Forge, unless `PATH` names that directory by an absolute entry: a `git` committed in a repository is not run through an empty or a relative entry. `npx craftar` is the case to know: npm puts `<cwd>/node_modules/.bin`, and every ancestor's, first on `PATH` as absolute entries, so a `git` committed there is still run, as before. (Since `0.28.2` those entries are skipped: see *to 0.28.2*.) An empty entry (a leading or a trailing `:`, `::`), a relative one (`.`, `bin`, an unexpanded `~/bin`) and an unset `PATH` are not looked in. On Windows only `git.exe` / `git.com` in an entry with a drive or a UNC host count.
|
|
483
498
|
- **A `git` reachable only that way is no longer found**, and craftar then behaves as on a machine without git: `craftar doctor` says `git not found` (an `error` when the workspace names a remote Forge, a `warn` otherwise); a remote Forge fails; `forge unify` (a `--save-plan` run aside), `forge promote` and `drift promote` refuse, since git is their undo; `forge impact` no longer matches a workspace through a clone's remotes; and a path Forge is read without its commit — headers read `no git`, and `craftar workspaces` cannot say whether that Forge moved.
|
|
484
499
|
- **No generated file changes; `craftar.lock` does, once, for a path Forge whose `git` is no longer found**: the next writing `sync` rewrites it with `forge.commit: null`, and the machine's registry records the same; `status` and `sync --check` stay as they were, and the commit comes back with the first writing `sync` that finds `git` again. No lock changes for a workspace whose `git` is found as before.
|
|
485
|
-
- **`git` and `dotnet` get a `PATH` of absolute entries only** — every `git` craftar runs, and `dotnet` for `craftar templates install` and for `doctor`'s probe; `docker` is started as before — so what they start in turn (`ssh`, a credential helper) is not looked for in the working directory either (measured on Linux for the `ssh` that `git` starts; on Windows what `git` itself searches was not measured, and what `dotnet` starts was not measured on either). By the same filter, a helper, a `git-remote-*` or `git-lfs` that lives only on a relative `PATH` entry — an unexpanded `~/bin` is one — is no longer on the `PATH` that `git` searches (not measured).
|
|
500
|
+
- **`git` and `dotnet` get a `PATH` of absolute entries only** — every `git` craftar runs, and `dotnet` for `craftar templates install` and for `doctor`'s probe; `docker` is started as before (until `0.28.2`) — so what they start in turn (`ssh`, a credential helper) is not looked for in the working directory either (measured on Linux for the `ssh` that `git` starts; on Windows what `git` itself searches was not measured, and what `dotnet` starts was not measured on either). By the same filter, a helper, a `git-remote-*` or `git-lfs` that lives only on a relative `PATH` entry — an unexpanded `~/bin` is one — is no longer on the `PATH` that `git` searches (not measured).
|
|
486
501
|
|
|
487
502
|
### to 0.27.1
|
|
488
503
|
|
|
489
504
|
- **A URL whose credentials sit behind an empty authority is refused, like any other URL holding credentials.** `https:///user:token@host/…` — extra slashes after the scheme, or a `\` among them — passed the check, though a client reads past them and uses the user and the password: curl past up to three slashes, a WHATWG URL parser past a `\` too. It is now refused wherever a URL with credentials is: `forge:` in `craftar.yaml` and `craftar.local.yaml` (`… › forge holds credentials in the URL — remove them and let git authenticate`), `init --forge` and its questions, and a remote of a Forge clone, which `forge impact` then does not match. For a profile's template `source` only the spelling with a `\` changes (`a source must not carry a user or a password`); the one with slashes was refused already. Every scheme but `file` is read that way; a URL with no user and no password behind the extra slashes (`https:///host/…`) is still accepted, and so is `ssh:///git@host/…`.
|
|
490
|
-
- **If a workspace named its Forge that way, the credential is already written down — rotate it.** craftar recorded the URL as given in `craftar.lock` (`forge.source`; the lock is a committed file) and in `$CRAFTAR_HOME/registry.json`, and the cache entry under `$CRAFTAR_HOME/forges/` carries it in its name; a template `source` spelled with the `\` was printed by `templates list` and `templates install`. Take the credential out of the URL, let git (or NuGet) authenticate, and `sync` again in every workspace that named it: the lock and the registry then record the new URL, and `craftar cache prune` removes the entry of the old one once no registered workspace names it (a `missing` one counts until `craftar workspaces prune`). Until then `craftar workspaces`, `craftar doctor` and `craftar cache prune` can still print what was recorded — the URL, or the name of the cache entry.
|
|
505
|
+
- **If a workspace named its Forge that way, the credential is already written down — rotate it.** craftar recorded the URL as given in `craftar.lock` (`forge.source`; the lock is a committed file) and in `$CRAFTAR_HOME/registry.json`, and the cache entry under `$CRAFTAR_HOME/forges/` carries it in its name; a template `source` spelled with the `\` was printed by `templates list` and `templates install`. Take the credential out of the URL, let git (or NuGet) authenticate, and `sync` again in every workspace that named it: the lock and the registry then record the new URL, and `craftar cache prune` removes the entry of the old one once no registered workspace names it (a `missing` one counts until `craftar workspaces prune`). Until then `craftar workspaces`, `craftar doctor` and `craftar cache prune` can still print what was recorded — the URL, or the name of the cache entry. Since `0.28.1` they print a fixed label for what the registry still records (see *to 0.28.1*).
|
|
491
506
|
- **No byte change**: no emitter changed, and no lock changes for a workspace whose `forge:` is still accepted. No workspace sees `update`; no exit code changes for a workspace whose `forge:` and whose profile's template `source`s are still accepted, and no `--json` key changes.
|
|
492
507
|
|
|
493
508
|
### to 0.27.0
|
package/dist/cli.js
CHANGED
|
@@ -37,7 +37,6 @@ import { localKeys, readLocalFile } from "./core/workspace-yaml.js";
|
|
|
37
37
|
import { HUNK_CLASSES, INGREDIENT_TYPES, UnifyPlanSchema } from "./schema/index.js";
|
|
38
38
|
import { EXAMPLE_SETTINGS } from "./emitters/shared.js";
|
|
39
39
|
import { YamlSyntaxError, parseYamlText } from "./core/yaml-read.js";
|
|
40
|
-
import { spawn } from "node:child_process";
|
|
41
40
|
import { recordedServices, pendingLine, upRefusal, composeArgv, downArgv, readPs, dockerVersion, dockerFault, dockerRunner, projectName, statusReport } from "./core/services.js";
|
|
42
41
|
import { errorText } from "./core/schema-fault.js";
|
|
43
42
|
import { declaredTemplates, DOTNET_MISSING, DOTNET_MISSING_FIX, dotnetVersion, feedMessage, installReport, installTemplates, profileLabel, templateRows, unpinnedSources } from "./core/templates.js";
|
|
@@ -51,7 +50,7 @@ process.stdout.on("error", (e) => { if (e.code === "EPIPE")
|
|
|
51
50
|
*/
|
|
52
51
|
const DIFF_EXIT_CODE_FETCH_MODE = "sync";
|
|
53
52
|
const program = new Command();
|
|
54
|
-
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.28.
|
|
53
|
+
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.28.2");
|
|
55
54
|
const SAVE_PLAN_GATE = { what: "plan", who: "unify", outside: "save plans outside the Forge", never: "unify never overwrites" };
|
|
56
55
|
const REPORT_GATE = { what: "report", who: "import", outside: "write the report outside the Forge", never: "import never overwrites" };
|
|
57
56
|
program
|
|
@@ -2256,13 +2255,10 @@ async function envFileOrNull(root) {
|
|
|
2256
2255
|
const env = path.join(root, ".env");
|
|
2257
2256
|
return (await exists(env)) ? env : null;
|
|
2258
2257
|
}
|
|
2259
|
-
/** Runs `docker compose` with the terminal's own streams and resolves with its exit code. No shell. */
|
|
2260
|
-
function runCompose(argv, real) {
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
child.on("error", reject);
|
|
2264
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
2265
|
-
});
|
|
2258
|
+
/** Runs `docker compose` with the terminal's own streams and resolves with its exit code. No shell; `docker` is found as git and dotnet are. */
|
|
2259
|
+
async function runCompose(argv, real) {
|
|
2260
|
+
const { code } = await spawnInherited({ file: "docker", args: argv, cwd: process.cwd(), env: { ...process.env, CRAFTAR_WORKSPACE: real }, stdin: "inherit" });
|
|
2261
|
+
return code ?? 1;
|
|
2266
2262
|
}
|
|
2267
2263
|
services
|
|
2268
2264
|
.command("up")
|