rewritable 0.3.0 → 0.6.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/README.md +261 -5
- package/bin/rwa.mjs +1000 -9
- package/package.json +2 -2
- package/seeds/rewritable.html +4356 -315
- package/src/agent-loop.mjs +155 -0
- package/src/apply-edits.mjs +664 -0
- package/src/atomic-write.mjs +38 -0
- package/src/backend.mjs +43 -0
- package/src/clone-extract.mjs +249 -0
- package/src/clone.mjs +161 -0
- package/src/commands.mjs +90 -10
- package/src/create.mjs +256 -0
- package/src/doc.mjs +69 -0
- package/src/dsl-compiler.mjs +357 -0
- package/src/edit.mjs +300 -0
- package/src/fetch-page.mjs +346 -0
- package/src/host.mjs +126 -0
- package/src/identity.mjs +257 -0
- package/src/import-claude.mjs +28 -4
- package/src/import-vision.mjs +1 -1
- package/src/import.mjs +76 -10
- package/src/ls.mjs +105 -0
- package/src/publish-site.mjs +85 -0
- package/src/publish.mjs +98 -0
- package/src/seed-extract.mjs +40 -0
- package/src/seed.mjs +1387 -5
- package/src/self-contained.mjs +115 -0
- package/src/skill-manifest.mjs +227 -0
- package/src/skin.mjs +350 -0
- package/src/skins.mjs +274 -0
- package/src/template.mjs +109 -0
package/src/template.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// `rwa new <kind>` template discovery + label strip
|
|
2
|
+
// (docs/plans/2026-05-05-cli-templates-design.md).
|
|
3
|
+
//
|
|
4
|
+
// A user labels one rwa file per kind with data-rwa-template="<kind>" on the
|
|
5
|
+
// first element of its body (#rwa-doc-mount's first child). `rwa new <kind>`
|
|
6
|
+
// scans cwd, finds the labeled file, and clones it — pristine seed + the
|
|
7
|
+
// template's INLINE_DOC, fresh UUID, label stripped. No registry, no shipped
|
|
8
|
+
// starters: the file you made yesterday is the template for the file you make
|
|
9
|
+
// tomorrow. CLI-only for v1; cross-folder discovery is deferred.
|
|
10
|
+
|
|
11
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { extractInlineDoc, KNOWN_KINDS } from './seed.mjs';
|
|
14
|
+
|
|
15
|
+
const HTML_RE = /\.html?$/i;
|
|
16
|
+
// The first opening tag inside the body (the template's root element).
|
|
17
|
+
const FIRST_TAG_RE = /<[a-zA-Z][^>]*>/;
|
|
18
|
+
const LABEL_ATTR_RE = /\s*\bdata-rwa-template=["'][^"']*["']/;
|
|
19
|
+
|
|
20
|
+
// The data-rwa-template value on the body's first element, or null.
|
|
21
|
+
function templateLabelOf(body) {
|
|
22
|
+
const tag = (body || '').match(FIRST_TAG_RE);
|
|
23
|
+
if (!tag) return null;
|
|
24
|
+
const m = tag[0].match(/\bdata-rwa-template=["']([^"']*)["']/);
|
|
25
|
+
return m ? m[1] : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Strip data-rwa-template="…" from the body's first opening tag (the cloned
|
|
30
|
+
* container is an instance, not the template). No-op when absent; only the first
|
|
31
|
+
* element is touched, so a later mention in prose survives.
|
|
32
|
+
* @param {string} body — the INLINE_DOC body
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function stripTemplateAttribute(body) {
|
|
36
|
+
const tag = (body || '').match(FIRST_TAG_RE);
|
|
37
|
+
if (!tag) return body;
|
|
38
|
+
const stripped = tag[0].replace(LABEL_ATTR_RE, '');
|
|
39
|
+
return body.slice(0, tag.index) + stripped + body.slice(tag.index + tag[0].length);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find the rwa container in `dir` labeled `data-rwa-template="<name>"`. Scans
|
|
44
|
+
* (non-recursive) `*.html`; cheap-pre-checks for the bootstrap id before parsing;
|
|
45
|
+
* skips malformed candidates; most-recent mtime wins when several match.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} dir — directory to scan (typically cwd)
|
|
48
|
+
* @param {string} name — the template kind
|
|
49
|
+
* @returns {Promise<{path:string, inlineDoc:string, ambiguous:boolean}|null>}
|
|
50
|
+
* @throws {Error} exitCode 2 when the directory holds more than 200 .html files
|
|
51
|
+
*/
|
|
52
|
+
export async function findTemplate(dir, name) {
|
|
53
|
+
let entries;
|
|
54
|
+
try { entries = await readdir(dir); } catch { return null; }
|
|
55
|
+
const htmls = entries.filter(n => HTML_RE.test(n));
|
|
56
|
+
if (htmls.length > 200) {
|
|
57
|
+
const e = new Error(`too many .html files in ${dir} (>200) to scan for a "${name}" template`);
|
|
58
|
+
e.exitCode = 2;
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
const matches = [];
|
|
62
|
+
for (const n of htmls) {
|
|
63
|
+
const p = join(dir, n);
|
|
64
|
+
let text;
|
|
65
|
+
try { text = await readFile(p, 'utf8'); } catch { continue; }
|
|
66
|
+
if (!text.includes('id="rwa-bootstrap"')) continue; // cheap: not an rwa file
|
|
67
|
+
let body;
|
|
68
|
+
try { body = extractInlineDoc(text); } catch { continue; } // malformed → skip, keep scanning
|
|
69
|
+
if (templateLabelOf(body) !== name) continue;
|
|
70
|
+
let mtime = 0;
|
|
71
|
+
try { mtime = (await stat(p)).mtimeMs; } catch { /* keep 0 */ }
|
|
72
|
+
matches.push({ path: p, inlineDoc: body, mtime });
|
|
73
|
+
}
|
|
74
|
+
if (!matches.length) return null;
|
|
75
|
+
matches.sort((a, b) => b.mtime - a.mtime); // most-recent first
|
|
76
|
+
return { path: matches[0].path, inlineDoc: matches[0].inlineDoc, ambiguous: matches.length > 1 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a bare word to a creation frame, template-first then built-in kind
|
|
81
|
+
* (design 2026-05-31 §3.2). This is THE single resolver shared by `rwa new <word>`
|
|
82
|
+
* and `rwa create <word> …` so the two surfaces never diverge.
|
|
83
|
+
*
|
|
84
|
+
* 1. a cwd file labeled data-rwa-template="<word>" → clone it
|
|
85
|
+
* → { source:'template', kind:'document', body:<stripped>, templatePath, ambiguous }
|
|
86
|
+
* 2. else <word> ∈ KNOWN_KINDS → emit that built-in kind
|
|
87
|
+
* → { source:'kind', kind:<word>, body:null } (body comes from kindOverrides)
|
|
88
|
+
* 3. else → null (caller decides: error, or Stage-2 inference)
|
|
89
|
+
*
|
|
90
|
+
* @param {string} word — the bare leading token
|
|
91
|
+
* @param {string} cwd — directory to scan for a labeled template
|
|
92
|
+
* @returns {Promise<{source:string, kind:string, body:string|null, templatePath?:string, ambiguous?:boolean}|null>}
|
|
93
|
+
*/
|
|
94
|
+
export async function resolveBareWord(word, cwd) {
|
|
95
|
+
const tmpl = await findTemplate(cwd, word);
|
|
96
|
+
if (tmpl) {
|
|
97
|
+
return {
|
|
98
|
+
source: 'template',
|
|
99
|
+
kind: 'document',
|
|
100
|
+
body: stripTemplateAttribute(tmpl.inlineDoc),
|
|
101
|
+
templatePath: tmpl.path,
|
|
102
|
+
ambiguous: tmpl.ambiguous,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (KNOWN_KINDS.includes(word)) {
|
|
106
|
+
return { source: 'kind', kind: word, body: null };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|