@plitzi/cli 0.37.1 → 0.37.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/CHANGELOG.md +145 -0
- package/README.md +18 -11
- package/dist/index.js +165 -21
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,150 @@
|
|
|
1
1
|
# @plitzi/cli
|
|
2
2
|
|
|
3
|
+
## 0.37.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ## Authoring and agents
|
|
8
|
+
- **A step's params are templates in full.** Only a bare name (`{{ post.slug }}`) used to be recognised, so a
|
|
9
|
+
condition or a loop in a `setState` or a webhook body was handed on as its own text — a flag set to
|
|
10
|
+
`{% if … %}1{% endif %}` stored the template, a non-empty string every later check read as true. Any `{{ }}` or
|
|
11
|
+
`{% %}` in a param is now evaluated; what it resolves to is data and is not evaluated again.
|
|
12
|
+
- **Twig tests work.** `x is defined`, `is empty`, `is null`, `is iterable`, `is even`, `is odd` (and `is not …`) used
|
|
13
|
+
to be read as comparisons with a variable of that name — `x is defined` answered true exactly when `x` was not
|
|
14
|
+
defined. Anything else on the right of `is` still compares.
|
|
15
|
+
- **A group takes an access chain.** `(rows|find('id', 3)).title` read the whole row; the key after the parenthesis
|
|
16
|
+
was never parsed.
|
|
17
|
+
- **An expression that chose to be empty renders empty** under `keepEmptyTokens`. A kept token is for a name still
|
|
18
|
+
waiting for a value; `{{ on ? 'active' : '' }}` said "nothing" on purpose, and handing back its text made the
|
|
19
|
+
empty branch a non-empty string.
|
|
20
|
+
- **Checkbox params written as text are booleans.** `dateConverter({ isUnix: 'false' })` read `'false'` as yes and
|
|
21
|
+
gave an ISO date back raw. Every utility's checkbox params are normalised before its callback runs.
|
|
22
|
+
- **`condition-starts-visible`** warns about a visibility computed from data on an element that starts on screen: it
|
|
23
|
+
is drawn until its provider answers and then hidden — an empty state or a "get started" card flashing past on
|
|
24
|
+
every load. `visible: false` makes it wait hidden. The export reads a visibility binding back in its place, so a
|
|
25
|
+
space whose condition is not its last binding round-trips unchanged.
|
|
26
|
+
- **A source named by its short id inside a binding's template is refused** with the full name, as it already was in
|
|
27
|
+
a flow: `{{ stats.total }}` resolved to nothing and the element showed its empty branch.
|
|
28
|
+
- **`activeOn(class, pageIds)`** marks a menu's current entry from `navigation.currentPageId` — one binding for a menu
|
|
29
|
+
kept in a layout. `variantFrom` now says `append: 'true'`, which is what it did.
|
|
30
|
+
- **Two elements with one id** name where the first one was written. **`template-never-resolved`** warns about a
|
|
31
|
+
condition left in an attribute, which only resolves `{{ name|filter }}` tokens.
|
|
32
|
+
- **The authoring skill is a folder**: `SKILL.md` plus references for layouts, data and visibility, templates,
|
|
33
|
+
flows, structure, testing and a review checklist. `plitzi create` copies all of it, and writes an `AGENTS.md`
|
|
34
|
+
(imported by `CLAUDE.md`) pointing any agent at it.
|
|
35
|
+
|
|
36
|
+
- **A new space starts on a page worth keeping.** The space `POST /spaces` and `plitzi create` both begin from is
|
|
37
|
+
redesigned: a top bar with the theme switch, a hero with two calls to action, and six guides — each a card linking to
|
|
38
|
+
its page of the docs (`https://plitzi.com/docs/…`). Every link used to be `#`, and one promised a course that does
|
|
39
|
+
not exist. The palette is a set of light/dark tokens in Geist, the guides are one list mapped to one card, and the
|
|
40
|
+
bands of the page share a `shell` class — the page is written the way the authoring skill asks a space to be.
|
|
41
|
+
- **The copy `plitzi create` writes passes its own project's lint.** An import of the package that would not fit
|
|
42
|
+
120 columns is wrapped one name per line, as the project's formatter would wrap it.
|
|
43
|
+
|
|
44
|
+
## Server actions and scheduled jobs
|
|
45
|
+
|
|
46
|
+
**Scheduled jobs across replicas, out of the box, on the database a deployment already runs.** The scheduler and the
|
|
47
|
+
workers were already the package's; where the jobs wait was left to each deployment to write — and a queue is the part
|
|
48
|
+
that is easiest to get subtly wrong. Two helpers now build the adapters over a store the deployment owns, without the
|
|
49
|
+
package opening a connection or keeping data of its own:
|
|
50
|
+
|
|
51
|
+
- `@plitzi/sdk-server/mongo` — `createMongoJobQueue({ db })` and `createMongoKv({ db })`, over a `Db` (or a getter for a
|
|
52
|
+
client that reconnects). Creates the indexes its queries need; `mongodb` is an optional peer, used for its types.
|
|
53
|
+
- `@plitzi/sdk-server/mysql` — `createMysqlJobQueue({ pool })` and `createMysqlKv({ pool })`, over a `mysql2` pool.
|
|
54
|
+
Each job is claimed by a single-row compare-and-set, so replicas claiming at once never deadlock. Creates its three
|
|
55
|
+
tables on first use, or hands them to your own migrations through `mysqlJobSchemaStatements()`.
|
|
56
|
+
|
|
57
|
+
Every instant is the database server's, never a replica's. The in-process queue and both helpers pass one contract test
|
|
58
|
+
suite: exactly-once enqueue, atomic claims, leases that lapse and are reaped, heartbeats, settlements ignored from a
|
|
59
|
+
worker that lost its claim, refunded hand-backs, schedules advanced by compare-and-set, operator retry and cancel.
|
|
60
|
+
|
|
61
|
+
**`closeOnSignals(server, { afterClose })`** closes the server on SIGTERM/SIGINT and exits only once it has — so a
|
|
62
|
+
deploy finishes the jobs a replica is running and leaves the waiting ones for the next. A second signal exits at once.
|
|
63
|
+
Opt-in: a host with its own signal handling calls `server.close()` itself. Projects from `plitzi create` use it.
|
|
64
|
+
|
|
65
|
+
**A replica that is told to stop finishes the jobs it is running — and only those.** `server.close()` (and the job
|
|
66
|
+
worker's `stop()`) now waits for every running job to end however long past its lease that is, and keeps renewing
|
|
67
|
+
their claims while it waits. It used to stop renewing the moment the drain began and give up at the lease: a job longer
|
|
68
|
+
than its lease was taken over by another replica and run a second time while the first was still finishing it, and the
|
|
69
|
+
process exited in the middle of it. What is still waiting is not touched — it stays in the shared queue for the replica
|
|
70
|
+
that is staying or the one the deploy starts — and a job claimed in the instant the stop arrived is handed back with its
|
|
71
|
+
attempt refunded, never started. A run is bounded by its own timeout, which is the number an orchestrator's grace period
|
|
72
|
+
has to cover.
|
|
73
|
+
|
|
74
|
+
- **An every-minute schedule fired every other minute.** After producing a fire, the scheduler asked for the next
|
|
75
|
+
one from `lateness + 1 minute`, and `cronNextFire` rounds up to a whole minute — so a sweep that ran even a second
|
|
76
|
+
late (with the default 15-second sweep, nearly all of them) skipped the minute it should have produced. It now asks
|
|
77
|
+
from the minute after the one the sweep ran in. Only expressions with consecutive-minute fires were affected;
|
|
78
|
+
hourly and daily schedules were not.
|
|
79
|
+
- **A job that gives up says which step failed and why.** Its `error` and the failed attempt in its history used to
|
|
80
|
+
read "the flow ended failed" for every failure there is; they now read `step "<id>" failed: <message>`, taken from
|
|
81
|
+
the run's outline — already redacted of every credential the run resolved, so nothing reaches the job that the
|
|
82
|
+
run history would not show.
|
|
83
|
+
- **`apiContainer` takes `refreshSeconds`**: it asks again on its own every N seconds, for a page showing something
|
|
84
|
+
still moving — a queue, a feed, a status board. It is the same refresh `performQuery` runs, so it works for both
|
|
85
|
+
runtimes (a browser request is sent again; a server provider asks for its own RSC slice again). It pauses while
|
|
86
|
+
the tab is hidden and never starts a refresh while the last one is in flight. `0`, the default, never does. Before
|
|
87
|
+
this a live server provider needed a plugin element of its own to call `useRscRefresh` on a timer: `onApiSuccess`
|
|
88
|
+
never fires for a server provider, so there was no way to author the loop. The builder shows it as "Refresh every
|
|
89
|
+
(s)" for either runtime.
|
|
90
|
+
- **`onApiSuccess` / `onApiError` fire for a server provider.** The declaration offered them for every
|
|
91
|
+
`apiContainer`, but they were decided from the browser request alone — which a `runtime: 'server'` provider never
|
|
92
|
+
makes — so a flow wired to them never ran. They now fire when the provider's slice arrives (or the payload arrives
|
|
93
|
+
without it), and again on each refresh, the same as a browser refetch. A payload resolved for another page fires
|
|
94
|
+
neither. **A space that wired a flow to one of them will now see it run.**
|
|
95
|
+
- **The theme toggle shows one icon by default.** It renders both — which one is right depends on stored state, and
|
|
96
|
+
markup that depended on it would differ between the server and the browser — and until now every space had to copy
|
|
97
|
+
ten lines of `customCss` to hide the other; one that did not showed a sun and a moon side by side. The SDK's base
|
|
98
|
+
layer now shows the icon of the scheme in use, and a space's own rule still wins.
|
|
99
|
+
- **`variantFrom(cls, source)`** in `@plitzi/sdk-authoring` binds which of a CLASS's variants an element wears to a
|
|
100
|
+
value in the data — a status pill that is amber while a job waits and green once it is done. Written by hand the
|
|
101
|
+
variant key is the trap: it names the selector the variants belong to, and the element's type (`text.base`) is a
|
|
102
|
+
different selector from its class (`statusPill.base`), so the element rendered with no variant and nothing
|
|
103
|
+
reported it. The helper takes the key from the class declaration.
|
|
104
|
+
- **`authorSpace` refuses a flow template that reads a source by its short name.** A binding completes the prefix
|
|
105
|
+
(`jobRows.item.id` → `list_jobRows.item.id`), so the short form is the one an author learns first; a step's params
|
|
106
|
+
are read as written, and there it resolved to nothing — a row's button posted an empty id and every layer below
|
|
107
|
+
reported success. The error names the full source. A root that is a step of the same flow is left alone.
|
|
108
|
+
- New example, [`05-with-server-actions/05-schedules`](../examples/05-with-server-actions/05-schedules): scheduled and
|
|
109
|
+
delayed jobs on a self-hosted server, with the `ActionJobQueue` and `kv` seams written out in full over one SQLite
|
|
110
|
+
file, two replicas sharing it, failover when one is killed, and a board that watches it happen.
|
|
111
|
+
|
|
112
|
+
## Rendering, data and styles
|
|
113
|
+
- **A server element inside a layout is resolved.** `collectServerElements` walked the page alone, so a
|
|
114
|
+
`runtime: 'server'` provider in a layout — a dashboard's sidebar, a section's header — was never resolved: the RSC
|
|
115
|
+
payload came back empty and the element rendered with nothing. It now walks the page and every shell in its layout
|
|
116
|
+
chain.
|
|
117
|
+
- **The twig `date` filter reads an epoch.** `new Date("1790143200000")` is Invalid Date, so an epoch in milliseconds
|
|
118
|
+
handed over as text — how an attribute passes one — formatted as nothing. A number, or a string of digits, is now
|
|
119
|
+
read as milliseconds.
|
|
120
|
+
- **An action is called by its name, not by its trigger kind.** `actionName` took the first trigger's title, and a
|
|
121
|
+
trigger nobody named is titled with its kind — so every rendered action in a workspace read "render" and every clock
|
|
122
|
+
"schedule". A title that only repeats the kind is no longer a name, and `defineAction` titles each trigger with the
|
|
123
|
+
action's name.
|
|
124
|
+
- **`variant` on an element that wears a class is that class's variant** when the class declares it and the type does
|
|
125
|
+
not. Keyed by the type, `text({ class: avatar, variant: 'violet' })` named `text--violet`, a selector nothing wears,
|
|
126
|
+
and rendered with no variant at all. Exporting a document back to authoring reads the same key back into `variant`.
|
|
127
|
+
- **`variantFrom` takes `{ slot, template }`**, where `template` turns a value into a variant name for data that does
|
|
128
|
+
not already speak in them — `"{{ source == 'code' ? 'on' : '' }}"`. The third argument was the slot name alone.
|
|
129
|
+
- **Entry declarations no longer alternate with `export {}`.** In `@plitzi/sdk-server` and `@plitzi/sdk-mcp` a repeated
|
|
130
|
+
`build:dev` left some `dist/<entry>.d.ts` as a ten-byte `export {}`, and consumers saw "has no exported member". The
|
|
131
|
+
cause was `insertTypesEntry` writing a types entry over the real declaration of the same path; it is gone.
|
|
132
|
+
- **Signing in keeps what a guest was doing.** `runtimeStatePersist` reset `runtime.state` whenever its owner changed,
|
|
133
|
+
and a guest becoming a user is a change — so a sign-in screen that remembered where to send somebody (`?redirect=`)
|
|
134
|
+
forgot it the moment the session arrived, and the sign-in ended on the fallback page instead. Only a change FROM an
|
|
135
|
+
account resets now: one account's state still never reaches the next, and a guest has no account to protect.
|
|
136
|
+
|
|
137
|
+
**A value with functions inside functions is written as it was given, and fast.** A CSS value was split into layers
|
|
138
|
+
at commas by a pattern that could only see one level of parentheses, so `var(--bg, light-dark(#fff, #111))` was cut at
|
|
139
|
+
its inner comma and half a function went on into the stylesheet. The half was then tokenised by an expression that
|
|
140
|
+
nested one quantifier inside another and backtracked exponentially on it — nearly two seconds for thirty characters,
|
|
141
|
+
on every generation of that selector's cache. Both are now one linear scan that splits only outside parentheses and
|
|
142
|
+
quotes. Generated caches keep the spacing the value was written with inside functions (`repeat(3, minmax(0, 1fr))`,
|
|
143
|
+
`color-mix(in oklab, oklch(…) 50%, transparent)`), where they used to drop it after the first comma.
|
|
144
|
+
|
|
145
|
+
- Updated dependencies
|
|
146
|
+
- @plitzi/sdk-authoring@0.37.2
|
|
147
|
+
|
|
3
148
|
## 0.37.1
|
|
4
149
|
|
|
5
150
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -8,28 +8,35 @@ npx @plitzi/cli create my-site
|
|
|
8
8
|
|
|
9
9
|
## `create`
|
|
10
10
|
|
|
11
|
-
Scaffolds a project that renders a Plitzi space, installs it, and leaves it ready to start.
|
|
12
|
-
|
|
11
|
+
Scaffolds a project that renders a Plitzi space, installs it, and leaves it ready to start. Three choices shape it —
|
|
12
|
+
the package manager, `--mode` and `--source` — and they are the person's to make, so `create` never makes them alone:
|
|
13
|
+
|
|
14
|
+
- **At a terminal**, anything not passed is asked for, with the likely answer offered as the default.
|
|
15
|
+
- **With nobody at the terminal** — an agent, CI — it stops before writing anything and prints the flags still
|
|
16
|
+
missing, so the agent asks the person rather than guessing. `--yes` takes the defaults (`server`, `local`, the
|
|
17
|
+
invoking package manager) for a script that genuinely means them.
|
|
13
18
|
|
|
14
19
|
| | `--source local` | `--source cloud` |
|
|
15
20
|
|---|---|---|
|
|
16
|
-
| **`--mode server`**
|
|
21
|
+
| **`--mode server`** | A page server of your own, rendering a space that lives in the project. No account. | A page server of your own, rendering the live space out of Plitzi. |
|
|
17
22
|
| **`--mode client`** | Vite + the SDK in the browser. No server at all, no account. | The SDK fetches the space from Plitzi with the public render key. |
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
plitzi create my-site
|
|
21
|
-
plitzi create my-site --
|
|
22
|
-
plitzi create my-site --
|
|
23
|
-
plitzi create
|
|
24
|
-
plitzi create my-site --
|
|
25
|
+
plitzi create my-site # asks the three choices
|
|
26
|
+
plitzi create my-site --package-manager npm --mode server --source local
|
|
27
|
+
plitzi create my-site --package-manager pnpm --mode client --source local # Vite, hot module replacement
|
|
28
|
+
plitzi create my-site --package-manager yarn --mode server --source cloud --key … # the live space
|
|
29
|
+
plitzi create my-site --yes # server + local + the invoking manager
|
|
30
|
+
plitzi create . --force --no-install --yes # into a directory that has work in it
|
|
25
31
|
```
|
|
26
32
|
|
|
27
33
|
## The package manager
|
|
28
34
|
|
|
29
35
|
`--package-manager npm|yarn|pnpm` says which one the project is written for: what it installs with, and what
|
|
30
|
-
every command in its README and its Playwright config names.
|
|
31
|
-
|
|
32
|
-
running `npx` once to scaffold a project you then work in with Yarn is exactly the case it gets
|
|
36
|
+
every command in its README and its Playwright config names. The one that invoked the CLI is only offered as the
|
|
37
|
+
default when it is asked for — `yarn dlx` and `pnpm dlx` get their own name suggested — because it is a guess about
|
|
38
|
+
the *invocation*: running `npx` once to scaffold a project you then work in with Yarn is exactly the case it gets
|
|
39
|
+
wrong.
|
|
33
40
|
|
|
34
41
|
A Yarn project also gets a `.yarnrc.yml` pinning `nodeLinker: node-modules`. Yarn 4 installs Plug'n'Play by
|
|
35
42
|
default and a server-mode project cannot start under it — `node --import tsx` dies resolving its own entry — so
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { Option, program } from "commander";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
5
|
+
import path, { dirname, join, relative, sep } from "node:path";
|
|
6
6
|
import readline from "node:readline/promises";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
9
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
10
10
|
import { blankSpaceSource } from "@plitzi/sdk-authoring";
|
|
11
11
|
//#region src/scaffold/client.ts
|
|
12
12
|
/**
|
|
@@ -666,9 +666,56 @@ ${spaceSection(answers)}
|
|
|
666
666
|
## The skills
|
|
667
667
|
|
|
668
668
|
\`.claude/skills/\` carries Plitzi's authoring skill, so an agent working in this repository knows how a space is
|
|
669
|
-
put together before it touches one.
|
|
669
|
+
put together before it touches one. Claude Code reads it automatically; \`AGENTS.md\` points any other agent at it.
|
|
670
670
|
`;
|
|
671
671
|
/**
|
|
672
|
+
* What any agent opening the project reads first, whichever agent it is.
|
|
673
|
+
*
|
|
674
|
+
* The skill in \`.claude/skills/\` is found by Claude Code on its own; other agents look for \`AGENTS.md\`. So this
|
|
675
|
+
* file carries the commands, where the space is, and the rules that go wrong most — and points at the skill for the
|
|
676
|
+
* rest. \`CLAUDE.md\` imports it, so both kinds of agent start from the same page.
|
|
677
|
+
*/
|
|
678
|
+
var agentsFile = (answers) => {
|
|
679
|
+
const code = (value) => `\`${value}\``;
|
|
680
|
+
const run = (script) => code(runCommand(answers.packageManager, script));
|
|
681
|
+
const local = answers.source === "local";
|
|
682
|
+
const where = local ? `The space is ${code("src/space.ts")}, declared with ${code("@plitzi/sdk-authoring")}. Edit that; never the JSON it produces.` : "The space lives in Plitzi and is edited in the builder (or by an agent over MCP); this project serves it.";
|
|
683
|
+
const commands = [
|
|
684
|
+
`| ${code(installCommand(answers.packageManager))} | install |`,
|
|
685
|
+
`| ${run("start")} | serve it |`,
|
|
686
|
+
...local ? [`| ${run("author")} | author the space and print its warnings |`] : [],
|
|
687
|
+
`| ${run("visual")} | open the page in a browser and check it rendered |`
|
|
688
|
+
];
|
|
689
|
+
const zeroWarnings = local ? `Zero warnings from ${run("author")}.` : "Zero warnings from authoring.";
|
|
690
|
+
return `# ${answers.name} — notes for agents
|
|
691
|
+
|
|
692
|
+
A Plitzi space, rendered ${answers.mode === "server" ? "by the server in this project (SSR + RSC)" : "in the browser by a Vite app"}.
|
|
693
|
+
${where}
|
|
694
|
+
|
|
695
|
+
## Commands
|
|
696
|
+
|
|
697
|
+
| | |
|
|
698
|
+
| --- | --- |
|
|
699
|
+
${commands.join("\n")}
|
|
700
|
+
|
|
701
|
+
## Before anything else
|
|
702
|
+
|
|
703
|
+
Read ${code(".claude/skills/plitzi-authoring/SKILL.md")} — how a space is written, and the references it links to for
|
|
704
|
+
layouts, data, templates and flows. The types of ${code("@plitzi/sdk-authoring")} document every factory and field.
|
|
705
|
+
|
|
706
|
+
## The rules that go wrong most
|
|
707
|
+
|
|
708
|
+
- Never write schema/style JSON by hand; author it. A refusal names the fix — fix the declaration.
|
|
709
|
+
- ${zeroWarnings}
|
|
710
|
+
- Chrome shared by pages is a layout; a look used twice is a class; a repeated block is a function or a ${code("map")}.
|
|
711
|
+
- Ids are one namespace for the whole space: name what is referred to, prefix ids made by a helper.
|
|
712
|
+
- Elements are visible by default. One the logic REVEALS starts hidden (${code("visible")}, or ${code("visible: false")} plus a computed binding) so nothing flashes while loading; one a flag HIDES stays shown while the flag is unset.
|
|
713
|
+
- Inside a template a source is spelled in full (${code("apiContainer_stats")}); an attribute only resolves ${code("{{ name|filter }}")}.
|
|
714
|
+
- Colours are tokens with light and dark values; times carry an explicit zone and say it.
|
|
715
|
+
- Go through ${code(".claude/skills/plitzi-authoring/reference/review-checklist.md")} before calling a change done.
|
|
716
|
+
`;
|
|
717
|
+
};
|
|
718
|
+
/**
|
|
672
719
|
* The credential, and the prefix it has to carry.
|
|
673
720
|
*
|
|
674
721
|
* Vite only exposes variables named `VITE_*` to the browser, which is a safety rail rather than a formality: a
|
|
@@ -697,6 +744,8 @@ var projectFiles = (answers) => ({
|
|
|
697
744
|
"tsconfig.json": tsconfig(answers),
|
|
698
745
|
".gitignore": gitignore(answers),
|
|
699
746
|
"README.md": readme(answers),
|
|
747
|
+
"AGENTS.md": agentsFile(answers),
|
|
748
|
+
"CLAUDE.md": "@AGENTS.md\n",
|
|
700
749
|
...answers.source === "cloud" ? { ".env": envFile(answers) } : {}
|
|
701
750
|
});
|
|
702
751
|
//#endregion
|
|
@@ -811,7 +860,7 @@ const plugins = {
|
|
|
811
860
|
const pluginNames = Object.keys(plugins);`;
|
|
812
861
|
var localMain = () => `import path from 'node:path';
|
|
813
862
|
|
|
814
|
-
import { consoleLogger, createJsonAdapters, createServer } from '@plitzi/sdk-server';
|
|
863
|
+
import { closeOnSignals, consoleLogger, createJsonAdapters, createServer } from '@plitzi/sdk-server';
|
|
815
864
|
|
|
816
865
|
import { authorSpace } from '@plitzi/sdk-authoring';
|
|
817
866
|
|
|
@@ -823,9 +872,15 @@ const PORT = Number(process.env.PORT ?? 8080);
|
|
|
823
872
|
* The space, held in this project.
|
|
824
873
|
*
|
|
825
874
|
* \`authorSpace\` turns the declaration in \`src/space.ts\` into the two documents a renderer wants. It runs at
|
|
826
|
-
* boot, so saving that file and letting \`--watch\` restart it is the whole edit loop
|
|
875
|
+
* boot, so saving that file and letting \`--watch\` restart it is the whole edit loop — and its warnings are printed
|
|
876
|
+
* here for the same reason: this restart is the output somebody editing the space is actually watching.
|
|
827
877
|
*/
|
|
828
|
-
const
|
|
878
|
+
const { schema, style, warnings } = authorSpace(space);
|
|
879
|
+
const offlineData = { schema, style };
|
|
880
|
+
|
|
881
|
+
for (const warning of warnings) {
|
|
882
|
+
console.warn(\`[author] \${warning.message}\`);
|
|
883
|
+
}
|
|
829
884
|
|
|
830
885
|
${PLUGINS}
|
|
831
886
|
|
|
@@ -849,10 +904,17 @@ const server = createServer({
|
|
|
849
904
|
|
|
850
905
|
server.listen(PORT, '127.0.0.1');
|
|
851
906
|
console.log(\`pages on http://127.0.0.1:\${PORT}/\`);
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* A deploy, a restart or ^C closes the server instead of dropping it: requests in flight are answered, and once the
|
|
910
|
+
* space runs scheduled actions, the jobs this server is running finish first — what is still waiting stays in the
|
|
911
|
+
* queue for whichever server runs next. A second ^C exits at once.
|
|
912
|
+
*/
|
|
913
|
+
closeOnSignals(server);
|
|
852
914
|
`;
|
|
853
915
|
var cloudMain = () => `import path from 'node:path';
|
|
854
916
|
|
|
855
|
-
import { consoleLogger, createCloudAdapters, createServer } from '@plitzi/sdk-server';
|
|
917
|
+
import { closeOnSignals, consoleLogger, createCloudAdapters, createServer } from '@plitzi/sdk-server';
|
|
856
918
|
|
|
857
919
|
const PORT = Number(process.env.PORT ?? 8080);
|
|
858
920
|
|
|
@@ -899,6 +961,13 @@ const server = createServer({
|
|
|
899
961
|
|
|
900
962
|
server.listen(PORT, '127.0.0.1');
|
|
901
963
|
console.log(\`pages on http://127.0.0.1:\${PORT}/\`);
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* A deploy, a restart or ^C closes the server instead of dropping it: requests in flight are answered, and once the
|
|
967
|
+
* space runs scheduled actions, the jobs this server is running finish first — what is still waiting stays in the
|
|
968
|
+
* queue for whichever server runs next. A second ^C exits at once.
|
|
969
|
+
*/
|
|
970
|
+
closeOnSignals(server);
|
|
902
971
|
`;
|
|
903
972
|
var serverFiles = (answers) => ({ "src/main.ts": answers.source === "cloud" ? cloudMain() : localMain() });
|
|
904
973
|
//#endregion
|
|
@@ -928,10 +997,23 @@ var SKILLS = [{
|
|
|
928
997
|
* however present it is on disk.
|
|
929
998
|
*/
|
|
930
999
|
var skillPath = (packageName, skill) => require.resolve(`${packageName}/skills/${skill}/SKILL.md`);
|
|
1000
|
+
/**
|
|
1001
|
+
* Every file under a skill's folder, by its path inside it.
|
|
1002
|
+
*
|
|
1003
|
+
* A skill is a folder, not a file: \`SKILL.md\` is the short part an agent reads first, and the references it links to
|
|
1004
|
+
* sit beside it and are opened when the task needs them. Copying \`SKILL.md\` alone left every one of those links
|
|
1005
|
+
* pointing at nothing.
|
|
1006
|
+
*/
|
|
1007
|
+
var filesUnder = (root, folder = root) => readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
|
|
1008
|
+
const path = join(folder, entry.name);
|
|
1009
|
+
if (entry.isDirectory()) return filesUnder(root, path);
|
|
1010
|
+
return entry.isFile() ? [[relative(root, path).split(sep).join("/"), readFileSync(path, "utf-8")]] : [];
|
|
1011
|
+
});
|
|
931
1012
|
var skillFiles = () => {
|
|
932
1013
|
const files = {};
|
|
933
1014
|
for (const skill of SKILLS) try {
|
|
934
|
-
|
|
1015
|
+
const root = dirname(skillPath(skill.package, skill.name));
|
|
1016
|
+
for (const [path, content] of filesUnder(root)) files[`.claude/skills/${skill.name}/${path}`] = content;
|
|
935
1017
|
} catch {
|
|
936
1018
|
continue;
|
|
937
1019
|
}
|
|
@@ -1121,6 +1203,8 @@ var scaffold = (answers) => ({
|
|
|
1121
1203
|
});
|
|
1122
1204
|
//#endregion
|
|
1123
1205
|
//#region src/commands/create.ts
|
|
1206
|
+
var MODES = ["server", "client"];
|
|
1207
|
+
var SOURCES = ["local", "cloud"];
|
|
1124
1208
|
/** A directory that does not exist yet is as empty as one can be, which is the answer this question wants. */
|
|
1125
1209
|
var isEmpty = async (target) => {
|
|
1126
1210
|
try {
|
|
@@ -1142,7 +1226,6 @@ var nearestExisting = async (target) => {
|
|
|
1142
1226
|
while (!await isDirectory(dir) && path.dirname(dir) !== dir) dir = path.dirname(dir);
|
|
1143
1227
|
return dir;
|
|
1144
1228
|
};
|
|
1145
|
-
var oneOf = (value, allowed, fallback) => allowed.includes(value) ? value : fallback;
|
|
1146
1229
|
/**
|
|
1147
1230
|
* What a first install is most likely refused over, and what to do about it.
|
|
1148
1231
|
*
|
|
@@ -1164,6 +1247,75 @@ var install = (manager, cwd) => new Promise((resolve) => {
|
|
|
1164
1247
|
child.on("error", () => resolve(false));
|
|
1165
1248
|
child.on("close", (code) => resolve(code === 0));
|
|
1166
1249
|
});
|
|
1250
|
+
/** One choice at the terminal: Enter takes the default, anything off the list is refused rather than guessed at. */
|
|
1251
|
+
var ask = async (rl, question) => {
|
|
1252
|
+
const reply = (await rl.question(`\n${question.help}\n${question.flag} (${question.choices.join("/")}) [${question.fallback}] > `)).trim();
|
|
1253
|
+
const chosen = question.choices.find((choice) => choice === (reply || question.fallback));
|
|
1254
|
+
if (!chosen) console.error(chalk.red(`"${reply}" is not one of ${question.choices.join(", ")}.`));
|
|
1255
|
+
return chosen;
|
|
1256
|
+
};
|
|
1257
|
+
var resolveDecisions = async (options) => {
|
|
1258
|
+
const detected = detectPackageManager();
|
|
1259
|
+
const packageManager = {
|
|
1260
|
+
flag: "--package-manager",
|
|
1261
|
+
choices: PACKAGE_MANAGERS,
|
|
1262
|
+
help: `The package manager the project will be worked in (this was run through ${detected}).`,
|
|
1263
|
+
given: PACKAGE_MANAGERS.find((manager) => manager === options.packageManager),
|
|
1264
|
+
fallback: detected
|
|
1265
|
+
};
|
|
1266
|
+
const mode = {
|
|
1267
|
+
flag: "--mode",
|
|
1268
|
+
choices: MODES,
|
|
1269
|
+
help: "server: SSR + RSC on a Node tier. client: browser only.",
|
|
1270
|
+
given: MODES.find((candidate) => candidate === options.mode),
|
|
1271
|
+
fallback: "server"
|
|
1272
|
+
};
|
|
1273
|
+
const source = {
|
|
1274
|
+
flag: "--source",
|
|
1275
|
+
choices: SOURCES,
|
|
1276
|
+
help: "local: the space travels in the project. cloud: read it from Plitzi.",
|
|
1277
|
+
given: SOURCES.find((candidate) => candidate === options.source),
|
|
1278
|
+
fallback: "local"
|
|
1279
|
+
};
|
|
1280
|
+
const missing = [
|
|
1281
|
+
packageManager,
|
|
1282
|
+
mode,
|
|
1283
|
+
source
|
|
1284
|
+
].filter((question) => question.given === void 0);
|
|
1285
|
+
if (missing.length === 0 || options.yes) return {
|
|
1286
|
+
packageManager: packageManager.given ?? packageManager.fallback,
|
|
1287
|
+
mode: mode.given ?? mode.fallback,
|
|
1288
|
+
source: source.given ?? source.fallback
|
|
1289
|
+
};
|
|
1290
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1291
|
+
console.error(chalk.red("\nplitzi create needs these decided by whoever the project is for. Ask them; do not assume:"));
|
|
1292
|
+
for (const question of missing) console.error(` ${`${question.flag} ${question.choices.join("|")}`.padEnd(36)} ${chalk.dim(question.help)}`);
|
|
1293
|
+
const defaults = missing.map((question) => `${question.flag} ${question.fallback}`).join(" ");
|
|
1294
|
+
console.error(chalk.dim(`\nThen run it again with those flags — or with --yes to take ${defaults}.`));
|
|
1295
|
+
process.exitCode = 1;
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const rl = readline.createInterface({
|
|
1299
|
+
input: process.stdin,
|
|
1300
|
+
output: process.stdout
|
|
1301
|
+
});
|
|
1302
|
+
try {
|
|
1303
|
+
const manager = packageManager.given ?? await ask(rl, packageManager);
|
|
1304
|
+
const tier = manager && (mode.given ?? await ask(rl, mode));
|
|
1305
|
+
const origin = tier && (source.given ?? await ask(rl, source));
|
|
1306
|
+
if (!manager || !tier || !origin) {
|
|
1307
|
+
process.exitCode = 1;
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
packageManager: manager,
|
|
1312
|
+
mode: tier,
|
|
1313
|
+
source: origin
|
|
1314
|
+
};
|
|
1315
|
+
} finally {
|
|
1316
|
+
rl.close();
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1167
1319
|
var askForKey = async (mode) => {
|
|
1168
1320
|
const rl = readline.createInterface({
|
|
1169
1321
|
input: process.stdin,
|
|
@@ -1184,17 +1336,9 @@ var create = async (directory, options) => {
|
|
|
1184
1336
|
process.exitCode = 1;
|
|
1185
1337
|
return;
|
|
1186
1338
|
}
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
* Asked for first, detected second.
|
|
1191
|
-
*
|
|
1192
|
-
* The invoking agent is a good guess and a bad rule: reaching for `npx` to run a scaffold once and then working
|
|
1193
|
-
* in the project with Yarn is an ordinary thing to do, and the guess writes a README, a Playwright config, a
|
|
1194
|
-
* `.gitignore` and an install into that project which all name the wrong one. `--package-manager` is how
|
|
1195
|
-
* somebody says which one they will actually be using.
|
|
1196
|
-
*/
|
|
1197
|
-
const packageManager = oneOf(options.packageManager, PACKAGE_MANAGERS, detectPackageManager());
|
|
1339
|
+
const decisions = await resolveDecisions(options);
|
|
1340
|
+
if (!decisions) return;
|
|
1341
|
+
const { packageManager, mode, source } = decisions;
|
|
1198
1342
|
const key = source === "cloud" ? options.key ?? await askForKey(mode) : "";
|
|
1199
1343
|
if (source === "cloud" && !key) {
|
|
1200
1344
|
console.error(chalk.red("\nNo key. Mint one under Credentials in the builder, then run this again."));
|
|
@@ -1256,6 +1400,6 @@ var create = async (directory, options) => {
|
|
|
1256
1400
|
* write three files correctly before they could tell whether any of it worked.
|
|
1257
1401
|
*/
|
|
1258
1402
|
program.name("plitzi").description("Plitzi command line");
|
|
1259
|
-
program.command("create").argument("[directory]", "Where to write the project. Defaults to the current directory.").description("Scaffold a project that renders a Plitzi space").
|
|
1403
|
+
program.command("create").argument("[directory]", "Where to write the project. Defaults to the current directory.").description("Scaffold a project that renders a Plitzi space").addOption(new Option("-m, --mode <mode>", "server (SSR + RSC on a Node tier) or client (browser only)").choices(["server", "client"])).addOption(new Option("-s, --source <source>", "local (the space travels in the project) or cloud (read it from Plitzi)").choices(["local", "cloud"])).option("-k, --key <key>", "Cloud only: the space key (asked for when omitted)").option("-e, --environment <environment>", "Which version to serve: main, or a published environment", "main").addOption(new Option("-p, --package-manager <manager>", "The package manager the project is written for. Asked for when left out.").choices([...PACKAGE_MANAGERS])).option("--no-install", "Write the files without installing dependencies").option("-f, --force", "Write into a directory that is not empty").option("-y, --yes", "Take the defaults for any choice not passed (server, local, the invoking package manager)").action(create);
|
|
1260
1404
|
program.parse(process.argv);
|
|
1261
1405
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/cli",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"description": "Command line for Plitzi: scaffold a server that renders a space you own.",
|
|
6
6
|
"files": [
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
"sideEffects": false,
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@plitzi/sdk-authoring": "0.37.
|
|
19
|
+
"@plitzi/sdk-authoring": "0.37.2",
|
|
20
20
|
"chalk": "^6.0.0",
|
|
21
21
|
"commander": "^15.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^26.6.
|
|
24
|
+
"@types/node": "^26.6.2",
|
|
25
25
|
"eslint": "^9.39.5",
|
|
26
26
|
"eslint-plugin-import": "^2.32.0",
|
|
27
27
|
"typescript": "^6.0.3",
|