floop 0.1.0__py3-none-any.whl
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.
- floop/__init__.py +3 -0
- floop/adapters.py +320 -0
- floop/cli.py +797 -0
- floop/preview.py +1002 -0
- floop/prototype.py +788 -0
- floop/skills.py +512 -0
- floop/tokens.py +1336 -0
- floop-0.1.0.dist-info/METADATA +225 -0
- floop-0.1.0.dist-info/RECORD +12 -0
- floop-0.1.0.dist-info/WHEEL +5 -0
- floop-0.1.0.dist-info/entry_points.txt +2 -0
- floop-0.1.0.dist-info/top_level.txt +1 -0
floop/cli.py
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
"""floop CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from floop import __version__
|
|
9
|
+
from floop.adapters import (
|
|
10
|
+
ADAPTERS,
|
|
11
|
+
SUPPORTED_AGENTS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.version_option(__version__, prog_name="floop")
|
|
17
|
+
def main():
|
|
18
|
+
"""floop — AI-native prototype quality toolkit.
|
|
19
|
+
|
|
20
|
+
Manage your design like code. Agent Skill + review workflow CLI.
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
Quick start:
|
|
24
|
+
floop init Initialize a floop project
|
|
25
|
+
floop enable copilot Install skills (GitHub Copilot)
|
|
26
|
+
floop enable cursor Install skills (Cursor)
|
|
27
|
+
floop enable claude Install skills (Claude Code)
|
|
28
|
+
floop enable trae Install skills (Trae IDE)
|
|
29
|
+
floop enable qwen-code Install skills (Qwen Code)
|
|
30
|
+
floop enable opencode Install skills (OpenCode)
|
|
31
|
+
floop enable openclaw Install skills (OpenClaw)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@main.command()
|
|
36
|
+
@click.option(
|
|
37
|
+
"--project-dir",
|
|
38
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
39
|
+
default=".",
|
|
40
|
+
help="Project root directory (default: current directory).",
|
|
41
|
+
)
|
|
42
|
+
def init(project_dir: Path):
|
|
43
|
+
"""Initialize a floop project.
|
|
44
|
+
|
|
45
|
+
Creates a .floop/ directory with subdirectories for prototypes,
|
|
46
|
+
design tokens, and project configuration.
|
|
47
|
+
"""
|
|
48
|
+
project_dir = project_dir.resolve()
|
|
49
|
+
floop_dir = project_dir / ".floop"
|
|
50
|
+
|
|
51
|
+
if floop_dir.exists():
|
|
52
|
+
click.secho("⚠ .floop/ already exists, skipping initialization.", fg="yellow")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# Create directory structure
|
|
56
|
+
dirs = {
|
|
57
|
+
"build": "Generated artifacts (token previews, component views, prototype pages)",
|
|
58
|
+
"tokens": "Design system tokens (colors, typography, spacing)",
|
|
59
|
+
"versions": "Prototype version snapshots (read-only archives)",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for name in dirs:
|
|
63
|
+
(floop_dir / name).mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
# Write config file
|
|
66
|
+
config = {
|
|
67
|
+
"version": __version__,
|
|
68
|
+
}
|
|
69
|
+
import json
|
|
70
|
+
config_path = floop_dir / "config.json"
|
|
71
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
# Write .gitignore inside .floop
|
|
74
|
+
gitignore_path = floop_dir / ".gitignore"
|
|
75
|
+
gitignore_path.write_text(
|
|
76
|
+
"# Generated artifacts — track via floop-server, not git\n"
|
|
77
|
+
"build/\n",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
click.secho("✓ floop project initialized", fg="green", bold=True)
|
|
82
|
+
click.echo(f" .floop/config.json")
|
|
83
|
+
click.echo(f" .floop/build/")
|
|
84
|
+
click.echo(f" .floop/tokens/")
|
|
85
|
+
click.echo(f" .floop/versions/")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@main.command()
|
|
89
|
+
@click.argument("agent", type=click.Choice(SUPPORTED_AGENTS, case_sensitive=False))
|
|
90
|
+
@click.option(
|
|
91
|
+
"--project-dir",
|
|
92
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
93
|
+
default=".",
|
|
94
|
+
help="Project root directory (default: current directory).",
|
|
95
|
+
)
|
|
96
|
+
def enable(agent: str, project_dir: Path):
|
|
97
|
+
"""Install floop skills into an AI agent platform.
|
|
98
|
+
|
|
99
|
+
\b
|
|
100
|
+
Supported agents:
|
|
101
|
+
copilot GitHub Copilot (VS Code) — .github/skills/ + .github/instructions/
|
|
102
|
+
cursor Cursor — .cursor/rules/
|
|
103
|
+
claude Claude Code — .claude/skills/ + CLAUDE.md
|
|
104
|
+
trae Trae IDE — .trae/project_rules.md
|
|
105
|
+
qwen-code Qwen Code (CLI) — AGENTS.md
|
|
106
|
+
opencode OpenCode (CLI) — .opencode/skills/ + AGENTS.md
|
|
107
|
+
openclaw OpenClaw — .openclaw/skills/ + AGENTS.md
|
|
108
|
+
"""
|
|
109
|
+
project_dir = project_dir.resolve()
|
|
110
|
+
adapter = ADAPTERS[agent]()
|
|
111
|
+
created = adapter.install(project_dir)
|
|
112
|
+
|
|
113
|
+
click.secho(f"✓ floop skills installed for {agent}", fg="green", bold=True)
|
|
114
|
+
for path in created:
|
|
115
|
+
rel = path.relative_to(project_dir)
|
|
116
|
+
click.echo(f" {rel}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main() # pragma: no cover
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# floop token — Design Token management (W3C DTCG)
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@main.group()
|
|
129
|
+
def token():
|
|
130
|
+
"""Manage design tokens (W3C DTCG format).
|
|
131
|
+
|
|
132
|
+
\b
|
|
133
|
+
Commands:
|
|
134
|
+
floop token init Generate default token files
|
|
135
|
+
floop token validate Validate token files
|
|
136
|
+
floop token view Generate HTML preview page
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@token.command("init")
|
|
141
|
+
@click.option(
|
|
142
|
+
"--project-dir",
|
|
143
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
144
|
+
default=".",
|
|
145
|
+
help="Project root directory (default: current directory).",
|
|
146
|
+
)
|
|
147
|
+
@click.option(
|
|
148
|
+
"--force",
|
|
149
|
+
is_flag=True,
|
|
150
|
+
default=False,
|
|
151
|
+
help="Overwrite existing token files.",
|
|
152
|
+
)
|
|
153
|
+
def token_init_cmd(project_dir: Path, force: bool):
|
|
154
|
+
"""Generate W3C DTCG token template files.
|
|
155
|
+
|
|
156
|
+
Creates three files in .floop/tokens/:
|
|
157
|
+
global.tokens.json Primitive design values
|
|
158
|
+
semantic.tokens.json Semantic aliases
|
|
159
|
+
component.tokens.json Component-level tokens
|
|
160
|
+
"""
|
|
161
|
+
from floop.tokens import token_init
|
|
162
|
+
|
|
163
|
+
project_dir = project_dir.resolve()
|
|
164
|
+
tokens_dir = project_dir / ".floop" / "tokens"
|
|
165
|
+
|
|
166
|
+
if not (project_dir / ".floop").exists():
|
|
167
|
+
click.secho(
|
|
168
|
+
"⚠ .floop/ not found. Run 'floop init' first.", fg="yellow", err=True
|
|
169
|
+
)
|
|
170
|
+
raise SystemExit(1)
|
|
171
|
+
|
|
172
|
+
existing = list(tokens_dir.glob("*.tokens.json"))
|
|
173
|
+
if existing and not force:
|
|
174
|
+
click.secho(
|
|
175
|
+
"⚠ Token files already exist. Use --force to overwrite.", fg="yellow"
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
created = token_init(tokens_dir)
|
|
180
|
+
|
|
181
|
+
click.secho("✓ Token files generated (W3C DTCG format)", fg="green", bold=True)
|
|
182
|
+
for path in created:
|
|
183
|
+
rel = path.relative_to(project_dir)
|
|
184
|
+
click.echo(f" {rel}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@token.command("validate")
|
|
188
|
+
@click.option(
|
|
189
|
+
"--project-dir",
|
|
190
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
191
|
+
default=".",
|
|
192
|
+
help="Project root directory (default: current directory).",
|
|
193
|
+
)
|
|
194
|
+
@click.option(
|
|
195
|
+
"--json-output",
|
|
196
|
+
"output_json",
|
|
197
|
+
is_flag=True,
|
|
198
|
+
default=False,
|
|
199
|
+
help="Output structured JSON (for Agent consumption).",
|
|
200
|
+
)
|
|
201
|
+
def token_validate_cmd(project_dir: Path, output_json: bool):
|
|
202
|
+
"""Validate design token files against W3C DTCG spec.
|
|
203
|
+
|
|
204
|
+
\b
|
|
205
|
+
Three validation layers:
|
|
206
|
+
L1 Format compliance (valid JSON, valid $type/$value)
|
|
207
|
+
L2 Reference integrity (broken refs, circular deps)
|
|
208
|
+
L3 Design suggestions (recommended semantic tokens)
|
|
209
|
+
"""
|
|
210
|
+
from floop.tokens import token_validate
|
|
211
|
+
|
|
212
|
+
project_dir = project_dir.resolve()
|
|
213
|
+
tokens_dir = project_dir / ".floop" / "tokens"
|
|
214
|
+
|
|
215
|
+
result = token_validate(tokens_dir)
|
|
216
|
+
|
|
217
|
+
if output_json:
|
|
218
|
+
click.echo(json.dumps(result, indent=2, ensure_ascii=False))
|
|
219
|
+
else:
|
|
220
|
+
# Human-readable output
|
|
221
|
+
stats = result["stats"]
|
|
222
|
+
click.echo(
|
|
223
|
+
f"Checked {stats['files']} file(s): "
|
|
224
|
+
f"{stats['tokens']} tokens, "
|
|
225
|
+
f"{stats['references']} references, "
|
|
226
|
+
f"{stats['groups']} groups"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
for err in result["errors"]:
|
|
230
|
+
loc = err["path"] or err["file"]
|
|
231
|
+
click.secho(f" ✗ [{err['code']}] {loc}: {err['message']}", fg="red")
|
|
232
|
+
if err.get("suggestion"):
|
|
233
|
+
click.echo(f" → {err['suggestion']}")
|
|
234
|
+
|
|
235
|
+
for warn in result["warnings"]:
|
|
236
|
+
loc = warn["path"] or warn["file"]
|
|
237
|
+
click.secho(
|
|
238
|
+
f" ⚠ [{warn['code']}] {loc}: {warn['message']}", fg="yellow"
|
|
239
|
+
)
|
|
240
|
+
if warn.get("suggestion"):
|
|
241
|
+
click.echo(f" → {warn['suggestion']}")
|
|
242
|
+
|
|
243
|
+
if result["valid"]:
|
|
244
|
+
click.secho("✓ All tokens valid", fg="green", bold=True)
|
|
245
|
+
else:
|
|
246
|
+
click.secho(
|
|
247
|
+
f"✗ {len(result['errors'])} error(s) found",
|
|
248
|
+
fg="red",
|
|
249
|
+
bold=True,
|
|
250
|
+
)
|
|
251
|
+
raise SystemExit(1)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@token.command("view")
|
|
255
|
+
@click.option(
|
|
256
|
+
"--project-dir",
|
|
257
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
258
|
+
default=".",
|
|
259
|
+
help="Project root directory (default: current directory).",
|
|
260
|
+
)
|
|
261
|
+
def token_view_cmd(project_dir: Path):
|
|
262
|
+
"""Generate an HTML preview page for design tokens.
|
|
263
|
+
|
|
264
|
+
Reads all .tokens.json files, resolves references, and generates
|
|
265
|
+
a visual preview at .floop/build/design-tokens.html.
|
|
266
|
+
"""
|
|
267
|
+
from floop.tokens import token_view
|
|
268
|
+
|
|
269
|
+
project_dir = project_dir.resolve()
|
|
270
|
+
tokens_dir = project_dir / ".floop" / "tokens"
|
|
271
|
+
|
|
272
|
+
if not tokens_dir.exists():
|
|
273
|
+
click.secho(
|
|
274
|
+
"⚠ .floop/tokens/ not found. Run 'floop init' and 'floop token init' first.",
|
|
275
|
+
fg="yellow",
|
|
276
|
+
err=True,
|
|
277
|
+
)
|
|
278
|
+
raise SystemExit(1)
|
|
279
|
+
|
|
280
|
+
token_files = list(tokens_dir.glob("*.tokens.json"))
|
|
281
|
+
if not token_files:
|
|
282
|
+
click.secho(
|
|
283
|
+
"⚠ No .tokens.json files found. Run 'floop token init' first.",
|
|
284
|
+
fg="yellow",
|
|
285
|
+
err=True,
|
|
286
|
+
)
|
|
287
|
+
raise SystemExit(1)
|
|
288
|
+
|
|
289
|
+
build_dir = project_dir / ".floop" / "build" / "tokens"
|
|
290
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
291
|
+
out_path = token_view(tokens_dir, out_dir=build_dir)
|
|
292
|
+
css_path = build_dir / "tokens.css"
|
|
293
|
+
click.secho("✓ Token preview generated", fg="green", bold=True)
|
|
294
|
+
click.echo(f" {out_path.relative_to(project_dir)}")
|
|
295
|
+
click.echo(f" {css_path.relative_to(project_dir)}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# floop preview — Local preview server
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@main.command()
|
|
304
|
+
@click.option(
|
|
305
|
+
"--project-dir",
|
|
306
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
307
|
+
default=".",
|
|
308
|
+
help="Project root directory (default: current directory).",
|
|
309
|
+
)
|
|
310
|
+
@click.option(
|
|
311
|
+
"--port",
|
|
312
|
+
type=int,
|
|
313
|
+
default=0,
|
|
314
|
+
help="Port number (default: auto-assign a free port).",
|
|
315
|
+
)
|
|
316
|
+
@click.option(
|
|
317
|
+
"--version",
|
|
318
|
+
"active_version",
|
|
319
|
+
default="trunk",
|
|
320
|
+
help="Version to preview (default: trunk = current build).",
|
|
321
|
+
)
|
|
322
|
+
def preview(project_dir: Path, port: int, active_version: str):
|
|
323
|
+
"""Start a local web server to preview floop output.
|
|
324
|
+
|
|
325
|
+
Generates a navigation index page in .floop/build/ and serves it on
|
|
326
|
+
a temporary local port. Open the printed URL in your browser to
|
|
327
|
+
browse design tokens, components, and prototype pages.
|
|
328
|
+
|
|
329
|
+
Use --version to start preview pinned to a named snapshot.
|
|
330
|
+
|
|
331
|
+
Press Ctrl+C to stop the server.
|
|
332
|
+
"""
|
|
333
|
+
import http.server
|
|
334
|
+
import functools
|
|
335
|
+
import socket
|
|
336
|
+
|
|
337
|
+
from floop.preview import generate_preview_index
|
|
338
|
+
|
|
339
|
+
project_dir = project_dir.resolve()
|
|
340
|
+
floop_dir = project_dir / ".floop"
|
|
341
|
+
|
|
342
|
+
if not floop_dir.exists():
|
|
343
|
+
click.secho(
|
|
344
|
+
"⚠ .floop/ not found. Run 'floop init' first.", fg="yellow", err=True
|
|
345
|
+
)
|
|
346
|
+
raise SystemExit(1)
|
|
347
|
+
|
|
348
|
+
build_dir = floop_dir / "build"
|
|
349
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
350
|
+
|
|
351
|
+
# Generate (or refresh) the navigation index page
|
|
352
|
+
generate_preview_index(build_dir, active_version=active_version)
|
|
353
|
+
|
|
354
|
+
# Serve from .floop/ so both build/ and versions/ are reachable
|
|
355
|
+
handler = functools.partial(
|
|
356
|
+
http.server.SimpleHTTPRequestHandler, directory=str(floop_dir)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Find a free port if port=0
|
|
360
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
361
|
+
s.bind(("127.0.0.1", port))
|
|
362
|
+
chosen_port = s.getsockname()[1]
|
|
363
|
+
|
|
364
|
+
server = http.server.HTTPServer(("127.0.0.1", chosen_port), handler)
|
|
365
|
+
|
|
366
|
+
click.secho("floop preview server", fg="green", bold=True)
|
|
367
|
+
click.echo(f" http://127.0.0.1:{chosen_port}/build/")
|
|
368
|
+
click.echo("\n Press Ctrl+C to stop.\n")
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
server.serve_forever()
|
|
372
|
+
except KeyboardInterrupt:
|
|
373
|
+
pass
|
|
374
|
+
finally:
|
|
375
|
+
server.server_close()
|
|
376
|
+
click.echo("\nServer stopped.")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# floop prd — Product Requirements Document management
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@main.group()
|
|
385
|
+
def prd():
|
|
386
|
+
"""Manage product requirements document (.floop/prd.md).
|
|
387
|
+
|
|
388
|
+
\b
|
|
389
|
+
Commands:
|
|
390
|
+
floop prd init Create prd.md template
|
|
391
|
+
floop prd validate Validate prd.md frontmatter
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@prd.command("init")
|
|
396
|
+
@click.option(
|
|
397
|
+
"--project-dir",
|
|
398
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
399
|
+
default=".",
|
|
400
|
+
help="Project root directory (default: current directory).",
|
|
401
|
+
)
|
|
402
|
+
def prd_init_cmd(project_dir: Path):
|
|
403
|
+
"""Create .floop/prd.md from template.
|
|
404
|
+
|
|
405
|
+
Generates a PRD document with YAML frontmatter (product, target_users,
|
|
406
|
+
core_flows, css_framework, status) and Markdown sections for you to fill in.
|
|
407
|
+
"""
|
|
408
|
+
from floop.prototype import prd_init
|
|
409
|
+
|
|
410
|
+
project_dir = project_dir.resolve()
|
|
411
|
+
try:
|
|
412
|
+
path = prd_init(project_dir)
|
|
413
|
+
except FileExistsError as exc:
|
|
414
|
+
click.secho(f"⚠ {exc}", fg="yellow", err=True)
|
|
415
|
+
raise SystemExit(1)
|
|
416
|
+
|
|
417
|
+
rel = path.relative_to(project_dir)
|
|
418
|
+
click.secho("✓ prd.md created", fg="green", bold=True)
|
|
419
|
+
click.echo(f" {rel}")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@prd.command("validate")
|
|
423
|
+
@click.option(
|
|
424
|
+
"--project-dir",
|
|
425
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
426
|
+
default=".",
|
|
427
|
+
help="Project root directory (default: current directory).",
|
|
428
|
+
)
|
|
429
|
+
def prd_validate_cmd(project_dir: Path):
|
|
430
|
+
"""Validate .floop/prd.md frontmatter fields."""
|
|
431
|
+
from floop.prototype import prd_validate
|
|
432
|
+
|
|
433
|
+
project_dir = project_dir.resolve()
|
|
434
|
+
errors, warnings = prd_validate(project_dir)
|
|
435
|
+
|
|
436
|
+
for warn in warnings:
|
|
437
|
+
click.secho(f" ⚠ {warn}", fg="yellow")
|
|
438
|
+
for err in errors:
|
|
439
|
+
click.secho(f" ✗ {err}", fg="red")
|
|
440
|
+
|
|
441
|
+
if errors:
|
|
442
|
+
click.secho(f"✗ {len(errors)} error(s) found", fg="red", bold=True)
|
|
443
|
+
raise SystemExit(1)
|
|
444
|
+
|
|
445
|
+
click.secho("✓ prd.md is valid", fg="green", bold=True)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# floop sitemap — Sitemap management
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@main.group()
|
|
454
|
+
def sitemap():
|
|
455
|
+
"""Manage sitemap definition (.floop/sitemap.md).
|
|
456
|
+
|
|
457
|
+
\b
|
|
458
|
+
Commands:
|
|
459
|
+
floop sitemap init Create sitemap.md template
|
|
460
|
+
floop sitemap validate Validate sitemap.md frontmatter
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@sitemap.command("init")
|
|
465
|
+
@click.option(
|
|
466
|
+
"--project-dir",
|
|
467
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
468
|
+
default=".",
|
|
469
|
+
help="Project root directory (default: current directory).",
|
|
470
|
+
)
|
|
471
|
+
def sitemap_init_cmd(project_dir: Path):
|
|
472
|
+
"""Create .floop/sitemap.md from template.
|
|
473
|
+
|
|
474
|
+
Generates a sitemap document with YAML frontmatter listing pages
|
|
475
|
+
(id, title, file, status) for you to fill in.
|
|
476
|
+
"""
|
|
477
|
+
from floop.prototype import sitemap_init
|
|
478
|
+
|
|
479
|
+
project_dir = project_dir.resolve()
|
|
480
|
+
try:
|
|
481
|
+
path = sitemap_init(project_dir)
|
|
482
|
+
except FileExistsError as exc:
|
|
483
|
+
click.secho(f"⚠ {exc}", fg="yellow", err=True)
|
|
484
|
+
raise SystemExit(1)
|
|
485
|
+
|
|
486
|
+
rel = path.relative_to(project_dir)
|
|
487
|
+
click.secho("✓ sitemap.md created", fg="green", bold=True)
|
|
488
|
+
click.echo(f" {rel}")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@sitemap.command("validate")
|
|
492
|
+
@click.option(
|
|
493
|
+
"--project-dir",
|
|
494
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
495
|
+
default=".",
|
|
496
|
+
help="Project root directory (default: current directory).",
|
|
497
|
+
)
|
|
498
|
+
def sitemap_validate_cmd(project_dir: Path):
|
|
499
|
+
"""Validate .floop/sitemap.md frontmatter fields."""
|
|
500
|
+
from floop.prototype import sitemap_validate
|
|
501
|
+
|
|
502
|
+
project_dir = project_dir.resolve()
|
|
503
|
+
errors, warnings = sitemap_validate(project_dir)
|
|
504
|
+
|
|
505
|
+
for warn in warnings:
|
|
506
|
+
click.secho(f" ⚠ {warn}", fg="yellow")
|
|
507
|
+
for err in errors:
|
|
508
|
+
click.secho(f" ✗ {err}", fg="red")
|
|
509
|
+
|
|
510
|
+
if errors:
|
|
511
|
+
click.secho(f"✗ {len(errors)} error(s) found", fg="red", bold=True)
|
|
512
|
+
raise SystemExit(1)
|
|
513
|
+
|
|
514
|
+
click.secho("✓ sitemap.md is valid", fg="green", bold=True)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
# ---------------------------------------------------------------------------
|
|
518
|
+
# floop component — Component library management
|
|
519
|
+
# ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@main.group()
|
|
523
|
+
def component():
|
|
524
|
+
"""Manage component library definition (.floop/components.yaml).
|
|
525
|
+
|
|
526
|
+
\b
|
|
527
|
+
Commands:
|
|
528
|
+
floop component init Create components.yaml template
|
|
529
|
+
floop component validate Validate components.yaml
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@component.command("init")
|
|
534
|
+
@click.option(
|
|
535
|
+
"--project-dir",
|
|
536
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
537
|
+
default=".",
|
|
538
|
+
help="Project root directory (default: current directory).",
|
|
539
|
+
)
|
|
540
|
+
def component_init_cmd(project_dir: Path):
|
|
541
|
+
"""Create .floop/components.yaml from template.
|
|
542
|
+
|
|
543
|
+
Generates a component library file with YAML structure
|
|
544
|
+
(id, title, status, tokens) for you to fill in.
|
|
545
|
+
"""
|
|
546
|
+
from floop.prototype import component_init
|
|
547
|
+
|
|
548
|
+
project_dir = project_dir.resolve()
|
|
549
|
+
try:
|
|
550
|
+
path = component_init(project_dir)
|
|
551
|
+
except FileExistsError as exc:
|
|
552
|
+
click.secho(f"⚠ {exc}", fg="yellow", err=True)
|
|
553
|
+
raise SystemExit(1)
|
|
554
|
+
|
|
555
|
+
rel = path.relative_to(project_dir)
|
|
556
|
+
click.secho("✓ components.yaml created", fg="green", bold=True)
|
|
557
|
+
click.echo(f" {rel}")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@component.command("validate")
|
|
561
|
+
@click.option(
|
|
562
|
+
"--project-dir",
|
|
563
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
564
|
+
default=".",
|
|
565
|
+
help="Project root directory (default: current directory).",
|
|
566
|
+
)
|
|
567
|
+
def component_validate_cmd(project_dir: Path):
|
|
568
|
+
"""Validate .floop/components.yaml fields."""
|
|
569
|
+
from floop.prototype import component_validate
|
|
570
|
+
|
|
571
|
+
project_dir = project_dir.resolve()
|
|
572
|
+
errors, warnings = component_validate(project_dir)
|
|
573
|
+
|
|
574
|
+
for warn in warnings:
|
|
575
|
+
click.secho(f" ⚠ {warn}", fg="yellow")
|
|
576
|
+
for err in errors:
|
|
577
|
+
click.secho(f" ✗ {err}", fg="red")
|
|
578
|
+
|
|
579
|
+
if errors:
|
|
580
|
+
click.secho(f"✗ {len(errors)} error(s) found", fg="red", bold=True)
|
|
581
|
+
raise SystemExit(1)
|
|
582
|
+
|
|
583
|
+
click.secho("✓ components.yaml is valid", fg="green", bold=True)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ---------------------------------------------------------------------------
|
|
587
|
+
# floop prototype — Journey Map management
|
|
588
|
+
# ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@main.group()
|
|
592
|
+
def prototype():
|
|
593
|
+
"""Manage prototype journey map (.floop/journey-map.csv).
|
|
594
|
+
|
|
595
|
+
\b
|
|
596
|
+
Commands:
|
|
597
|
+
floop prototype init Build journey-map.csv from sitemap.md
|
|
598
|
+
floop prototype validate Validate journey HTMLs against journey-map.csv
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@prototype.command("init")
|
|
603
|
+
@click.option(
|
|
604
|
+
"--project-dir",
|
|
605
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
606
|
+
default=".",
|
|
607
|
+
help="Project root directory (default: current directory).",
|
|
608
|
+
)
|
|
609
|
+
def prototype_init_cmd(project_dir: Path):
|
|
610
|
+
"""Build .floop/journey-map.csv from sitemap.md.
|
|
611
|
+
|
|
612
|
+
Reads all pages in sitemap.md frontmatter and generates a CSV mapping
|
|
613
|
+
sitemap domains to HTML files. The domain is taken from each page's
|
|
614
|
+
optional 'domain' field; if absent it is derived from the file path
|
|
615
|
+
(e.g. build/journey/auth/login.html → domain 'auth').
|
|
616
|
+
|
|
617
|
+
The CSV is always regenerated — safe to re-run after updating sitemap.md.
|
|
618
|
+
"""
|
|
619
|
+
from floop.prototype import prototype_init
|
|
620
|
+
|
|
621
|
+
project_dir = project_dir.resolve()
|
|
622
|
+
try:
|
|
623
|
+
path = prototype_init(project_dir)
|
|
624
|
+
except FileNotFoundError as exc:
|
|
625
|
+
click.secho(f"⚠ {exc}", fg="yellow", err=True)
|
|
626
|
+
raise SystemExit(1)
|
|
627
|
+
|
|
628
|
+
rel = path.relative_to(project_dir)
|
|
629
|
+
click.secho("✓ journey-map.csv generated", fg="green", bold=True)
|
|
630
|
+
click.echo(f" {rel}")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@prototype.command("validate")
|
|
634
|
+
@click.option(
|
|
635
|
+
"--project-dir",
|
|
636
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
637
|
+
default=".",
|
|
638
|
+
help="Project root directory (default: current directory).",
|
|
639
|
+
)
|
|
640
|
+
def prototype_validate_cmd(project_dir: Path):
|
|
641
|
+
"""Validate journey HTML files against journey-map.csv and sitemap.md.
|
|
642
|
+
|
|
643
|
+
\b
|
|
644
|
+
Two checks:
|
|
645
|
+
1. Every HTML under .floop/build/journey/ is mapped in journey-map.csv
|
|
646
|
+
2. Every domain in journey-map.csv exists in sitemap.md pages
|
|
647
|
+
"""
|
|
648
|
+
from floop.prototype import prototype_validate
|
|
649
|
+
|
|
650
|
+
project_dir = project_dir.resolve()
|
|
651
|
+
errors, warnings = prototype_validate(project_dir)
|
|
652
|
+
|
|
653
|
+
for warn in warnings:
|
|
654
|
+
click.secho(f" ⚠ {warn}", fg="yellow")
|
|
655
|
+
for err in errors:
|
|
656
|
+
click.secho(f" ✗ {err}", fg="red")
|
|
657
|
+
|
|
658
|
+
if errors:
|
|
659
|
+
click.secho(f"✗ {len(errors)} error(s) found", fg="red", bold=True)
|
|
660
|
+
raise SystemExit(1)
|
|
661
|
+
|
|
662
|
+
click.secho("✓ prototype is valid", fg="green", bold=True)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# ---------------------------------------------------------------------------
|
|
666
|
+
# floop version — Trunk-based prototype version snapshots
|
|
667
|
+
# ---------------------------------------------------------------------------
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
@main.group()
|
|
671
|
+
def version():
|
|
672
|
+
"""Manage prototype versions (trunk-based snapshots).
|
|
673
|
+
|
|
674
|
+
\b
|
|
675
|
+
Commands:
|
|
676
|
+
floop version create Snapshot current build into a named version
|
|
677
|
+
floop version list List all versions
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@version.command("create")
|
|
682
|
+
@click.argument("name")
|
|
683
|
+
@click.option("-m", "--message", default="", help="Version description.")
|
|
684
|
+
@click.option(
|
|
685
|
+
"--project-dir",
|
|
686
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
687
|
+
default=".",
|
|
688
|
+
help="Project root directory (default: current directory).",
|
|
689
|
+
)
|
|
690
|
+
def version_create_cmd(name: str, message: str, project_dir: Path):
|
|
691
|
+
"""Snapshot .floop/build/ into .floop/versions/NAME/.
|
|
692
|
+
|
|
693
|
+
NAME must be unique (e.g. v1.0, v1.1-homepage-revamp).
|
|
694
|
+
Always run this before sharing a build with a client.
|
|
695
|
+
"""
|
|
696
|
+
from floop.prototype import version_create
|
|
697
|
+
|
|
698
|
+
project_dir = project_dir.resolve()
|
|
699
|
+
if not (project_dir / ".floop").exists():
|
|
700
|
+
click.secho(
|
|
701
|
+
"⚠ .floop/ not found. Run 'floop init' first.", fg="yellow", err=True
|
|
702
|
+
)
|
|
703
|
+
raise SystemExit(1)
|
|
704
|
+
|
|
705
|
+
try:
|
|
706
|
+
version_dir = version_create(project_dir, name, message)
|
|
707
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
708
|
+
click.secho(f"⚠ {exc}", fg="yellow", err=True)
|
|
709
|
+
raise SystemExit(1)
|
|
710
|
+
|
|
711
|
+
rel = version_dir.relative_to(project_dir)
|
|
712
|
+
click.secho(f"✓ Version '{name}' created", fg="green", bold=True)
|
|
713
|
+
click.echo(f" {rel}")
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@version.command("list")
|
|
717
|
+
@click.option(
|
|
718
|
+
"--project-dir",
|
|
719
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
720
|
+
default=".",
|
|
721
|
+
help="Project root directory (default: current directory).",
|
|
722
|
+
)
|
|
723
|
+
def version_list_cmd(project_dir: Path):
|
|
724
|
+
"""List all prototype versions."""
|
|
725
|
+
from floop.prototype import version_list
|
|
726
|
+
|
|
727
|
+
project_dir = project_dir.resolve()
|
|
728
|
+
versions = version_list(project_dir)
|
|
729
|
+
|
|
730
|
+
if not versions:
|
|
731
|
+
click.echo("No versions found. Run 'floop version create' to create one.")
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
for v in versions:
|
|
735
|
+
date = v.get("created_at", "")[:10]
|
|
736
|
+
msg = v.get("message", "")
|
|
737
|
+
suffix = f" {msg}" if msg else ""
|
|
738
|
+
click.echo(f" {v['version']} ({date}){suffix}")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# ---------------------------------------------------------------------------
|
|
742
|
+
# floop journey — Journey backward-check commands
|
|
743
|
+
# ---------------------------------------------------------------------------
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@main.group()
|
|
747
|
+
def journey():
|
|
748
|
+
"""Manage journey HTML pages.
|
|
749
|
+
|
|
750
|
+
\b
|
|
751
|
+
Commands:
|
|
752
|
+
floop journey check Backward-check a journey HTML for token/component gaps
|
|
753
|
+
"""
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@journey.command("check")
|
|
757
|
+
@click.argument(
|
|
758
|
+
"html_file",
|
|
759
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
760
|
+
)
|
|
761
|
+
@click.option(
|
|
762
|
+
"--project-dir",
|
|
763
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
764
|
+
default=".",
|
|
765
|
+
help="Project root directory (default: current directory).",
|
|
766
|
+
)
|
|
767
|
+
def journey_check_cmd(html_file: Path, project_dir: Path):
|
|
768
|
+
"""Backward-check a journey HTML file for token and component gaps.
|
|
769
|
+
|
|
770
|
+
Scans HTML_FILE for missing token references, unused components,
|
|
771
|
+
and missing head links (tokens.css / components.js).
|
|
772
|
+
"""
|
|
773
|
+
from floop.prototype import journey_check
|
|
774
|
+
|
|
775
|
+
project_dir = project_dir.resolve()
|
|
776
|
+
html_file = html_file.resolve()
|
|
777
|
+
|
|
778
|
+
if not (project_dir / ".floop").exists():
|
|
779
|
+
click.secho(
|
|
780
|
+
"⚠ .floop/ not found. Run 'floop init' first.", fg="yellow", err=True
|
|
781
|
+
)
|
|
782
|
+
raise SystemExit(1)
|
|
783
|
+
|
|
784
|
+
errors, warnings = journey_check(project_dir, html_file)
|
|
785
|
+
|
|
786
|
+
for warn in warnings:
|
|
787
|
+
click.secho(f" ⚠ {warn}", fg="yellow")
|
|
788
|
+
for err in errors:
|
|
789
|
+
click.secho(f" ✗ {err}", fg="red")
|
|
790
|
+
|
|
791
|
+
if errors:
|
|
792
|
+
click.secho(
|
|
793
|
+
f"✗ {len(errors)} error(s) found", fg="red", bold=True
|
|
794
|
+
)
|
|
795
|
+
raise SystemExit(1)
|
|
796
|
+
|
|
797
|
+
click.secho("✓ journey check passed", fg="green", bold=True)
|