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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/cli.js +995 -0
  4. 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
+ [![CI](https://github.com/getlytos/lytos-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/getlytos/lytos-cli/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/lytos)](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
+ }