@miroshnik/elves 2026.1019.2 → 2026.1020.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/bin/elves.ts
CHANGED
|
@@ -1686,7 +1686,7 @@ function rewriteCopyRefs(root: string) {
|
|
|
1686
1686
|
* A mention of `.agents` that isn't the thin project's own: the manifest, the shim, `~/.agents` and `$HOME/.agents` of
|
|
1687
1687
|
* the machine aren't one.
|
|
1688
1688
|
*/
|
|
1689
|
-
const COPY_MENTION = /(?<![\w~}-]|~\/|HOME\/|HOME}\/)\.agents(?![\w-])(
|
|
1689
|
+
const COPY_MENTION = /(?<![\w~}-]|~\/|HOME\/|HOME}\/)\.agents(?![\w-])(?!\\?\/(?:elves\\?\.json|slot\\?\.sh)\b)/;
|
|
1690
1690
|
|
|
1691
1691
|
/**
|
|
1692
1692
|
* What still names `.agents` once the copy is gone: exceptions `.agents/**` in linters, formatters and configs aren't
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miroshnik/elves",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1020.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Elves — a development flow with AI agents: shared rules and skills for any agent; install — bunx @miroshnik/elves install -g; the installer and skill scripts — TypeScript under Bun, tests — bun test in tests/",
|
|
6
6
|
"repository": {
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
72
72
|
"knip": "^6.41.0"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "373c60f3e34f9333931d83450b27d1957a4278ba"
|
|
75
75
|
}
|
package/skills/task-env/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ no host port per app, no port search. The scheme and the reasons — epic
|
|
|
16
16
|
"Environment" in Elves (`tests/capabilities/task-env/task-env.md`).
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
bun --no-env-file <skill dir>/scripts/task-env.ts up [--profile <p>]...
|
|
19
|
+
bun --no-env-file <skill dir>/scripts/task-env.ts up [<N>] [--profile <p>]... # the task's stack by the branch, its addresses
|
|
20
20
|
bun --no-env-file <skill dir>/scripts/task-env.ts e2e [--profile <p>]... [-- <command>] # the e2e stack anew, the command with its addresses
|
|
21
21
|
bun --no-env-file <skill dir>/scripts/task-env.ts down [<N>] # the task's stacks with their volumes
|
|
22
22
|
bun --no-env-file <skill dir>/scripts/task-env.ts prune # stacks of the project's closed tasks
|
|
@@ -43,7 +43,7 @@ task's own Compose project, and nothing of it is shared with other tasks.
|
|
|
43
43
|
|
|
44
44
|
| what | value |
|
|
45
45
|
|---|---|
|
|
46
|
-
| task | the number from the branch `[<area>/]<type>/<N>-<slug>`;
|
|
46
|
+
| task | the number from the branch `[<area>/]<type>/<N>-<slug>`; in the middle of a rebase — the branch being rebased (`rebase-merge/head-name`); no number — the main checkout's stack, but never from a linked worktree: there `up` and `down` without a number refuse (give `up <N>` or finish the rebase) |
|
|
47
47
|
| `<project>` | the repository of `origin` (the last part of its URL without `.git`), no remote — the main checkout's directory; lowercase letters, digits and `-`. Not the directory Compose takes by default — a worktree's is random |
|
|
48
48
|
| stack | Compose project `<project>-<N>`, e2e — `<project>-<N>-e2e`, without a task — `<project>`, `<project>-e2e` |
|
|
49
49
|
| profiles | `--profile <name>` (repeated) goes to Compose: a heavy service in `profiles` runs only in a task that asks for it |
|
|
@@ -58,15 +58,17 @@ import { fileURLToPath } from "node:url";
|
|
|
58
58
|
* name, #620).
|
|
59
59
|
*/
|
|
60
60
|
export const HELP: Record<string, string> = {
|
|
61
|
-
up: `task-env up [--profile <p>]...
|
|
61
|
+
up: `task-env up [<N>] [--profile <p>]...
|
|
62
62
|
|
|
63
63
|
Brings the task's stack up — the Compose project <project>-<N> by the branch <type>/<N>-slug, <project> without a
|
|
64
|
-
number
|
|
64
|
+
number (in the middle of a rebase — the branch being rebased; a linked worktree without a task branch is
|
|
65
|
+
refused: give N) — and prints its addresses: env.http services of .agents/elves.json through the proxy, published ports on
|
|
65
66
|
127.0.0.1; the HTTP addresses go to the compose file's interpolation in TASK_ENV_<SERVICE>_URL; --profile — Compose
|
|
66
67
|
profiles. Code 0 — up, 1 — a service failed (its state and log), 2 — not tried.
|
|
67
68
|
|
|
68
69
|
Examples:
|
|
69
70
|
task-env up
|
|
71
|
+
task-env up 42
|
|
70
72
|
task-env up --profile worker`,
|
|
71
73
|
e2e: `task-env e2e [--profile <p>]... [-- <command>...]
|
|
72
74
|
|
|
@@ -547,6 +549,35 @@ const git = (dir: string, ...args: string[]) => {
|
|
|
547
549
|
return r.status === 0 ? r.stdout.trim() : "";
|
|
548
550
|
};
|
|
549
551
|
|
|
552
|
+
/**
|
|
553
|
+
* The checkout's branch: `branch --show-current`, in the middle of a rebase (HEAD is detached, the branch is empty) — the
|
|
554
|
+
* branch being rebased from `rebase-merge/head-name` or `rebase-apply/head-name` of the worktree's git directory (#639).
|
|
555
|
+
*/
|
|
556
|
+
export function currentBranch(root: string): string {
|
|
557
|
+
const now = git(root, "branch", "--show-current");
|
|
558
|
+
if (now) return now;
|
|
559
|
+
for (const dir of ["rebase-merge", "rebase-apply"]) {
|
|
560
|
+
const file = git(root, "rev-parse", "--path-format=absolute", "--git-path", `${dir}/head-name`);
|
|
561
|
+
if (!file || !existsSync(file)) continue;
|
|
562
|
+
const ref = readFileSync(file, "utf8").trim();
|
|
563
|
+
if (ref.startsWith("refs/heads/")) return ref.slice("refs/heads/".length);
|
|
564
|
+
}
|
|
565
|
+
return "";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** A linked worktree, not the main checkout: its git directory differs from the repository's common one. */
|
|
569
|
+
export function isLinkedWorktree(root: string): boolean {
|
|
570
|
+
const dir = git(root, "rev-parse", "--path-format=absolute", "--git-dir");
|
|
571
|
+
const common = git(root, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
572
|
+
return !!dir && !!common && dir !== common;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** A linked worktree without a task branch: the main checkout's stack isn't its — the refusal, otherwise null. */
|
|
576
|
+
function noTaskRefusal(root: string, command: string): string | null {
|
|
577
|
+
if (!isLinkedWorktree(root) || taskBranch(currentBranch(root))) return null;
|
|
578
|
+
return `task-env ${command}: the worktree ${root} has no task branch (HEAD is detached or the branch isn't <type>/<N>-slug), and the main checkout's stack isn't taken from a worktree; finish the rebase or switch to the task branch, or give the number: task-env ${command} <N>`;
|
|
579
|
+
}
|
|
580
|
+
|
|
550
581
|
/**
|
|
551
582
|
* `<project>` of stacks and addresses: the repository of `origin` (the last part of its URL without `.git`), without a
|
|
552
583
|
* remote — the main checkout's directory. Not the directory Compose takes by default: a worktree's is random
|
|
@@ -613,7 +644,7 @@ interface Project {
|
|
|
613
644
|
http: Record<string, number>;
|
|
614
645
|
}
|
|
615
646
|
|
|
616
|
-
function projectOf(cwd: string): Project | string {
|
|
647
|
+
function projectOf(cwd: string, task?: number): Project | string {
|
|
617
648
|
const root = git(cwd, "rev-parse", "--show-toplevel") || cwd;
|
|
618
649
|
const file = COMPOSE_FILES.find((f) => existsSync(path.join(root, f)));
|
|
619
650
|
if (!file) return `no compose file in ${root} (${COMPOSE_FILES.join(", ")}) — the task's stack is the project's Compose`;
|
|
@@ -621,7 +652,7 @@ function projectOf(cwd: string): Project | string {
|
|
|
621
652
|
const http = httpServices(root);
|
|
622
653
|
if (typeof http === "string") return http;
|
|
623
654
|
const files = [file, ...(extra ? [extra] : [])].flatMap((f) => ["-f", f]);
|
|
624
|
-
return { root, files, name: projectName(root), task: taskBranch(
|
|
655
|
+
return { root, files, name: projectName(root), task: task ?? taskBranch(currentBranch(root))?.issue, http };
|
|
625
656
|
}
|
|
626
657
|
|
|
627
658
|
/** `docker compose` in cwd; vars — over the script's environment: Compose takes them into interpolation. */
|
|
@@ -952,11 +983,20 @@ function stackCommand(group: string, args: string[], env: NodeJS.ProcessEnv): nu
|
|
|
952
983
|
// the stacks are found by the Compose project: the task's worktree may be gone already
|
|
953
984
|
const root = git(process.cwd(), "rev-parse", "--show-toplevel") || process.cwd();
|
|
954
985
|
if (group === "prune") return prune(root, env);
|
|
955
|
-
|
|
986
|
+
if (!args[0]) {
|
|
987
|
+
const refusal = noTaskRefusal(root, "down");
|
|
988
|
+
if (refusal) return console.log(refusal), 2;
|
|
989
|
+
}
|
|
990
|
+
return stackDown(root, args[0] ? Number(args[0]) : taskBranch(currentBranch(root))?.issue, env);
|
|
956
991
|
}
|
|
957
|
-
const
|
|
992
|
+
const number = group === "up" && /^\d+$/.test(args[0] ?? "") ? Number(args[0]) : undefined;
|
|
993
|
+
const opts = profilesOf(number === undefined ? args : args.slice(1));
|
|
958
994
|
if (!opts || (group === "up" && opts.rest.length)) return undefined;
|
|
959
|
-
|
|
995
|
+
if (number === undefined) {
|
|
996
|
+
const refusal = noTaskRefusal(git(process.cwd(), "rev-parse", "--show-toplevel") || process.cwd(), group);
|
|
997
|
+
if (refusal) return console.log(refusal), 2;
|
|
998
|
+
}
|
|
999
|
+
const project = projectOf(process.cwd(), number);
|
|
960
1000
|
if (typeof project === "string") return console.log(`task-env ${group}: ${project}`), 2;
|
|
961
1001
|
return group === "up" ? stackUp(project, opts.profiles, env) : stackE2e(project, opts.profiles, opts.rest, env);
|
|
962
1002
|
}
|