bun-sticky 1.0.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/.github/FUNDING.yml +5 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/release.yml +36 -0
- package/CLAUDE.md +81 -0
- package/PUBLISH-PROTOCOL.md +63 -0
- package/README.md +134 -0
- package/index.ts +255 -0
- package/lib/parser.ts +75 -0
- package/lib/scorer.ts +225 -0
- package/lib/tier.ts +36 -0
- package/package.json +35 -0
- package/project.faf +23 -0
- package/tests/__snapshots__/sticky.test.ts.snap +225 -0
- package/tests/sticky.test.ts +1321 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
name: Bun Tests
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Bun
|
|
18
|
+
uses: oven-sh/setup-bun@v2
|
|
19
|
+
with:
|
|
20
|
+
bun-version: latest
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: bun install
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: bun test
|
|
27
|
+
|
|
28
|
+
- name: Type check
|
|
29
|
+
run: bun run tsc --noEmit
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
name: Publish to npm
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Bun
|
|
17
|
+
uses: oven-sh/setup-bun@v2
|
|
18
|
+
with:
|
|
19
|
+
bun-version: latest
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: bun install
|
|
23
|
+
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: bun test
|
|
26
|
+
|
|
27
|
+
- name: Setup Node for npm publish
|
|
28
|
+
uses: actions/setup-node@v4
|
|
29
|
+
with:
|
|
30
|
+
node-version: '20'
|
|
31
|
+
registry-url: 'https://registry.npmjs.org'
|
|
32
|
+
|
|
33
|
+
- name: Publish
|
|
34
|
+
run: npm publish
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Bun Sticky
|
|
2
|
+
|
|
3
|
+
Fastest bun under the sum. Bun-native .faf CLI.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
```bash
|
|
7
|
+
bunx bun-sticky score # Score current project
|
|
8
|
+
bunx bun-sticky init app # Create project.faf
|
|
9
|
+
bunx bun-sticky help # Show commands
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
```
|
|
14
|
+
bun-sticky/
|
|
15
|
+
├── index.ts # CLI + ASCII banner
|
|
16
|
+
├── lib/
|
|
17
|
+
│ ├── parser.ts # Zero-dep YAML parser
|
|
18
|
+
│ ├── scorer.ts # Wolfejam slot-based scoring
|
|
19
|
+
│ └── tier.ts # 9-tier system
|
|
20
|
+
├── tests/
|
|
21
|
+
│ └── sticky.test.ts # 177 tests, WJTTC championship
|
|
22
|
+
└── package.json # Zero dependencies
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Core Concepts
|
|
26
|
+
|
|
27
|
+
**Wolfejam Slot-Based Scoring** (NOT Elon weights)
|
|
28
|
+
- 21 total slots across 5 categories
|
|
29
|
+
- Type-aware: CLI=9 slots, Fullstack=21 slots
|
|
30
|
+
- Score = Filled Slots / Applicable Slots × 100
|
|
31
|
+
|
|
32
|
+
**Tier System**
|
|
33
|
+
| Score | Tier | Emoji |
|
|
34
|
+
|-------|------|-------|
|
|
35
|
+
| 100% | Trophy | 🏆 |
|
|
36
|
+
| 99%+ | Gold | 🥇 |
|
|
37
|
+
| 95%+ | Silver | 🥈 |
|
|
38
|
+
| 85%+ | Bronze | 🥉 |
|
|
39
|
+
| 70%+ | Green | 🟢 |
|
|
40
|
+
| 55%+ | Yellow | 🟡 |
|
|
41
|
+
| <55% | Red | 🔴 |
|
|
42
|
+
|
|
43
|
+
## Key Files
|
|
44
|
+
| File | Line | What |
|
|
45
|
+
|------|------|------|
|
|
46
|
+
| `lib/scorer.ts` | 16 | SLOTS definition (21 slots) |
|
|
47
|
+
| `lib/scorer.ts` | 68 | TYPE_CATEGORIES mapping |
|
|
48
|
+
| `lib/scorer.ts` | 173 | calculateScore() |
|
|
49
|
+
| `lib/tier.ts` | 22 | getTier() |
|
|
50
|
+
| `lib/parser.ts` | 1 | parseYaml() zero-dep |
|
|
51
|
+
|
|
52
|
+
## Design Principles
|
|
53
|
+
1. **Zero Dependencies** - Pure Bun APIs
|
|
54
|
+
2. **TypeScript Native** - No build step
|
|
55
|
+
3. **Speed First** - Sub-50ms cold start
|
|
56
|
+
4. **WJTTC Testing** - Championship-grade test suite
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
| Command | Description |
|
|
60
|
+
|---------|-------------|
|
|
61
|
+
| `score` | Show FAF score + tier |
|
|
62
|
+
| `init <name>` | Create project.faf |
|
|
63
|
+
| `sync` | Sync project.faf → CLAUDE.md |
|
|
64
|
+
| `version` | Show version |
|
|
65
|
+
| `help` | Show help |
|
|
66
|
+
|
|
67
|
+
## Testing
|
|
68
|
+
```bash
|
|
69
|
+
bun test
|
|
70
|
+
# 177 tests, 1254 assertions
|
|
71
|
+
# Full Bun API: test.each, mock, spyOn, snapshots
|
|
72
|
+
# WJTTC Championship Grade
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Part of FAF Ecosystem
|
|
76
|
+
- **faf-cli** - Full Node.js CLI (15,000+ downloads)
|
|
77
|
+
- **bun-sticky** - Bun-native lite CLI (you are here)
|
|
78
|
+
- **xai-faf-zig** - Ultra-fast Zig implementation
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
*Zero dependencies. Pure Bun. Wolfejam slot-based scoring.*
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Bun Sticky - Publish Protocol
|
|
2
|
+
|
|
3
|
+
## Pre-Publish Checklist
|
|
4
|
+
|
|
5
|
+
### 1. Code Quality
|
|
6
|
+
- [ ] All 45 tests passing (`bun test`)
|
|
7
|
+
- [ ] Zero TypeScript errors
|
|
8
|
+
- [ ] Zero runtime dependencies
|
|
9
|
+
- [ ] Pure Bun APIs only
|
|
10
|
+
|
|
11
|
+
### 2. Version Bump
|
|
12
|
+
```bash
|
|
13
|
+
# Update version in:
|
|
14
|
+
# - package.json
|
|
15
|
+
# - index.ts (VERSION constant)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 3. Test Suite
|
|
19
|
+
```bash
|
|
20
|
+
bun test
|
|
21
|
+
# Expect: 45 pass, 0 fail
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 4. Manual Verification
|
|
25
|
+
```bash
|
|
26
|
+
bun run index.ts --version
|
|
27
|
+
bun run index.ts help
|
|
28
|
+
bun run index.ts score
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Publish Commands
|
|
32
|
+
|
|
33
|
+
### npm Registry
|
|
34
|
+
```bash
|
|
35
|
+
# Login (first time)
|
|
36
|
+
npm login
|
|
37
|
+
|
|
38
|
+
# Publish
|
|
39
|
+
npm publish
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Bun Package Manager (future)
|
|
43
|
+
```bash
|
|
44
|
+
# When Bun has its own registry
|
|
45
|
+
bun publish
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Post-Publish
|
|
49
|
+
|
|
50
|
+
1. Tag release in git
|
|
51
|
+
2. Update ZIG-n-RUST.md benchmarks
|
|
52
|
+
3. Announce in faf-cli ecosystem
|
|
53
|
+
|
|
54
|
+
## Version History
|
|
55
|
+
|
|
56
|
+
| Version | Date | Notes |
|
|
57
|
+
|---------|------|-------|
|
|
58
|
+
| 1.0.0 | TBD | Initial release - Wolfejam slot-based scoring |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
*Fastest bun under the sum.*
|
|
63
|
+
*Zero dependencies. Pure Bun.*
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# bun-sticky
|
|
2
|
+
|
|
3
|
+
Fastest bun under the sum. Zero dependencies. Pure Bun.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bunx bun-sticky score
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
▄▄ ▄▀▀▀ ▀█▀ █ ▄▀▀ █▄▀ █ █
|
|
13
|
+
████ ▀▀█▄ █ █ █ █▀▄ █
|
|
14
|
+
██████ ▄▄▄▀ █ █ ▀▀▀ █ █ █
|
|
15
|
+
████████
|
|
16
|
+
████████ █▀▄ █ █ █▀▄
|
|
17
|
+
██████ ██▀ █ █ █ █
|
|
18
|
+
████ █▄▀ ▀▄▀ █ █
|
|
19
|
+
▀▀
|
|
20
|
+
|
|
21
|
+
🥐 Bun Sticky v1.0.0 .faf CLI
|
|
22
|
+
Fastest bun under the sum.
|
|
23
|
+
|
|
24
|
+
────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
Project: my-app
|
|
27
|
+
Type: cli
|
|
28
|
+
|
|
29
|
+
Project ████████████ 3/3
|
|
30
|
+
Human ████████░░░░ 4/6
|
|
31
|
+
|
|
32
|
+
🥉 78% Bronze
|
|
33
|
+
Filled: 7/9 slots
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun add -g bun-sticky
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or run directly:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bunx bun-sticky score
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun-sticky score # Show FAF score + tier
|
|
52
|
+
bun-sticky init myapp # Create project.faf
|
|
53
|
+
bun-sticky sync # Sync to CLAUDE.md
|
|
54
|
+
bun-sticky help # Show commands
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What is FAF?
|
|
58
|
+
|
|
59
|
+
**FAF** (Foundational AI-context Format) is project DNA for AI assistants. A `project.faf` file tells Claude, Cursor, Copilot, and other AI tools what your project is about.
|
|
60
|
+
|
|
61
|
+
**bun-sticky** scores your project's AI-readiness using the Wolfejam slot-based system.
|
|
62
|
+
|
|
63
|
+
## Scoring
|
|
64
|
+
|
|
65
|
+
21 slots across 5 categories. Type-aware scoring:
|
|
66
|
+
|
|
67
|
+
| Type | Slots | Categories |
|
|
68
|
+
|------|-------|------------|
|
|
69
|
+
| CLI | 9 | project + human |
|
|
70
|
+
| Library | 9 | project + human |
|
|
71
|
+
| API | 17 | project + backend + universal + human |
|
|
72
|
+
| Webapp | 16 | project + frontend + universal + human |
|
|
73
|
+
| Fullstack | 21 | all |
|
|
74
|
+
|
|
75
|
+
**Score = Filled Slots / Applicable Slots × 100**
|
|
76
|
+
|
|
77
|
+
## Tiers
|
|
78
|
+
|
|
79
|
+
| Score | Tier |
|
|
80
|
+
|-------|------|
|
|
81
|
+
| 100% | 🏆 Trophy |
|
|
82
|
+
| 99%+ | 🥇 Gold |
|
|
83
|
+
| 95%+ | 🥈 Silver |
|
|
84
|
+
| 85%+ | 🥉 Bronze |
|
|
85
|
+
| 70%+ | 🟢 Green |
|
|
86
|
+
| 55%+ | 🟡 Yellow |
|
|
87
|
+
| <55% | 🔴 Red |
|
|
88
|
+
|
|
89
|
+
## Speed
|
|
90
|
+
|
|
91
|
+
Built for Bun's speed:
|
|
92
|
+
|
|
93
|
+
- **Cold start**: <50ms
|
|
94
|
+
- **Score command**: <100ms
|
|
95
|
+
- **Zero dependencies**: Pure Bun APIs
|
|
96
|
+
- **TypeScript native**: No build step
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
177 tests. Championship-grade WJTTC test suite.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bun test
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Full Bun test API coverage: `test.each`, `mock`, `spyOn`, `snapshots`, custom matchers.
|
|
107
|
+
|
|
108
|
+
## FAF Ecosystem
|
|
109
|
+
|
|
110
|
+
| Package | Runtime | Downloads |
|
|
111
|
+
|---------|---------|-----------|
|
|
112
|
+
| [faf-cli](https://npmjs.com/package/faf-cli) | Node.js | 15,000+ |
|
|
113
|
+
| **bun-sticky** | Bun | you are here |
|
|
114
|
+
| xai-faf-zig | Zig | ultra-fast |
|
|
115
|
+
| xai-faf-rust | Rust | WASM |
|
|
116
|
+
|
|
117
|
+
## Philosophy
|
|
118
|
+
|
|
119
|
+
Built the Anthropic way:
|
|
120
|
+
|
|
121
|
+
- First principles
|
|
122
|
+
- Zero dependencies
|
|
123
|
+
- Native Bun APIs
|
|
124
|
+
- TypeScript native
|
|
125
|
+
|
|
126
|
+
Wolfejam slot-based scoring. Not Elon weights.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
*Part of the FAF ecosystem. Made for Claude Code.*
|
package/index.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* 🥐 Bun Sticky - Fastest bun under the sum.
|
|
4
|
+
*
|
|
5
|
+
* Built the Anthropic way:
|
|
6
|
+
* - First principles
|
|
7
|
+
* - Zero dependencies
|
|
8
|
+
* - Native Bun APIs
|
|
9
|
+
* - TypeScript native
|
|
10
|
+
*
|
|
11
|
+
* Wolfejam slot-based scoring (NOT Elon weights).
|
|
12
|
+
* For Claude Codesters.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseYaml, getNestedValue } from "./lib/parser.ts";
|
|
16
|
+
import { calculateScore, FafScore } from "./lib/scorer.ts";
|
|
17
|
+
import { getTier } from "./lib/tier.ts";
|
|
18
|
+
|
|
19
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
20
|
+
// CONSTANTS
|
|
21
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
+
|
|
23
|
+
const VERSION = "1.0.0";
|
|
24
|
+
|
|
25
|
+
// Bun Brand Colors (ANSI 256)
|
|
26
|
+
const BUN_BLUE = "\x1b[38;5;39m"; // #00a6e1
|
|
27
|
+
const BUN_PINK = "\x1b[38;5;205m"; // #ec4899
|
|
28
|
+
const BUN_ORANGE = "\x1b[38;5;215m"; // #f89b4b (medium - croissant)
|
|
29
|
+
const BUN_ORANGE_DARK = "\x1b[38;5;208m"; // dark orange (BUN)
|
|
30
|
+
const BUN_YELLOW = "\x1b[38;5;220m"; // #febc2e
|
|
31
|
+
const BUN_CREAM = "\x1b[38;5;223m"; // Croissant color
|
|
32
|
+
const WHITE = "\x1b[97m"; // bright white (STICKY)
|
|
33
|
+
|
|
34
|
+
// Standard colors
|
|
35
|
+
const CYAN = "\x1b[36m";
|
|
36
|
+
const GREEN = "\x1b[32m";
|
|
37
|
+
const YELLOW = "\x1b[33m";
|
|
38
|
+
const RED = "\x1b[31m";
|
|
39
|
+
const BOLD = "\x1b[1m";
|
|
40
|
+
const DIM = "\x1b[2m";
|
|
41
|
+
const RESET = "\x1b[0m";
|
|
42
|
+
|
|
43
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
// ASCII ART BANNER
|
|
45
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
46
|
+
|
|
47
|
+
const BANNER = `
|
|
48
|
+
${BUN_BLUE}────────────────────────────────────────────────${RESET}
|
|
49
|
+
|
|
50
|
+
${BUN_ORANGE}▄▄${RESET} ${WHITE}▄▀▀▀ ▀█▀ █ ▄▀▀ █▄▀ █ █${RESET}
|
|
51
|
+
${BUN_ORANGE}████${RESET} ${WHITE}▀▀█▄ █ █ █ █▀▄ █${RESET}
|
|
52
|
+
${BUN_ORANGE}██████${RESET} ${WHITE}▄▄▄▀ █ █ ▀▀▀ █ █ █${RESET}
|
|
53
|
+
${BUN_ORANGE}████████${RESET}
|
|
54
|
+
${BUN_ORANGE}████████${RESET} ${BUN_ORANGE_DARK}█▀▄ █ █ █▀▄${RESET}
|
|
55
|
+
${BUN_ORANGE}██████${RESET} ${BUN_ORANGE_DARK}██▀ █ █ █ █${RESET}
|
|
56
|
+
${BUN_ORANGE}████${RESET} ${BUN_ORANGE_DARK}█▄▀ ▀▄▀ █ █${RESET}
|
|
57
|
+
${BUN_ORANGE}▀▀${RESET}
|
|
58
|
+
|
|
59
|
+
${WHITE}🥐 Bun Sticky v${VERSION} .faf CLI${RESET}
|
|
60
|
+
${BUN_ORANGE_DARK}Fastest bun under the sum.${RESET}
|
|
61
|
+
|
|
62
|
+
${BUN_BLUE}────────────────────────────────────────────────${RESET}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
66
|
+
// COMMANDS
|
|
67
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
68
|
+
|
|
69
|
+
async function cmdScore(): Promise<void> {
|
|
70
|
+
const file = Bun.file("project.faf");
|
|
71
|
+
|
|
72
|
+
if (!(await file.exists())) {
|
|
73
|
+
console.log(`${RED}No project.faf found${RESET}`);
|
|
74
|
+
console.log(`${DIM}Run: bun-sticky init <name>${RESET}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const content = await file.text();
|
|
79
|
+
const faf = parseYaml(content);
|
|
80
|
+
const result = calculateScore(faf);
|
|
81
|
+
const tier = getTier(result.score);
|
|
82
|
+
|
|
83
|
+
console.log(BANNER);
|
|
84
|
+
|
|
85
|
+
// Project name & type
|
|
86
|
+
const name = (getNestedValue(faf, "project.name") as string) || "Unknown";
|
|
87
|
+
console.log(` Project: ${BOLD}${name}${RESET}`);
|
|
88
|
+
console.log(` Type: ${DIM}${result.projectType}${RESET}`);
|
|
89
|
+
console.log();
|
|
90
|
+
|
|
91
|
+
// Section breakdown (only show applicable sections)
|
|
92
|
+
const { sections } = result;
|
|
93
|
+
|
|
94
|
+
if (sections.project.total > 0) {
|
|
95
|
+
console.log(` ${DIM}Project${RESET} ${formatBar(sections.project.percentage)} ${sections.project.filled}/${sections.project.total}`);
|
|
96
|
+
}
|
|
97
|
+
if (sections.frontend.total > 0) {
|
|
98
|
+
console.log(` ${DIM}Frontend${RESET} ${formatBar(sections.frontend.percentage)} ${sections.frontend.filled}/${sections.frontend.total}`);
|
|
99
|
+
}
|
|
100
|
+
if (sections.backend.total > 0) {
|
|
101
|
+
console.log(` ${DIM}Backend${RESET} ${formatBar(sections.backend.percentage)} ${sections.backend.filled}/${sections.backend.total}`);
|
|
102
|
+
}
|
|
103
|
+
if (sections.universal.total > 0) {
|
|
104
|
+
console.log(` ${DIM}Universal${RESET} ${formatBar(sections.universal.percentage)} ${sections.universal.filled}/${sections.universal.total}`);
|
|
105
|
+
}
|
|
106
|
+
if (sections.human.total > 0) {
|
|
107
|
+
console.log(` ${DIM}Human${RESET} ${formatBar(sections.human.percentage)} ${sections.human.filled}/${sections.human.total}`);
|
|
108
|
+
}
|
|
109
|
+
console.log();
|
|
110
|
+
|
|
111
|
+
// Total
|
|
112
|
+
console.log(` ${tier.color}${tier.emoji} ${BOLD}${result.score}%${RESET} ${tier.color}${tier.name}${RESET}`);
|
|
113
|
+
console.log(` ${DIM}Filled: ${result.filled}/${result.total} slots${RESET}`);
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatBar(percent: number): string {
|
|
118
|
+
const width = 12;
|
|
119
|
+
const filled = Math.round((percent / 100) * width);
|
|
120
|
+
const empty = width - filled;
|
|
121
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
122
|
+
|
|
123
|
+
if (percent >= 85) return `${GREEN}${bar}${RESET}`;
|
|
124
|
+
if (percent >= 70) return `${GREEN}${bar}${RESET}`;
|
|
125
|
+
if (percent >= 55) return `${YELLOW}${bar}${RESET}`;
|
|
126
|
+
return `${RED}${bar}${RESET}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function cmdInit(name: string): Promise<void> {
|
|
130
|
+
const file = Bun.file("project.faf");
|
|
131
|
+
|
|
132
|
+
if (await file.exists()) {
|
|
133
|
+
console.log(`${YELLOW}project.faf already exists${RESET}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const template = `# ${name} - Project DNA
|
|
138
|
+
# Generated by Bun Sticky
|
|
139
|
+
|
|
140
|
+
faf_version: 2.5.0
|
|
141
|
+
|
|
142
|
+
project:
|
|
143
|
+
name: ${name}
|
|
144
|
+
goal: Define your project goal here
|
|
145
|
+
main_language: TypeScript
|
|
146
|
+
type: cli
|
|
147
|
+
version: 0.1.0
|
|
148
|
+
|
|
149
|
+
human_context:
|
|
150
|
+
who: Your target users
|
|
151
|
+
what: What this project does
|
|
152
|
+
why: Why it exists
|
|
153
|
+
where: Where it runs
|
|
154
|
+
when: When to use it
|
|
155
|
+
how: How to get started
|
|
156
|
+
|
|
157
|
+
stack:
|
|
158
|
+
runtime: Bun
|
|
159
|
+
build: bun build
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
await Bun.write("project.faf", template);
|
|
163
|
+
console.log(BANNER);
|
|
164
|
+
console.log(` ${GREEN}Created${RESET} project.faf`);
|
|
165
|
+
console.log(` ${DIM}Run: bun-sticky score${RESET}`);
|
|
166
|
+
console.log();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function cmdSync(): Promise<void> {
|
|
170
|
+
const fafFile = Bun.file("project.faf");
|
|
171
|
+
|
|
172
|
+
if (!(await fafFile.exists())) {
|
|
173
|
+
console.log(`${RED}No project.faf found${RESET}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const content = await fafFile.text();
|
|
178
|
+
const faf = parseYaml(content);
|
|
179
|
+
const name = (getNestedValue(faf, "project.name") as string) || "Project";
|
|
180
|
+
const goal = (getNestedValue(faf, "project.goal") as string) || "";
|
|
181
|
+
const result = calculateScore(faf);
|
|
182
|
+
const tier = getTier(result.score);
|
|
183
|
+
|
|
184
|
+
// Generate CLAUDE.md
|
|
185
|
+
const claudeMd = `# ${name}
|
|
186
|
+
|
|
187
|
+
${goal}
|
|
188
|
+
|
|
189
|
+
## Score: ${tier.emoji} ${result.score}%
|
|
190
|
+
|
|
191
|
+
Filled: ${result.filled}/${result.total} slots
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
*Synced by Bun Sticky*
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
await Bun.write("CLAUDE.md", claudeMd);
|
|
198
|
+
console.log(BANNER);
|
|
199
|
+
console.log(` ${GREEN}Synced${RESET} project.faf → CLAUDE.md`);
|
|
200
|
+
console.log();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cmdHelp(): void {
|
|
204
|
+
console.log(BANNER);
|
|
205
|
+
console.log(` ${BOLD}Commands${RESET}`);
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(` score Show FAF score + tier`);
|
|
208
|
+
console.log(` init <n> Create project.faf`);
|
|
209
|
+
console.log(` sync Sync to CLAUDE.md`);
|
|
210
|
+
console.log(` version Show version`);
|
|
211
|
+
console.log(` help Show this help`);
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(` ${DIM}Zero dependencies. Pure Bun.${RESET}`);
|
|
214
|
+
console.log(` ${DIM}Wolfejam slot-based scoring.${RESET}`);
|
|
215
|
+
console.log();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function cmdVersion(): void {
|
|
219
|
+
console.log(`bun-sticky v${VERSION}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
// MAIN
|
|
224
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
225
|
+
|
|
226
|
+
const args = process.argv.slice(2);
|
|
227
|
+
const cmd = args[0] || "help";
|
|
228
|
+
|
|
229
|
+
switch (cmd) {
|
|
230
|
+
case "score":
|
|
231
|
+
await cmdScore();
|
|
232
|
+
break;
|
|
233
|
+
case "init":
|
|
234
|
+
const name = args[1];
|
|
235
|
+
if (!name) {
|
|
236
|
+
console.log(`${RED}Usage: bun-sticky init <name>${RESET}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
await cmdInit(name);
|
|
240
|
+
break;
|
|
241
|
+
case "sync":
|
|
242
|
+
await cmdSync();
|
|
243
|
+
break;
|
|
244
|
+
case "version":
|
|
245
|
+
case "-v":
|
|
246
|
+
case "--version":
|
|
247
|
+
cmdVersion();
|
|
248
|
+
break;
|
|
249
|
+
case "help":
|
|
250
|
+
case "-h":
|
|
251
|
+
case "--help":
|
|
252
|
+
default:
|
|
253
|
+
cmdHelp();
|
|
254
|
+
break;
|
|
255
|
+
}
|
package/lib/parser.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML Parser for .faf files
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies. Pure Bun.
|
|
5
|
+
* Handles: scalars, nested objects, arrays
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function parseYaml(content: string): Record<string, unknown> {
|
|
9
|
+
const result: Record<string, unknown> = {};
|
|
10
|
+
const lines = content.split("\n");
|
|
11
|
+
const stack: { indent: number; obj: Record<string, unknown> }[] = [
|
|
12
|
+
{ indent: -1, obj: result },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
// Skip comments and empty lines
|
|
17
|
+
if (line.trim().startsWith("#") || line.trim() === "") continue;
|
|
18
|
+
|
|
19
|
+
const indent = line.search(/\S/);
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
|
|
22
|
+
// Handle key: value pairs
|
|
23
|
+
const colonIndex = trimmed.indexOf(":");
|
|
24
|
+
if (colonIndex === -1) continue;
|
|
25
|
+
|
|
26
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
27
|
+
const value = trimmed.slice(colonIndex + 1).trim();
|
|
28
|
+
|
|
29
|
+
// Pop stack to find parent
|
|
30
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
31
|
+
stack.pop();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parent = stack[stack.length - 1].obj;
|
|
35
|
+
|
|
36
|
+
if (value === "" || value === "|" || value === ">") {
|
|
37
|
+
// Nested object or multiline
|
|
38
|
+
const newObj: Record<string, unknown> = {};
|
|
39
|
+
parent[key] = newObj;
|
|
40
|
+
stack.push({ indent, obj: newObj });
|
|
41
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
42
|
+
// Inline array
|
|
43
|
+
parent[key] = value
|
|
44
|
+
.slice(1, -1)
|
|
45
|
+
.split(",")
|
|
46
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
|
47
|
+
} else if (value.startsWith("- ")) {
|
|
48
|
+
// Array item (simple case)
|
|
49
|
+
parent[key] = [value.slice(2).trim()];
|
|
50
|
+
} else {
|
|
51
|
+
// Scalar value
|
|
52
|
+
parent[key] = value.replace(/^["']|["']$/g, "");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
60
|
+
const parts = path.split(".");
|
|
61
|
+
let current: unknown = obj;
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
if (current === null || current === undefined) return undefined;
|
|
64
|
+
if (typeof current !== "object") return undefined;
|
|
65
|
+
current = (current as Record<string, unknown>)[part];
|
|
66
|
+
}
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasValue(obj: Record<string, unknown>, path: string): boolean {
|
|
71
|
+
const value = getNestedValue(obj, path);
|
|
72
|
+
if (value === undefined || value === null) return false;
|
|
73
|
+
if (typeof value === "string" && value.trim() === "") return false;
|
|
74
|
+
return true;
|
|
75
|
+
}
|