lytos-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/cli.js +995 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frederic Galliné
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Lytos — CLI
|
|
2
|
+
|
|
3
|
+
[](https://github.com/getlytos/lytos-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/lytos-cli)
|
|
5
|
+
|
|
6
|
+
> The command-line tool for [Lytos](https://github.com/getlytos/lytos-method) — a human-first method for working with AI agents.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g lytos-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then use:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
lytos init # Install Lytos in your project
|
|
20
|
+
lytos board # Regenerate BOARD.md from issue frontmatter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or use without installing:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx lytos init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
One command to install the method, one command to validate your setup, one command to see your sprint.
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|---------|-------------|
|
|
37
|
+
| `lytos init` | Scaffold `.lytos/` in your project (interactive, detects your stack) |
|
|
38
|
+
| `lytos board` | Regenerate BOARD.md from issue YAML frontmatter |
|
|
39
|
+
| `lytos lint` | Validate `.lytos/` structure and content *(coming soon)* |
|
|
40
|
+
| `lytos doctor` | Full diagnostic — missing files, broken links, stale memory *(coming soon)* |
|
|
41
|
+
| `lytos status` | Display sprint DAG in terminal *(coming soon)* |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Built with Lytos
|
|
46
|
+
|
|
47
|
+
This project uses Lytos to develop itself. The `.lytos/` directory contains the real manifest, sprint, issues, and memory for this project — not templates.
|
|
48
|
+
|
|
49
|
+
If you want to contribute, open this repo in Claude Code and say: **"Help me understand this project."**
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Author
|
|
54
|
+
|
|
55
|
+
Created by **Frederic Galliné** — [ubeez.com](https://ubeez.com)
|
|
56
|
+
|
|
57
|
+
- GitHub: [@FredericGalline](https://github.com/FredericGalline)
|
|
58
|
+
- X: [@fred](https://x.com/fred)
|
|
59
|
+
|
|
60
|
+
Part of the [Lytos](https://github.com/getlytos/lytos-method) project.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT — see [LICENSE](./LICENSE)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command as Command3 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { existsSync as existsSync3 } from "fs";
|
|
9
|
+
import { basename, resolve } from "path";
|
|
10
|
+
import { createInterface } from "readline";
|
|
11
|
+
|
|
12
|
+
// src/lib/detect-stack.ts
|
|
13
|
+
import { existsSync, readFileSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var detectors = [
|
|
16
|
+
{
|
|
17
|
+
file: "package.json",
|
|
18
|
+
detect: (content) => {
|
|
19
|
+
const pkg = JSON.parse(content);
|
|
20
|
+
const deps = {
|
|
21
|
+
...pkg.dependencies,
|
|
22
|
+
...pkg.devDependencies
|
|
23
|
+
};
|
|
24
|
+
const stack = {};
|
|
25
|
+
if (deps.typescript || deps["ts-node"]) {
|
|
26
|
+
stack.language = "TypeScript";
|
|
27
|
+
} else {
|
|
28
|
+
stack.language = "JavaScript";
|
|
29
|
+
}
|
|
30
|
+
if (deps.next) stack.framework = "Next.js";
|
|
31
|
+
else if (deps.nuxt) stack.framework = "Nuxt";
|
|
32
|
+
else if (deps.react) stack.framework = "React";
|
|
33
|
+
else if (deps.vue) stack.framework = "Vue";
|
|
34
|
+
else if (deps.svelte || deps["@sveltejs/kit"]) stack.framework = "SvelteKit";
|
|
35
|
+
else if (deps.express) stack.framework = "Express";
|
|
36
|
+
else if (deps.fastify) stack.framework = "Fastify";
|
|
37
|
+
else if (deps.hono) stack.framework = "Hono";
|
|
38
|
+
else if (deps.astro) stack.framework = "Astro";
|
|
39
|
+
if (deps.prisma || deps["@prisma/client"]) stack.database = "Prisma";
|
|
40
|
+
else if (deps.mongoose) stack.database = "MongoDB (Mongoose)";
|
|
41
|
+
else if (deps.pg) stack.database = "PostgreSQL";
|
|
42
|
+
else if (deps.mysql2) stack.database = "MySQL";
|
|
43
|
+
else if (deps.drizzle || deps["drizzle-orm"]) stack.database = "Drizzle ORM";
|
|
44
|
+
if (deps.vitest) stack.tests = "Vitest";
|
|
45
|
+
else if (deps.jest) stack.tests = "Jest";
|
|
46
|
+
else if (deps["@playwright/test"]) stack.tests = "Playwright";
|
|
47
|
+
else if (deps.mocha) stack.tests = "Mocha";
|
|
48
|
+
if (existsSync("bun.lockb")) stack.packageManager = "Bun";
|
|
49
|
+
else if (existsSync("pnpm-lock.yaml")) stack.packageManager = "pnpm";
|
|
50
|
+
else if (existsSync("yarn.lock")) stack.packageManager = "Yarn";
|
|
51
|
+
else stack.packageManager = "npm";
|
|
52
|
+
return stack;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
file: "requirements.txt",
|
|
57
|
+
detect: (content) => {
|
|
58
|
+
const stack = { language: "Python" };
|
|
59
|
+
const lower = content.toLowerCase();
|
|
60
|
+
if (lower.includes("fastapi")) stack.framework = "FastAPI";
|
|
61
|
+
else if (lower.includes("django")) stack.framework = "Django";
|
|
62
|
+
else if (lower.includes("flask")) stack.framework = "Flask";
|
|
63
|
+
if (lower.includes("sqlalchemy")) stack.database = "SQLAlchemy";
|
|
64
|
+
else if (lower.includes("psycopg")) stack.database = "PostgreSQL";
|
|
65
|
+
else if (lower.includes("pymongo")) stack.database = "MongoDB";
|
|
66
|
+
if (lower.includes("pytest")) stack.tests = "Pytest";
|
|
67
|
+
return stack;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
file: "pyproject.toml",
|
|
72
|
+
detect: (content) => {
|
|
73
|
+
const stack = { language: "Python" };
|
|
74
|
+
const lower = content.toLowerCase();
|
|
75
|
+
if (lower.includes("fastapi")) stack.framework = "FastAPI";
|
|
76
|
+
else if (lower.includes("django")) stack.framework = "Django";
|
|
77
|
+
else if (lower.includes("flask")) stack.framework = "Flask";
|
|
78
|
+
if (lower.includes("pytest")) stack.tests = "Pytest";
|
|
79
|
+
return stack;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
file: "go.mod",
|
|
84
|
+
detect: (content) => {
|
|
85
|
+
const stack = { language: "Go" };
|
|
86
|
+
if (content.includes("gin-gonic")) stack.framework = "Gin";
|
|
87
|
+
else if (content.includes("gorilla/mux")) stack.framework = "Gorilla Mux";
|
|
88
|
+
else if (content.includes("labstack/echo")) stack.framework = "Echo";
|
|
89
|
+
else if (content.includes("gofiber")) stack.framework = "Fiber";
|
|
90
|
+
return stack;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
file: "Cargo.toml",
|
|
95
|
+
detect: (content) => {
|
|
96
|
+
const stack = { language: "Rust" };
|
|
97
|
+
if (content.includes("actix-web")) stack.framework = "Actix Web";
|
|
98
|
+
else if (content.includes("axum")) stack.framework = "Axum";
|
|
99
|
+
else if (content.includes("rocket")) stack.framework = "Rocket";
|
|
100
|
+
return stack;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
file: "composer.json",
|
|
105
|
+
detect: (content) => {
|
|
106
|
+
const pkg = JSON.parse(content);
|
|
107
|
+
const stack = { language: "PHP" };
|
|
108
|
+
const req = JSON.stringify(pkg.require || {}).toLowerCase();
|
|
109
|
+
if (req.includes("laravel")) stack.framework = "Laravel";
|
|
110
|
+
else if (req.includes("symfony")) stack.framework = "Symfony";
|
|
111
|
+
else if (req.includes("wordpress")) stack.framework = "WordPress";
|
|
112
|
+
if (req.includes("phpunit")) stack.tests = "PHPUnit";
|
|
113
|
+
return stack;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
function detectStack(cwd) {
|
|
118
|
+
let result = {};
|
|
119
|
+
for (const detector of detectors) {
|
|
120
|
+
const filePath = join(cwd, detector.file);
|
|
121
|
+
if (existsSync(filePath)) {
|
|
122
|
+
try {
|
|
123
|
+
const content = readFileSync(filePath, "utf-8");
|
|
124
|
+
result = { ...result, ...detector.detect(content, cwd) };
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/lib/scaffold.ts
|
|
133
|
+
import { mkdirSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
134
|
+
import { join as join2 } from "path";
|
|
135
|
+
|
|
136
|
+
// src/lib/templates.ts
|
|
137
|
+
var REPO_URL = "https://github.com/getlytos/lytos-method";
|
|
138
|
+
function manifestTemplate(ctx) {
|
|
139
|
+
const stackRows = [
|
|
140
|
+
`| Language | ${ctx.stack.language || ""} |`,
|
|
141
|
+
`| Framework | ${ctx.stack.framework || ""} |`,
|
|
142
|
+
`| Database | ${ctx.stack.database || ""} |`,
|
|
143
|
+
`| Tests | ${ctx.stack.tests || ""} |`
|
|
144
|
+
].join("\n");
|
|
145
|
+
return `# Manifest \u2014 ${ctx.projectName}
|
|
146
|
+
|
|
147
|
+
*This file is the project's constitution. It is read by agents at the start of each work session.*
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Identity
|
|
152
|
+
|
|
153
|
+
| Field | Value |
|
|
154
|
+
|-------|-------|
|
|
155
|
+
| Name | ${ctx.projectName} |
|
|
156
|
+
| Description | |
|
|
157
|
+
| Owner | |
|
|
158
|
+
| Repo | |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Why this project exists
|
|
163
|
+
|
|
164
|
+
*3-5 sentences. The "why" of this project.*
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## What this project is
|
|
169
|
+
|
|
170
|
+
-
|
|
171
|
+
|
|
172
|
+
## What this project is not
|
|
173
|
+
|
|
174
|
+
-
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Tech stack
|
|
179
|
+
|
|
180
|
+
| Component | Technology |
|
|
181
|
+
|-----------|------------|
|
|
182
|
+
${stackRows}
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Project vocabulary
|
|
187
|
+
|
|
188
|
+
| Term | Definition |
|
|
189
|
+
|------|-----------|
|
|
190
|
+
| | |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Development principles
|
|
195
|
+
|
|
196
|
+
*When an agent hesitates between two approaches, it consults these principles to decide. Formulate as trade-offs: "we prefer X over Y, because Z."*
|
|
197
|
+
|
|
198
|
+
-
|
|
199
|
+
-
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## AI models by complexity
|
|
204
|
+
|
|
205
|
+
*Map your own models based on your budget and tools. Update when better models come out.*
|
|
206
|
+
|
|
207
|
+
| Complexity | Usage | Model |
|
|
208
|
+
|------------|-------|-------|
|
|
209
|
+
| \`light\` | Documentation, formatting, renaming, boilerplate | |
|
|
210
|
+
| \`standard\` | Day-to-day development, code review, tests | |
|
|
211
|
+
| \`heavy\` | Complex architecture, critical algorithms, security | |
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Important links
|
|
216
|
+
|
|
217
|
+
| Resource | URL |
|
|
218
|
+
|----------|-----|
|
|
219
|
+
| Main repo | |
|
|
220
|
+
| Documentation | |
|
|
221
|
+
| Staging | |
|
|
222
|
+
| Production | |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
*Last updated: ${ctx.date}*
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
function memoryTemplate(ctx) {
|
|
230
|
+
return `# Memory \u2014 ${ctx.projectName}
|
|
231
|
+
|
|
232
|
+
*This file is the project memory's table of contents. Do not read everything \u2014 load only what is relevant to the current task.*
|
|
233
|
+
|
|
234
|
+
> **Last updated**: ${ctx.date}
|
|
235
|
+
> **Number of entries**: 0
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Section index
|
|
240
|
+
|
|
241
|
+
| File | Content | Load when... |
|
|
242
|
+
|------|---------|--------------|
|
|
243
|
+
| [architecture.md](./cortex/architecture.md) | Architectural decisions, technical choices | Any structural task |
|
|
244
|
+
| [backend.md](./cortex/backend.md) | Server-side patterns and pitfalls | Backend task |
|
|
245
|
+
| [frontend.md](./cortex/frontend.md) | Client-side patterns and pitfalls | Frontend task |
|
|
246
|
+
| [patterns.md](./cortex/patterns.md) | Recurring code patterns | Code review, new code |
|
|
247
|
+
| [bugs.md](./cortex/bugs.md) | Recurring problems and solutions | Debug, fix |
|
|
248
|
+
| [business.md](./cortex/business.md) | Business context, vocabulary | Business logic, UX |
|
|
249
|
+
| [sprints.md](./cortex/sprints.md) | Sprint history | Planning |
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Living summary
|
|
254
|
+
|
|
255
|
+
*3-5 lines. The current state of the project at a glance.*
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
*The folder is the structure. The file is the content. This table of contents is the map.*
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
var cortexFiles = [
|
|
263
|
+
{
|
|
264
|
+
name: "architecture.md",
|
|
265
|
+
title: "Architecture & Technical Decisions",
|
|
266
|
+
description: "Load this file for any task that affects the project structure.",
|
|
267
|
+
example: `### ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} \u2014 Database choice
|
|
268
|
+
|
|
269
|
+
**Context**: Hesitation between SQLite (simple) and PostgreSQL (robust).
|
|
270
|
+
**Decision**: PostgreSQL from the start.
|
|
271
|
+
**Consequence**: Requires Docker for local dev, but no painful migration later.`
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "backend.md",
|
|
275
|
+
title: "Backend",
|
|
276
|
+
description: "Load this file for any backend task: API, database, services.",
|
|
277
|
+
example: `### Key files
|
|
278
|
+
|
|
279
|
+
| File | Role |
|
|
280
|
+
|------|------|
|
|
281
|
+
| \`src/main.py\` | Application entry point |
|
|
282
|
+
| \`src/models/\` | Data models |
|
|
283
|
+
| \`src/routes/\` | API endpoints |`
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "frontend.md",
|
|
287
|
+
title: "Frontend",
|
|
288
|
+
description: "Load this file for any frontend task: UI, components, styles.",
|
|
289
|
+
example: `### Key files
|
|
290
|
+
|
|
291
|
+
| File | Role |
|
|
292
|
+
|------|------|
|
|
293
|
+
| \`src/App.tsx\` | Root component |
|
|
294
|
+
| \`src/components/\` | Reusable components |
|
|
295
|
+
| \`src/hooks/\` | Custom hooks |`
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "patterns.md",
|
|
299
|
+
title: "Discovered Patterns",
|
|
300
|
+
description: "Load this file for code review, refactoring, or writing new code.",
|
|
301
|
+
example: `### Pattern name
|
|
302
|
+
|
|
303
|
+
**What**: One-sentence description of the pattern.
|
|
304
|
+
**Where**: File(s) where it is applied.
|
|
305
|
+
**Why it works**: What makes it effective in this context.`
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "bugs.md",
|
|
309
|
+
title: "Recurring Problems & Solutions",
|
|
310
|
+
description: "Load this file before debugging \u2014 the problem may have already been solved.",
|
|
311
|
+
example: `| Problem | Cause | Solution |
|
|
312
|
+
|---------|-------|----------|
|
|
313
|
+
| Tests fail on CI but pass locally | Missing env variables in pipeline | Add secrets in CI settings |`
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "business.md",
|
|
317
|
+
title: "Business Context",
|
|
318
|
+
description: "Load this file for any task involving business logic or UX.",
|
|
319
|
+
example: `### Business concept name
|
|
320
|
+
|
|
321
|
+
**Rule**: What the business requires.
|
|
322
|
+
**Why**: The business reason (not technical).
|
|
323
|
+
**Code impact**: What this means concretely in the code.`
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "sprints.md",
|
|
327
|
+
title: "Sprint History",
|
|
328
|
+
description: "Load this file at sprint start, retrospective, or planning.",
|
|
329
|
+
example: `| Sprint | Objective | Result | Key learning |
|
|
330
|
+
|--------|-----------|--------|--------------|`
|
|
331
|
+
}
|
|
332
|
+
];
|
|
333
|
+
function cortexTemplate(file) {
|
|
334
|
+
return `# Memory \u2014 ${file.title}
|
|
335
|
+
|
|
336
|
+
*${file.description}*
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
<!-- Example to adapt or remove:
|
|
341
|
+
|
|
342
|
+
${file.example}
|
|
343
|
+
|
|
344
|
+
-->
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
function getCortexFiles() {
|
|
348
|
+
return cortexFiles;
|
|
349
|
+
}
|
|
350
|
+
function boardTemplate(ctx) {
|
|
351
|
+
return `# Issue Board \u2014 ${ctx.projectName}
|
|
352
|
+
|
|
353
|
+
> Each issue = a \`ISS-XXXX-title.md\` file in the folder matching its status.
|
|
354
|
+
>
|
|
355
|
+
> **Last updated**: ${ctx.date}
|
|
356
|
+
> **Next number**: ISS-0001
|
|
357
|
+
|
|
358
|
+
> Regenerate: \`npx lytos board\`
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Issue index
|
|
363
|
+
|
|
364
|
+
### 0-icebox (ideas)
|
|
365
|
+
|
|
366
|
+
_No issues._
|
|
367
|
+
|
|
368
|
+
### 1-backlog (prioritized)
|
|
369
|
+
|
|
370
|
+
_No issues._
|
|
371
|
+
|
|
372
|
+
### 2-sprint (committed)
|
|
373
|
+
|
|
374
|
+
_No issues._
|
|
375
|
+
|
|
376
|
+
### 3-in-progress (in dev)
|
|
377
|
+
|
|
378
|
+
_No issues._
|
|
379
|
+
|
|
380
|
+
### 4-review (review/test)
|
|
381
|
+
|
|
382
|
+
_No issues._
|
|
383
|
+
|
|
384
|
+
### 5-done (completed)
|
|
385
|
+
|
|
386
|
+
_No issues._
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
*The YAML frontmatter is the source of truth. The folder is the visual status. The BOARD.md is the map.*
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
function claudeTemplate(_ctx) {
|
|
394
|
+
return `# CLAUDE.md
|
|
395
|
+
|
|
396
|
+
This project uses **Lytos** \u2014 a human-first method for working with AI agents.
|
|
397
|
+
|
|
398
|
+
## First session (setup)
|
|
399
|
+
|
|
400
|
+
If the manifest is empty or incomplete, read first:
|
|
401
|
+
- .lytos/LYTOS.md \u2014 understand the method and how to help fill the files
|
|
402
|
+
|
|
403
|
+
## Every session
|
|
404
|
+
|
|
405
|
+
Read these files in order:
|
|
406
|
+
1. .lytos/manifest.md \u2014 the project constitution (identity, stack, principles, AI models)
|
|
407
|
+
2. .lytos/memory/MEMORY.md \u2014 the memory summary (then load relevant cortex/ sections)
|
|
408
|
+
3. .lytos/rules/default-rules.md \u2014 quality criteria
|
|
409
|
+
|
|
410
|
+
## To work on a task
|
|
411
|
+
|
|
412
|
+
4. .lytos/issue-board/BOARD.md \u2014 board state
|
|
413
|
+
5. .lytos/skills/session-start.md \u2014 full start and end-of-task procedure
|
|
414
|
+
|
|
415
|
+
## Rules
|
|
416
|
+
|
|
417
|
+
- The YAML frontmatter of issues is the source of truth
|
|
418
|
+
- Don't interpret silently \u2014 ask if an instruction is ambiguous
|
|
419
|
+
- At end of task: update frontmatter, move the file, update BOARD.md
|
|
420
|
+
- Check the issue's \`complexity\` field + the manifest table for which model to use
|
|
421
|
+
|
|
422
|
+
Documentation: ${REPO_URL}
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
function cursorrTemplate(_ctx) {
|
|
426
|
+
return `This project uses Lytos \u2014 a human-first method for working with AI agents.
|
|
427
|
+
|
|
428
|
+
First session (setup): if the manifest is empty, read @.lytos/LYTOS.md to understand the method.
|
|
429
|
+
|
|
430
|
+
Every session, read in order:
|
|
431
|
+
1. @.lytos/manifest.md \u2014 the project constitution
|
|
432
|
+
2. @.lytos/memory/MEMORY.md \u2014 the memory summary (then relevant cortex/ sections)
|
|
433
|
+
3. @.lytos/rules/default-rules.md \u2014 quality criteria
|
|
434
|
+
|
|
435
|
+
To work on a task:
|
|
436
|
+
4. @.lytos/issue-board/BOARD.md \u2014 board state
|
|
437
|
+
5. @.lytos/skills/session-start.md \u2014 start and end-of-task procedure
|
|
438
|
+
|
|
439
|
+
Rules:
|
|
440
|
+
- The YAML frontmatter of issues is the source of truth
|
|
441
|
+
- Don't interpret silently \u2014 ask if an instruction is ambiguous
|
|
442
|
+
- At end of task: update frontmatter, move the file, update BOARD.md
|
|
443
|
+
- Check the issue's complexity field + the manifest table for the model to use
|
|
444
|
+
|
|
445
|
+
Documentation: ${REPO_URL}
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/lib/scaffold.ts
|
|
450
|
+
var REPO_RAW = "https://raw.githubusercontent.com/getlytos/lytos-method/main";
|
|
451
|
+
var SKILLS = [
|
|
452
|
+
"session-start",
|
|
453
|
+
"code-review",
|
|
454
|
+
"testing",
|
|
455
|
+
"documentation",
|
|
456
|
+
"git-workflow",
|
|
457
|
+
"code-structure",
|
|
458
|
+
"deployment",
|
|
459
|
+
"security",
|
|
460
|
+
"api-design"
|
|
461
|
+
];
|
|
462
|
+
var REMOTE_FILES = [
|
|
463
|
+
...SKILLS.map((s) => ({
|
|
464
|
+
remote: `skills/${s}.md`,
|
|
465
|
+
local: `skills/${s}.md`
|
|
466
|
+
})),
|
|
467
|
+
{ remote: "rules/default-rules.md", local: "rules/default-rules.md" },
|
|
468
|
+
{ remote: "rules/README.md", local: "rules/README.md" },
|
|
469
|
+
{ remote: "LYTOS.md", local: "LYTOS.md" },
|
|
470
|
+
{ remote: "templates/sprint.md", local: "templates/sprint.md" },
|
|
471
|
+
{
|
|
472
|
+
remote: "issue-board/templates/issue-feature.md",
|
|
473
|
+
local: "issue-board/templates/issue-feature.md"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
remote: "issue-board/templates/issue-task.md",
|
|
477
|
+
local: "issue-board/templates/issue-task.md"
|
|
478
|
+
}
|
|
479
|
+
];
|
|
480
|
+
async function download(url) {
|
|
481
|
+
const response = await fetch(url);
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
throw new Error(`Failed to download ${url}: ${response.status}`);
|
|
484
|
+
}
|
|
485
|
+
return response.text();
|
|
486
|
+
}
|
|
487
|
+
function ensureDir(dir, dryRun) {
|
|
488
|
+
if (!dryRun && !existsSync2(dir)) {
|
|
489
|
+
mkdirSync(dir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function writeFile(path, content, dryRun, result) {
|
|
493
|
+
if (dryRun) {
|
|
494
|
+
result.filesCreated.push(path);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
ensureDir(join2(path, ".."), false);
|
|
498
|
+
writeFileSync(path, content, "utf-8");
|
|
499
|
+
result.filesCreated.push(path);
|
|
500
|
+
}
|
|
501
|
+
async function scaffold(options) {
|
|
502
|
+
const result = {
|
|
503
|
+
filesCreated: [],
|
|
504
|
+
filesSkipped: [],
|
|
505
|
+
warnings: []
|
|
506
|
+
};
|
|
507
|
+
const lytosDir = join2(options.cwd, ".lytos");
|
|
508
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
509
|
+
const ctx = {
|
|
510
|
+
projectName: options.projectName,
|
|
511
|
+
date: today,
|
|
512
|
+
stack: options.stack
|
|
513
|
+
};
|
|
514
|
+
const dirs = [
|
|
515
|
+
"memory/cortex",
|
|
516
|
+
"rules",
|
|
517
|
+
"skills",
|
|
518
|
+
"scripts",
|
|
519
|
+
"templates",
|
|
520
|
+
"issue-board/0-icebox",
|
|
521
|
+
"issue-board/1-backlog",
|
|
522
|
+
"issue-board/2-sprint",
|
|
523
|
+
"issue-board/3-in-progress",
|
|
524
|
+
"issue-board/4-review",
|
|
525
|
+
"issue-board/5-done",
|
|
526
|
+
"issue-board/templates"
|
|
527
|
+
];
|
|
528
|
+
for (const dir of dirs) {
|
|
529
|
+
ensureDir(join2(lytosDir, dir), options.dryRun);
|
|
530
|
+
}
|
|
531
|
+
const kanbanDirs = [
|
|
532
|
+
"0-icebox",
|
|
533
|
+
"1-backlog",
|
|
534
|
+
"2-sprint",
|
|
535
|
+
"3-in-progress",
|
|
536
|
+
"4-review",
|
|
537
|
+
"5-done"
|
|
538
|
+
];
|
|
539
|
+
for (const dir of kanbanDirs) {
|
|
540
|
+
writeFile(
|
|
541
|
+
join2(lytosDir, "issue-board", dir, ".gitkeep"),
|
|
542
|
+
"",
|
|
543
|
+
options.dryRun,
|
|
544
|
+
result
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
writeFile(
|
|
548
|
+
join2(lytosDir, "manifest.md"),
|
|
549
|
+
manifestTemplate(ctx),
|
|
550
|
+
options.dryRun,
|
|
551
|
+
result
|
|
552
|
+
);
|
|
553
|
+
writeFile(
|
|
554
|
+
join2(lytosDir, "memory", "MEMORY.md"),
|
|
555
|
+
memoryTemplate(ctx),
|
|
556
|
+
options.dryRun,
|
|
557
|
+
result
|
|
558
|
+
);
|
|
559
|
+
writeFile(
|
|
560
|
+
join2(lytosDir, "issue-board", "BOARD.md"),
|
|
561
|
+
boardTemplate(ctx),
|
|
562
|
+
options.dryRun,
|
|
563
|
+
result
|
|
564
|
+
);
|
|
565
|
+
for (const cortexFile of getCortexFiles()) {
|
|
566
|
+
writeFile(
|
|
567
|
+
join2(lytosDir, "memory", "cortex", cortexFile.name),
|
|
568
|
+
cortexTemplate(cortexFile),
|
|
569
|
+
options.dryRun,
|
|
570
|
+
result
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
for (const file of REMOTE_FILES) {
|
|
574
|
+
try {
|
|
575
|
+
const content = await download(`${REPO_RAW}/${file.remote}`);
|
|
576
|
+
writeFile(
|
|
577
|
+
join2(lytosDir, file.local),
|
|
578
|
+
content,
|
|
579
|
+
options.dryRun,
|
|
580
|
+
result
|
|
581
|
+
);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
result.warnings.push(
|
|
584
|
+
`Could not download ${file.remote}: ${err instanceof Error ? err.message : String(err)}`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (options.tool === "claude") {
|
|
589
|
+
writeFile(
|
|
590
|
+
join2(options.cwd, "CLAUDE.md"),
|
|
591
|
+
claudeTemplate(ctx),
|
|
592
|
+
options.dryRun,
|
|
593
|
+
result
|
|
594
|
+
);
|
|
595
|
+
} else if (options.tool === "cursor") {
|
|
596
|
+
writeFile(
|
|
597
|
+
join2(options.cwd, ".cursorrules"),
|
|
598
|
+
cursorrTemplate(ctx),
|
|
599
|
+
options.dryRun,
|
|
600
|
+
result
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/lib/output.ts
|
|
607
|
+
var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
|
|
608
|
+
function color(code, text) {
|
|
609
|
+
if (noColor) return text;
|
|
610
|
+
return `\x1B[${code}m${text}\x1B[0m`;
|
|
611
|
+
}
|
|
612
|
+
var green = (t) => color("32", t);
|
|
613
|
+
var red = (t) => color("31", t);
|
|
614
|
+
var blue = (t) => color("34", t);
|
|
615
|
+
var bold = (t) => color("1", t);
|
|
616
|
+
function info(msg) {
|
|
617
|
+
console.error(`${blue("\u2192")} ${msg}`);
|
|
618
|
+
}
|
|
619
|
+
function ok(msg) {
|
|
620
|
+
console.error(`${green("\u2713")} ${msg}`);
|
|
621
|
+
}
|
|
622
|
+
function warn(msg) {
|
|
623
|
+
console.error(`${red("!")} ${msg}`);
|
|
624
|
+
}
|
|
625
|
+
function error(msg) {
|
|
626
|
+
console.error(`${red("\u2717")} ${msg}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/commands/init.ts
|
|
630
|
+
function prompt(question, defaultValue) {
|
|
631
|
+
const rl = createInterface({
|
|
632
|
+
input: process.stdin,
|
|
633
|
+
output: process.stderr
|
|
634
|
+
});
|
|
635
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
636
|
+
return new Promise((resolve2) => {
|
|
637
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
638
|
+
rl.close();
|
|
639
|
+
resolve2(answer.trim() || defaultValue || "");
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
async function promptChoice(question, choices) {
|
|
644
|
+
console.error("");
|
|
645
|
+
console.error(question);
|
|
646
|
+
for (const choice of choices) {
|
|
647
|
+
console.error(` ${choice.key}) ${choice.label}`);
|
|
648
|
+
}
|
|
649
|
+
console.error("");
|
|
650
|
+
const answer = await prompt("Choice");
|
|
651
|
+
const match = choices.find((c) => c.key === answer);
|
|
652
|
+
return match ? match.key : choices[0].key;
|
|
653
|
+
}
|
|
654
|
+
var initCommand = new Command("init").description("Scaffold Lytos in your project").option("--name <name>", "Project name").option(
|
|
655
|
+
"--tool <tool>",
|
|
656
|
+
"AI tool to configure (claude, cursor, none)",
|
|
657
|
+
""
|
|
658
|
+
).option("--yes", "Skip prompts, use defaults", false).option("--force", "Override existing .lytos/ directory", false).option("--dry-run", "Show what would be created without creating", false).action(async (opts) => {
|
|
659
|
+
const cwd = process.cwd();
|
|
660
|
+
const lytosDir = resolve(cwd, ".lytos");
|
|
661
|
+
if (existsSync3(lytosDir) && !opts.force) {
|
|
662
|
+
error(
|
|
663
|
+
".lytos/ already exists. Use --force to override."
|
|
664
|
+
);
|
|
665
|
+
process.exit(2);
|
|
666
|
+
}
|
|
667
|
+
info("Detecting project stack...");
|
|
668
|
+
const stack = detectStack(cwd);
|
|
669
|
+
if (stack.language) {
|
|
670
|
+
ok(`Detected: ${stack.language}${stack.framework ? ` + ${stack.framework}` : ""}${stack.tests ? ` + ${stack.tests}` : ""}`);
|
|
671
|
+
} else {
|
|
672
|
+
info("No known stack detected \u2014 you can fill it in manually.");
|
|
673
|
+
}
|
|
674
|
+
let projectName = opts.name;
|
|
675
|
+
if (!projectName && !opts.yes) {
|
|
676
|
+
projectName = await prompt(
|
|
677
|
+
"Project name",
|
|
678
|
+
basename(cwd)
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
projectName = projectName || basename(cwd);
|
|
682
|
+
let tool = opts.tool;
|
|
683
|
+
if (!tool && !opts.yes) {
|
|
684
|
+
const choice = await promptChoice("Which AI tool do you use?", [
|
|
685
|
+
{ key: "1", label: "Claude Code" },
|
|
686
|
+
{ key: "2", label: "Cursor" },
|
|
687
|
+
{ key: "3", label: "Other / None" }
|
|
688
|
+
]);
|
|
689
|
+
tool = choice === "1" ? "claude" : choice === "2" ? "cursor" : "none";
|
|
690
|
+
}
|
|
691
|
+
tool = tool || "none";
|
|
692
|
+
if (opts.dryRun) {
|
|
693
|
+
console.error("");
|
|
694
|
+
info(`${bold("Dry run")} \u2014 nothing will be created.`);
|
|
695
|
+
}
|
|
696
|
+
console.error("");
|
|
697
|
+
info(`Installing Lytos in .lytos/...`);
|
|
698
|
+
console.error("");
|
|
699
|
+
const result = await scaffold({
|
|
700
|
+
projectName,
|
|
701
|
+
tool,
|
|
702
|
+
stack,
|
|
703
|
+
cwd,
|
|
704
|
+
dryRun: opts.dryRun
|
|
705
|
+
});
|
|
706
|
+
for (const w of result.warnings) {
|
|
707
|
+
warn(w);
|
|
708
|
+
}
|
|
709
|
+
ok(`${result.filesCreated.length} files created`);
|
|
710
|
+
console.error("");
|
|
711
|
+
console.error(bold(green("Lytos is installed.")));
|
|
712
|
+
console.error("");
|
|
713
|
+
console.error(" Next step \u2014 open your AI tool and say:");
|
|
714
|
+
console.error("");
|
|
715
|
+
console.error(
|
|
716
|
+
` ${bold('"Help me configure Lytos for this project."')}`
|
|
717
|
+
);
|
|
718
|
+
console.error("");
|
|
719
|
+
console.error(
|
|
720
|
+
" The AI will read the briefing, understand the method, and ask"
|
|
721
|
+
);
|
|
722
|
+
console.error(
|
|
723
|
+
" you the right questions to fill your manifest."
|
|
724
|
+
);
|
|
725
|
+
console.error("");
|
|
726
|
+
console.error(" Installed structure:");
|
|
727
|
+
console.error(" .lytos/");
|
|
728
|
+
console.error(" \u251C\u2500\u2500 LYTOS.md <- AI briefing");
|
|
729
|
+
console.error(
|
|
730
|
+
" \u251C\u2500\u2500 manifest.md <- fill with your AI's help"
|
|
731
|
+
);
|
|
732
|
+
console.error(" \u251C\u2500\u2500 memory/");
|
|
733
|
+
console.error(" \u2502 \u251C\u2500\u2500 MEMORY.md");
|
|
734
|
+
console.error(
|
|
735
|
+
" \u2502 \u2514\u2500\u2500 cortex/ <- will fill up as you work"
|
|
736
|
+
);
|
|
737
|
+
console.error(" \u251C\u2500\u2500 skills/ <- 9 operational skills");
|
|
738
|
+
console.error(" \u251C\u2500\u2500 rules/ <- quality criteria");
|
|
739
|
+
console.error(" \u251C\u2500\u2500 issue-board/ <- Kanban board");
|
|
740
|
+
console.error(" \u2514\u2500\u2500 templates/ <- sprint template");
|
|
741
|
+
console.error("");
|
|
742
|
+
console.error(
|
|
743
|
+
` Documentation: https://github.com/getlytos/lytos-method`
|
|
744
|
+
);
|
|
745
|
+
console.error("");
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// src/commands/board.ts
|
|
749
|
+
import { Command as Command2 } from "commander";
|
|
750
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
751
|
+
import { join as join4 } from "path";
|
|
752
|
+
|
|
753
|
+
// src/lib/board-generator.ts
|
|
754
|
+
import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
|
|
755
|
+
import { join as join3 } from "path";
|
|
756
|
+
|
|
757
|
+
// src/lib/frontmatter.ts
|
|
758
|
+
function parseFrontmatter(content) {
|
|
759
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
760
|
+
if (!match) return null;
|
|
761
|
+
const frontmatter = {};
|
|
762
|
+
for (const line of match[1].trim().split("\n")) {
|
|
763
|
+
const colonIndex = line.indexOf(":");
|
|
764
|
+
if (colonIndex === -1) continue;
|
|
765
|
+
const key = line.slice(0, colonIndex).trim();
|
|
766
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
767
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
768
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
769
|
+
const items = value.slice(1, -1).split(",").map((v) => v.trim().replace(/^["']|["']$/g, "")).filter((v) => v.length > 0);
|
|
770
|
+
frontmatter[key] = items;
|
|
771
|
+
} else {
|
|
772
|
+
frontmatter[key] = value;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return frontmatter;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/lib/board-generator.ts
|
|
779
|
+
var STATUS_FOLDERS = [
|
|
780
|
+
"0-icebox",
|
|
781
|
+
"1-backlog",
|
|
782
|
+
"2-sprint",
|
|
783
|
+
"3-in-progress",
|
|
784
|
+
"4-review",
|
|
785
|
+
"5-done"
|
|
786
|
+
];
|
|
787
|
+
var STATUS_LABELS = {
|
|
788
|
+
"0-icebox": "ideas",
|
|
789
|
+
"1-backlog": "prioritized",
|
|
790
|
+
"2-sprint": "committed",
|
|
791
|
+
"3-in-progress": "in dev",
|
|
792
|
+
"4-review": "review/test",
|
|
793
|
+
"5-done": "completed"
|
|
794
|
+
};
|
|
795
|
+
function collectIssues(boardDir) {
|
|
796
|
+
const issues = [];
|
|
797
|
+
const warnings = [];
|
|
798
|
+
let maxNum = 0;
|
|
799
|
+
for (const folder of STATUS_FOLDERS) {
|
|
800
|
+
const folderPath = join3(boardDir, folder);
|
|
801
|
+
if (!existsSync4(folderPath)) continue;
|
|
802
|
+
const files = readdirSync(folderPath).filter((f) => f.startsWith("ISS-") && f.endsWith(".md")).sort();
|
|
803
|
+
for (const file of files) {
|
|
804
|
+
const content = readFileSync2(join3(folderPath, file), "utf-8");
|
|
805
|
+
const fm = parseFrontmatter(content);
|
|
806
|
+
if (!fm) {
|
|
807
|
+
warnings.push(`${folder}/${file}: no YAML frontmatter found`);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const fmStatus = typeof fm.status === "string" ? fm.status : "";
|
|
811
|
+
if (fmStatus && fmStatus !== folder) {
|
|
812
|
+
warnings.push(
|
|
813
|
+
`${folder}/${file}: folder is ${folder} but frontmatter says status: ${fmStatus}`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
issues.push({
|
|
817
|
+
frontmatter: fm,
|
|
818
|
+
filename: file,
|
|
819
|
+
folder,
|
|
820
|
+
status: fmStatus || folder
|
|
821
|
+
});
|
|
822
|
+
const numMatch = file.match(/ISS-(\d+)/);
|
|
823
|
+
if (numMatch) {
|
|
824
|
+
maxNum = Math.max(maxNum, parseInt(numMatch[1], 10));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const nextNumber = `ISS-${String(maxNum + 1).padStart(4, "0")}`;
|
|
829
|
+
return { issues, warnings, nextNumber };
|
|
830
|
+
}
|
|
831
|
+
function generateBoardMarkdown(data) {
|
|
832
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
833
|
+
const lines = [];
|
|
834
|
+
lines.push("# Issue Board");
|
|
835
|
+
lines.push("");
|
|
836
|
+
lines.push(
|
|
837
|
+
"> This file is generated by `lytos board`. The source of truth is the YAML frontmatter of each issue."
|
|
838
|
+
);
|
|
839
|
+
lines.push(">");
|
|
840
|
+
lines.push(`> **Last generated**: ${today}`);
|
|
841
|
+
lines.push(`> **Next number**: ${data.nextNumber}`);
|
|
842
|
+
lines.push("");
|
|
843
|
+
lines.push("---");
|
|
844
|
+
lines.push("");
|
|
845
|
+
lines.push("## Issue index");
|
|
846
|
+
lines.push("");
|
|
847
|
+
for (const folder of STATUS_FOLDERS) {
|
|
848
|
+
const label = STATUS_LABELS[folder];
|
|
849
|
+
lines.push(`### ${folder} (${label})`);
|
|
850
|
+
lines.push("");
|
|
851
|
+
const folderIssues = data.issues.filter((i) => i.status === folder);
|
|
852
|
+
if (folderIssues.length === 0) {
|
|
853
|
+
lines.push("_No issues._");
|
|
854
|
+
lines.push("");
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (folder === "5-done") {
|
|
858
|
+
lines.push("| # | Title | Completed |");
|
|
859
|
+
lines.push("|---|-------|-----------|");
|
|
860
|
+
for (const issue of folderIssues) {
|
|
861
|
+
const id = issue.frontmatter.id || "?";
|
|
862
|
+
const title = issue.frontmatter.title || "?";
|
|
863
|
+
const updated = issue.frontmatter.updated || "?";
|
|
864
|
+
const relPath = `${issue.folder}/${issue.filename}`;
|
|
865
|
+
lines.push(`| [${id}](${relPath}) | ${title} | ${updated} |`);
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
const hasDeps = folderIssues.some((i) => {
|
|
869
|
+
const deps = i.frontmatter.depends;
|
|
870
|
+
return Array.isArray(deps) ? deps.length > 0 : !!deps;
|
|
871
|
+
});
|
|
872
|
+
if (hasDeps) {
|
|
873
|
+
lines.push("| # | Title | Priority | Effort | Depends |");
|
|
874
|
+
lines.push("|---|-------|----------|--------|---------|");
|
|
875
|
+
} else {
|
|
876
|
+
lines.push("| # | Title | Priority | Effort |");
|
|
877
|
+
lines.push("|---|-------|----------|--------|");
|
|
878
|
+
}
|
|
879
|
+
for (const issue of folderIssues) {
|
|
880
|
+
const id = issue.frontmatter.id || "?";
|
|
881
|
+
const title = issue.frontmatter.title || "?";
|
|
882
|
+
const priority = issue.frontmatter.priority || "?";
|
|
883
|
+
const effort = issue.frontmatter.effort || "?";
|
|
884
|
+
const relPath = `${issue.folder}/${issue.filename}`;
|
|
885
|
+
if (hasDeps) {
|
|
886
|
+
const deps = issue.frontmatter.depends;
|
|
887
|
+
const depsStr = Array.isArray(deps) && deps.length > 0 ? deps.join(", ") : "\u2014";
|
|
888
|
+
lines.push(
|
|
889
|
+
`| [${id}](${relPath}) | ${title} | ${priority} | ${effort} | ${depsStr} |`
|
|
890
|
+
);
|
|
891
|
+
} else {
|
|
892
|
+
lines.push(
|
|
893
|
+
`| [${id}](${relPath}) | ${title} | ${priority} | ${effort} |`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
lines.push("");
|
|
899
|
+
}
|
|
900
|
+
lines.push("---");
|
|
901
|
+
lines.push("");
|
|
902
|
+
lines.push(
|
|
903
|
+
"*Generated by `lytos board` \u2014 the YAML frontmatter is the source of truth.*"
|
|
904
|
+
);
|
|
905
|
+
lines.push("");
|
|
906
|
+
return lines.join("\n");
|
|
907
|
+
}
|
|
908
|
+
function boardToJson(data) {
|
|
909
|
+
return {
|
|
910
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
911
|
+
nextNumber: data.nextNumber,
|
|
912
|
+
warnings: data.warnings,
|
|
913
|
+
columns: STATUS_FOLDERS.map((folder) => ({
|
|
914
|
+
folder,
|
|
915
|
+
label: STATUS_LABELS[folder],
|
|
916
|
+
issues: data.issues.filter((i) => i.status === folder).map((i) => ({
|
|
917
|
+
id: i.frontmatter.id,
|
|
918
|
+
title: i.frontmatter.title,
|
|
919
|
+
priority: i.frontmatter.priority,
|
|
920
|
+
effort: i.frontmatter.effort,
|
|
921
|
+
complexity: i.frontmatter.complexity,
|
|
922
|
+
depends: i.frontmatter.depends,
|
|
923
|
+
status: i.status,
|
|
924
|
+
file: `${i.folder}/${i.filename}`
|
|
925
|
+
}))
|
|
926
|
+
}))
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/commands/board.ts
|
|
931
|
+
function findBoardDir(cwd) {
|
|
932
|
+
const candidates = [
|
|
933
|
+
join4(cwd, ".lytos", "issue-board"),
|
|
934
|
+
join4(cwd, "issue-board")
|
|
935
|
+
];
|
|
936
|
+
for (const candidate of candidates) {
|
|
937
|
+
if (existsSync5(candidate)) return candidate;
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
var boardCommand = new Command2("board").description("Regenerate BOARD.md from issue frontmatter").option(
|
|
942
|
+
"--check",
|
|
943
|
+
"Check if BOARD.md is up to date (exit 1 if not)",
|
|
944
|
+
false
|
|
945
|
+
).option("--json", "Output board data as JSON", false).action((opts) => {
|
|
946
|
+
const cwd = process.cwd();
|
|
947
|
+
const boardDir = findBoardDir(cwd);
|
|
948
|
+
if (!boardDir) {
|
|
949
|
+
error(
|
|
950
|
+
"No issue-board/ directory found. Run `lytos init` first."
|
|
951
|
+
);
|
|
952
|
+
process.exit(2);
|
|
953
|
+
}
|
|
954
|
+
const data = collectIssues(boardDir);
|
|
955
|
+
for (const w of data.warnings) {
|
|
956
|
+
warn(w);
|
|
957
|
+
}
|
|
958
|
+
if (opts.json) {
|
|
959
|
+
console.log(JSON.stringify(boardToJson(data), null, 2));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const newContent = generateBoardMarkdown(data);
|
|
963
|
+
if (opts.check) {
|
|
964
|
+
const boardPath2 = join4(boardDir, "BOARD.md");
|
|
965
|
+
if (!existsSync5(boardPath2)) {
|
|
966
|
+
error("BOARD.md does not exist.");
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
const existing = readFileSync3(boardPath2, "utf-8");
|
|
970
|
+
const normalize = (s) => s.replace(/\*\*Last generated\*\*:.*/, "").trim();
|
|
971
|
+
if (normalize(existing) === normalize(newContent)) {
|
|
972
|
+
ok("BOARD.md is up to date.");
|
|
973
|
+
return;
|
|
974
|
+
} else {
|
|
975
|
+
error(
|
|
976
|
+
"BOARD.md is outdated. Run `lytos board` to regenerate."
|
|
977
|
+
);
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const boardPath = join4(boardDir, "BOARD.md");
|
|
982
|
+
writeFileSync2(boardPath, newContent, "utf-8");
|
|
983
|
+
ok(
|
|
984
|
+
`BOARD.md generated: ${data.issues.length} issues found`
|
|
985
|
+
);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// src/cli.ts
|
|
989
|
+
var program = new Command3();
|
|
990
|
+
program.name("lytos").description(
|
|
991
|
+
"CLI tool for Lytos \u2014 a human-first method for working with AI agents"
|
|
992
|
+
).version("0.1.1");
|
|
993
|
+
program.addCommand(initCommand);
|
|
994
|
+
program.addCommand(boardCommand);
|
|
995
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lytos-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI tool for Lytos — a human-first method for working with AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lytos": "dist/cli.js",
|
|
8
|
+
"lytos-cli": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"pretest": "tsup",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"lint": "eslint src/",
|
|
20
|
+
"format": "prettier --write 'src/**/*.ts'",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"lytos",
|
|
26
|
+
"ai",
|
|
27
|
+
"agents",
|
|
28
|
+
"cli",
|
|
29
|
+
"scaffold",
|
|
30
|
+
"methodology",
|
|
31
|
+
"skills",
|
|
32
|
+
"memory",
|
|
33
|
+
"rules"
|
|
34
|
+
],
|
|
35
|
+
"author": "Frederic Galliné <frederic@galline.fr> (https://ubeez.com)",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/getlytos/lytos-cli.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/getlytos/lytos-method",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/getlytos/lytos-cli/issues"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"commander": "^12.1.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^9.39.4",
|
|
53
|
+
"@types/node": "^20.14.0",
|
|
54
|
+
"eslint": "^9.5.0",
|
|
55
|
+
"prettier": "^3.3.0",
|
|
56
|
+
"tsup": "^8.1.0",
|
|
57
|
+
"typescript": "^5.5.0",
|
|
58
|
+
"typescript-eslint": "^8.58.1",
|
|
59
|
+
"vitest": "^2.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|