create-hozu 0.6.0 → 0.8.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 +29 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +83 -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 +39 -92
- package/skill/example/{server.ts → app.ts} +10 -9
- package/skill/example/features/bookmarks/feature.ts +13 -0
- package/skill/example/features/bookmarks/views.ts +2 -95
- package/skill/example/hozu.config.ts +4 -1
- package/skill/topics/auth.md +10 -6
- package/skill/topics/content.md +1 -1
- package/skill/topics/contracts.md +8 -5
- package/skill/topics/data.md +19 -6
- package/skill/topics/deploy.md +13 -9
- package/skill/topics/diagnostics.md +28 -11
- 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 +11 -6
- package/skill/topics/machine.md +8 -2
- package/skill/topics/pages.md +18 -4
- package/skill/topics/patterns.md +34 -4
- package/skill/topics/recipes.md +48 -0
- package/skill/topics/testing.md +42 -17
- package/skill/topics/views.md +12 -7
- package/skill/topics/widgets.md +6 -4
- package/templates/app/app.ts +7 -0
- package/templates/app/features/site/feature.ts +8 -0
- package/templates/app/features/site/views.ts +1 -7
- package/templates/app/hozu.config.ts +3 -1
- package/templates/guide.md +4 -16
- package/skill/changing.md +0 -91
- package/skill/example/serve.ts +0 -14
- package/templates/app/serve.ts +0 -14
- package/templates/app/server.ts +0 -6
package/dist/guide.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/** The released 0.3–0.7 guide, as `create-hozu` wrote it (0.6 added the browse line). */
|
|
16
|
+
export declare const GUIDE_07 = "# __NAME__\n\nA web app built with Hozu (`@hozu/*`). Hozu is not in your training data.\n\n## Before writing code\n- __READ__\n- Changing existing code: read `__SKILL__/changing.md` first.\n- The files in `__SKILL__/` are the whole API. Do not read the framework source in `node_modules/@hozu`.\n\n## The loop\n```\n__RUN__ hozu add feature tasks --page / --with detail,toggle,filter,remove # then edit the texts it lists\n # add auth to the list for sign-in and per-user data\n__RUN__ hozu map # outline of the app with file:line, before a change\n__RUN__ hozu check # after every change: types, rules, contracts\n__RUN__ hozu check --update-lock # only to accept a clean, intended behaviour change\n__RUN__ hozu get / --select button --forms # try pages without a server: text, attributes, forms\n__RUN__ hozu post / --field title=Ship --next / # submit a form like a browser\n__RUN__ hozu browse / --do 'click Save' # real browser, no server: errors, widgets, text after steps\n```\n__NOTE__\n## Rules\n- After `hozu add feature`, do not print the generated files: edit the texts it lists; `hozu map` shows the rest.\n- Apply the fix each diagnostic gives; do not work around a rule.\n- Every behaviour change comes with a contract change.\n- Do not edit `__SKILL__/`: `__RUN__ hozu skill` rewrites it for the installed Hozu version.\n";
|
|
17
|
+
export declare function currentGuide(): string;
|
|
18
|
+
/** The 0.8 guide for one target, between the markers. */
|
|
19
|
+
export declare const guideBlock: (target: Target, name: string, runner: Runner, template?: string) => string;
|
|
20
|
+
export type BlockResult = {
|
|
21
|
+
kind: 'replaced' | 'marked' | 'written' | 'current';
|
|
22
|
+
code: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'custom';
|
|
25
|
+
block: string;
|
|
26
|
+
};
|
|
27
|
+
/** The new Hozu block of a CLAUDE.md / AGENTS.md: between markers, or the whole file when it is a known template. */
|
|
28
|
+
export declare function migrateGuide(existing: string | null, target: Target, dir: string, runner: Runner, template?: string): BlockResult;
|
|
29
|
+
//# 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;AAK7C,yFAAyF;AACzF,eAAO,MAAM,QAAQ,66CAyBpB,CAAA;AAqBD,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,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC,qHAAqH;AACrH,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,CAkBb"}
|
package/dist/guide.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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 BROWSE = "__RUN__ hozu browse / --do 'click Save' # real browser, no server: errors, widgets, text after steps\n";
|
|
16
|
+
/** The released 0.3–0.7 guide, as `create-hozu` wrote it (0.6 added the browse line). */
|
|
17
|
+
export const GUIDE_07 = `# __NAME__
|
|
18
|
+
|
|
19
|
+
A web app built with Hozu (\`@hozu/*\`). Hozu is not in your training data.
|
|
20
|
+
|
|
21
|
+
## Before writing code
|
|
22
|
+
- __READ__
|
|
23
|
+
- Changing existing code: read \`__SKILL__/changing.md\` first.
|
|
24
|
+
- The files in \`__SKILL__/\` are the whole API. Do not read the framework source in \`node_modules/@hozu\`.
|
|
25
|
+
|
|
26
|
+
## The loop
|
|
27
|
+
\`\`\`
|
|
28
|
+
__RUN__ hozu add feature tasks --page / --with detail,toggle,filter,remove # then edit the texts it lists
|
|
29
|
+
# add auth to the list for sign-in and per-user data
|
|
30
|
+
__RUN__ hozu map # outline of the app with file:line, before a change
|
|
31
|
+
__RUN__ hozu check # after every change: types, rules, contracts
|
|
32
|
+
__RUN__ hozu check --update-lock # only to accept a clean, intended behaviour change
|
|
33
|
+
__RUN__ hozu get / --select button --forms # try pages without a server: text, attributes, forms
|
|
34
|
+
__RUN__ hozu post / --field title=Ship --next / # submit a form like a browser
|
|
35
|
+
${BROWSE}\`\`\`
|
|
36
|
+
__NOTE__
|
|
37
|
+
## Rules
|
|
38
|
+
- After \`hozu add feature\`, do not print the generated files: edit the texts it lists; \`hozu map\` shows the rest.
|
|
39
|
+
- Apply the fix each diagnostic gives; do not work around a rule.
|
|
40
|
+
- Every behaviour change comes with a contract change.
|
|
41
|
+
- Do not edit \`__SKILL__/\`: \`__RUN__ hozu skill\` rewrites it for the installed Hozu version.
|
|
42
|
+
`;
|
|
43
|
+
const KNOWN = [GUIDE_07, GUIDE_07.replace(BROWSE, '')];
|
|
44
|
+
const fill = (text, values) => Object.entries(values).reduce((out, [key, value]) => out.replaceAll(`__${key}__`, value), text);
|
|
45
|
+
const note = (runner, skillRunner) => runner === skillRunner
|
|
46
|
+
? ''
|
|
47
|
+
: `\nThe skill writes commands as \`${skillRunner} …\`; in this app use \`${runner} …\`.\n`;
|
|
48
|
+
function filled(template, target, name, runner, skillRunner) {
|
|
49
|
+
return fill(fill(template, { READ: target.read }), {
|
|
50
|
+
NAME: name,
|
|
51
|
+
RUN: runner,
|
|
52
|
+
SKILL: target.skill,
|
|
53
|
+
NOTE: note(runner, skillRunner),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function currentGuide() {
|
|
57
|
+
return readFileSync(fileURLToPath(new URL('../templates/guide.md', import.meta.url)), 'utf8');
|
|
58
|
+
}
|
|
59
|
+
/** The 0.8 guide for one target, between the markers. */
|
|
60
|
+
export const guideBlock = (target, name, runner, template = currentGuide()) => `${BEGIN}\n${filled(template, target, name, runner, 'npx').trimEnd()}\n${END}\n`;
|
|
61
|
+
/** The new Hozu block of a CLAUDE.md / AGENTS.md: between markers, or the whole file when it is a known template. */
|
|
62
|
+
export function migrateGuide(existing, target, dir, runner, template = currentGuide()) {
|
|
63
|
+
const begin = existing?.indexOf(BEGIN) ?? -1;
|
|
64
|
+
const end = existing?.indexOf(END) ?? -1;
|
|
65
|
+
const own = begin >= 0 && end > begin ? existing.slice(begin, end) : existing;
|
|
66
|
+
const name = own?.match(/^# (.+)$/m)?.[1] ?? basename(dir);
|
|
67
|
+
const detected = RUNNERS.find((r) => own?.includes(`${r} hozu `)) ?? runner;
|
|
68
|
+
const block = guideBlock(target, name, detected, template);
|
|
69
|
+
if (existing === null)
|
|
70
|
+
return { kind: 'written', code: block };
|
|
71
|
+
if (begin >= 0 && end > begin) {
|
|
72
|
+
const code = existing.slice(0, begin) + block + existing.slice(end + END.length).replace(/^\n/, '');
|
|
73
|
+
return { kind: code === existing ? 'current' : 'replaced', code };
|
|
74
|
+
}
|
|
75
|
+
for (const r of RUNNERS) {
|
|
76
|
+
if (KNOWN.some((known) => existing === filled(known, target, name, r, 'pnpm exec')))
|
|
77
|
+
return { kind: 'marked', code: block };
|
|
78
|
+
if (existing === filled(template, target, name, r, 'npx'))
|
|
79
|
+
return { kind: 'marked', code: block };
|
|
80
|
+
}
|
|
81
|
+
return { kind: 'custom', block };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=guide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guide.js","sourceRoot":"","sources":["../src/guide.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAIxC,MAAM,CAAC,MAAM,OAAO,GAAsB,CAAC,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAE9E,MAAM,CAAC,MAAM,KAAK,GAAG,kFAAkF,CAAA;AACvG,MAAM,CAAC,MAAM,GAAG,GAAG,gBAAgB,CAAA;AAEnC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,qBAAqB,EAAE,IAAI,EAAE,8CAA8C,EAAE;IAC1G;QACE,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE,qBAAqB;QAC5B,IAAI,EAAE,qEAAqE;KAC5E;CACO,CAAA;AAIV,MAAM,MAAM,GACV,4GAA4G,CAAA;AAE9G,yFAAyF;AACzF,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;EAkBtB,MAAM;;;;;;;CAOP,CAAA;AAED,MAAM,KAAK,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAA;AAEtD,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,MAA8B,EAAE,EAAE,CAC5D,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;AAEjG,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,WAAmB,EAAE,EAAE,CACnD,MAAM,KAAK,WAAW;IACpB,CAAC,CAAC,EAAE;IACJ,CAAC,CAAC,oCAAoC,WAAW,2BAA2B,MAAM,SAAS,CAAA;AAE/F,SAAS,MAAM,CAAC,QAAgB,EAAE,MAAc,EAAE,IAAY,EAAE,MAAc,EAAE,WAAmB;IACjG,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE;QACjD,IAAI,EAAE,IAAI;QACV,GAAG,EAAE,MAAM;QACX,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC;KAChC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,uBAAuB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;AAC/F,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,MAAc,EAAE,QAAQ,GAAG,YAAY,EAAE,EAAE,EAAE,CACpG,GAAG,KAAK,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI,CAAA;AAMlF,qHAAqH;AACrH,MAAM,UAAU,YAAY,CAC1B,QAAuB,EACvB,MAAc,EACd,GAAW,EACX,MAAc,EACd,QAAQ,GAAG,YAAY,EAAE;IAEzB,MAAM,KAAK,GAAG,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IACxC,MAAM,GAAG,GAAG,KAAK,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,QAAS,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAC9E,MAAM,IAAI,GAAG,GAAG,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAA;IAC1D,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,MAAM,CAAA;IAC3E,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC1D,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IAC9D,IAAI,KAAK,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QACnG,OAAO,EAAE,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,CAAA;IACnE,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;YACjF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;QACxC,IAAI,QAAQ,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IACnG,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AAClC,CAAC"}
|
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, GUIDE_07, 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,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAE3G,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, GUIDE_07, 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,103 +5,50 @@ 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`).
|
|
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`.
|
|
23
20
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
npx hozu check --update-lock # accept an intended behaviour change
|
|
33
|
-
npx hozu add feature items --page / --with auth,detail,toggle,filter,remove
|
|
34
|
-
npx hozu map # outline with file:line
|
|
35
|
-
npx hozu get / --select button --forms # a page, no server needed
|
|
36
|
-
npx hozu post / --field title=A --next / # a no-JS form post, cookies shown
|
|
37
|
-
npx hozu browse / --do 'fill Search=a' --do 'click Save' # real browser: errors, widgets, text
|
|
38
|
-
npx hozu docs views # one topic
|
|
39
|
-
```
|
|
40
|
-
`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` |
|
|
41
29
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
freshness: 'static', tags: () => [itemsTag()] })
|
|
50
|
-
export const addItem = mutation({ input: z.object({ title: z.string().min(2, 'Too short') }), output: Item,
|
|
51
|
-
errors: { Duplicate: z.object({ title: z.string() }) }, invalidates: () => [itemsTag()] })
|
|
52
|
-
export const items = machine({
|
|
53
|
-
context: z.object({ draft: z.string(), error: z.string().nullable() }),
|
|
54
|
-
initialContext: { draft: '', error: null },
|
|
55
|
-
initial: 'idle',
|
|
56
|
-
states: ({ ctx }) => ({
|
|
57
|
-
idle: { on: [on(Add, { target: 'adding', assign: (e) => { ctx.draft = e.title; ctx.error = null } })] },
|
|
58
|
-
adding: {
|
|
59
|
-
invoke: invoke(addItem, {
|
|
60
|
-
input: { title: ctx.draft },
|
|
61
|
-
done: { target: 'idle', assign: () => { ctx.draft = '' } },
|
|
62
|
-
failed: {
|
|
63
|
-
Duplicate: { target: 'idle', assign: () => { ctx.error = 'Already listed' } },
|
|
64
|
-
Unexpected: { target: 'idle', assign: (e) => { ctx.error = e.message } },
|
|
65
|
-
},
|
|
66
|
-
}),
|
|
67
|
-
},
|
|
68
|
-
}),
|
|
69
|
-
})
|
|
70
|
-
// views.ts
|
|
71
|
-
export const Board = ui.view({
|
|
72
|
-
machine: items,
|
|
73
|
-
render: ({ ctx, when }) =>
|
|
74
|
-
ui.main({ class: 'mx-auto max-w-xl' }, [
|
|
75
|
-
ui.form({ on: { submit: ui.send(Add, { title: ui.dom.form('title') }) } }, [
|
|
76
|
-
ui.input({ name: 'title', required: true, value: ctx.draft }),
|
|
77
|
-
ui.button({ type: 'submit' }, ['Add']),
|
|
78
|
-
]),
|
|
79
|
-
ctx.error !== null && ui.p({ role: 'alert' }, [ctx.error]),
|
|
80
|
-
when(['adding'], [ui.p({ 'aria-busy': 'true' }, [`Adding ${ctx.draft}…`])]),
|
|
81
|
-
ui.query(listItems, {}, {
|
|
82
|
-
ready: (list) => ui.ul({}, [ui.each(list, 'id', (i) => ui.li({}, [i.title, i.done ? ' ✓' : '']))]),
|
|
83
|
-
failed: { Unexpected: () => ui.p({ role: 'alert' }, ['Unavailable']) },
|
|
84
|
-
}),
|
|
85
|
-
]),
|
|
86
|
-
})
|
|
87
|
-
export const todos = feature({ id: 'todos', intent: { summary: 'A to-do list' },
|
|
88
|
-
declarations: { Add, itemsTag, listItems, addItem, items, Board } })
|
|
89
|
-
```
|
|
90
|
-
Every declaration goes in `declarations` once, under its name. Resolvers:
|
|
91
|
-
`implement(addItem, ({ title }, { fail }) => exists ? fail('Duplicate', { title }) : save(title))`.
|
|
30
|
+
## Rules no diagnostic checks
|
|
31
|
+
- **Query resolvers only read.** Writes belong in mutation and endpoint resolvers; a prefetched link runs queries.
|
|
32
|
+
- **Other users, reloads, sign-out:** verify them in one `browse` chain with `--js both` (`--as <name>` per user).
|
|
33
|
+
- **Contracts only where a transition decides:** a guard, a `navigate`, a `fn` or computed value. Copy-only
|
|
34
|
+
transitions are reviewed in the lock.
|
|
35
|
+
- **`'live'` only for push** to pages that are already open.
|
|
36
|
+
- **`invalidates` drives the client refresh** of every query with those tags, whatever its freshness.
|
|
92
37
|
|
|
93
|
-
## Topics (`hozu docs <topic>`)
|
|
38
|
+
## Topics (`npx hozu docs <topic>`)
|
|
94
39
|
| Task | Topic |
|
|
95
40
|
|---|---|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
41
|
+
| a new feature, the files, a complete example | `feature` |
|
|
42
|
+
| elements, attributes, events, lists, links, reuse | `views` |
|
|
43
|
+
| states, events, invoke, timers, guards | `machine` |
|
|
44
|
+
| queries, mutations, tags, `fn()`, resolvers | `data` |
|
|
45
|
+
| contracts and the lock | `contracts` |
|
|
46
|
+
| routes, search, pages, `head`, 403 / 404 | `pages` |
|
|
47
|
+
| forms, checkboxes, bulk forms, no JS | `forms` |
|
|
102
48
|
| sign-in, sessions, per-user data | `auth` |
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
49
|
+
| filters, modes, per-item actions, load more | `patterns` |
|
|
50
|
+
| worked changes | `recipes` |
|
|
51
|
+
| webhooks, JSON APIs, redirects | `endpoints` |
|
|
52
|
+
| browser APIs, DOM libraries | `widgets` |
|
|
53
|
+
| languages, env, HTTP, Markdown, tests, deploying | `i18n` `env` `http` `content` `testing` `deploy` |
|
|
107
54
|
| 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
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { feature } from '@hozu/core'
|
|
2
|
+
import * as model from './model.ts'
|
|
3
|
+
import * as views from './views.ts'
|
|
4
|
+
|
|
5
|
+
export const bookmarks = feature({
|
|
6
|
+
id: 'bookmarks',
|
|
7
|
+
intent: {
|
|
8
|
+
summary:
|
|
9
|
+
'A shared reading list: add bookmarks with a kind, mark them read, filter unread, one page each.',
|
|
10
|
+
invariants: ['Titles are unique, case-insensitive', 'New bookmarks are listed first'],
|
|
11
|
+
},
|
|
12
|
+
declarations: [model, views],
|
|
13
|
+
})
|