craftar 0.16.0 → 0.17.0
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 +11 -3
- package/dist/cli.js +89 -1
- package/dist/cli.js.map +1 -1
- package/dist/core/cache.js +226 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/doctor.js +3 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/home-lock.js +25 -4
- package/dist/core/home-lock.js.map +1 -1
- package/dist/core/remote.js +204 -31
- package/dist/core/remote.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Craft, sync and convert AI-coding workspace harnesses — rules, agents, commands, skills, MCP servers — across every client workspace you maintain and every AI coder each team uses.
|
|
4
4
|
|
|
5
|
-
> Status: **phase 0 prototype**. `import`, `sync`, `status`, `diff`, `explain`, `ls` work end-to-end for the `claude-code` and `kiro` targets and are validated byte-for-byte against a real workspace (see *Oracle*). The Forge commands `forge variants`, `forge diff` (with a suggested class per hunk) and `forge unify` (`--take base|variant`, `take: param` to turn a value into a `{{key}}` parameter, and `take: section` to turn a client block into a section) also work, covered by the unit and golden suites rather than the oracle, and `import` is render-aware: importing into an existing Forge reuses a base whose render equals the workspace file, and edits an existing profile and the recipes it owns in place. Ingredient bodies can hold client-specific **sections** (`<!-- craftar:section <name> -->` … `<!-- /craftar:section -->`), overridden by a profile's `sections` and a workspace's `overrides.sections`; import infers each client's content and `forge unify` can extract a section from a variant, and a Forge that uses them declares `schema: 2` (see *Forge layout*). The read-only catalogue — `recipes`, `ingredients` and `targets` with the capability matrix — works too, and a workspace can name its Forge by git URL with an optional `ref`, read from a per-machine cache, with a lock that records the ref, commit, recipes and targets it was generated from (see *Remote Forge*). `add recipe` / `remove recipe` change a workspace's recipe choice in `craftar.yaml`, refusing an edit that would not resolve. A writing `sync` records each workspace in a per-machine registry, and `craftar workspaces` lists them all with their status (see *Workspace registry*). `craftar forge impact` lists the workspaces registered on this machine that read a Forge (by path, by a git remote of the clone, or through another clone of the same remote) with what their next sync would do against it, and `forge unify` plans them before and after its writes, names the ones its warnings concern, and with `--prune-recipes` deletes a recipe left identical to its sibling once every one of them plans the same. `craftar doctor` reports, one line per finding, what is off on this machine and in this workspace. Everything else in the spec (profiles with IDP/PM-tool integrations, local services, `dotnet new` project templates, `craftar ui`, `craftar mcp`) is not built yet.
|
|
5
|
+
> Status: **phase 0 prototype**. `import`, `sync`, `status`, `diff`, `explain`, `ls` work end-to-end for the `claude-code` and `kiro` targets and are validated byte-for-byte against a real workspace (see *Oracle*). The Forge commands `forge variants`, `forge diff` (with a suggested class per hunk) and `forge unify` (`--take base|variant`, `take: param` to turn a value into a `{{key}}` parameter, and `take: section` to turn a client block into a section) also work, covered by the unit and golden suites rather than the oracle, and `import` is render-aware: importing into an existing Forge reuses a base whose render equals the workspace file, and edits an existing profile and the recipes it owns in place. Ingredient bodies can hold client-specific **sections** (`<!-- craftar:section <name> -->` … `<!-- /craftar:section -->`), overridden by a profile's `sections` and a workspace's `overrides.sections`; import infers each client's content and `forge unify` can extract a section from a variant, and a Forge that uses them declares `schema: 2` (see *Forge layout*). The read-only catalogue — `recipes`, `ingredients` and `targets` with the capability matrix — works too, and a workspace can name its Forge by git URL with an optional `ref`, read from a per-machine cache, with a lock that records the ref, commit, recipes and targets it was generated from (see *Remote Forge*). `add recipe` / `remove recipe` change a workspace's recipe choice in `craftar.yaml`, refusing an edit that would not resolve. A writing `sync` records each workspace in a per-machine registry, and `craftar workspaces` lists them all with their status (see *Workspace registry*). `craftar forge impact` lists the workspaces registered on this machine that read a Forge (by path, by a git remote of the clone, or through another clone of the same remote) with what their next sync would do against it, and `forge unify` plans them before and after its writes, names the ones its warnings concern, and with `--prune-recipes` deletes a recipe left identical to its sibling once every one of them plans the same. `craftar doctor` reports, one line per finding, what is off on this machine and in this workspace. `craftar cache prune` removes the cache entries and trees nothing uses. Everything else in the spec (profiles with IDP/PM-tool integrations, local services, `dotnet new` project templates, `craftar ui`, `craftar mcp`) is not built yet.
|
|
6
6
|
|
|
7
7
|
## The idea in one paragraph
|
|
8
8
|
|
|
@@ -72,6 +72,7 @@ From then on, change a rule in `forge/ingredients/rules/<name>/rule.md`, run `cr
|
|
|
72
72
|
| `craftar ls` | Recipes and ingredients resolved for this workspace. |
|
|
73
73
|
| `craftar workspaces [--fetch] [--json]` | Every workspace a writing `sync` registered on this machine, sorted by path: name, path, profile, stack (`slot=recipe`), targets, the Forge, the last sync, and a status computed now — `up to date`, `outdated` (`new`, `update` or `orphan` pending), `drift` (a `drift`, `orphan-drift` or `collision`), `no lock`, `missing` (directory or `craftar.yaml` gone) or `error` (it no longer loads, plans or reads its lock), plus `forge moved` when the Forge's commit differs from the lock's. Read without the network: a remote Forge comes from the cache; `--fetch` fetches first. Each row's warnings print after the table, prefixed with its path. Times are UTC. Writes nothing. Exits 0 whenever the table prints. Takes no `--workspace`. `--json` is a contract: `{registry, fetch, workspaces, warnings}`. |
|
|
74
74
|
| `craftar doctor [--workspace <dir>] [--fetch] [--strict] [--json]` | Checks this machine — Node ≥ 22, `git`, `$CRAFTAR_HOME`, the registry (entries whose workspace is gone), the Forge cache (its size, entries nothing names, a fetch that never completed) — and, when run inside one (the current directory, or `--workspace`), this workspace: the configuration and its Forge (cached, fetched or pinned), whether the workspace sits inside its Forge, the plan's warnings, declared parameters with no value, the `${NAME}` variables a written MCP server expects and this environment lacks (names only, never values), the lock, `status`, and registration. One line per finding — `ok`, `warn` or `error` — each problem with the command that fixes it; each cause is reported once. It only reports: nothing is written to a workspace, a lock or the registry, and the Forge cache is used as every reader uses it (`.used` stamps, a rebuilt tree; under `--fetch`, a fetch and the cleanup of trees unused for 14 days). Read without the network; `--fetch` fetches a remote Forge first. Exits 1 on an `error`, or on a `warn` under `--strict`; an explicit `--workspace` without `craftar.yaml` exits 1, while the current directory without one runs the machine checks only. `--json` is a contract: `{version, workspace, fetch, strict, checks: [{id, scope, level, message, fix}], summary}`. |
|
|
75
|
+
| `craftar cache prune [--dry-run] [--workspace <dir>] [--json]` | Removes the Forge cache entries nothing uses — no registered workspace (`missing` ones included until `craftar workspaces prune`) and not the current workspace — entries whose first fetch never completed (even when named), and trees unused for 14 days. Whole entries are kept with a warning when the registry is off or unreadable, or the current `craftar.yaml` does not read. Deletes by default; `--dry-run` shows. Never touches a workspace, a lock or the registry; no network. One removing prune at a time per `$CRAFTAR_HOME` (`forges/prune.lock`). An explicit `--workspace` without `craftar.yaml` exits 1. `--json` is a contract: `{home, dryRun, removed: [{path, kind, reason, bytes}], kept: [{path, reason, namedBy}], freedBytes, warnings}`. |
|
|
75
76
|
| `craftar workspaces forget <dir>` / `craftar workspaces prune` | `forget` removes one entry (the directory is not touched; an unregistered path exits 1); `prune` removes every `missing` entry and never an `error` one. Nothing else removes an entry. |
|
|
76
77
|
| `craftar recipes [--forge <dir> [--profile <name>] \| --workspace <dir>] [--json]` | Every recipe of the Forge, sorted by name: slot, the parents it extends, the ingredients it lists itself and the profiles that reach it. In a workspace (or with `--forge <dir> --profile <name>`) each recipe says whether it is in use and why — `profile`, `workspace` (`recipes.add`), `extends <child>` — and a recipe `recipes.remove` names reads `removed by this workspace`. A context that does not resolve (two recipes on one slot, an unknown recipe, a cycle) still lists everything, marks nothing and warns. Read-only: it resolves, never plans. `--json` is a contract: `{forge, context, recipes, warnings}`. |
|
|
77
78
|
| `craftar ingredients [--recipe <name>] [--type <type>] [--forge <dir> [--profile <name>] \| --workspace <dir>] [--json]` | Every ingredient of the Forge, grouped by type: output name, targets, the recipes that list it, and whether the context uses it or the workspace disables it; a reference a recipe makes to an ingredient the Forge does not hold is listed under `missing`, never dropped. `--recipe <name>` lists what choosing that recipe brings, with the parents it extends, parents first; two of them on one slot is a warning. `--type` keeps one type. Read-only. `--json` is a contract: `{forge, context, recipe, ingredients, missing, warnings}`. |
|
|
@@ -273,7 +274,7 @@ Copy a workspace with `.claude/` and `.kiro/` into `fixtures/` (keep `local-stac
|
|
|
273
274
|
|
|
274
275
|
`forge:` may be a git URL — `https://…`, `ssh://…`, `file://…`, or the SCP-like `git@host:org/forge.git` (any SSH user) — with an optional `ref`: a branch (followed: its tip at each fetch), a tag or a full 40-hex SHA (fixed); `refs/heads/<x>` / `refs/tags/<x>` when a name is both; no `ref` follows the remote's default branch. Abbreviated SHAs are refused. A `ref` next to a path `forge:` is ignored, with a warning.
|
|
275
276
|
|
|
276
|
-
- **The cache.** Each machine keeps one copy per URL in `$CRAFTAR_HOME/forges/` (`~/.craftar` by default): a bare repository and one tree per resolved commit, never written by Craftar, shared by every workspace on the machine. Each tree holds the commit's bytes on every platform: the host's `core.autocrlf`, `core.eol` and global attributes file do not apply to it; the Forge's own `.gitattributes` does. Trees unused for 14 days are removed after a fetch. The cache and the workspace registry (below) are the two places, both under `$CRAFTAR_HOME`, where Craftar writes outside a workspace and a Forge on its own.
|
|
277
|
+
- **The cache.** Each machine keeps one copy per URL in `$CRAFTAR_HOME/forges/` (`~/.craftar` by default): a bare repository and one tree per resolved commit, never written by Craftar, shared by every workspace on the machine. Each tree holds the commit's bytes on every platform: the host's `core.autocrlf`, `core.eol` and global attributes file do not apply to it; the Forge's own `.gitattributes` does. Trees unused for 14 days are removed after a fetch. `craftar cache prune` removes whole entries nothing uses and old trees on demand (see *Commands*). The cache and the workspace registry (below) are the two places, both under `$CRAFTAR_HOME`, where Craftar writes outside a workspace and a Forge on its own.
|
|
277
278
|
- **The network.** `sync` (with `--check` and `--dry-run`) and `diff --exit-code` fetch first and exit 1 when they cannot; `status`, `diff`, `explain`, `ls`, `recipes`, `ingredients`, `add recipe`, `remove recipe`, `forge variants` and `forge diff` fall back to the cached copy with a warning; `--offline` uses the cached copy on purpose; with no cached copy every one of them exits 1. `targets` never fetches: it reads the cached copy, and without one prints the matrix unmarked, with a warning. A full SHA already cached is not fetched again.
|
|
278
279
|
- **Authentication belongs to git** (credential helper, SSH agent). A URL holding credentials (`https://user:token@…`, a password in any scheme) is refused in `craftar.yaml` and `craftar.local.yaml`, naming the file and never the value. In CI, configure git (e.g. a credential helper or an SSH deploy key), never a token in `forge:`; without a terminal, git does not prompt.
|
|
279
280
|
- **Writing into the Forge** stays local: `import` and `forge unify` need a clone passed as `--forge <dir>`; Craftar never pushes. After importing into the clone, push it for `sync` to see the import.
|
|
@@ -290,11 +291,18 @@ A writing `sync` records the workspace in `$CRAFTAR_HOME/registry.json` (`~/.cra
|
|
|
290
291
|
|
|
291
292
|
## Roadmap (from the functional spec)
|
|
292
293
|
|
|
293
|
-
Phase 0 (this): schema, import, sync/status/diff/explain, claude-code + kiro + agents-md targets, lock, drift, orphans; the read-only catalogue (`recipes`, `ingredients`, `targets` with the capability matrix); remote Forge (git URL + `ref`, a per-machine cache, the schema 2 lock); the workspace registry and `craftar workspaces`; `craftar add recipe` / `remove recipe`; `craftar init` with flags; `craftar doctor`; `craftar forge impact` and `forge unify --prune-recipes`.
|
|
294
|
+
Phase 0 (this): schema, import, sync/status/diff/explain, claude-code + kiro + agents-md targets, lock, drift, orphans; the read-only catalogue (`recipes`, `ingredients`, `targets` with the capability matrix); remote Forge (git URL + `ref`, a per-machine cache, the schema 2 lock); the workspace registry and `craftar workspaces`; `craftar add recipe` / `remove recipe`; `craftar init` with flags; `craftar doctor`; `craftar cache prune`; `craftar forge impact` and `forge unify --prune-recipes`.
|
|
294
295
|
Next: an interactive `craftar init` (choose the Forge and the profile); cloning a profile's repositories at init; profile-driven integrations (PM tool → MCP, IDP → MCP); `service` ingredients with compose fragments (`craftar services up`); `dotnet new` template registration; rulesync bridge for Codex/Kimi/Cursor specifics; `craftar docs validate`; GitHub Action and Azure Pipelines task around `sync --check`; `craftar ui`; `craftar mcp` + Claude Code / Agent Plugin packaging.
|
|
295
296
|
|
|
296
297
|
## Upgrading
|
|
297
298
|
|
|
299
|
+
### to 0.17.0
|
|
300
|
+
|
|
301
|
+
- **New command `craftar cache prune`**: removes the Forge cache entries nothing uses, entries whose first fetch never completed, and trees unused for 14 days. Its `--json` output is a tooling contract.
|
|
302
|
+
- **`craftar doctor`'s `cache` fix is now `craftar cache prune`** (it was "remove `<dir>` by hand (craftar cache prune is planned)"): a value change of `fix` in `doctor --json`. A `~removing-*` directory under `forges/` (an interrupted prune's leftover) is no longer reported or counted as an entry.
|
|
303
|
+
- **A process waiting on a cache entry's lock re-creates the entry** when another process removed it meanwhile, instead of failing with `ENOENT`.
|
|
304
|
+
- **No byte change**: no emitter, `plan`, `status`, `sync` or lock changed. No workspace sees `update`.
|
|
305
|
+
|
|
298
306
|
### to 0.16.0
|
|
299
307
|
|
|
300
308
|
- **New command `craftar forge impact`**: lists every workspace registered on this machine that reads a Forge with what its next sync would do against it. Its `--json` output is a tooling contract.
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { classifyForge } from "./core/remote.js";
|
|
|
8
8
|
import { forget, listWorkspaces, prune, register, registryFile } from "./core/registry.js";
|
|
9
9
|
import { forgeWorkspaces, planAll, nextSync, impactOf, concerned, refineRegistryState } from "./core/impact.js";
|
|
10
10
|
import { runDoctor } from "./core/doctor.js";
|
|
11
|
+
import { pruneCache } from "./core/cache.js";
|
|
11
12
|
import { resolveHome } from "./core/home-lock.js";
|
|
12
13
|
import { loadWorkspace, plan, readLock, status, apply, resolveForge, resolveForgeSource, WORKSPACE_FILE, LOCAL_FILE } from "./core/sync.js";
|
|
13
14
|
import { resolve, sectionKey } from "./core/resolve.js";
|
|
@@ -34,7 +35,7 @@ process.stdout.on("error", (e) => { if (e.code === "EPIPE")
|
|
|
34
35
|
*/
|
|
35
36
|
const DIFF_EXIT_CODE_FETCH_MODE = "sync";
|
|
36
37
|
const program = new Command();
|
|
37
|
-
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.
|
|
38
|
+
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.17.0");
|
|
38
39
|
/* ---------------------------------------------------------------- import */
|
|
39
40
|
program
|
|
40
41
|
.command("import")
|
|
@@ -1527,6 +1528,34 @@ forge
|
|
|
1527
1528
|
console.log(` next: run \`craftar status --workspace <dir>\` in a workspace on profile ${o.profile} to see what moved`);
|
|
1528
1529
|
}
|
|
1529
1530
|
});
|
|
1531
|
+
/* ---------------------------------------------------------------- cache ---------------------------------------------------------------- */
|
|
1532
|
+
const cache = program.command("cache").description("The per-machine Forge cache in $CRAFTAR_HOME/forges/");
|
|
1533
|
+
cache
|
|
1534
|
+
.command("prune")
|
|
1535
|
+
.description("Remove the Forge cache entries nothing uses — no registered workspace and not this one — the entries whose first fetch never completed, and trees unused for 14 days; --dry-run only shows. Never touches a workspace, a lock or the registry; no network")
|
|
1536
|
+
// No commander default: an explicit --workspace without craftar.yaml exits 1, the current directory without one runs (spec 26 §4.1).
|
|
1537
|
+
.option("--dry-run", "show what would be removed; remove nothing", false)
|
|
1538
|
+
.option("--workspace <dir>", "the workspace whose Forge counts as used besides the registry (default: the current directory, when it holds craftar.yaml)")
|
|
1539
|
+
.option("--json", "machine-readable output", false)
|
|
1540
|
+
.action(async (o) => {
|
|
1541
|
+
let workspace = null;
|
|
1542
|
+
if (o.workspace !== undefined) {
|
|
1543
|
+
if (!(await exists(path.join(o.workspace, WORKSPACE_FILE))))
|
|
1544
|
+
fail(`no ${WORKSPACE_FILE} in ${path.resolve(o.workspace)} — run cache prune inside a workspace, or without --workspace`);
|
|
1545
|
+
workspace = o.workspace;
|
|
1546
|
+
}
|
|
1547
|
+
else if (await exists(path.join(process.cwd(), WORKSPACE_FILE)))
|
|
1548
|
+
workspace = process.cwd();
|
|
1549
|
+
const result = await pruneCache(craftarHome(), {
|
|
1550
|
+
dryRun: o.dryRun,
|
|
1551
|
+
workspace,
|
|
1552
|
+
registryOff: registryOff(),
|
|
1553
|
+
});
|
|
1554
|
+
if (o.json)
|
|
1555
|
+
console.log(JSON.stringify(result.report, null, 2));
|
|
1556
|
+
else
|
|
1557
|
+
printPrune(result);
|
|
1558
|
+
});
|
|
1530
1559
|
program.parseAsync().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|
|
1531
1560
|
/* ---------------------------------------------------------------- helpers */
|
|
1532
1561
|
/** The network rule of a command, `--offline`, and `$CRAFTAR_HOME` (spec 13 §4.1, §6.3). */
|
|
@@ -1557,6 +1586,65 @@ function printDoctor(r) {
|
|
|
1557
1586
|
console.log(` ${paint(c.level)} ${c.id.padEnd(13)} ${c.message}${suffix(c)}`);
|
|
1558
1587
|
console.log(`summary: ${r.summary.ok} ok, ${r.summary.warn} warn, ${r.summary.error} error`);
|
|
1559
1588
|
}
|
|
1589
|
+
/** The text report of `craftar cache prune` (spec 26 §4.4) — presentation, not contract. */
|
|
1590
|
+
function printPrune(result) {
|
|
1591
|
+
const mb = (b) => `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
|
1592
|
+
if (result.header === null) {
|
|
1593
|
+
console.log("nothing to prune");
|
|
1594
|
+
for (const w of result.report.warnings)
|
|
1595
|
+
console.log(` ${pc.yellow("warn")} ${w}`);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const { forges, entries, bytes } = result.header;
|
|
1599
|
+
console.log(pc.bold(`craftar cache prune — ${forges} (${entries} ${entries === 1 ? "entry" : "entries"}, ${mb(bytes)})`));
|
|
1600
|
+
// Compute label width: for entries/leftovers, just the key; for trees, `<key>/trees/<first 10 of commit>…`.
|
|
1601
|
+
const labelOf = (r) => {
|
|
1602
|
+
const p = r.path;
|
|
1603
|
+
if ("kind" in r && r.kind === "tree") {
|
|
1604
|
+
// forges/<key>/trees/<commit> → <key>/trees/<commit10>…
|
|
1605
|
+
const parts = p.split("/");
|
|
1606
|
+
return `${parts[1]}/trees/${parts[3].slice(0, 10)}…`;
|
|
1607
|
+
}
|
|
1608
|
+
// forges/<key> or forges/~removing-<...> → last segment
|
|
1609
|
+
return p.split("/").pop();
|
|
1610
|
+
};
|
|
1611
|
+
const whyRemoved = (r) => {
|
|
1612
|
+
if (r.reason === "orphan")
|
|
1613
|
+
return "orphan: nothing names it";
|
|
1614
|
+
if (r.reason === "incomplete")
|
|
1615
|
+
return "incomplete: a fetch never completed";
|
|
1616
|
+
if (r.reason === "unused")
|
|
1617
|
+
return "unused for 14 days or more";
|
|
1618
|
+
return "leftover of an interrupted prune";
|
|
1619
|
+
};
|
|
1620
|
+
const whyKept = (k) => {
|
|
1621
|
+
if (k.reason === "named")
|
|
1622
|
+
return `named by ${k.namedBy.join(", ")}`;
|
|
1623
|
+
if (k.reason === "busy")
|
|
1624
|
+
return "in use";
|
|
1625
|
+
if (k.reason === "held-open")
|
|
1626
|
+
return "held open";
|
|
1627
|
+
return "not checked";
|
|
1628
|
+
};
|
|
1629
|
+
// Compute column widths W (label) and V (why) across removed and kept rows.
|
|
1630
|
+
const allLabels = [...result.report.removed, ...result.report.kept].map(labelOf);
|
|
1631
|
+
const removedWhys = result.report.removed.map(whyRemoved);
|
|
1632
|
+
const keptWhys = result.report.kept.map(whyKept);
|
|
1633
|
+
const W = Math.max(1, ...allLabels.map((l) => l.length));
|
|
1634
|
+
const V = Math.max(1, ...removedWhys.map((w) => w.length), ...keptWhys.map((w) => w.length));
|
|
1635
|
+
const verb = result.report.dryRun ? "would remove" : "removed";
|
|
1636
|
+
for (const r of result.report.removed) {
|
|
1637
|
+
console.log(` ${verb.padEnd(12)} ${labelOf(r).padEnd(W)} ${whyRemoved(r).padEnd(V)} ${mb(r.bytes)}`);
|
|
1638
|
+
}
|
|
1639
|
+
for (const k of result.report.kept) {
|
|
1640
|
+
console.log(` ${"kept".padEnd(12)} ${labelOf(k).padEnd(W)} ${whyKept(k)}`);
|
|
1641
|
+
}
|
|
1642
|
+
for (const w of result.report.warnings) {
|
|
1643
|
+
console.log(` ${pc.yellow("warn")} ${w}`);
|
|
1644
|
+
}
|
|
1645
|
+
const freedVerb = result.report.dryRun ? "would free" : "freed";
|
|
1646
|
+
console.log(`${freedVerb} ${mb(result.report.freedBytes)}`);
|
|
1647
|
+
}
|
|
1560
1648
|
/** `CRAFTAR_NO_REGISTRY` — "no registry", read like NO_COLOR: any non-empty value turns it off (spec 21 §4.1). */
|
|
1561
1649
|
function registryOff() {
|
|
1562
1650
|
return Boolean(process.env.CRAFTAR_NO_REGISTRY);
|