@zenera/cli 1.1.0 → 1.1.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/dist/commands/check.js +47 -14
- package/dist/commands/sandbox.js +226 -22
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +9 -2
- package/dist/image.d.ts +16 -0
- package/dist/image.js +85 -0
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/main.js +0 -0
- package/dist/podman.d.ts +57 -1
- package/dist/podman.js +177 -12
- package/dist/projects.d.ts +18 -0
- package/dist/projects.js +60 -1
- package/dist/sandbox.d.ts +12 -1
- package/dist/sandbox.js +35 -6
- package/dist/scaffold.d.ts +6 -0
- package/dist/scaffold.js +49 -11
- package/dist/term.d.ts +2 -0
- package/dist/term.js +14 -0
- package/dist/validate.d.ts +20 -3
- package/dist/validate.js +306 -11
- package/package.json +2 -2
- package/templates/.github/copilot-instructions.md +154 -42
- package/templates/.github/prompts/new-skill.prompt.md +13 -6
- package/templates/sandbox/Dockerfile +19 -0
|
@@ -133,11 +133,15 @@ my-project/
|
|
|
133
133
|
│ │ └── adjuster.md
|
|
134
134
|
│ └── skills/
|
|
135
135
|
│ ├── house_style/
|
|
136
|
-
│ │ ├── SKILL.md
|
|
137
|
-
│ │ └── examples.md
|
|
136
|
+
│ │ ├── SKILL.md the instructions
|
|
137
|
+
│ │ └── examples.md its own files, reachable at /skills/house_style
|
|
138
138
|
│ ├── water_damage/
|
|
139
139
|
│ │ └── SKILL.md
|
|
140
|
-
│ └──
|
|
140
|
+
│ └── refund_policy/
|
|
141
|
+
│ ├── SKILL.md
|
|
142
|
+
│ └── scripts/calculate.py run at /skills/refund_policy/scripts — §3.4.1
|
|
143
|
+
├── assets/ reference material, read-only at /assets
|
|
144
|
+
├── sandbox/Dockerfile the image commands run in — §3.7
|
|
141
145
|
└── sessions/ run state, memory, whatever the agent wrote
|
|
142
146
|
```
|
|
143
147
|
|
|
@@ -240,6 +244,7 @@ model: fast # fallback for agents that do not pin their own
|
|
|
240
244
|
embeddings: {} # named vectorisers — §3.1.1
|
|
241
245
|
embedding: small # the one `AgentProject.embedder()` returns when asked for no name
|
|
242
246
|
skills: agents/skills # one directory, or a list
|
|
247
|
+
assets: assets # reference material, read-only at /assets — §3.11
|
|
243
248
|
|
|
244
249
|
agents: # the only required key; at least one entry
|
|
245
250
|
- name: intake
|
|
@@ -465,18 +470,24 @@ yet.
|
|
|
465
470
|
|
|
466
471
|
### 3.4 Skills
|
|
467
472
|
|
|
468
|
-
A skill is curated, reusable instruction content — plus
|
|
469
|
-
**on demand** instead of permanently occupying the system prompt.
|
|
470
|
-
|
|
473
|
+
A skill is curated, reusable instruction content — plus the files and tools it
|
|
474
|
+
needs — loaded **on demand** instead of permanently occupying the system prompt.
|
|
475
|
+
One layout, and only one:
|
|
471
476
|
|
|
472
477
|
```
|
|
473
|
-
agents/skills/refund_policy.md
|
|
474
|
-
agents/skills/refund_policy/
|
|
478
|
+
agents/skills/refund_policy/SKILL.md the instructions
|
|
479
|
+
agents/skills/refund_policy/rates.csv whatever they refer to
|
|
475
480
|
```
|
|
476
481
|
|
|
482
|
+
The folder name is the skill name. A bare `agents/skills/refund_policy.md` is
|
|
483
|
+
**not a skill location**: `zen check` reports it as `skill.flat`, and the fix is
|
|
484
|
+
to move it to `refund_policy/SKILL.md`. A skill with nowhere to put a table, an
|
|
485
|
+
example or a script can only ever be prose, which is the half of the idea that
|
|
486
|
+
does not need a file.
|
|
487
|
+
|
|
477
488
|
Frontmatter is a deliberately small subset of YAML — `key: value`, plus `[a, b]`
|
|
478
489
|
flow lists for `tags` and `tools`. **Every key is optional**: `name` defaults to
|
|
479
|
-
the
|
|
490
|
+
the folder name, `description` to the first non-empty line of the body.
|
|
480
491
|
|
|
481
492
|
```markdown
|
|
482
493
|
---
|
|
@@ -510,8 +521,58 @@ skill is needed_, not as a title:
|
|
|
510
521
|
provider from turn 0 (the schema never changes) but **refuse to execute** until
|
|
511
522
|
the skill is active. This is how a tool can be gated without breaking the cache.
|
|
512
523
|
|
|
513
|
-
|
|
514
|
-
|
|
524
|
+
### 3.4.1 Skills that ship files and scripts
|
|
525
|
+
|
|
526
|
+
The whole catalog is mounted **read-only at `/skills`** before anything is
|
|
527
|
+
loaded, so a skill's own files sit at `/skills/<name>/...`. The runtime says so
|
|
528
|
+
in the activation, once, when the skill loads:
|
|
529
|
+
|
|
530
|
+
> This skill's files are at /skills/refund_policy, read-only. Paths written in
|
|
531
|
+
> it are relative to that directory.
|
|
532
|
+
|
|
533
|
+
That line is what turns a script in a skill from decoration into something the
|
|
534
|
+
agent can run:
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
agents/skills/refund_policy/
|
|
538
|
+
├── SKILL.md
|
|
539
|
+
├── rates.csv
|
|
540
|
+
└── scripts/calculate.py
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
```markdown
|
|
544
|
+
Run `python /skills/refund_policy/scripts/calculate.py <order-id>` and use the
|
|
545
|
+
figure it prints. Read `/skills/refund_policy/rates.csv` if you need the band.
|
|
546
|
+
Do not compute the amount yourself.
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Reach for a script whenever the answer is arithmetic, a lookup or a fixed
|
|
550
|
+
transformation.** It is the cheapest tool there is: no schema in the prefix, no
|
|
551
|
+
vendor, versioned beside the prose that calls it, and its output is a fact
|
|
552
|
+
rather than a guess (§1.4).
|
|
553
|
+
|
|
554
|
+
Rules that follow from the mount:
|
|
555
|
+
|
|
556
|
+
- The agent needs `sandbox:*` to run the script and `read_file` to open a data
|
|
557
|
+
file. Grant them, or the skill's own instructions cannot be followed (§3.5).
|
|
558
|
+
- **The interpreter must already be in the sandbox image.** A skill whose
|
|
559
|
+
script needs `pandas` is a `sandbox/Dockerfile` line that was never written —
|
|
560
|
+
not a first line that runs `pip install` on every turn (§3.7).
|
|
561
|
+
- **`/skills` is read-only.** A script that produces a file must take an output
|
|
562
|
+
path and write it under `/workspace`.
|
|
563
|
+
- **Write the absolute `/skills/<name>/...` path** in the skill body. The text is
|
|
564
|
+
loaded into a prompt, not executed from its directory, and the working
|
|
565
|
+
directory is `/workspace`.
|
|
566
|
+
- Say what to do when the script fails, as with any other instruction (§4.2).
|
|
567
|
+
- With several `skills:` directories, one catalog is the whole of `/skills` and
|
|
568
|
+
several take `/skills/<folder>` each — check the rendered path before writing
|
|
569
|
+
it into the body.
|
|
570
|
+
- The mount is created before anything is loaded, so `allow:` limits what an
|
|
571
|
+
agent can **load**, not what it can **read**. Do not put anything in the
|
|
572
|
+
catalog that some agents must not see.
|
|
573
|
+
|
|
574
|
+
`zen check` lists what each skill folder ships, so a script that was never
|
|
575
|
+
committed shows up as a skill with no files.
|
|
515
576
|
|
|
516
577
|
### 3.5 Tools
|
|
517
578
|
|
|
@@ -635,6 +696,7 @@ sandbox:
|
|
|
635
696
|
| Field | Default | Meaning |
|
|
636
697
|
| --------- | --------------------------------------------- | -------------------------------------------- |
|
|
637
698
|
| `image` | `docker.io/library/python:3.14-slim-bookworm` | The base image commands run in |
|
|
699
|
+
| `build` | none | A Dockerfile to build instead |
|
|
638
700
|
| `cpus` | the host's | Fractional cores |
|
|
639
701
|
| `memory` | the host's | MiB |
|
|
640
702
|
| `network` | `bridge` | `bridge` / `none` / `host` |
|
|
@@ -648,6 +710,30 @@ sandbox:
|
|
|
648
710
|
repository — and anything credential-shaped (`KEY`, `TOKEN`, `SECRET`,
|
|
649
711
|
`PASSWORD`, `CREDENTIAL`) is refused at load.
|
|
650
712
|
|
|
713
|
+
**`build:`, when no published image fits.** A project that needs two runtimes,
|
|
714
|
+
or a pinned toolchain, names a Dockerfile instead of an image — `zen init`
|
|
715
|
+
writes one at `sandbox/Dockerfile` with Python and Node in it:
|
|
716
|
+
|
|
717
|
+
```yaml
|
|
718
|
+
sandbox:
|
|
719
|
+
persist: true
|
|
720
|
+
build:
|
|
721
|
+
dockerfile: sandbox/Dockerfile
|
|
722
|
+
# context: . # what the build may COPY from; the Dockerfile's folder by default
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`image:` and `build:` cannot both be set — a Dockerfile names its own base in
|
|
726
|
+
its `FROM` line. The tag is a hash of the Dockerfile and its context, so
|
|
727
|
+
editing it produces a new image and, like any other change here, a new
|
|
728
|
+
container. `zen check` builds it and runs a command in it, so a Dockerfile that
|
|
729
|
+
does not build fails the check rather than the next run.
|
|
730
|
+
|
|
731
|
+
**Put what the project always needs in the image, not in a prompt.** This is the
|
|
732
|
+
real use of `build:`: a `RUN apt-get install ripgrep` in the Dockerfile is
|
|
733
|
+
installed once, for everyone, forever. The same instruction written into a
|
|
734
|
+
prompt is executed on every run, by an agent that is root in a filesystem that
|
|
735
|
+
is about to be thrown away.
|
|
736
|
+
|
|
651
737
|
Agents share one container, because they share the workspace and a hand-off is
|
|
652
738
|
meant to be continuous. An agent that needs something else says so and gets its
|
|
653
739
|
own, with its block merged over the top-level one:
|
|
@@ -682,15 +768,16 @@ sandbox:
|
|
|
682
768
|
```
|
|
683
769
|
|
|
684
770
|
With it, the container is _stopped_ rather than removed, and the next run of
|
|
685
|
-
that session starts the same one back up with everything still installed.
|
|
686
|
-
|
|
687
|
-
|
|
771
|
+
that session starts the same one back up with everything still installed. A
|
|
772
|
+
container is per session rather than per project, so they accumulate: `zen
|
|
773
|
+
sandbox status` lists them, `zen sandbox disk` totals what they and the project
|
|
774
|
+
directories cost, and `zen sandbox clean` removes them.
|
|
688
775
|
|
|
689
776
|
Changing any field renames the container, so bumping the image gets a fresh one
|
|
690
777
|
rather than an old one quietly persisting with the wrong contents. That is also
|
|
691
778
|
the one sharp edge of `persist: true`: a config change abandons the old
|
|
692
779
|
container with whatever was installed in it, so a long-lived setup still belongs
|
|
693
|
-
in `image:` rather than in an accumulated rootfs.
|
|
780
|
+
in `image:` — or in `sandbox/Dockerfile` — rather than in an accumulated rootfs.
|
|
694
781
|
|
|
695
782
|
Granting the group is what makes the project need Podman: `zen run` checks the
|
|
696
783
|
engine before the first turn and exits `5` with an install command if it is
|
|
@@ -792,7 +879,7 @@ zen run "what changed?" one shot; stdout is the answer
|
|
|
792
879
|
zen run --session <id> continue a session
|
|
793
880
|
zen run --workspace ./repo what the agent may read and write
|
|
794
881
|
zen run --model careful override the default model for this run
|
|
795
|
-
zen run --image <ref> override the sandbox image for this run
|
|
882
|
+
zen run --image <ref> override the sandbox image for this run, ignoring build:
|
|
796
883
|
zen run --read-only withhold every tool that can write
|
|
797
884
|
```
|
|
798
885
|
|
|
@@ -816,6 +903,22 @@ EXA_API_KEY=...
|
|
|
816
903
|
Never commit. Never inline a key into `agents.yaml` — use `${VAR}`. Never print a
|
|
817
904
|
key in a log line, a test fixture, or a chat message.
|
|
818
905
|
|
|
906
|
+
### 3.11 `assets/`
|
|
907
|
+
|
|
908
|
+
`assets/` next to `agents.yaml` — or `assets: <path>` — is reference material
|
|
909
|
+
every agent can read and none can write. It is mounted at `/assets`: the file
|
|
910
|
+
tools read, list and search it, `run_command` sees it bind-mounted read-only,
|
|
911
|
+
and every tool that would change it refuses.
|
|
912
|
+
|
|
913
|
+
Put a handbook, a specification, a schema or a style guide there — what agents
|
|
914
|
+
consult while working, as opposed to the workspace, which is the work. Do not
|
|
915
|
+
point `assets:` at the project root: that hands every agent the `sessions/`
|
|
916
|
+
directory, every transcript of every run, including the one reading it.
|
|
917
|
+
`zen check` warns when it would.
|
|
918
|
+
|
|
919
|
+
It is project-wide by design. An agent that may see only part of the material
|
|
920
|
+
is a different project, not a different key.
|
|
921
|
+
|
|
819
922
|
---
|
|
820
923
|
|
|
821
924
|
## 4. Writing prompts
|
|
@@ -1468,10 +1571,13 @@ Before finishing any change here:
|
|
|
1468
1571
|
|
|
1469
1572
|
**Skills**
|
|
1470
1573
|
|
|
1574
|
+
- [ ] Every skill is `<name>/SKILL.md` — no bare `<name>.md` in the catalog
|
|
1471
1575
|
- [ ] Every skill has a `description` that says _when it is needed_
|
|
1472
1576
|
- [ ] `preload` is reserved for content the model would never decline
|
|
1473
1577
|
- [ ] `preload` entries also appear in `allow` where `allow` is used
|
|
1474
1578
|
- [ ] Catalog >~30 entries → `discovery: search`
|
|
1579
|
+
- [ ] A skill that ships a script writes its `/skills/<name>/...` path, the agent
|
|
1580
|
+
holds `sandbox:*`, and the sandbox image already has the interpreter
|
|
1475
1581
|
|
|
1476
1582
|
**Tools**
|
|
1477
1583
|
|
|
@@ -1481,7 +1587,7 @@ Before finishing any change here:
|
|
|
1481
1587
|
- [ ] `sandbox:*` is granted only where a shell is actually needed
|
|
1482
1588
|
- [ ] `sandbox.persist: true`, unless a throwaway rootfs is wanted on purpose
|
|
1483
1589
|
- [ ] The `sandbox:` image carries what the work needs, rather than the prompt
|
|
1484
|
-
installing it every run
|
|
1590
|
+
installing it every run — add it to `sandbox/Dockerfile` if `build:` is used
|
|
1485
1591
|
- [ ] `sandbox.env` lists names only, and nothing credential-shaped
|
|
1486
1592
|
- [ ] `exa:*` is granted only where the live web is actually needed, and the
|
|
1487
1593
|
prompt says when to trust it over what the model already believes
|
|
@@ -1507,31 +1613,33 @@ Before finishing any change here:
|
|
|
1507
1613
|
|
|
1508
1614
|
## 10. Where to change what
|
|
1509
1615
|
|
|
1510
|
-
| Symptom | Change this
|
|
1511
|
-
| ------------------------------------------ |
|
|
1512
|
-
| Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt
|
|
1513
|
-
| Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically
|
|
1514
|
-
| Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description
|
|
1515
|
-
| Never loads the skill it should | The skill's `description`; or `preload` it
|
|
1516
|
-
| Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search`
|
|
1517
|
-
| Invents a number | A skill holding the figure, or a
|
|
1518
|
-
|
|
|
1519
|
-
|
|
|
1520
|
-
|
|
|
1521
|
-
|
|
|
1522
|
-
|
|
|
1523
|
-
|
|
|
1524
|
-
|
|
|
1525
|
-
|
|
|
1526
|
-
|
|
|
1527
|
-
|
|
|
1528
|
-
|
|
|
1529
|
-
|
|
|
1530
|
-
|
|
|
1531
|
-
|
|
|
1532
|
-
|
|
|
1533
|
-
|
|
|
1534
|
-
|
|
|
1616
|
+
| Symptom | Change this |
|
|
1617
|
+
| ------------------------------------------ | ------------------------------------------------------------------- |
|
|
1618
|
+
| Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
|
|
1619
|
+
| Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
|
|
1620
|
+
| Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
|
|
1621
|
+
| Never loads the skill it should | The skill's `description`; or `preload` it |
|
|
1622
|
+
| Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
|
|
1623
|
+
| Invents a number | A skill holding the figure, or a script that computes it — §3.4.1 |
|
|
1624
|
+
| A skill in the catalog is never offered | It is a bare `<name>.md`; move it to `<name>/SKILL.md` — §3.4 |
|
|
1625
|
+
| Cannot find a file its own skill names | Absolute `/skills/<name>/…` path, and `sandbox:*` — §3.4.1 |
|
|
1626
|
+
| Rewrites a whole file to change one line | A prompt line preferring `apply_patch` — §3.6 |
|
|
1627
|
+
| Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
|
|
1628
|
+
| Cannot run the build or the tests | Grant `sandbox:*`; add the toolchain to `sandbox/Dockerfile` |
|
|
1629
|
+
| Installs the same packages on every run | Put them in `sandbox/Dockerfile`, or `sandbox.persist: true` — §3.7 |
|
|
1630
|
+
| Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
|
|
1631
|
+
| Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
|
|
1632
|
+
| Every web call refuses | No Exa key: `zen key add exa` — `zen check` warns — §3.8 |
|
|
1633
|
+
| Answers instead of routing | Router prompt prohibition; check `handoffs:` |
|
|
1634
|
+
| Routes to the wrong specialist | The target agents' `description:` fields |
|
|
1635
|
+
| Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
|
|
1636
|
+
| Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
|
|
1637
|
+
| Forks when the steps actually depend | Prompt line: branches cannot see each other |
|
|
1638
|
+
| Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
|
|
1639
|
+
| Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
|
|
1640
|
+
| Shows no reasoning while it works | Turn summaries on for that model — §7.6 |
|
|
1641
|
+
| Forgets across conversations | Continue the session rather than starting a new one |
|
|
1642
|
+
| Breaks at load with a named path | Read the message — it names the exact key |
|
|
1535
1643
|
|
|
1536
1644
|
---
|
|
1537
1645
|
|
|
@@ -1542,6 +1650,10 @@ Before finishing any change here:
|
|
|
1542
1650
|
prompt style. Collapse into one with a catalog.
|
|
1543
1651
|
- **Facts in prompts.** A fee schedule inside `INSTRUCTIONS.md`. It cannot be
|
|
1544
1652
|
versioned, cannot be shared, and is paid for on every call.
|
|
1653
|
+
- **The bare skill file.** `agents/skills/<name>.md` instead of a folder. It can
|
|
1654
|
+
never grow the table or script the next revision of the rule will want — §3.4.
|
|
1655
|
+
- **Arithmetic in prose.** A skill that walks the model through a calculation it
|
|
1656
|
+
will get wrong, in a folder that could have held the script — §3.4.1.
|
|
1545
1657
|
- **Politeness padding.** "Please try your best to be helpful." Costs tokens,
|
|
1546
1658
|
changes nothing.
|
|
1547
1659
|
- **Commented implementation notes.** `agents.yaml` explaining how skill
|
|
@@ -10,9 +10,10 @@ change without the prompt changing.
|
|
|
10
10
|
|
|
11
11
|
Ask what the skill must say and when it applies, if I have not already said.
|
|
12
12
|
|
|
13
|
-
1.
|
|
14
|
-
`agents/skills/<name
|
|
15
|
-
|
|
13
|
+
1. Create `agents/skills/<name>/SKILL.md`. That is the only skill layout there
|
|
14
|
+
is — a bare `agents/skills/<name>.md` fails `zen check` — and the folder is
|
|
15
|
+
what lets the skill carry companions later: a rate table, an example letter,
|
|
16
|
+
a schema, a script.
|
|
16
17
|
2. Write the `description`. This is the routing key and the only thing the model
|
|
17
18
|
sees before deciding to load the skill, so write it as **the condition under
|
|
18
19
|
which the skill is needed**, not as a title. `Water policy` is a title;
|
|
@@ -23,12 +24,18 @@ Ask what the skill must say and when it applies, if I have not already said.
|
|
|
23
24
|
wording, the boundaries of the rule, and what to do when the case falls
|
|
24
25
|
outside it. Put the facts here rather than in a prompt — that is the point of
|
|
25
26
|
the file.
|
|
26
|
-
4.
|
|
27
|
+
4. Put anything the rule computes or looks up in a file beside `SKILL.md` and
|
|
28
|
+
name it from the body by its absolute path — `python
|
|
29
|
+
/skills/<name>/scripts/calculate.py <id>`, not `scripts/calculate.py`. The
|
|
30
|
+
folder is mounted read-only at `/skills/<name>/`, so the script writes
|
|
31
|
+
nothing there; the agent needs `sandbox:*` to run it, and the interpreter has
|
|
32
|
+
to be in the `sandbox:` image already.
|
|
33
|
+
5. Declare `tools:` in the frontmatter only for tools that must not run until
|
|
27
34
|
this skill is active. They are advertised from turn 0 and refuse to execute
|
|
28
35
|
while the skill is dormant, which is how gating happens without breaking the
|
|
29
36
|
prompt cache.
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
6. Leave `name` out unless it must differ from the folder name, and leave
|
|
38
|
+
`version`/`tags` out unless something uses them.
|
|
32
39
|
|
|
33
40
|
Do not `preload:` it unless every case genuinely needs it — a preloaded skill is
|
|
34
41
|
a longer prompt, paid for on every call. Bind it under `agents[].skills.allow`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# The container `run_command` runs in. Built by `zen run` and `zen check`, and
|
|
2
|
+
# named by the `sandbox: build:` block in agents.yaml.
|
|
3
|
+
#
|
|
4
|
+
# Python and Node both, because an agent asked to look at a repository does not
|
|
5
|
+
# get to choose which one it finds. Add what this project always needs here
|
|
6
|
+
# rather than letting an agent install it at run time: a root `pip install` or
|
|
7
|
+
# `apt-get` lands in the container's own filesystem, which is thrown away.
|
|
8
|
+
|
|
9
|
+
FROM node:24-bookworm-slim AS node
|
|
10
|
+
FROM python:3.14-slim-bookworm
|
|
11
|
+
|
|
12
|
+
# Copy Node.js runtime and global modules from official Node image
|
|
13
|
+
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
|
14
|
+
COPY --from=node /usr/local/include/node /usr/local/include/node
|
|
15
|
+
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
16
|
+
|
|
17
|
+
# Symlink npm and npx executables
|
|
18
|
+
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
19
|
+
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|