create-hozu 0.1.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/LICENSE +21 -0
- package/README.md +22 -0
- package/bin/create-hozu.js +4 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +64 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/skill/SKILL.md +202 -0
- package/skill/changing.md +40 -0
- package/skill/diagnostics.md +39 -0
- package/skill/example/app.css +1 -0
- package/skill/example/features/bookmarks/model.ts +129 -0
- package/skill/example/features/bookmarks/views.ts +248 -0
- package/skill/example/hozu.config.ts +28 -0
- package/skill/example/routes.ts +11 -0
- package/skill/example/serve.ts +14 -0
- package/skill/example/server.ts +34 -0
- package/skill/patterns.md +56 -0
- package/skill/reference.md +139 -0
- package/templates/app/app.css +1 -0
- package/templates/app/features/site/views.ts +15 -0
- package/templates/app/gitignore +2 -0
- package/templates/app/hozu.config.ts +13 -0
- package/templates/app/routes.ts +3 -0
- package/templates/app/serve.ts +14 -0
- package/templates/app/server.ts +6 -0
- package/templates/app/tsconfig.json +20 -0
- package/templates/guide.md +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 olevatorr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# create-hozu
|
|
2
|
+
|
|
3
|
+
Create a Hozu app, set up for Claude Code or for agents that read AGENTS.md.
|
|
4
|
+
|
|
5
|
+
Part of [Hozu](https://github.com/olevatorr/Hozu#readme), an AI-first web framework. It creates an app and sets it up for a coding agent:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm create hozu@latest my-app -- --agent claude # CLAUDE.md + .claude/skills/hozu
|
|
9
|
+
npm create hozu@latest my-app -- --agent agents # AGENTS.md + .agents/skills/hozu (Codex, Cursor, Copilot…)
|
|
10
|
+
npm create hozu@latest my-app -- --agent both
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Without `--agent` it asks.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm create hozu@latest my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node 22.18 or newer. Documentation: the [README](https://github.com/olevatorr/Hozu#readme) and the
|
|
20
|
+
agent skill that `create-hozu` writes into each app.
|
|
21
|
+
|
|
22
|
+
MIT © olevatorr
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"AAkBA,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmD1D"}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { AGENTS, createApp, runnerOf } from './index.js';
|
|
6
|
+
const usage = `Usage: create-hozu [directory] [--agent claude|agents|both]
|
|
7
|
+
|
|
8
|
+
--agent claude CLAUDE.md + .claude/skills/hozu (Claude Code)
|
|
9
|
+
--agent agents AGENTS.md + .agents/skills/hozu (Codex, Cursor, Copilot and other agents)
|
|
10
|
+
--agent both both
|
|
11
|
+
`;
|
|
12
|
+
const supported = () => {
|
|
13
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
14
|
+
return major > 22 || (major === 22 && minor >= 18);
|
|
15
|
+
};
|
|
16
|
+
export async function main(argv) {
|
|
17
|
+
if (!supported()) {
|
|
18
|
+
process.stderr.write(`Hozu needs Node 22.18 or newer (this is ${process.versions.node}).\n`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
args: argv,
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
options: { agent: { type: 'string' }, help: { type: 'boolean', short: 'h' } },
|
|
25
|
+
});
|
|
26
|
+
if (values.help) {
|
|
27
|
+
process.stdout.write(usage);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
const interactive = process.stdin.isTTY === true;
|
|
31
|
+
const ask = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
32
|
+
try {
|
|
33
|
+
let dir = positionals[0];
|
|
34
|
+
if (!dir) {
|
|
35
|
+
if (!ask)
|
|
36
|
+
throw new Error('Give a directory: create-hozu my-app --agent claude');
|
|
37
|
+
dir = (await ask.question('Project directory (my-hozu-app): ')).trim() || 'my-hozu-app';
|
|
38
|
+
}
|
|
39
|
+
let agent = values.agent;
|
|
40
|
+
if (agent && !AGENTS.includes(agent))
|
|
41
|
+
throw new Error(`--agent must be one of ${AGENTS.join(', ')}`);
|
|
42
|
+
if (!agent) {
|
|
43
|
+
if (!ask)
|
|
44
|
+
throw new Error('Choose the coding agent: --agent claude|agents|both');
|
|
45
|
+
const answer = (await ask.question('Which coding agent will work on this app?\n 1) Claude Code (CLAUDE.md)\n 2) Other agents (AGENTS.md)\n 3) Both\nChoose 1-3 (1): ')).trim();
|
|
46
|
+
agent = AGENTS[Number(answer || '1') - 1] ?? 'claude';
|
|
47
|
+
}
|
|
48
|
+
const target = resolve(dir);
|
|
49
|
+
const version = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
50
|
+
const runner = runnerOf(process.env.npm_config_user_agent);
|
|
51
|
+
await createApp(target, { name: basename(target), agent, version, runner });
|
|
52
|
+
const install = runner === 'npx' ? 'npm install' : runner === 'bunx' ? 'bun install' : `${runner.split(' ')[0]} install`;
|
|
53
|
+
process.stdout.write(`\nCreated ${basename(target)} for ${agent === 'both' ? 'Claude Code and other agents' : agent === 'claude' ? 'Claude Code' : 'other agents'}.\n\n cd ${dir}\n ${install}\n ${runner === 'pnpm exec' ? 'pnpm' : runner === 'npx' ? 'npm' : runner.split(' ')[0]} start\n\n`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
process.stderr.write(`create-hozu: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
ask?.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,MAAM,EAAc,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAEpE,MAAM,KAAK,GAAG;;;;;CAKb,CAAA;AAED,MAAM,SAAS,GAAG,GAAG,EAAE;IACrB,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3E,OAAO,KAAK,GAAG,EAAE,IAAI,CAAC,KAAK,KAAK,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC,CAAA;AACpD,CAAC,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2CAA2C,OAAO,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAA;QAC5F,OAAO,CAAC,CAAA;IACV,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QACxC,IAAI,EAAE,IAAI;QACV,gBAAgB,EAAE,IAAI;QACtB,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;KAC9E,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC3B,OAAO,CAAC,CAAA;IACV,CAAC;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAA;IAChD,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAClG,IAAI,CAAC;QACH,IAAI,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;QACxB,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;YAChF,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,aAAa,CAAA;QACzF,CAAC;QACD,IAAI,KAAK,GAAG,MAAM,CAAC,KAA0B,CAAA;QAC7C,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACpG,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;YAChF,MAAM,MAAM,GAAG,CACb,MAAM,GAAG,CAAC,QAAQ,CAChB,qIAAqI,CACtI,CACF,CAAC,IAAI,EAAE,CAAA;YACR,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,QAAQ,CAAA;QACvD,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,OAAO,GACX,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAC/E,CAAC,OAAO,CAAA;QACT,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAA;QAC1D,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;QAC3E,MAAM,OAAO,GACX,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAA;QAC1G,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,aAAa,QAAQ,CAAC,MAAM,CAAC,QAAQ,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc,aAAa,GAAG,OAAO,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAC/Q,CAAA;QACD,OAAO,CAAC,CAAA;IACV,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAChG,OAAO,CAAC,CAAA;IACV,CAAC;YAAS,CAAC;QACT,GAAG,EAAE,KAAK,EAAE,CAAA;IACd,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type Agent = 'claude' | 'agents' | 'both';
|
|
2
|
+
export type Runner = 'pnpm exec' | 'npx' | 'yarn' | 'bunx';
|
|
3
|
+
export declare const AGENTS: readonly Agent[];
|
|
4
|
+
export declare const defaultSkill: string;
|
|
5
|
+
export declare const runnerOf: (userAgent: string | undefined) => Runner;
|
|
6
|
+
export interface AgentOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
runner: Runner;
|
|
9
|
+
skillSource?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function writeAgentFiles(dir: string, agent: Agent, options: AgentOptions): Promise<string[]>;
|
|
12
|
+
export interface CreateOptions extends AgentOptions {
|
|
13
|
+
agent: Agent;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const packageJson: (name: string, version: string) => {
|
|
17
|
+
name: string;
|
|
18
|
+
private: boolean;
|
|
19
|
+
type: string;
|
|
20
|
+
scripts: {
|
|
21
|
+
check: string;
|
|
22
|
+
start: string;
|
|
23
|
+
build: string;
|
|
24
|
+
};
|
|
25
|
+
dependencies: {
|
|
26
|
+
'@hozu/adapter-node': string;
|
|
27
|
+
'@hozu/core': string;
|
|
28
|
+
'@hozu/css': string;
|
|
29
|
+
'@hozu/data': string;
|
|
30
|
+
'@hozu/runtime-server': string;
|
|
31
|
+
'@hozu/schema-zod': string;
|
|
32
|
+
zod: string;
|
|
33
|
+
};
|
|
34
|
+
devDependencies: {
|
|
35
|
+
'@hozu/cli': string;
|
|
36
|
+
'@types/node': string;
|
|
37
|
+
typescript: string;
|
|
38
|
+
};
|
|
39
|
+
engines: {
|
|
40
|
+
node: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export declare function createApp(dir: string, options: CreateOptions): Promise<string[]>;
|
|
44
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAChD,MAAM,MAAM,MAAM,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1D,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EAAiC,CAAA;AAGpE,eAAO,MAAM,YAAY,QAAsB,CAAA;AAiB/C,eAAO,MAAM,QAAQ,cAAe,MAAM,GAAG,SAAS,KAAG,MAGxD,CAAA;AAWD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBzG;AAYD,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,MAAM;;;;;QAKrD,KAAK;QACL,KAAK;QACL,KAAK;;;QAGL,oBAAoB;QACpB,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,sBAAsB;QACtB,kBAAkB;QAClB,GAAG;;;QAGH,WAAW;QACX,aAAa;QACb,UAAU;;;QAED,IAAI;;CACf,CAAA;AAEF,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBtF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const AGENTS = ['claude', 'agents', 'both'];
|
|
5
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
6
|
+
export const defaultSkill = join(root, 'skill');
|
|
7
|
+
const templates = join(root, 'templates');
|
|
8
|
+
const targets = (agent) => [
|
|
9
|
+
{
|
|
10
|
+
guide: 'CLAUDE.md',
|
|
11
|
+
skill: '.claude/skills/hozu',
|
|
12
|
+
read: 'Use the `hozu` skill (`__SKILL__/SKILL.md`).',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
guide: 'AGENTS.md',
|
|
16
|
+
skill: '.agents/skills/hozu',
|
|
17
|
+
read: 'Read `__SKILL__/SKILL.md` before writing or changing any Hozu code.',
|
|
18
|
+
},
|
|
19
|
+
].filter((_, i) => agent === 'both' || (agent === 'claude' ? i === 0 : i === 1));
|
|
20
|
+
export const runnerOf = (userAgent) => {
|
|
21
|
+
const name = userAgent?.split('/')[0];
|
|
22
|
+
return name === 'npm' ? 'npx' : name === 'yarn' ? 'yarn' : name === 'bun' ? 'bunx' : 'pnpm exec';
|
|
23
|
+
};
|
|
24
|
+
const exists = (path) => stat(path).then(() => true, () => false);
|
|
25
|
+
const fill = (text, values) => Object.entries(values).reduce((out, [key, value]) => out.replaceAll(`__${key}__`, value), text);
|
|
26
|
+
export async function writeAgentFiles(dir, agent, options) {
|
|
27
|
+
const written = [];
|
|
28
|
+
const guide = await readFile(join(templates, 'guide.md'), 'utf8');
|
|
29
|
+
for (const t of targets(agent)) {
|
|
30
|
+
const skill = join(dir, t.skill);
|
|
31
|
+
await rm(skill, { recursive: true, force: true });
|
|
32
|
+
await mkdir(dirname(skill), { recursive: true });
|
|
33
|
+
await cp(options.skillSource ?? defaultSkill, skill, { recursive: true });
|
|
34
|
+
written.push(t.skill);
|
|
35
|
+
const file = join(dir, t.guide);
|
|
36
|
+
if (await exists(file))
|
|
37
|
+
continue;
|
|
38
|
+
const values = {
|
|
39
|
+
NAME: options.name,
|
|
40
|
+
RUN: options.runner,
|
|
41
|
+
SKILL: t.skill,
|
|
42
|
+
NOTE: options.runner === 'pnpm exec'
|
|
43
|
+
? ''
|
|
44
|
+
: `\nThe skill writes commands as \`pnpm exec …\`; in this app use \`${options.runner} …\`.\n`,
|
|
45
|
+
};
|
|
46
|
+
await writeFile(file, fill(fill(guide, { READ: t.read }), values));
|
|
47
|
+
written.push(t.guide);
|
|
48
|
+
}
|
|
49
|
+
return written;
|
|
50
|
+
}
|
|
51
|
+
async function files(dir) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
54
|
+
const path = join(dir, entry.name);
|
|
55
|
+
if (entry.isDirectory())
|
|
56
|
+
out.push(...(await files(path)));
|
|
57
|
+
else
|
|
58
|
+
out.push(path);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
export const packageJson = (name, version) => ({
|
|
63
|
+
name,
|
|
64
|
+
private: true,
|
|
65
|
+
type: 'module',
|
|
66
|
+
scripts: {
|
|
67
|
+
check: 'tsc --noEmit -p . && hozu validate',
|
|
68
|
+
start: 'node serve.ts',
|
|
69
|
+
build: 'hozu build',
|
|
70
|
+
},
|
|
71
|
+
dependencies: {
|
|
72
|
+
'@hozu/adapter-node': `^${version}`,
|
|
73
|
+
'@hozu/core': `^${version}`,
|
|
74
|
+
'@hozu/css': `^${version}`,
|
|
75
|
+
'@hozu/data': `^${version}`,
|
|
76
|
+
'@hozu/runtime-server': `^${version}`,
|
|
77
|
+
'@hozu/schema-zod': `^${version}`,
|
|
78
|
+
zod: '^4.6.5',
|
|
79
|
+
},
|
|
80
|
+
devDependencies: {
|
|
81
|
+
'@hozu/cli': `^${version}`,
|
|
82
|
+
'@types/node': '^22.20.4',
|
|
83
|
+
typescript: '^7.0.2',
|
|
84
|
+
},
|
|
85
|
+
engines: { node: '>=22.18' },
|
|
86
|
+
});
|
|
87
|
+
export async function createApp(dir, options) {
|
|
88
|
+
if ((await exists(dir)) && (await readdir(dir)).length)
|
|
89
|
+
throw new Error(`${dir} is not empty. Choose a new directory name.`);
|
|
90
|
+
const app = join(templates, 'app');
|
|
91
|
+
const written = [];
|
|
92
|
+
for (const source of await files(app)) {
|
|
93
|
+
const rel = relative(app, source);
|
|
94
|
+
const target = join(dir, rel === 'gitignore' ? '.gitignore' : rel);
|
|
95
|
+
await mkdir(dirname(target), { recursive: true });
|
|
96
|
+
await writeFile(target, fill(await readFile(source, 'utf8'), { NAME: options.name }));
|
|
97
|
+
written.push(relative(dir, target));
|
|
98
|
+
}
|
|
99
|
+
await writeFile(join(dir, 'package.json'), `${JSON.stringify(packageJson(options.name, options.version), null, 2)}\n`);
|
|
100
|
+
written.push('package.json');
|
|
101
|
+
written.push(...(await writeAgentFiles(dir, options.agent, options)));
|
|
102
|
+
return written;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-hozu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a Hozu app, set up for Claude Code or for agents that read AGENTS.md",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hozu",
|
|
7
|
+
"framework",
|
|
8
|
+
"ai",
|
|
9
|
+
"agents",
|
|
10
|
+
"ssr",
|
|
11
|
+
"islands",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/olevatorr/Hozu/tree/main/packages/create-hozu#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/olevatorr/Hozu/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/olevatorr/Hozu.git",
|
|
21
|
+
"directory": "packages/create-hozu"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "olevatorr",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"create-hozu": "./bin/create-hozu.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin",
|
|
37
|
+
"dist",
|
|
38
|
+
"templates",
|
|
39
|
+
"skill"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.18"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hozu
|
|
3
|
+
description: Build or change an app with the Hozu framework (packages @hozu/*, files like hozu.config.ts, features/*/model.ts, views.ts). Use it before writing any Hozu code. It is the complete authoring reference, so you do not need to read the framework source.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hozu authoring guide
|
|
7
|
+
|
|
8
|
+
Hozu is not in your training data. These files are the whole API; do not read `node_modules/@hozu`.
|
|
9
|
+
- **Changing an app:** read `changing.md` first, then only the app's own files.
|
|
10
|
+
- **Building an app:** read this file and `patterns.md`, then copy the shape of `example/` (a verified app).
|
|
11
|
+
- **`reference.md`** when the task needs it: routes, DOM fields, no-JS forms, `head`, 404/500, field errors,
|
|
12
|
+
sessions, languages, env, HTTP, Markdown, images, preview, PWA, page tests, deployment.
|
|
13
|
+
- **A diagnostic you do not understand:** `diagnostics.md`.
|
|
14
|
+
|
|
15
|
+
## Mental model
|
|
16
|
+
- The app is **data**: builders record an IR that is validated, then rendered. Only machine views ship JS.
|
|
17
|
+
- **References are recorded, not evaluated.** In `render`, `states`, `assign` and `guard`, `ctx.x`, `item.title`,
|
|
18
|
+
`params.id` are placeholders: never use `if`, `?:`, `&&`, `.filter()`, `.map()`, `===` or template strings on
|
|
19
|
+
them. Use `op.*` for comparisons and updates, `ui.if` / `ui.each` for structure, and a `fn()` for anything else.
|
|
20
|
+
Plain JS on *constants* is fine: `['a', 'b'].map((k) => ui.option(...))`.
|
|
21
|
+
- **Side effects only through `query` / `mutation`.** Queries render with `ui.query`; a mutation runs when the
|
|
22
|
+
feature's one machine *enters* a state whose `invoke` calls it, and returns as `done` / `failed`.
|
|
23
|
+
- **Every transition needs a contract.** A behaviour change without a contract change is an error.
|
|
24
|
+
- **Absent means absent.** Optional fields are omitted, never `null`. Only `route({ params, search })` and
|
|
25
|
+
`ui.link(route, params, search)` spell "none" as `null`.
|
|
26
|
+
|
|
27
|
+
## Files
|
|
28
|
+
```
|
|
29
|
+
hozu.config.ts project(): schema adapter, site, routes, pages, features
|
|
30
|
+
routes.ts route() declarations
|
|
31
|
+
server.ts resolvers(project, implement => [...]): query/mutation implementations
|
|
32
|
+
serve.ts createServer({ build, styles, resolvers }).listen(PORT)
|
|
33
|
+
app.css @import "tailwindcss";
|
|
34
|
+
features/<name>/
|
|
35
|
+
model.ts schemas, events, query / mutation / tag / fn, the machine
|
|
36
|
+
views.ts views, contracts, feature()
|
|
37
|
+
```
|
|
38
|
+
Relative imports end in `.ts`. Any other split works too.
|
|
39
|
+
|
|
40
|
+
## Checks (from the app directory)
|
|
41
|
+
```
|
|
42
|
+
pnpm exec tsc --noEmit -p . # types
|
|
43
|
+
pnpm exec hozu validate # all rules + contracts; --json adds patches
|
|
44
|
+
pnpm exec hozu validate --update-lock # accept a clean, intended behaviour change
|
|
45
|
+
PORT=4700 node serve.ts & echo $! # run it; stop it with kill <pid>, not pkill -f
|
|
46
|
+
```
|
|
47
|
+
Each diagnostic has a `file:line`, a cause and a fix: apply the fix, do not work around the rule.
|
|
48
|
+
|
|
49
|
+
## model.ts
|
|
50
|
+
```ts
|
|
51
|
+
export const Item = z.object({ id: z.string(), title: z.string(), read: z.boolean() })
|
|
52
|
+
export const Add = event({ payload: z.object({ title: z.string() }) })
|
|
53
|
+
|
|
54
|
+
export const itemsTag = tag({ param: null }) // or tag({ param: z.string() }) → itemsTag(x)
|
|
55
|
+
export const listItems = query({
|
|
56
|
+
input: z.object({}), output: z.array(Item),
|
|
57
|
+
scope: 'public', // 'user' = per-session data (needs project session)
|
|
58
|
+
freshness: 'static', // | { revalidate: s } | { swr: s } | 'live'
|
|
59
|
+
tags: () => [itemsTag()], // optional; (input) => [...]
|
|
60
|
+
})
|
|
61
|
+
export const getItem = query({ input: Key, output: Item, errors: { NotFound: Key }, … })
|
|
62
|
+
export const addItem = mutation({
|
|
63
|
+
input: z.object({ title: z.string().min(2, 'Use at least 2 characters') }), output: Item,
|
|
64
|
+
errors: { Duplicate: z.object({ title: z.string() }) }, // optional: declared failures
|
|
65
|
+
invalidates: () => [itemsTag()], // refreshes queries with these tags
|
|
66
|
+
})
|
|
67
|
+
export const unread = fn({ // pure JS, self-contained: no imports or closures
|
|
68
|
+
input: z.object({ items: z.array(Item) }), output: z.array(Item),
|
|
69
|
+
impl: ({ items }) => items.filter((i) => !i.read),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const m = machine({
|
|
73
|
+
context: z.object({ draft: z.string(), error: z.string().nullable() }),
|
|
74
|
+
initialContext: { draft: '', error: null },
|
|
75
|
+
initial: 'idle',
|
|
76
|
+
states: ({ ctx }) => ({
|
|
77
|
+
idle: {
|
|
78
|
+
on: [
|
|
79
|
+
on(Draft, { target: 'idle', assign: (e) => [op.set(ctx.draft, e.text)] }),
|
|
80
|
+
on(Add, { target: 'adding', guard: (e) => op.neq(e.title, ''), // first matching guard wins
|
|
81
|
+
assign: (e) => [op.set(ctx.draft, e.title), op.set(ctx.error, null)] }),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
adding: {
|
|
85
|
+
ignore: [Draft, Add], // events dropped while busy (HZ005)
|
|
86
|
+
invoke: invoke(addItem, { // runs on entering the state
|
|
87
|
+
input: { title: ctx.draft },
|
|
88
|
+
done: [{ target: 'idle', assign: () => [op.set(ctx.draft, '')],
|
|
89
|
+
navigate: (r) => ui.link(itemPage, { id: r.id }) }], // optional
|
|
90
|
+
failed: { // every declared error + Unexpected
|
|
91
|
+
Duplicate: [{ target: 'idle', assign: () => [op.set(ctx.error, 'Already exists')] }],
|
|
92
|
+
Unexpected: [{ target: 'idle', assign: (e) => [op.set(ctx.error, e.message)] }],
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
flash: { after: [{ ms: 3000, target: 'idle' }] }, // timers; `final: true` for terminal states
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
- `op.set`, `op.append(list, item)`, `op.inc(n, by)`, `op.removeWhere(list, 'key', value)`.
|
|
101
|
+
- Guards: `op.eq / neq / lt / lte / gt / gte`, `op.and(...)`, `op.or(...)`, `op.not(g)`, or a boolean `fn`. The
|
|
102
|
+
reference goes on the left: `op.eq(ctx.tab, 'design')`.
|
|
103
|
+
- A transition to the same state re-enters it and re-runs its `invoke`; busy states `ignore` instead.
|
|
104
|
+
|
|
105
|
+
## views.ts
|
|
106
|
+
```ts
|
|
107
|
+
export const Board = ui.view({
|
|
108
|
+
machine: m, // optional: without it, no ctx / when / events, 0 JS
|
|
109
|
+
route: home, // optional: render gets { params, search } typed by the route
|
|
110
|
+
render: ({ ctx, when, search }) =>
|
|
111
|
+
ui.main({ class: 'mx-auto max-w-xl' }, [
|
|
112
|
+
ui.form({ on: { submit: ui.send(Add, { title: ui.dom.form('title') }) } }, [
|
|
113
|
+
ui.input({ name: 'title', required: true, value: ctx.draft,
|
|
114
|
+
on: { input: ui.send(Draft, { text: ui.dom.value }) } }),
|
|
115
|
+
ui.button({ type: 'submit' }, ['Add']),
|
|
116
|
+
]),
|
|
117
|
+
ui.if(op.neq(ctx.error, null), [ui.p({ role: 'alert' }, [ctx.error])], []),
|
|
118
|
+
when(['adding'], [ui.p({ 'aria-busy': 'true' }, ['Adding…'])]),
|
|
119
|
+
ui.query(listItems, {}, {
|
|
120
|
+
ready: (items) => ui.ul({}, [ui.each(items, 'id', (i) =>
|
|
121
|
+
ui.li({}, [ui.a({ href: ui.link(itemPage, { id: i.id }) }, [i.title])]))]),
|
|
122
|
+
pending: ui.p({}, ['Loading…']), // or null
|
|
123
|
+
failed: { Unexpected: () => ui.p({ role: 'alert' }, ['Unavailable']) },
|
|
124
|
+
}),
|
|
125
|
+
]),
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
- `ui.<tag>(attrs, children)`; attribute values are literals, references or guards.
|
|
129
|
+
- `class` is a static string of Tailwind classes that must exist (HZ026). Conditional classes:
|
|
130
|
+
`toggle: { 'bg-indigo-600 text-white': op.eq(ctx.tab, t) }`. CSS variables: `vars: { '--hue': item.hue }`.
|
|
131
|
+
- Events: `on: { click: ui.send(Event, payload) }`. Payload fields: literals, references, `ui.dom.value`,
|
|
132
|
+
`ui.dom.form('name')`, `ui.dom.checked`, `ui.dom.valueAsNumber`, `ui.dom.key`.
|
|
133
|
+
- `ui.each(list, 'id', (item) => node)` (key `null` for primitives).
|
|
134
|
+
- Links: `ui.link(route, params, search)`, never a string path (HZ032). The third argument exists only when the
|
|
135
|
+
route declares `search` (`null` = all defaults). Filters that belong in the URL are `search` links, not context.
|
|
136
|
+
- A form whose submit reads only `ui.dom.form(...)`, literals, context, params and search also works without JS.
|
|
137
|
+
|
|
138
|
+
### Contracts
|
|
139
|
+
```ts
|
|
140
|
+
export const adds = contract(m, {
|
|
141
|
+
given: { state: 'idle' }, // context: optional, defaults to initialContext
|
|
142
|
+
when: [
|
|
143
|
+
{ send: Add, payload: { title: 'A' } },
|
|
144
|
+
{ done: addItem, result: { id: 'i9', title: 'A', read: false } },
|
|
145
|
+
], // or { failed: addItem, error: 'Duplicate', data } / { elapse: ms }
|
|
146
|
+
expect: {
|
|
147
|
+
state: 'idle',
|
|
148
|
+
changes: { error: null }, // only what changes; every other field must stay equal
|
|
149
|
+
effects: [{ effect: addItem, input: { title: 'A' } }, { navigate: '/items/i9' }], // optional: none
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
Cover each `on`, `done`, `failed` and `after` once. `given.context` sets up a full context; nested objects in
|
|
154
|
+
`changes` are patches too, arrays are replaced. HZ016 prints each missing contract ready to paste.
|
|
155
|
+
|
|
156
|
+
### Feature
|
|
157
|
+
```ts
|
|
158
|
+
export const items = feature({
|
|
159
|
+
id: 'items',
|
|
160
|
+
intent: { summary: 'A reading list', invariants: ['Titles are unique'] }, // invariants optional
|
|
161
|
+
declarations: { Add, Draft, itemsTag, listItems, getItem, addItem, unread, m, Board, Detail, adds },
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
Every declaration goes in `declarations` once, under its name. Optional: `imports: [otherFeature]`,
|
|
165
|
+
`exports: [Event, query, …]` (all other features may use), `styles: [new URL('./x.css', import.meta.url)]`.
|
|
166
|
+
|
|
167
|
+
## hozu.config.ts
|
|
168
|
+
```ts
|
|
169
|
+
export default project({
|
|
170
|
+
schema: zodAdapter,
|
|
171
|
+
styles: new URL('./app.css', import.meta.url),
|
|
172
|
+
site: { url: 'http://localhost:3000', name: 'Items', lang: 'en' },
|
|
173
|
+
routes: { home, itemPage },
|
|
174
|
+
pages: [
|
|
175
|
+
ui.page(home, { views: [Board], head: { render: () => ({ title: 'Items', description: 'All items.' }) } }),
|
|
176
|
+
ui.page(itemPage, {
|
|
177
|
+
views: [Detail],
|
|
178
|
+
head: {
|
|
179
|
+
query: getItem, // its failure sets the status (NotFound → 404)
|
|
180
|
+
input: (params) => ({ id: params.id }),
|
|
181
|
+
render: (item) => ({ title: item.title, description: item.title, type: 'article' }),
|
|
182
|
+
},
|
|
183
|
+
entries: { query: listItems, input: {}, params: (item) => ({ id: item.id }) }, // sitemap
|
|
184
|
+
}),
|
|
185
|
+
],
|
|
186
|
+
features: [items],
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
```ts
|
|
190
|
+
// routes.ts
|
|
191
|
+
export const home = route({ path: '/', params: null, search: z.object({ show: Show.default('all') }) })
|
|
192
|
+
export const itemPage = route({ path: '/items/:id', params: z.object({ id: z.string() }), search: null })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## server.ts
|
|
196
|
+
```ts
|
|
197
|
+
export const createResolvers = () => resolvers(project, (implement) => [
|
|
198
|
+
implement(getItem, ({ id }, { fail }) => items.find((i) => i.id === id) ?? fail('NotFound', { id })),
|
|
199
|
+
implement(addItem, ({ title }, { fail }) => /* … */ fail('Duplicate', { title })), // → failed.Duplicate
|
|
200
|
+
])
|
|
201
|
+
```
|
|
202
|
+
Input failing its schema returns the error `Invalid` (`{ message, fields }`; see `reference.md`).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changing a Hozu app
|
|
2
|
+
|
|
3
|
+
Keep the loop short: read once, edit everything, check once, verify once.
|
|
4
|
+
|
|
5
|
+
## 1. Read
|
|
6
|
+
- The change request.
|
|
7
|
+
- The app: `features/<name>/*.ts`, `server.ts`, `routes.ts`, `hozu.config.ts`. Below, *model* is where the
|
|
8
|
+
app keeps schemas, events, effects and the machine (`model.ts` in the recommended layout), and *views* is
|
|
9
|
+
where it keeps views, contracts and `feature()`.
|
|
10
|
+
- Nothing else. The API is in `SKILL.md`; open `patterns.md` only for a pattern you have not seen in the app,
|
|
11
|
+
and `reference.md` only for a topic it lists.
|
|
12
|
+
|
|
13
|
+
## 2. Where each kind of change goes
|
|
14
|
+
| Change | Touch, in this order |
|
|
15
|
+
|---|---|
|
|
16
|
+
| New data field (e.g. `priority`) | model: the domain and input schemas → `server.ts` (seed data, store it) → views: show it, also in the detail view if there is one. If the user picks it in a form: a `<select name="…">` inside the form, sent with the submit as `ui.dom.form('…')` (HZ033 checks the options), plus the matching field in the event payload and the mutation input. |
|
|
17
|
+
| New server action (e.g. "clear done") | model: a `mutation` with `invalidates`, an event, and `on(Event)` into a new busy state that `invoke`s the mutation, with `done` and every `failed` handled and the same `ignore` list as the other busy states → views: the control, the contracts, and both new declarations in `feature({ declarations })` → `server.ts`: `implement(...)` it. |
|
|
18
|
+
| New UI-only state (a filter, a tab) | model: the context field, its initial value, an event and an `on` that `op.set`s it (add the event to every busy state's `ignore`) → views: the control, a contract, the event in `declarations`. |
|
|
19
|
+
| New page | `routes.ts` (`params`, `search`) → a view with `route`, added to `declarations` → `ui.page(...)` in `hozu.config.ts` (with `head`, and `entries` when the route has params). |
|
|
20
|
+
| New filter / sort / page number that should be in the URL | the route's `search` schema (with a default) → links with `ui.link(route, params, { key: value })` → read `search.key` in the view. No machine change. |
|
|
21
|
+
|
|
22
|
+
Whenever the machine changes:
|
|
23
|
+
- Add one contract per new transition; HZ016 prints each missing one ready to paste. A new context field needs
|
|
24
|
+
no change to existing contracts: `given` defaults to the initial context and `changes` lists only what changes.
|
|
25
|
+
- Every busy state `ignore`s every event its visible controls can send. HZ005 prints the missing `ignore` entries.
|
|
26
|
+
|
|
27
|
+
## 3. Check (once, after all edits)
|
|
28
|
+
```
|
|
29
|
+
pnpm exec tsc --noEmit -p . && pnpm exec hozu validate
|
|
30
|
+
```
|
|
31
|
+
Fix what they report. When the behaviour change is intended and everything is clean, run
|
|
32
|
+
`pnpm exec hozu validate --update-lock`. HZ018 asks for this.
|
|
33
|
+
|
|
34
|
+
## 4. Verify (once)
|
|
35
|
+
Start the server with `PORT=4700 node serve.ts & echo $!`, then use one curl script:
|
|
36
|
+
- pages: `curl -s localhost:4700/…`;
|
|
37
|
+
- mutations:
|
|
38
|
+
`curl -s -X POST localhost:4700/_hozu/effect -H 'content-type: application/json' -d '{"effect":"<feature>.<mutation>","input":{…},"keys":[]}'`.
|
|
39
|
+
|
|
40
|
+
Stop the server with `kill <pid>`, not `pkill -f` (that kills your own shell).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Hozu diagnostics
|
|
2
|
+
|
|
3
|
+
Every diagnostic carries `file:line`, a cause and a fix, and often a snippet or patch. Apply the fix; do not work
|
|
4
|
+
around the rule.
|
|
5
|
+
|
|
6
|
+
| Code | Meaning | Usual fix |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| HZ001 | state unreachable | add a transition to it or delete it |
|
|
9
|
+
| HZ002 | event handled nowhere | handle it in a state or remove it |
|
|
10
|
+
| HZ003 / HZ007 | unknown effect / reference | add it to `feature({ declarations })`, or fix the name (the patch suggests one) |
|
|
11
|
+
| HZ004 | a declared error is not handled | add every `failed` key, plus `Unexpected`, in `invoke` and `ui.query` |
|
|
12
|
+
| HZ005 | a node sends an event in a state that does not handle it | `ignore: [Event]` in that state, or show the node only via `when` |
|
|
13
|
+
| HZ006 | crossing a feature boundary | import the feature and use its `exports` |
|
|
14
|
+
| HZ008 | a path does not exist in the schema | fix the property name |
|
|
15
|
+
| HZ009 | a guardless transition shadows later ones | put guarded transitions first |
|
|
16
|
+
| HZ014 | wrong builder output | follow the builder signature |
|
|
17
|
+
| HZ015 / HZ017 | a contract fails / contract data does not match its schema | fix the machine or the contract (decide the intended behaviour first) |
|
|
18
|
+
| HZ016 | a transition without a contract | add the contract from the snippet |
|
|
19
|
+
| HZ018 | behaviour changed without a contract change | update the contracts, then `--update-lock` |
|
|
20
|
+
| HZ021 | a query or mutation without a resolver | `implement(...)` it in server.ts |
|
|
21
|
+
| HZ022 | user data in a cacheable region | keep `scope: 'user'` queries out of cached pages |
|
|
22
|
+
| HZ024 / HZ025 | route params mismatch (keys, or a schema that does not fit `:x?`/`:x+`/`:x*`) / page with params but no `entries` | align them / add `entries` |
|
|
23
|
+
| HZ026 | a class produces no CSS | fix the Tailwind class |
|
|
24
|
+
| HZ027 | a DOM field used outside an event, or wrong for this event | read `ui.dom.*` only in `ui.send` payloads |
|
|
25
|
+
| HZ028 | `img` without width/height | add both |
|
|
26
|
+
| HZ030 | `ui.html` of untrusted data | render text instead |
|
|
27
|
+
| HZ031 | a literal not allowed by its schema | use an allowed value (the patch suggests one) |
|
|
28
|
+
| HZ032 | internal link written as a string | `ui.link(route, params)` |
|
|
29
|
+
| HZ033 | DOM text into an enum, number or boolean field | a `<select>` with enum options / `valueAsNumber` / `checked` |
|
|
30
|
+
| HZ034 | a state both handles and ignores an event | remove it from one of the two |
|
|
31
|
+
| HZ035 | search schema is not a flat object of scalars with defaults | `z.object({ key: scalar.default(…) })` |
|
|
32
|
+
| HZ036 | (warning) a form needs JavaScript | read its values with `ui.dom.form('name')` |
|
|
33
|
+
| HZ037 | a redirect is not a path, hides a page or another redirect, or targets an unknown route | change or remove the `from` key; point `to` at `ui.link(...)` |
|
|
34
|
+
| HZ038 | `http.headers` sets a header the framework owns, or an invalid name/value | remove it (`cache-control` is derived; CSP is `createServer({ csp })`) |
|
|
35
|
+
| HZ039 | `basePath` is not `''` or `/segment[/segment…]` | e.g. `'/shop'`, no trailing slash |
|
|
36
|
+
| HZ040 | a locale lacks a message, or uses other `{placeholders}` | add/translate the key in that locale |
|
|
37
|
+
| HZ041 | a machine uses a message, `ui.format` or `locale` | store a code in context; choose the message in the view |
|
|
38
|
+
| HZ043 | `site.offline` has params, no page, or per-request data | point it at a static page, or remove `offline` |
|
|
39
|
+
| HZ042 | `site.locales` empty / missing `site.lang` / not a canonical tag, or `ui.alternate` of an undeclared locale | fix the list (`'zh-TW'`, not `'zh_tw'`) |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|