craftar 0.17.4 → 0.18.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 +16 -3
- package/dist/cli.js +99 -25
- package/dist/cli.js.map +1 -1
- package/dist/core/impact.js +33 -17
- package/dist/core/impact.js.map +1 -1
- package/dist/core/init-flow.js +502 -0
- package/dist/core/init-flow.js.map +1 -0
- package/dist/core/init.js +90 -20
- package/dist/core/init.js.map +1 -1
- package/dist/core/recipe-edit.js +10 -2
- package/dist/core/recipe-edit.js.map +1 -1
- package/dist/core/resolve.js +6 -1
- package/dist/core/resolve.js.map +1 -1
- package/dist/core/sync.js +31 -13
- package/dist/core/sync.js.map +1 -1
- package/dist/prompt.js +75 -0
- package/dist/prompt.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,12 @@ npx tsx src/cli.ts --help
|
|
|
30
30
|
## Quick start: start an empty workspace from a Forge
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
#
|
|
33
|
+
# In a terminal, craftar init asks for what the flags do not give:
|
|
34
|
+
# the Forge, the profile, then the recipes and the targets to keep or adjust.
|
|
35
|
+
cd C:/Projects/acme-portal-workspace
|
|
36
|
+
craftar init
|
|
37
|
+
|
|
38
|
+
# Or pass everything for scripts and CI:
|
|
34
39
|
craftar init --workspace C:/Projects/acme-portal-workspace \
|
|
35
40
|
--forge C:/Projects/forge \
|
|
36
41
|
--profile acme-portal
|
|
@@ -63,7 +68,7 @@ From then on, change a rule in `forge/ingredients/rules/<name>/rule.md`, run `cr
|
|
|
63
68
|
|
|
64
69
|
| Command | What it does |
|
|
65
70
|
|---|---|
|
|
66
|
-
| `craftar init --forge <dir\|url> --profile <name> [--ref <ref>] [--targets <a,b>] [--add-recipe <name>]... [--remove-recipe <name>]... [--replace] [--no-sync] [--offline] [--workspace .]` | Starts a workspace: writes a `craftar.yaml` holding `forge` (a directory as the path from the workspace, POSIX; a URL as given), `ref` (a remote Forge only), `profile`, `recipes` (only the lists the recipe flags leave non-empty) and `targets` (only with `--targets`; otherwise the workspace follows the profile's), then runs the first `sync` — which also registers the workspace. The file is written only once the configuration loads (a remote Forge is fetched as `sync` fetches it; `--offline` reads the cache), resolves and plans, and the sync applies that same plan. `--remove-recipe` and `--add-recipe` go through `remove recipe` / `add recipe`'s core (removes first, then adds; `--replace` swaps a slot holder); a call whose every name changes nothing prints its reasons as a `note`. `--workspace` is created when missing. Files already on disk and not in a lock are adopted when equal to the plan and skipped as a `collision` otherwise, never overwritten, with one warning pointing at `craftar import`. `--no-sync` writes `craftar.yaml` only and prints what the first `sync` would do. Refused, with nothing written and no directory created: an existing `craftar.yaml`; a `--workspace` that is a file; credentials in `--forge` (the value is never printed); `--ref` with a directory Forge; an unknown target; a recipe both added and removed, or any refusal of `add recipe` / `remove recipe`; a `craftar.local.yaml` that sets `forge` or `profile`, or a key a given flag would write (`ref`, `recipes`, `targets`) — its value would replace the flag's; a Forge, profile or plan that does not load. |
|
|
71
|
+
| `craftar init [--forge <dir\|url>] [--profile <name>] [--ref <ref>] [--targets <a,b>] [--add-recipe <name>]... [--remove-recipe <name>]... [--replace] [--no-sync] [--offline] [--workspace .]` | Starts a workspace: writes a `craftar.yaml` holding `forge` (a directory as the path from the workspace, POSIX; a URL as given), `ref` (a remote Forge only), `profile`, `recipes` (only the lists the recipe flags leave non-empty) and `targets` (only with `--targets`; otherwise the workspace follows the profile's), then runs the first `sync` — which also registers the workspace. **In a terminal** (stdin and stdout both a TTY), `--forge` and `--profile` are optional: what is missing is asked for — the Forge (offering the one last synced on this machine, from the registry), a remote Forge's ref, the profile (from the Forge's list), then the recipes and the targets the profile gives, to keep or adjust; a flag given is never asked for. One confirmation before anything is written, showing what the first sync would do. Ctrl-C, the end of input, or `no` at the confirmation: nothing written, exit 1 (`init cancelled — nothing written`). The run ends with an `again craftar init …` line: the same run as flags, for CI. Off a terminal (a pipe, CI), a missing flag is refused, exit 1, naming the flag; where the terminal does not report a TTY, the flags are required. With both flags given nothing is asked, in a terminal or not. The file is written only once the configuration loads (a remote Forge is fetched as `sync` fetches it; `--offline` reads the cache), resolves and plans, and the sync applies that same plan. `--remove-recipe` and `--add-recipe` go through `remove recipe` / `add recipe`'s core (removes first, then adds; `--replace` swaps a slot holder); a call whose every name changes nothing prints its reasons as a `note`. `--workspace` is created when missing. Files already on disk and not in a lock are adopted when equal to the plan and skipped as a `collision` otherwise, never overwritten, with one warning pointing at `craftar import`. `--no-sync` writes `craftar.yaml` only and prints what the first `sync` would do. Refused, with nothing written and no directory created: an existing `craftar.yaml`; a `--workspace` that is a file, or under one; credentials in `--forge` (the value is never printed); `--ref` with a directory Forge; an unknown target; a recipe both added and removed, or any refusal of `add recipe` / `remove recipe`; a `craftar.local.yaml` that sets `forge` or `profile`, or a key a given flag would write (`ref`, `recipes`, `targets`) — its value would replace the flag's; a Forge, profile or plan that does not load. |
|
|
67
72
|
| `craftar import --from claude-code --forge <dir> --profile <name> [--workspace .] [--write-config]` | Reads `.claude/{rules,agents,commands,skills,scripts,hooks}`, `.mcp.json` and, when present, `.kiro/steering` (for inclusion modes and hand-written steering). Creates ingredients, recipes (`base`, one `stack-*` per scoped rule, `<profile>-steering`) and a profile, or updates them. An ingredient already in the Forge is reused when the base, **rendered** for this profile (its declared defaults, the profile's `params`, the workspace's `overrides.params`), equals the workspace file; failing that, when a unique, proved assignment of the base's declared `{{keys}}` reproduces it, the values are **inferred** into the profile's `params` (one value per key per run; never a change another ingredient would feel). A base with section markers is rendered with the profile's `sections` and the workspace's `overrides.sections` first; when the workspace file differs from it only inside sections, the unique content that reproduces the file byte for byte is **inferred** into the profile's `sections` (tried after params, never combined with them; the report counts lines and never prints the content). An import that writes a section value, or compares a workspace file against a base holding markers, sets `schema: 2` in `craftar.forge.yaml`. A reused base takes the place its `<name>--<profile>` variant held: the profile moves to the shared recipe when that orders the base there, and an owned recipe gets the base in the variant's slot, so the rule order (and `AGENTS.md`) stays as it was. Anything else becomes a `<name>--<profile>` variant that emits under the original name, with the reason, so the workspace still round-trips while you decide what to unify. A shared recipe is never widened for one client: a profile whose ingredients differ from `base` or a `stack-*` gets its own `<recipe>--<profile>`. An existing profile is merged in place (`params`, `sections`, the recipes import owns, missing `targets`; every other field and comment kept, and an empty `{}` / `[]` it does not change stays on its line), as is a recipe the profile owns; `--write-config` merges an existing `craftar.yaml` (`forge`, `profile`, `targets`). The report adds `rendered` (with `sections <names>` when a section value filled the render), `inferred`, `sectioned`, `param <key>: <old> → <new>`, `section <key> <name>: <old> → <new>`, `forge craftar.forge.yaml edited (schema: 2)` and `profile … created\|edited\|unchanged` lines. It refuses, with the Forge untouched, a Forge that does not load, a profile or recipe or `craftar.yaml` that does not round-trip through the YAML writer, a misplaced profile, a recipe import writes whose file and `name` disagree, a workspace override that does not parse, a literal `{{key}}` the profile would render, changing a recipe or variant another profile uses, a workspace file holding a section marker at column 0 (sync would not reproduce it: indent it), a Forge base with malformed section markers, and a `craftar.forge.yaml` it needs to bump to `schema: 2` that does not round-trip (set it by hand). Ingredients holding a secret-like value (tokens, private keys, high-entropy MCP `env`/`args` values) are rejected and listed by location — the value is never printed. A UTF-16 file with a byte-order mark is decoded for the scan; UTF-16 without one is not detected. An ingredient that would not load back into the Forge (a name that is not slug-like, a non-string MCP `env` value) refuses the import, naming its source; so does an ingredient already in the Forge whose `ingredient.yaml` has an unknown key or a YAML syntax error, naming that file. Every read and check runs before the first write, so an import that fails leaves the Forge untouched and says so. |
|
|
68
73
|
| `craftar status` | With a remote Forge, the header's second line names it — `forge <url> @ <ref \| <branch> (default branch)> <commit>`, plus `· lock <commit>` when the Forge moved since the last sync — and `--json` carries `forge: {kind, source, ref, defaultBranch, commit, lockCommit, fetched}` for every Forge. Classifies every file the Forge would produce: `new`, `update`, `unchanged`, `adopt`, `drift`, `collision`, `orphan`, `orphan-drift`. A `.json` file not yet in `craftar.lock` that parses to the planned value with its keys in the same order (it differs only in whitespace, escapes, number spelling or duplicate keys) is `adopt`, and the first `sync` rewrites it with the planned bytes — by design. For `claude-code`'s `.mcp.json` that is two-space JSON keeping the file's BOM and line endings; Kiro's JSON files become CRLF without a BOM, like every Kiro text file. A different key order is a `collision` — except for integer-like keys, which `JSON.parse` reorders. A locked JSON file reformatted by hand is `drift`, not `adopt`. `--json` for tooling. Exits 1 on malformed section markers and on a section marker in an ingredient the workspace resolves while `craftar.forge.yaml` declares `schema: 1` (or none), naming the Forge file and line, and when a rendered file would still hold a marker line, naming the ingredient, file and rendered line — as do `sync`, `diff`, `explain` and `ls`. Warns about a section value that names no ingredient, or no marker of an ingredient the workspace resolves, and about marker lines in an emitted file not every target renders as text (copied as they are). |
|
|
69
74
|
| `craftar sync` | Renders sections (no rendered body holds a marker; an emitted file not every target renders as text keeps its markers, with a warning), then writes the plan and `craftar.lock` (schema 2: the Forge's source, requested `ref` and commit, the resolved recipes and targets — rewritten only when that content changes). With a remote Forge it fetches first and exits 1 when the fetch fails, `--dry-run` and `--check` included. `--dry-run` shows without writing. `--check` exits 1 when anything is out of sync (CI). `--overwrite-drift` regenerates hand-edited files (explicit, never default). A writing `sync` (not `--dry-run`, not `--check`) also records the workspace in `$CRAFTAR_HOME/registry.json` (see *Workspace registry*); a registry it cannot write is a warning, never a failed `sync`. |
|
|
@@ -289,18 +294,26 @@ A writing `sync` records the workspace in `$CRAFTAR_HOME/registry.json` (`~/.cra
|
|
|
289
294
|
- **`CRAFTAR_NO_REGISTRY`** — read the name as "no registry", the way `NO_COLOR` means "no colour". Set to any non-empty value, it turns the registry off: `sync` neither reads nor writes it, and `craftar workspaces` exits 1. An empty value counts as unset. Set it on CI runners and throwaway checkouts, whose directories would only pile up as `missing`.
|
|
290
295
|
- **A registry that cannot be written** (an unwritable `$CRAFTAR_HOME`, a busy lock after 60 s, a file this craftar cannot read) leaves `sync`'s files, lock and exit code as they would be, and adds one warning. Craftar never overwrites a registry it cannot read: repair or remove it yourself.
|
|
291
296
|
- The registry is per machine: paths are native and absolute, and SSH and HTTPS spellings of one remote are two Forges in it, as they are two cache entries.
|
|
297
|
+
- **`craftar init` reads it for the Forge it offers** when it asks: the Forge of the most recent `sync` on this machine, skipping a directory that is gone. The questions never write it — the first `sync` registers the new workspace as any `sync` does. With `CRAFTAR_NO_REGISTRY` set, or a registry it cannot read, no Forge is offered and the question is asked without a default.
|
|
292
298
|
- **The registry also tells a Forge its workspaces**: `forge impact` and `forge unify` read it (never write it); a workspace synced on CI, another machine or under `CRAFTAR_NO_REGISTRY` is not known to them, which is why `--prune-recipes` says so.
|
|
293
299
|
|
|
294
300
|
## Roadmap (from the functional spec)
|
|
295
301
|
|
|
296
302
|
Phase 0 — foundations (done): schema, import, sync/status/diff/explain, claude-code + kiro + agents-md targets, lock, drift, orphans; `forge variants`, `forge diff` and `forge unify` (`take: param`, `take: section`); sections.
|
|
297
303
|
|
|
298
|
-
Phase 1 — client profile (in progress). Shipped: the read-only catalogue (`recipes`, `ingredients`, `targets` with the capability matrix); remote Forge (git URL + `ref`, a per-machine cache, the schema 2 lock) and `craftar cache prune`; 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
|
|
304
|
+
Phase 1 — client profile (in progress). Shipped: the read-only catalogue (`recipes`, `ingredients`, `targets` with the capability matrix); remote Forge (git URL + `ref`, a per-machine cache, the schema 2 lock) and `craftar cache prune`; 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`; an interactive `craftar init` (choose the Forge and the profile). Next: cloning a profile's repositories at init; profile-driven integrations (PM tool → MCP, IDP → MCP).
|
|
299
305
|
|
|
300
306
|
Later: `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.
|
|
301
307
|
|
|
302
308
|
## Upgrading
|
|
303
309
|
|
|
310
|
+
### to 0.18.0
|
|
311
|
+
|
|
312
|
+
- **In a terminal, `craftar init` with `--forge` or `--profile` missing asks instead of failing.** Off a terminal (stdin or stdout is not a TTY) it still exits 1 on stderr, but the line changed: commander's `required option '--profile <name>' not specified` became `--profile is required when craftar init does not run in a terminal — pass it, or run craftar init in a terminal to be asked` (and the `--forge` / both forms). A script matching the old text must match the new.
|
|
313
|
+
- **The check moved.** An existing `craftar.yaml`, a `--workspace` that is a file and a bad flag value are now reported before the missing flag.
|
|
314
|
+
- **`craftar init --workspace <dir>` under a path that is a file** is refused with `cannot use <path> as a workspace: it is not a directory` instead of a raw `ENOTDIR` error.
|
|
315
|
+
- **No byte change**: no emitter, `plan`, `status`, `sync` or lock changed. No workspace sees `update`.
|
|
316
|
+
|
|
304
317
|
### to 0.17.4
|
|
305
318
|
|
|
306
319
|
- **A change confined to bytes that are not valid UTF-8 is now seen.** The content hash decoded every file as UTF-8, which folds each invalid byte into the same replacement character, so two files differing only there hashed the same: `sync` reported a raw-copied file (a script or hook outside the text extensions, a binary in a skill directory) `unchanged` after its Forge copy changed and never wrote it, `forge variants` called such a pair identical, and `forge diff` showed no hunk. A file that is not valid UTF-8 is now hashed as its bytes, with line endings and a leading UTF-8 BOM normalized as for any other file; valid UTF-8 hashes exactly as before.
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import YAML from "yaml";
|
|
|
6
6
|
import { importClaudeCode } from "./importers/claude-code.js";
|
|
7
7
|
import { classifyForge } from "./core/remote.js";
|
|
8
8
|
import { forget, listWorkspaces, prune, register, registryFile } from "./core/registry.js";
|
|
9
|
-
import { forgeWorkspaces, planAll, nextSync, impactOf, concerned, refineRegistryState } from "./core/impact.js";
|
|
9
|
+
import { forgeWorkspaces, planAll, nextSync, impactOf, concerned, refineRegistryState, countStates, NEXT_SYNC_STATES } from "./core/impact.js";
|
|
10
10
|
import { runDoctor } from "./core/doctor.js";
|
|
11
11
|
import { pruneCache } from "./core/cache.js";
|
|
12
12
|
import { resolveHome } from "./core/home-lock.js";
|
|
@@ -22,9 +22,11 @@ import { exists, gitDirty, gitIsRepo, gitUnheld, loadForge } from "./core/forge.
|
|
|
22
22
|
import { fingerprintDir } from "./core/fingerprint.js";
|
|
23
23
|
import { hunkAt, planFrom, applyPlan, writeUnified, checkRecipeCascade, rewriteRecipes, pruneCandidates, pruneRecipes, note, } from "./core/unify.js";
|
|
24
24
|
import { checkParamWrites, writeParamFile } from "./core/param-writes.js";
|
|
25
|
-
import { planInit } from "./core/init.js";
|
|
25
|
+
import { checkInitFlags, initLine, planInit } from "./core/init.js";
|
|
26
|
+
import { askInit, confirmInit, againLine } from "./core/init-flow.js";
|
|
27
|
+
import { readlineIo } from "./prompt.js";
|
|
26
28
|
import { editRecipesText, planRecipeEdit, recipeDiffLine } from "./core/recipe-edit.js";
|
|
27
|
-
import { localKeys } from "./core/workspace-yaml.js";
|
|
29
|
+
import { localKeys, readLocalFile } from "./core/workspace-yaml.js";
|
|
28
30
|
import { HUNK_CLASSES, INGREDIENT_TYPES, UnifyPlanSchema } from "./schema/index.js";
|
|
29
31
|
process.stdout.on("error", (e) => { if (e.code === "EPIPE")
|
|
30
32
|
process.exit(0); });
|
|
@@ -35,7 +37,7 @@ process.stdout.on("error", (e) => { if (e.code === "EPIPE")
|
|
|
35
37
|
*/
|
|
36
38
|
const DIFF_EXIT_CODE_FETCH_MODE = "sync";
|
|
37
39
|
const program = new Command();
|
|
38
|
-
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.
|
|
40
|
+
program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.18.0");
|
|
39
41
|
/* ---------------------------------------------------------------- import */
|
|
40
42
|
program
|
|
41
43
|
.command("import")
|
|
@@ -142,9 +144,9 @@ async function applyAndReport(ws, p, st, lock, opts = {}) {
|
|
|
142
144
|
/* ---------------------------------------------------------------- init */
|
|
143
145
|
program
|
|
144
146
|
.command("init")
|
|
145
|
-
.description("Start a workspace from a Forge and a profile: write a craftar.yaml proved to resolve and plan, then run the first sync; refused when craftar.yaml already exists")
|
|
146
|
-
.
|
|
147
|
-
.
|
|
147
|
+
.description("Start a workspace from a Forge and a profile: write a craftar.yaml proved to resolve and plan, then run the first sync; in a terminal, asks for the Forge and the profile not given as flags; refused when craftar.yaml already exists")
|
|
148
|
+
.option("--forge <dir|url>", "the Forge: a directory (written relative to the workspace) or a git URL; asked for in a terminal when omitted")
|
|
149
|
+
.option("--profile <name>", "the client profile in the Forge; asked for in a terminal when omitted")
|
|
148
150
|
.option("--ref <ref>", "branch, tag or full SHA of a remote Forge")
|
|
149
151
|
.option("--targets <a,b>", "targets to write in craftar.yaml (claude-code, kiro, agents-md); omitted, the workspace follows the profile's")
|
|
150
152
|
.option("--add-recipe <name>", "add a recipe to the profile's (repeatable)", collect, [])
|
|
@@ -153,26 +155,98 @@ program
|
|
|
153
155
|
.option("--no-sync", "write craftar.yaml only, and say what the first sync would do")
|
|
154
156
|
.option("--offline", "use the cached copy of a remote Forge, without fetching", false)
|
|
155
157
|
.option("--workspace <dir>", "workspace root, created when missing", ".")
|
|
156
|
-
.action(async (o) => {
|
|
158
|
+
.action(async (o, cmd) => {
|
|
157
159
|
const root = path.resolve(o.workspace);
|
|
160
|
+
// N1: craftar.yaml already exists
|
|
158
161
|
const n1 = () => fail(`${WORKSPACE_FILE} already exists in ${root} — change recipes with craftar add recipe / remove recipe, or edit it`);
|
|
159
162
|
const file = path.join(root, WORKSPACE_FILE);
|
|
160
163
|
if (await exists(file))
|
|
161
164
|
n1();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
// N9: --workspace is a file, or any ancestor up to an existing path is a file
|
|
166
|
+
{
|
|
167
|
+
let p = root;
|
|
168
|
+
for (;;) {
|
|
169
|
+
const st = await fs.stat(p).catch(() => null);
|
|
170
|
+
if (st) {
|
|
171
|
+
if (!st.isDirectory())
|
|
172
|
+
fail(`cannot use ${p} as a workspace: it is not a directory`);
|
|
173
|
+
break; // found an existing directory
|
|
174
|
+
}
|
|
175
|
+
const parent = path.dirname(p);
|
|
176
|
+
if (parent === p)
|
|
177
|
+
break; // reached filesystem root
|
|
178
|
+
p = parent;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Check the flags that were given (N3, N7, N11, N6)
|
|
182
|
+
checkInitFlags({
|
|
166
183
|
forge: o.forge,
|
|
167
|
-
profile: o.profile,
|
|
168
184
|
ref: o.ref,
|
|
169
185
|
targets: o.targets === undefined ? undefined : o.targets.split(",").map((t) => t.trim()),
|
|
170
186
|
addRecipes: o.addRecipe,
|
|
171
187
|
removeRecipes: o.removeRecipe,
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
});
|
|
189
|
+
// Mode: flags when both given; else interactive when both streams are TTYs; else N2
|
|
190
|
+
const bothGiven = o.forge !== undefined && o.profile !== undefined;
|
|
191
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
192
|
+
if (!bothGiven && !isTTY) {
|
|
193
|
+
// N2: refuse with our message
|
|
194
|
+
if (o.forge === undefined && o.profile === undefined) {
|
|
195
|
+
fail("--forge and --profile are required when craftar init does not run in a terminal — pass them, or run craftar init in a terminal to be asked");
|
|
196
|
+
}
|
|
197
|
+
else if (o.profile === undefined) {
|
|
198
|
+
fail("--profile is required when craftar init does not run in a terminal — pass it, or run craftar init in a terminal to be asked");
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
fail("--forge is required when craftar init does not run in a terminal — pass it, or run craftar init in a terminal to be asked");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Read the local file once (after N1, N9 and flag checks, before any question or planInit)
|
|
205
|
+
const local = await readLocalFile(root);
|
|
206
|
+
let input;
|
|
207
|
+
let answers = null;
|
|
208
|
+
if (!bothGiven) {
|
|
209
|
+
const io = readlineIo(process.stdin, process.stdout, { terminal: true });
|
|
210
|
+
const result = await askInit(root, {
|
|
211
|
+
forge: o.forge,
|
|
212
|
+
profile: o.profile,
|
|
213
|
+
ref: o.ref,
|
|
214
|
+
targets: o.targets === undefined ? undefined : o.targets.split(",").map((t) => t.trim()),
|
|
215
|
+
addRecipes: o.addRecipe,
|
|
216
|
+
removeRecipes: o.removeRecipe,
|
|
217
|
+
replace: o.replace,
|
|
218
|
+
}, local, io, { ...load(o, "sync"), registryOff: registryOff() });
|
|
219
|
+
if (result.kind === "cancelled")
|
|
220
|
+
fail("init cancelled — nothing written");
|
|
221
|
+
input = result.input;
|
|
222
|
+
answers = result.answers;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
input = {
|
|
226
|
+
forge: o.forge,
|
|
227
|
+
profile: o.profile,
|
|
228
|
+
ref: o.ref,
|
|
229
|
+
targets: o.targets === undefined ? undefined : o.targets.split(",").map((t) => t.trim()),
|
|
230
|
+
addRecipes: o.addRecipe,
|
|
231
|
+
removeRecipes: o.removeRecipe,
|
|
232
|
+
replace: o.replace,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const init = await planInit(root, input, { ...load(o, "sync"), local });
|
|
236
|
+
if (answers !== null) {
|
|
237
|
+
const io = readlineIo(process.stdin, process.stdout, { terminal: true });
|
|
238
|
+
const confirm = await confirmInit(init, root, io, { sync: o.sync });
|
|
239
|
+
if (confirm === "cancelled")
|
|
240
|
+
fail("init cancelled — nothing written");
|
|
241
|
+
}
|
|
174
242
|
const { ws, plan: p } = init;
|
|
175
|
-
|
|
243
|
+
const workspaceGiven = cmd.getOptionValueSource("workspace") === "cli";
|
|
244
|
+
const again = () => {
|
|
245
|
+
if (answers !== null) {
|
|
246
|
+
console.log(againLine(answers, { workspace: workspaceGiven ? o.workspace : undefined, sync: o.sync, offline: o.offline }));
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
// Every refusal is above: only now does the directory, and craftar.yaml, come to exist
|
|
176
250
|
await fs.mkdir(root, { recursive: true });
|
|
177
251
|
try {
|
|
178
252
|
await fs.writeFile(file, init.text, { flag: "wx" });
|
|
@@ -182,21 +256,22 @@ program
|
|
|
182
256
|
n1();
|
|
183
257
|
throw e;
|
|
184
258
|
}
|
|
185
|
-
const from = { flag: "", local: ` (from ${LOCAL_FILE})`, profile: " (from the profile)" }[init.targetsFrom];
|
|
186
259
|
console.log(pc.bold(`craftar init — wrote ${WORKSPACE_FILE} in ${root}`));
|
|
187
|
-
console.log(`
|
|
260
|
+
console.log(` ${initLine(init)}`);
|
|
188
261
|
for (const n of init.notes)
|
|
189
262
|
console.log(` note ${n}`);
|
|
190
263
|
if (!o.sync) {
|
|
191
264
|
console.log(nextSyncLine(init.statuses));
|
|
192
265
|
for (const w of p.warnings)
|
|
193
266
|
console.log(` ${pc.yellow("warn")} ${w}`);
|
|
267
|
+
again();
|
|
194
268
|
return;
|
|
195
269
|
}
|
|
196
270
|
const r = await applyAndReport(ws, p, init.statuses, init.lock);
|
|
197
271
|
const collisions = r.skipped.filter((s) => s.state === "collision").length;
|
|
198
272
|
if (collisions)
|
|
199
273
|
console.log(` ${pc.yellow("warn")} ${collisions} file(s) already in the workspace differ from the Forge and were left as they are — to bring them into the Forge, run craftar import`);
|
|
274
|
+
again();
|
|
200
275
|
});
|
|
201
276
|
/* ---------------------------------------------------------------- sync */
|
|
202
277
|
program
|
|
@@ -276,14 +351,13 @@ program
|
|
|
276
351
|
process.exitCode = 1;
|
|
277
352
|
});
|
|
278
353
|
/* ---------------------------------------------------------------- add / remove recipe */
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Spec 22's line: what the next sync would do (also `init --no-sync`, spec 23 §4.4).
|
|
356
|
+
* Uses `countStates` from `src/core/impact.ts`; a new `FileState` does not compile until it is
|
|
357
|
+
* placed in `FILE_STATE_ORDER_MAP` there.
|
|
358
|
+
*/
|
|
283
359
|
function nextSyncLine(st) {
|
|
284
|
-
const counts =
|
|
285
|
-
.filter(([, n]) => n > 0)
|
|
286
|
-
.map(([k, n]) => `${n} ${k}`);
|
|
360
|
+
const counts = countStates(st).map(([k, n]) => `${n} ${k}`);
|
|
287
361
|
return `next sync: ${counts.length ? `${counts.join(", ")} — run \`craftar sync\`` : "nothing to sync"}`;
|
|
288
362
|
}
|
|
289
363
|
function recipeCommand(op) {
|