create-hozu 0.7.0 → 0.9.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/dist/guide.d.ts +27 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +48 -0
- package/dist/guide.js.map +1 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +40 -97
- package/skill/example/{server.ts → app.ts} +10 -9
- package/skill/example/features/bookmarks/views.ts +1 -61
- package/skill/example/hozu.config.ts +2 -0
- package/skill/topics/auth.md +10 -6
- package/skill/topics/components.md +67 -0
- package/skill/topics/content.md +1 -1
- package/skill/topics/contracts.md +6 -3
- package/skill/topics/data.md +16 -5
- package/skill/topics/deploy.md +13 -9
- package/skill/topics/diagnostics.md +36 -9
- package/skill/topics/endpoints.md +21 -9
- package/skill/topics/feature.md +79 -0
- package/skill/topics/forms.md +14 -3
- package/skill/topics/http.md +1 -1
- package/skill/topics/i18n.md +10 -5
- package/skill/topics/machine.md +1 -1
- package/skill/topics/pages.md +18 -4
- package/skill/topics/patterns.md +19 -1
- package/skill/topics/recipes.md +1 -1
- package/skill/topics/testing.md +42 -17
- package/skill/topics/views.md +9 -6
- package/templates/app/app.ts +7 -0
- package/templates/app/hozu.config.ts +1 -0
- package/templates/guide.md +4 -16
- package/skill/changing.md +0 -45
- package/skill/example/serve.ts +0 -14
- package/skill/topics/widgets.md +0 -30
- package/templates/app/serve.ts +0 -14
- package/templates/app/server.ts +0 -6
package/dist/guide.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type Runner = 'pnpm exec' | 'npx' | 'yarn' | 'bunx';
|
|
2
|
+
export declare const RUNNERS: readonly Runner[];
|
|
3
|
+
export declare const BEGIN = "<!-- hozu: generated by hozu skill; edits between these markers are replaced -->";
|
|
4
|
+
export declare const END = "<!-- /hozu -->";
|
|
5
|
+
export declare const TARGETS: readonly [{
|
|
6
|
+
readonly guide: 'CLAUDE.md';
|
|
7
|
+
readonly skill: '.claude/skills/hozu';
|
|
8
|
+
readonly read: 'Use the `hozu` skill (`__SKILL__/SKILL.md`).';
|
|
9
|
+
}, {
|
|
10
|
+
readonly guide: 'AGENTS.md';
|
|
11
|
+
readonly skill: '.agents/skills/hozu';
|
|
12
|
+
readonly read: 'Read `__SKILL__/SKILL.md` before writing or changing any Hozu code.';
|
|
13
|
+
}];
|
|
14
|
+
export type Target = (typeof TARGETS)[number];
|
|
15
|
+
export declare function currentGuide(): string;
|
|
16
|
+
/** The 0.8 guide for one target, between the markers. */
|
|
17
|
+
export declare const guideBlock: (target: Target, name: string, runner: Runner, template?: string) => string;
|
|
18
|
+
export type BlockResult = {
|
|
19
|
+
kind: 'replaced' | 'written' | 'current';
|
|
20
|
+
code: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'custom';
|
|
23
|
+
block: string;
|
|
24
|
+
};
|
|
25
|
+
/** The new Hozu block of a CLAUDE.md / AGENTS.md, between markers; a file without markers is `custom`. */
|
|
26
|
+
export declare function migrateGuide(existing: string | null, target: Target, dir: string, runner: Runner, template?: string): BlockResult;
|
|
27
|
+
//# sourceMappingURL=guide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guide.d.ts","sourceRoot":"","sources":["../src/guide.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,MAAM,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1D,eAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAAyC,CAAA;AAE9E,eAAO,MAAM,KAAK,qFAAqF,CAAA;AACvG,eAAO,MAAM,GAAG,mBAAmB,CAAA;AAEnC,eAAO,MAAM,OAAO;aAChB,KAAK,EAAE,WAAW;aAAE,KAAK,EAAE,qBAAqB;aAAE,IAAI,EAAE,8CAA8C;;aAEtG,KAAK,EAAE,WAAW;aAClB,KAAK,EAAE,qBAAqB;aAC5B,IAAI,EAAE,qEAAqE;EAErE,CAAA;AAEV,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAmB7C,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,WAAY,MAAM,QAAQ,MAAM,UAAU,MAAM,8BACW,CAAA;AAElF,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC,0GAA0G;AAC1G,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,QAAQ,SAAiB,GACxB,WAAW,CAab"}
|
package/dist/guide.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const RUNNERS = ['pnpm exec', 'npx', 'yarn', 'bunx'];
|
|
5
|
+
export const BEGIN = '<!-- hozu: generated by hozu skill; edits between these markers are replaced -->';
|
|
6
|
+
export const END = '<!-- /hozu -->';
|
|
7
|
+
export const TARGETS = [
|
|
8
|
+
{ guide: 'CLAUDE.md', skill: '.claude/skills/hozu', read: 'Use the `hozu` skill (`__SKILL__/SKILL.md`).' },
|
|
9
|
+
{
|
|
10
|
+
guide: 'AGENTS.md',
|
|
11
|
+
skill: '.agents/skills/hozu',
|
|
12
|
+
read: 'Read `__SKILL__/SKILL.md` before writing or changing any Hozu code.',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
const fill = (text, values) => Object.entries(values).reduce((out, [key, value]) => out.replaceAll(`__${key}__`, value), text);
|
|
16
|
+
const note = (runner, skillRunner) => runner === skillRunner
|
|
17
|
+
? ''
|
|
18
|
+
: `\nThe skill writes commands as \`${skillRunner} …\`; in this app use \`${runner} …\`.\n`;
|
|
19
|
+
function filled(template, target, name, runner, skillRunner) {
|
|
20
|
+
return fill(fill(template, { READ: target.read }), {
|
|
21
|
+
NAME: name,
|
|
22
|
+
RUN: runner,
|
|
23
|
+
SKILL: target.skill,
|
|
24
|
+
NOTE: note(runner, skillRunner),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function currentGuide() {
|
|
28
|
+
return readFileSync(fileURLToPath(new URL('../templates/guide.md', import.meta.url)), 'utf8');
|
|
29
|
+
}
|
|
30
|
+
/** The 0.8 guide for one target, between the markers. */
|
|
31
|
+
export const guideBlock = (target, name, runner, template = currentGuide()) => `${BEGIN}\n${filled(template, target, name, runner, 'npx').trimEnd()}\n${END}\n`;
|
|
32
|
+
/** The new Hozu block of a CLAUDE.md / AGENTS.md, between markers; a file without markers is `custom`. */
|
|
33
|
+
export function migrateGuide(existing, target, dir, runner, template = currentGuide()) {
|
|
34
|
+
const begin = existing?.indexOf(BEGIN) ?? -1;
|
|
35
|
+
const end = existing?.indexOf(END) ?? -1;
|
|
36
|
+
const own = begin >= 0 && end > begin ? existing.slice(begin, end) : existing;
|
|
37
|
+
const name = own?.match(/^# (.+)$/m)?.[1] ?? basename(dir);
|
|
38
|
+
const detected = RUNNERS.find((r) => own?.includes(`${r} hozu `)) ?? runner;
|
|
39
|
+
const block = guideBlock(target, name, detected, template);
|
|
40
|
+
if (existing === null)
|
|
41
|
+
return { kind: 'written', code: block };
|
|
42
|
+
if (begin >= 0 && end > begin) {
|
|
43
|
+
const code = existing.slice(0, begin) + block + existing.slice(end + END.length).replace(/^\n/, '');
|
|
44
|
+
return { kind: code === existing ? 'current' : 'replaced', code };
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'custom', block };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=guide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guide.js","sourceRoot":"","sources":["../src/guide.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type Runner } from './guide.ts';
|
|
1
2
|
export type Agent = 'claude' | 'agents' | 'both';
|
|
2
|
-
export type
|
|
3
|
+
export type { BlockResult, Runner, Target } from './guide.ts';
|
|
4
|
+
export { BEGIN, currentGuide, END, guideBlock, migrateGuide, RUNNERS, TARGETS } from './guide.ts';
|
|
3
5
|
export declare const AGENTS: readonly Agent[];
|
|
4
6
|
export declare const defaultSkill: string;
|
|
5
7
|
export declare const runnerOf: (userAgent: string | undefined) => Runner;
|
|
@@ -8,7 +10,14 @@ export interface AgentOptions {
|
|
|
8
10
|
runner: Runner;
|
|
9
11
|
skillSource?: string;
|
|
10
12
|
}
|
|
11
|
-
export
|
|
13
|
+
export interface AgentFiles {
|
|
14
|
+
written: string[];
|
|
15
|
+
custom: {
|
|
16
|
+
guide: string;
|
|
17
|
+
block: string;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
export declare function writeAgentFiles(dir: string, agent: Agent, options: AgentOptions): Promise<AgentFiles>;
|
|
12
21
|
export interface CreateOptions extends AgentOptions {
|
|
13
22
|
agent: Agent;
|
|
14
23
|
version: string;
|
|
@@ -24,6 +33,7 @@ export declare const packageJson: (name: string, version: string) => {
|
|
|
24
33
|
};
|
|
25
34
|
dependencies: {
|
|
26
35
|
'@hozu/adapter-node': string;
|
|
36
|
+
'@hozu/cli': string;
|
|
27
37
|
'@hozu/core': string;
|
|
28
38
|
'@hozu/css': string;
|
|
29
39
|
'@hozu/data': string;
|
|
@@ -33,7 +43,6 @@ export declare const packageJson: (name: string, version: string) => {
|
|
|
33
43
|
zod: string;
|
|
34
44
|
};
|
|
35
45
|
devDependencies: {
|
|
36
|
-
'@hozu/cli': string;
|
|
37
46
|
'@hozu/testing': string;
|
|
38
47
|
'@types/node': string;
|
|
39
48
|
typescript: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,MAAM,EAAW,MAAM,YAAY,CAAA;AAE/D,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAChD,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEjG,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EAAiC,CAAA;AAGpE,eAAO,MAAM,YAAY,QAAsB,CAAA;AAM/C,eAAO,MAAM,QAAQ,cAAe,MAAM,GAAG,SAAS,KAAG,MAGxD,CAAA;AAQD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC3C;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,CAkB3G;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,WAAW;QACX,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,sBAAsB;QACtB,kBAAkB;QAClB,iBAAiB;QACjB,GAAG;;;QAGH,eAAe;QACf,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
CHANGED
|
@@ -1,52 +1,37 @@
|
|
|
1
1
|
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { migrateGuide, TARGETS } from './guide.js';
|
|
5
|
+
export { BEGIN, currentGuide, END, guideBlock, migrateGuide, RUNNERS, TARGETS } from './guide.js';
|
|
4
6
|
export const AGENTS = ['claude', 'agents', 'both'];
|
|
5
7
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
6
8
|
export const defaultSkill = join(root, 'skill');
|
|
7
9
|
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));
|
|
10
|
+
const targets = (agent) => TARGETS.filter((_, i) => agent === 'both' || (agent === 'claude' ? i === 0 : i === 1));
|
|
20
11
|
export const runnerOf = (userAgent) => {
|
|
21
12
|
const name = userAgent?.split('/')[0];
|
|
22
13
|
return name === 'npm' ? 'npx' : name === 'yarn' ? 'yarn' : name === 'bun' ? 'bunx' : 'pnpm exec';
|
|
23
14
|
};
|
|
24
15
|
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
16
|
export async function writeAgentFiles(dir, agent, options) {
|
|
27
|
-
const
|
|
28
|
-
const guide = await readFile(join(templates, 'guide.md'), 'utf8');
|
|
17
|
+
const out = { written: [], custom: [] };
|
|
29
18
|
for (const t of targets(agent)) {
|
|
30
19
|
const skill = join(dir, t.skill);
|
|
31
20
|
await rm(skill, { recursive: true, force: true });
|
|
32
21
|
await mkdir(dirname(skill), { recursive: true });
|
|
33
22
|
await cp(options.skillSource ?? defaultSkill, skill, { recursive: true });
|
|
34
|
-
written.push(t.skill);
|
|
23
|
+
out.written.push(t.skill);
|
|
35
24
|
const file = join(dir, t.guide);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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);
|
|
25
|
+
const existing = (await exists(file)) ? await readFile(file, 'utf8') : null;
|
|
26
|
+
const result = migrateGuide(existing, t, dir, options.runner);
|
|
27
|
+
if (result.kind === 'custom')
|
|
28
|
+
out.custom.push({ guide: t.guide, block: result.block });
|
|
29
|
+
else if (result.kind !== 'current') {
|
|
30
|
+
await writeFile(file, result.code);
|
|
31
|
+
out.written.push(t.guide);
|
|
32
|
+
}
|
|
48
33
|
}
|
|
49
|
-
return
|
|
34
|
+
return out;
|
|
50
35
|
}
|
|
51
36
|
async function files(dir) {
|
|
52
37
|
const out = [];
|
|
@@ -65,11 +50,12 @@ export const packageJson = (name, version) => ({
|
|
|
65
50
|
type: 'module',
|
|
66
51
|
scripts: {
|
|
67
52
|
check: 'hozu check',
|
|
68
|
-
start: '
|
|
53
|
+
start: 'hozu serve',
|
|
69
54
|
build: 'hozu build',
|
|
70
55
|
},
|
|
71
56
|
dependencies: {
|
|
72
57
|
'@hozu/adapter-node': `^${version}`,
|
|
58
|
+
'@hozu/cli': `^${version}`,
|
|
73
59
|
'@hozu/core': `^${version}`,
|
|
74
60
|
'@hozu/css': `^${version}`,
|
|
75
61
|
'@hozu/data': `^${version}`,
|
|
@@ -79,7 +65,6 @@ export const packageJson = (name, version) => ({
|
|
|
79
65
|
zod: '^4.6.5',
|
|
80
66
|
},
|
|
81
67
|
devDependencies: {
|
|
82
|
-
'@hozu/cli': `^${version}`,
|
|
83
68
|
'@hozu/testing': `^${version}`,
|
|
84
69
|
'@types/node': '^22.20.4',
|
|
85
70
|
typescript: '^7.0.2',
|
|
@@ -95,12 +80,12 @@ export async function createApp(dir, options) {
|
|
|
95
80
|
const rel = relative(app, source);
|
|
96
81
|
const target = join(dir, rel === 'gitignore' ? '.gitignore' : rel);
|
|
97
82
|
await mkdir(dirname(target), { recursive: true });
|
|
98
|
-
await writeFile(target,
|
|
83
|
+
await writeFile(target, (await readFile(source, 'utf8')).replaceAll('__NAME__', options.name));
|
|
99
84
|
written.push(relative(dir, target));
|
|
100
85
|
}
|
|
101
86
|
await writeFile(join(dir, 'package.json'), `${JSON.stringify(packageJson(options.name, options.version), null, 2)}\n`);
|
|
102
87
|
written.push('package.json');
|
|
103
|
-
written.push(...(await writeAgentFiles(dir, options.agent, options)));
|
|
88
|
+
written.push(...(await writeAgentFiles(dir, options.agent, options)).written);
|
|
104
89
|
return written;
|
|
105
90
|
}
|
|
106
91
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACpF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -5,108 +5,51 @@ description: Build or change an app with the Hozu framework (packages @hozu/*, f
|
|
|
5
5
|
|
|
6
6
|
# Hozu
|
|
7
7
|
|
|
8
|
-
Hozu is not in your training data
|
|
9
|
-
|
|
10
|
-
- **Changing:** read `changing.md`, then only the lines `hozu map` points to.
|
|
11
|
-
- **Anything else:** `hozu docs <topic>` prints one short topic (index below). Diagnostics name the topic too.
|
|
8
|
+
Hozu is not in your training data: this file and `npx hozu docs <topic>` are the whole API (skip
|
|
9
|
+
`node_modules/@hozu`). Diagnostics teach the other rules when you break them: apply their fix.
|
|
12
10
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`done` / `failed` leave it. Contracts are needed only where a transition decides (a guard, `navigate`, a `fn`).
|
|
23
|
-
- A filter in the URL starts the machine: `seed: ({ search }) => ({ q: search.q })` on the view, then read `ctx.q`.
|
|
24
|
-
`machine({ on })` holds transitions every idle state shares; `fn` bodies may call helpers from the same module.
|
|
11
|
+
## The change loop
|
|
12
|
+
1. `npx hozu map`: the session shape, the verify line, every file's role, each declaration's `file:line`. Open
|
|
13
|
+
only the lines the change touches. A new app: `npx hozu add feature <name> --page / --with …`, then
|
|
14
|
+
`npx hozu docs feature`.
|
|
15
|
+
2. Edit everything the change needs (table below).
|
|
16
|
+
3. `npx hozu check` once. For an intended behaviour change, `npx hozu check --update-lock`, then list the accepted
|
|
17
|
+
`now:` lines in your summary.
|
|
18
|
+
4. Verify once with the line `hozu map` prints: `npx hozu browse <path> --session '…' --js both --do '…'`. It runs
|
|
19
|
+
the same app as `npm start`, with and without JS; do not start a server or use `curl`.
|
|
25
20
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
npx hozu add feature items --page / --with auth,detail,toggle,filter,remove
|
|
36
|
-
npx hozu map # outline with file:line
|
|
37
|
-
npx hozu get / --select button --forms # a page, no server needed
|
|
38
|
-
npx hozu post / --field title=A --next / # a no-JS form post, cookies shown
|
|
39
|
-
npx hozu browse / --do 'fill Search=a' --do 'click Save' # real browser: errors, widgets, text
|
|
40
|
-
npx hozu docs views # one topic
|
|
41
|
-
```
|
|
42
|
-
`get` / `post` / `browse` replace a running server for checks; `npm start` runs the app. Relative imports end in `.ts`.
|
|
21
|
+
## What to touch
|
|
22
|
+
| Change | Touch |
|
|
23
|
+
|---|---|
|
|
24
|
+
| UI-only state (a tab) | model: a context field, an event, an `on` whose `assign` sets it → views: the control |
|
|
25
|
+
| Filter / sort in the URL | the route's `search` (with a default) → `ui.link(route, params, { key })` → `search.key`; filtering while typing: `seed: ({ search }) => ({ key: search.key })`, read `ctx.key` |
|
|
26
|
+
| A per-item action stored on the server (pin, archive) | model: the item field, an event, a mutation that `invalidates` the list tag, `on(E, { target: 'pinning', assign: (e) => { ctx.target = e.id } })`, a state with `invoke` → views: the per-item form (`hozu docs patterns`) → app: store it, sort in the list resolver |
|
|
27
|
+
| A control every state handles | `machine({ on: [...] })` |
|
|
28
|
+
| New page | `routes.ts` → a view with `route` → `ui.page(...)` in `hozu.config.ts` |
|
|
29
|
+
| UI (a button, a field) | `ui.use` of a kit component; the catalog: `npx hozu docs components` |
|
|
43
30
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
freshness: 'static', tags: () => [itemsTag()] })
|
|
52
|
-
export const addItem = mutation({ input: z.object({ title: z.string().min(2, 'Too short') }), output: Item,
|
|
53
|
-
errors: { Duplicate: z.object({ title: z.string() }) }, invalidates: () => [itemsTag()] })
|
|
54
|
-
export const items = machine({
|
|
55
|
-
context: z.object({ draft: z.string(), error: z.string().nullable() }),
|
|
56
|
-
initialContext: { draft: '', error: null },
|
|
57
|
-
initial: 'idle',
|
|
58
|
-
states: ({ ctx }) => ({
|
|
59
|
-
idle: { on: [on(Add, { target: 'adding', assign: (e) => { ctx.draft = e.title; ctx.error = null } })] },
|
|
60
|
-
adding: {
|
|
61
|
-
invoke: invoke(addItem, {
|
|
62
|
-
input: { title: ctx.draft },
|
|
63
|
-
done: { target: 'idle', assign: () => { ctx.draft = '' } },
|
|
64
|
-
failed: {
|
|
65
|
-
Duplicate: { target: 'idle', assign: () => { ctx.error = 'Already listed' } },
|
|
66
|
-
Unexpected: { target: 'idle', assign: (e) => { ctx.error = e.message } },
|
|
67
|
-
},
|
|
68
|
-
}),
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
})
|
|
72
|
-
// views.ts
|
|
73
|
-
export const Board = ui.view({
|
|
74
|
-
machine: items,
|
|
75
|
-
render: ({ ctx, when }) =>
|
|
76
|
-
ui.main({ class: 'mx-auto max-w-xl' }, [
|
|
77
|
-
ui.form({ on: { submit: ui.send(Add, { title: ui.dom.form('title') }) } }, [
|
|
78
|
-
ui.input({ name: 'title', required: true, value: ctx.draft }),
|
|
79
|
-
ui.button({ type: 'submit' }, ['Add']),
|
|
80
|
-
]),
|
|
81
|
-
ctx.error !== null && ui.p({ role: 'alert' }, [ctx.error]),
|
|
82
|
-
when(['adding'], [ui.p({ 'aria-busy': 'true' }, [`Adding ${ctx.draft}…`])]),
|
|
83
|
-
ui.query(listItems, {}, {
|
|
84
|
-
ready: (list) => ui.ul({}, [ui.each(list, 'id', (i) => ui.li({}, [i.title, i.done ? ' ✓' : '']))]),
|
|
85
|
-
failed: { Unexpected: () => ui.p({ role: 'alert' }, ['Unavailable']) },
|
|
86
|
-
}),
|
|
87
|
-
]),
|
|
88
|
-
})
|
|
89
|
-
// feature.ts
|
|
90
|
-
import * as model from './model.ts'
|
|
91
|
-
import * as views from './views.ts'
|
|
92
|
-
export const todos = feature({ id: 'todos', intent: { summary: 'A to-do list' }, declarations: [model, views] })
|
|
93
|
-
```
|
|
94
|
-
Every declaration a listed module exports is registered under its name; schemas and helpers are ignored. Resolvers:
|
|
95
|
-
`implement(addItem, ({ title }, { fail }) => exists ? fail('Duplicate', { title }) : save(title))`.
|
|
31
|
+
## Rules no diagnostic checks
|
|
32
|
+
- **Query resolvers only read.** Writes belong in mutation and endpoint resolvers; a prefetched link runs queries.
|
|
33
|
+
- **Other users, reloads, sign-out:** verify them in one `browse` chain with `--js both` (`--as <name>` per user).
|
|
34
|
+
- **Contracts only where a transition decides:** a guard, a `navigate`, a `fn` or computed value. Copy-only
|
|
35
|
+
transitions are reviewed in the lock.
|
|
36
|
+
- **`'live'` only for push** to pages that are already open.
|
|
37
|
+
- **`invalidates` drives the client refresh** of every query with those tags, whatever its freshness.
|
|
96
38
|
|
|
97
|
-
## Topics (`hozu docs <topic>`)
|
|
39
|
+
## Topics (`npx hozu docs <topic>`)
|
|
98
40
|
| Task | Topic |
|
|
99
41
|
|---|---|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
42
|
+
| a new feature, the files, a complete example | `feature` |
|
|
43
|
+
| elements, attributes, events, lists, links, reuse | `views` |
|
|
44
|
+
| states, events, invoke, timers, guards | `machine` |
|
|
45
|
+
| queries, mutations, tags, `fn()`, resolvers | `data` |
|
|
46
|
+
| contracts and the lock | `contracts` |
|
|
47
|
+
| routes, search, pages, `head`, 403 / 404 | `pages` |
|
|
48
|
+
| forms, checkboxes, bulk forms, no JS | `forms` |
|
|
106
49
|
| sign-in, sessions, per-user data | `auth` |
|
|
107
|
-
|
|
|
108
|
-
| worked changes
|
|
109
|
-
| webhooks
|
|
110
|
-
| browser APIs
|
|
111
|
-
| languages, env, HTTP, Markdown,
|
|
50
|
+
| filters, modes, per-item actions, load more | `patterns` |
|
|
51
|
+
| worked changes | `recipes` |
|
|
52
|
+
| webhooks, JSON APIs, redirects | `endpoints` |
|
|
53
|
+
| components, kits, browser APIs, DOM libraries | `components` |
|
|
54
|
+
| languages, env, HTTP, Markdown, tests, deploying | `i18n` `env` `http` `content` `testing` `deploy` |
|
|
112
55
|
| a diagnostic code | `diagnostics` |
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { resolvers } from '@hozu/data'
|
|
2
|
+
import { app } from '@hozu/runtime-server'
|
|
2
3
|
import { addBookmark, getBookmark, listBookmarks, toggleRead } from './features/bookmarks/model.ts'
|
|
3
4
|
import project from './hozu.config.ts'
|
|
4
5
|
|
|
5
6
|
type Kind = 'article' | 'video' | 'podcast'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const items = [
|
|
9
|
+
{ id: 'b1', title: 'Closed-world UI', kind: 'article' as Kind, read: false },
|
|
10
|
+
{ id: 'b2', title: 'Islands explained', kind: 'video' as Kind, read: true },
|
|
11
|
+
]
|
|
12
|
+
let seq = items.length
|
|
13
|
+
export default app({
|
|
14
|
+
resolvers: resolvers(project, (implement) => [
|
|
14
15
|
implement(listBookmarks, () => items.map((b) => ({ ...b }))),
|
|
15
16
|
implement(getBookmark, ({ id }, { fail }) => {
|
|
16
17
|
const b = items.find((x) => x.id === id)
|
|
@@ -30,5 +31,5 @@ export function createResolvers() {
|
|
|
30
31
|
b.read = !b.read
|
|
31
32
|
return { ...b }
|
|
32
33
|
}),
|
|
33
|
-
])
|
|
34
|
-
}
|
|
34
|
+
]),
|
|
35
|
+
})
|
|
@@ -5,12 +5,10 @@ import {
|
|
|
5
5
|
addBookmark,
|
|
6
6
|
bookmarksMachine,
|
|
7
7
|
Draft,
|
|
8
|
-
DUPLICATE,
|
|
9
8
|
getBookmark,
|
|
10
9
|
isEmpty,
|
|
11
10
|
listBookmarks,
|
|
12
11
|
ToggleRead,
|
|
13
|
-
toggleRead,
|
|
14
12
|
visible,
|
|
15
13
|
} from './model.ts'
|
|
16
14
|
|
|
@@ -131,16 +129,10 @@ export const Detail = ui.view({
|
|
|
131
129
|
},
|
|
132
130
|
},
|
|
133
131
|
),
|
|
134
|
-
ui.a({ href: ui.link(home, null
|
|
132
|
+
ui.a({ href: ui.link(home, null), class: 'underline' }, ['Back']),
|
|
135
133
|
]),
|
|
136
134
|
})
|
|
137
135
|
|
|
138
|
-
export const typesDraft = contract(bookmarksMachine, {
|
|
139
|
-
given: { state: 'idle' },
|
|
140
|
-
when: [{ send: Draft, payload: { text: 'Hozu' } }],
|
|
141
|
-
expect: { state: 'idle', changes: { draft: 'Hozu' } },
|
|
142
|
-
})
|
|
143
|
-
|
|
144
136
|
export const addsBookmark = contract(bookmarksMachine, {
|
|
145
137
|
given: { state: 'idle' },
|
|
146
138
|
when: [
|
|
@@ -157,55 +149,3 @@ export const addsBookmark = contract(bookmarksMachine, {
|
|
|
157
149
|
],
|
|
158
150
|
},
|
|
159
151
|
})
|
|
160
|
-
|
|
161
|
-
export const rejectsDuplicate = contract(bookmarksMachine, {
|
|
162
|
-
given: { state: 'adding' },
|
|
163
|
-
when: [{ failed: addBookmark, error: 'Duplicate', data: { title: 'Hozu talk' } }],
|
|
164
|
-
expect: { state: 'idle', changes: { error: DUPLICATE } },
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
export const rejectsInvalidTitle = contract(bookmarksMachine, {
|
|
168
|
-
given: { state: 'adding' },
|
|
169
|
-
when: [
|
|
170
|
-
{
|
|
171
|
-
failed: addBookmark,
|
|
172
|
-
error: 'Invalid',
|
|
173
|
-
data: {
|
|
174
|
-
message: 'title: Use at least 2 characters',
|
|
175
|
-
fields: { title: 'Use at least 2 characters', kind: null },
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
expect: { state: 'idle', changes: { fields: { title: 'Use at least 2 characters' } } },
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
export const addFails = contract(bookmarksMachine, {
|
|
183
|
-
given: { state: 'adding' },
|
|
184
|
-
when: [{ failed: addBookmark, error: 'Unexpected', data: { message: 'offline' } }],
|
|
185
|
-
expect: { state: 'idle', changes: { error: 'offline' } },
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
export const togglesRead = contract(bookmarksMachine, {
|
|
189
|
-
given: { state: 'idle' },
|
|
190
|
-
when: [
|
|
191
|
-
{ send: ToggleRead, payload: { id: 'b1' } },
|
|
192
|
-
{ done: toggleRead, result: { id: 'b1', title: 'A', kind: 'article', read: true } },
|
|
193
|
-
],
|
|
194
|
-
expect: {
|
|
195
|
-
state: 'idle',
|
|
196
|
-
changes: { target: 'b1' },
|
|
197
|
-
effects: [{ effect: toggleRead, input: { id: 'b1' } }],
|
|
198
|
-
},
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
export const toggleMissing = contract(bookmarksMachine, {
|
|
202
|
-
given: { state: 'toggling' },
|
|
203
|
-
when: [{ failed: toggleRead, error: 'NotFound', data: { id: 'b9' } }],
|
|
204
|
-
expect: { state: 'idle' },
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
export const toggleFails = contract(bookmarksMachine, {
|
|
208
|
-
given: { state: 'toggling' },
|
|
209
|
-
when: [{ failed: toggleRead, error: 'Unexpected', data: { message: 'offline' } }],
|
|
210
|
-
expect: { state: 'idle', changes: { error: 'offline' } },
|
|
211
|
-
})
|
|
@@ -7,6 +7,7 @@ import { bookmarkPage, home } from './routes.ts'
|
|
|
7
7
|
|
|
8
8
|
export default project({
|
|
9
9
|
schema: zodAdapter,
|
|
10
|
+
app: new URL('./app.ts', import.meta.url),
|
|
10
11
|
styles: new URL('./app.css', import.meta.url),
|
|
11
12
|
site: { url: 'http://localhost:3000', name: 'Bookmarks', lang: 'en' },
|
|
12
13
|
routes: { home, bookmarkPage },
|
|
@@ -20,6 +21,7 @@ export default project({
|
|
|
20
21
|
head: {
|
|
21
22
|
query: getBookmark,
|
|
22
23
|
input: (params) => ({ id: params.id }),
|
|
24
|
+
failed: { NotFound: 404 },
|
|
23
25
|
render: (b) => ({ title: b.title, description: b.title, type: 'article' }),
|
|
24
26
|
},
|
|
25
27
|
entries: { query: listBookmarks, input: {}, params: (b) => ({ id: b.id }) },
|
package/skill/topics/auth.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Sign-in, sessions, per-user data
|
|
2
2
|
|
|
3
3
|
- **Start from the scaffold:** `hozu add feature notes --page / --with auth` writes `features/account` (sign-in page,
|
|
4
|
-
sign-out, `me`),
|
|
5
|
-
|
|
6
|
-
`SESSION_SECURE=true` behind HTTPS).
|
|
4
|
+
sign-out, `me`), per-user resolvers, and a redirect to `/login` when signed out. Replace the name-only sign-in with
|
|
5
|
+
real credentials before production; set `SESSION_SECRET` (production without it refuses to start).
|
|
7
6
|
- `project({ session: z.object({ user: z.string() }) })` declares the identity. Queries with `scope: 'user'` and all
|
|
8
7
|
mutations receive `session`; public queries never do. A mutation calls `setSession(value)` (or `null` to sign out).
|
|
9
|
-
-
|
|
10
|
-
HttpOnly
|
|
11
|
-
|
|
8
|
+
- Sessions live on the server: the default store is `memorySessions()` (from `@hozu/runtime-server`); the cookie holds
|
|
9
|
+
only an opaque, signed, HttpOnly id, so `setSession(null)` revokes it and the session never reaches browser
|
|
10
|
+
JavaScript. It is per process: a restart signs everyone out, and an edge or multi-instance deployment passes a
|
|
11
|
+
shared store explicitly (`createHandler({ session })`).
|
|
12
|
+
- `setSession` also applies in a failing mutation (expiry: `setSession(null)` then `fail('Expired', …)`).
|
|
13
|
+
- After a sign-in or sign-out the page's queries are re-read with the new session; nothing from the old one stays.
|
|
14
|
+
- Guard pages: `head: { query: me, …, failed: { Unauthorized: login } }`; a role check answers 403 with
|
|
15
|
+
`failed: { Unauthorized: login, Forbidden: 403 }` (`hozu docs pages`).
|
|
12
16
|
- Calling another API with a token: keep the token in the session (server side) and read it in the resolver.
|