noslop 0.1.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 +170 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +281 -0
- package/dist/lib/content.d.ts +143 -0
- package/dist/lib/content.js +463 -0
- package/dist/lib/content.test.d.ts +1 -0
- package/dist/lib/content.test.js +746 -0
- package/dist/lib/templates.d.ts +35 -0
- package/dist/lib/templates.js +264 -0
- package/dist/lib/templates.test.d.ts +1 -0
- package/dist/lib/templates.test.js +110 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +372 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ruben
|
|
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,170 @@
|
|
|
1
|
+
# noslop
|
|
2
|
+
|
|
3
|
+
> Let Claude Code manage your X posts - no slop, just good content
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/noslop)
|
|
6
|
+
[](https://github.com/rubenartus/noslop/actions/workflows/ci.yml)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
10
|
+
A CLI tool designed for AI assistants like Claude Code to draft, schedule, and manage your social media posts. All content lives as simple markdown files in folders - easy to version control, easy for AI to work with, easy for you to review.
|
|
11
|
+
|
|
12
|
+
The TUI gives you a quick way to check what's scheduled, mark posts as ready, and keep your AI on track.
|
|
13
|
+
|
|
14
|
+
## Screenshot
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **AI-first CLI** - Built for Claude Code to draft, schedule, and reschedule posts on demand
|
|
21
|
+
- **Simple markdown files** - All content stored as `.md` files in `drafts/` and `posts/` folders
|
|
22
|
+
- **Human-friendly TUI** - Review and direct the work with keyboard shortcuts
|
|
23
|
+
- **Content workflow** - Draft → Ready → Post → Published pipeline
|
|
24
|
+
- **Schedule view** - Calendar view of what's coming up
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# npm
|
|
30
|
+
npm install -g noslop
|
|
31
|
+
|
|
32
|
+
# pnpm
|
|
33
|
+
pnpm add -g noslop
|
|
34
|
+
|
|
35
|
+
# Or run directly with npx
|
|
36
|
+
npx noslop
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Initialize a new project
|
|
43
|
+
noslop init
|
|
44
|
+
|
|
45
|
+
# Create a new draft
|
|
46
|
+
noslop new "My First Post"
|
|
47
|
+
|
|
48
|
+
# Open interactive TUI
|
|
49
|
+
noslop
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
### Core Commands
|
|
55
|
+
|
|
56
|
+
| Command | Description |
|
|
57
|
+
|---------|-------------|
|
|
58
|
+
| `noslop` | Open interactive TUI |
|
|
59
|
+
| `noslop init [name]` | Initialize new project |
|
|
60
|
+
| `noslop help` | Show all commands |
|
|
61
|
+
|
|
62
|
+
### Content Management
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---------|-------------|
|
|
66
|
+
| `noslop new <title>` | Create new draft |
|
|
67
|
+
| `noslop list [-d\|--drafts] [-p\|--posts]` | List content |
|
|
68
|
+
| `noslop status` | Show project summary |
|
|
69
|
+
| `noslop show <id>` | Show full content |
|
|
70
|
+
|
|
71
|
+
### Workflow Actions
|
|
72
|
+
|
|
73
|
+
| Command | Description |
|
|
74
|
+
|---------|-------------|
|
|
75
|
+
| `noslop ready <id>` | Mark draft as ready |
|
|
76
|
+
| `noslop unready <id>` | Mark as in-progress |
|
|
77
|
+
| `noslop post <id>` | Move to posts folder |
|
|
78
|
+
| `noslop unpost <id>` | Move back to drafts |
|
|
79
|
+
| `noslop publish <id> <url>` | Add published URL |
|
|
80
|
+
| `noslop schedule <id> <datetime>` | Set schedule (YYYY-MM-DD HH:MM) |
|
|
81
|
+
| `noslop delete <id>` | Delete a draft |
|
|
82
|
+
|
|
83
|
+
## TUI Keyboard Shortcuts
|
|
84
|
+
|
|
85
|
+
| Key | Action |
|
|
86
|
+
|-----|--------|
|
|
87
|
+
| `Tab` | Switch between Drafts/Posts |
|
|
88
|
+
| `↑/↓` | Navigate items |
|
|
89
|
+
| `Enter` | Toggle ready (drafts) / Add URL (posts) |
|
|
90
|
+
| `Space` | Move to Posts / Move to Drafts |
|
|
91
|
+
| `Backspace` | Delete draft (in-progress only) |
|
|
92
|
+
| `s` | Toggle schedule view |
|
|
93
|
+
| `←/→` | Navigate weeks (schedule view) |
|
|
94
|
+
| `q` | Quit |
|
|
95
|
+
|
|
96
|
+
## Project Structure
|
|
97
|
+
|
|
98
|
+
After running `noslop init`, your project will have:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
your-project/
|
|
102
|
+
├── CLAUDE.md # Your brand voice & guidelines
|
|
103
|
+
├── NOSLOP.md # CLI documentation (auto-generated)
|
|
104
|
+
├── drafts/ # Work in progress
|
|
105
|
+
│ └── post-name/
|
|
106
|
+
│ ├── x.md # Post content
|
|
107
|
+
│ └── assets/
|
|
108
|
+
└── posts/ # Published content
|
|
109
|
+
└── post-name/
|
|
110
|
+
└── x.md
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Post File Format
|
|
114
|
+
|
|
115
|
+
Each post is stored in `x.md` with this structure:
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
# Post Title
|
|
119
|
+
|
|
120
|
+
## Post
|
|
121
|
+
\`\`\`
|
|
122
|
+
Your post content here
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## Status
|
|
126
|
+
draft | ready
|
|
127
|
+
|
|
128
|
+
## Media
|
|
129
|
+
Description of media to attach
|
|
130
|
+
|
|
131
|
+
## Scheduled
|
|
132
|
+
2026-01-27 09:00
|
|
133
|
+
|
|
134
|
+
## Posted
|
|
135
|
+
2026-01-27 09:15
|
|
136
|
+
|
|
137
|
+
## Published
|
|
138
|
+
https://x.com/user/status/123
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Working with AI Assistants
|
|
142
|
+
|
|
143
|
+
noslop is designed to work with Claude Code and other AI assistants:
|
|
144
|
+
|
|
145
|
+
1. **CLAUDE.md** - Add your brand voice, tone guidelines, and content rules
|
|
146
|
+
2. **NOSLOP.md** - Auto-generated CLI documentation for AI reference
|
|
147
|
+
3. Use CLI commands instead of manual file editing
|
|
148
|
+
|
|
149
|
+
## Requirements
|
|
150
|
+
|
|
151
|
+
- Node.js 18 or higher
|
|
152
|
+
- Terminal with Unicode support
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
157
|
+
|
|
158
|
+
1. Fork the repository
|
|
159
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
160
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
161
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
162
|
+
5. Open a Pull Request
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
[MIT](LICENSE)
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
Made with love for content creators who prefer the terminal
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { isNoslopProject, getAllContent, findItem, createDraft, updateStatus, updateSchedule, moveToPosts, moveToDrafts, addPublishedUrl, deleteDraft, sortBySchedule, } from './lib/content.js';
|
|
4
|
+
import { initProject, ensureNoslopMd } from './lib/templates.js';
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name('noslop')
|
|
8
|
+
.description('Social media content workflow CLI - no slop, just good content')
|
|
9
|
+
.version('0.1.0');
|
|
10
|
+
// Default command: open TUI
|
|
11
|
+
program.action(async () => {
|
|
12
|
+
if (!isNoslopProject()) {
|
|
13
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
// Ensure NOSLOP.md is up to date with current version
|
|
17
|
+
ensureNoslopMd();
|
|
18
|
+
// Dynamic import to avoid loading React/Ink for non-TUI commands
|
|
19
|
+
const { renderTUI } = await import('./tui.js');
|
|
20
|
+
renderTUI();
|
|
21
|
+
});
|
|
22
|
+
// Init command
|
|
23
|
+
program
|
|
24
|
+
.command('init')
|
|
25
|
+
.description('Initialize a new noslop project in current directory')
|
|
26
|
+
.argument('[name]', 'Project name (optional)')
|
|
27
|
+
.action((name) => {
|
|
28
|
+
if (isNoslopProject()) {
|
|
29
|
+
console.log('Already a noslop project (drafts/ or posts/ exists).');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
initProject(process.cwd(), name);
|
|
33
|
+
console.log('Initialized noslop project:');
|
|
34
|
+
console.log(' - drafts/ (your work in progress)');
|
|
35
|
+
console.log(' - posts/ (published content)');
|
|
36
|
+
console.log(' - CLAUDE.md (your brand guidelines)');
|
|
37
|
+
console.log(' - NOSLOP.md (tool documentation)');
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log('Next: Run `noslop new "Your First Post"` to create a draft.');
|
|
40
|
+
});
|
|
41
|
+
// New draft command
|
|
42
|
+
program
|
|
43
|
+
.command('new')
|
|
44
|
+
.description('Create a new draft')
|
|
45
|
+
.argument('<title>', 'Title for the new draft')
|
|
46
|
+
.action((title) => {
|
|
47
|
+
if (!isNoslopProject()) {
|
|
48
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const item = createDraft(title);
|
|
52
|
+
console.log(`Created draft: ${item.folder}/`);
|
|
53
|
+
console.log(` Edit: ${item.xFile}`);
|
|
54
|
+
});
|
|
55
|
+
// List command
|
|
56
|
+
program
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('List all content')
|
|
59
|
+
.option('-d, --drafts', 'Show only drafts')
|
|
60
|
+
.option('-p, --posts', 'Show only posts')
|
|
61
|
+
.action((options) => {
|
|
62
|
+
if (!isNoslopProject()) {
|
|
63
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const { drafts, posts } = getAllContent();
|
|
67
|
+
if (!options.posts) {
|
|
68
|
+
const sortedDrafts = sortBySchedule(drafts);
|
|
69
|
+
if (sortedDrafts.length > 0) {
|
|
70
|
+
console.log('DRAFTS:');
|
|
71
|
+
for (const d of sortedDrafts) {
|
|
72
|
+
const status = d.status === 'ready' ? '✓' : '○';
|
|
73
|
+
const schedule = d.scheduledAt ? ` (${d.scheduledAt})` : '';
|
|
74
|
+
console.log(` ${status} ${d.folder}${schedule}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log('DRAFTS: (none)');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!options.drafts) {
|
|
82
|
+
if (!options.posts) {
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
85
|
+
if (posts.length > 0) {
|
|
86
|
+
console.log('POSTS:');
|
|
87
|
+
for (const p of posts) {
|
|
88
|
+
const url = p.published?.startsWith('http') ? ' ✓' : '';
|
|
89
|
+
const date = p.postedAt ? ` (${p.postedAt})` : '';
|
|
90
|
+
console.log(` ${p.folder}${date}${url}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log('POSTS: (none)');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// Status command
|
|
99
|
+
program
|
|
100
|
+
.command('status')
|
|
101
|
+
.description('Show project status summary')
|
|
102
|
+
.action(() => {
|
|
103
|
+
if (!isNoslopProject()) {
|
|
104
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const { drafts, posts } = getAllContent();
|
|
108
|
+
const ready = drafts.filter(d => d.status === 'ready').length;
|
|
109
|
+
const wip = drafts.length - ready;
|
|
110
|
+
const published = posts.filter(p => p.published?.startsWith('http')).length;
|
|
111
|
+
console.log(`Drafts: ${drafts.length} (${ready} ready, ${wip} in progress)`);
|
|
112
|
+
console.log(`Posts: ${posts.length} (${published} with URL)`);
|
|
113
|
+
// Next scheduled
|
|
114
|
+
const scheduled = sortBySchedule(drafts).filter(d => d.scheduledAt);
|
|
115
|
+
if (scheduled.length > 0) {
|
|
116
|
+
console.log(`Next: ${scheduled[0].folder} @ ${scheduled[0].scheduledAt}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Show command
|
|
120
|
+
program
|
|
121
|
+
.command('show')
|
|
122
|
+
.description('Show full content of a post/draft')
|
|
123
|
+
.argument('<id>', 'Folder name or ID')
|
|
124
|
+
.action((id) => {
|
|
125
|
+
if (!isNoslopProject()) {
|
|
126
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const item = findItem(id);
|
|
130
|
+
if (!item) {
|
|
131
|
+
console.log(`Not found: ${id}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
console.log(`# ${item.title}`);
|
|
135
|
+
console.log(`Folder: ${item.folder}`);
|
|
136
|
+
console.log(`Status: ${item.status}`);
|
|
137
|
+
if (item.scheduledAt) {
|
|
138
|
+
console.log(`Scheduled: ${item.scheduledAt}`);
|
|
139
|
+
}
|
|
140
|
+
if (item.postedAt) {
|
|
141
|
+
console.log(`Posted: ${item.postedAt}`);
|
|
142
|
+
}
|
|
143
|
+
if (item.published) {
|
|
144
|
+
console.log(`URL: ${item.published}`);
|
|
145
|
+
}
|
|
146
|
+
if (item.media !== 'None') {
|
|
147
|
+
console.log(`Media: ${item.media}`);
|
|
148
|
+
}
|
|
149
|
+
console.log('---');
|
|
150
|
+
console.log(item.post);
|
|
151
|
+
});
|
|
152
|
+
// Ready command
|
|
153
|
+
program
|
|
154
|
+
.command('ready')
|
|
155
|
+
.description('Mark draft as ready to post')
|
|
156
|
+
.argument('<id>', 'Folder name or ID')
|
|
157
|
+
.action((id) => {
|
|
158
|
+
if (!isNoslopProject()) {
|
|
159
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
const item = findItem(id);
|
|
163
|
+
if (!item) {
|
|
164
|
+
console.log(`Not found: ${id}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
updateStatus(item, 'ready');
|
|
168
|
+
console.log(`Marked as ready: ${item.folder}`);
|
|
169
|
+
});
|
|
170
|
+
// Unready command
|
|
171
|
+
program
|
|
172
|
+
.command('unready')
|
|
173
|
+
.description('Mark draft as in-progress')
|
|
174
|
+
.argument('<id>', 'Folder name or ID')
|
|
175
|
+
.action((id) => {
|
|
176
|
+
if (!isNoslopProject()) {
|
|
177
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const item = findItem(id);
|
|
181
|
+
if (!item) {
|
|
182
|
+
console.log(`Not found: ${id}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
updateStatus(item, 'draft');
|
|
186
|
+
console.log(`Marked as in-progress: ${item.folder}`);
|
|
187
|
+
});
|
|
188
|
+
// Post command (move to posts)
|
|
189
|
+
program
|
|
190
|
+
.command('post')
|
|
191
|
+
.description('Move draft to posts folder')
|
|
192
|
+
.argument('<id>', 'Folder name or ID')
|
|
193
|
+
.action((id) => {
|
|
194
|
+
if (!isNoslopProject()) {
|
|
195
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const item = findItem(id);
|
|
199
|
+
if (!item) {
|
|
200
|
+
console.log(`Not found: ${id}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
moveToPosts(item);
|
|
204
|
+
console.log(`Moved to posts: ${item.folder}`);
|
|
205
|
+
});
|
|
206
|
+
// Unpost command (move back to drafts)
|
|
207
|
+
program
|
|
208
|
+
.command('unpost')
|
|
209
|
+
.description('Move post back to drafts')
|
|
210
|
+
.argument('<id>', 'Folder name or ID')
|
|
211
|
+
.action((id) => {
|
|
212
|
+
if (!isNoslopProject()) {
|
|
213
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const item = findItem(id);
|
|
217
|
+
if (!item) {
|
|
218
|
+
console.log(`Not found: ${id}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
moveToDrafts(item);
|
|
222
|
+
console.log(`Moved to drafts: ${item.folder}`);
|
|
223
|
+
});
|
|
224
|
+
// Publish command
|
|
225
|
+
program
|
|
226
|
+
.command('publish')
|
|
227
|
+
.description('Add published URL to post')
|
|
228
|
+
.argument('<id>', 'Folder name or ID')
|
|
229
|
+
.argument('<url>', 'Published URL')
|
|
230
|
+
.action((id, url) => {
|
|
231
|
+
if (!isNoslopProject()) {
|
|
232
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const item = findItem(id);
|
|
236
|
+
if (!item) {
|
|
237
|
+
console.log(`Not found: ${id}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
addPublishedUrl(item, url);
|
|
241
|
+
console.log(`Published: ${item.folder}`);
|
|
242
|
+
console.log(` URL: ${url}`);
|
|
243
|
+
});
|
|
244
|
+
// Schedule command
|
|
245
|
+
program
|
|
246
|
+
.command('schedule')
|
|
247
|
+
.description('Set/update scheduled time')
|
|
248
|
+
.argument('<id>', 'Folder name or ID')
|
|
249
|
+
.argument('<datetime>', 'Date and time (YYYY-MM-DD HH:MM)')
|
|
250
|
+
.action((id, datetime) => {
|
|
251
|
+
if (!isNoslopProject()) {
|
|
252
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
const item = findItem(id);
|
|
256
|
+
if (!item) {
|
|
257
|
+
console.log(`Not found: ${id}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
updateSchedule(item, datetime);
|
|
261
|
+
console.log(`Scheduled: ${item.folder} @ ${datetime}`);
|
|
262
|
+
});
|
|
263
|
+
// Delete command
|
|
264
|
+
program
|
|
265
|
+
.command('delete')
|
|
266
|
+
.description('Delete a draft')
|
|
267
|
+
.argument('<id>', 'Folder name or ID')
|
|
268
|
+
.action((id) => {
|
|
269
|
+
if (!isNoslopProject()) {
|
|
270
|
+
console.log('Not a noslop project. Run `noslop init` first.');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const item = findItem(id);
|
|
274
|
+
if (!item) {
|
|
275
|
+
console.log(`Not found: ${id}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
deleteDraft(item);
|
|
279
|
+
console.log(`Deleted: ${item.folder}`);
|
|
280
|
+
});
|
|
281
|
+
program.parse();
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status of a content item (draft or ready to post)
|
|
3
|
+
*/
|
|
4
|
+
export type ContentStatus = 'draft' | 'ready';
|
|
5
|
+
/**
|
|
6
|
+
* Represents a content item (draft or post)
|
|
7
|
+
*/
|
|
8
|
+
export interface ContentItem {
|
|
9
|
+
id: string;
|
|
10
|
+
folder: string;
|
|
11
|
+
title: string;
|
|
12
|
+
post: string;
|
|
13
|
+
published: string;
|
|
14
|
+
media: string;
|
|
15
|
+
postedAt: string;
|
|
16
|
+
scheduledAt: string;
|
|
17
|
+
status: ContentStatus;
|
|
18
|
+
path: string;
|
|
19
|
+
xFile: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate that a string is a valid URL
|
|
23
|
+
* @param str - String to validate
|
|
24
|
+
* @returns True if valid URL, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
export declare function isValidUrl(str: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Get the paths to posts and drafts directories
|
|
29
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
30
|
+
* @returns Object with postsDir and draftsDir paths
|
|
31
|
+
*/
|
|
32
|
+
export declare function getContentDirs(cwd?: string): {
|
|
33
|
+
postsDir: string;
|
|
34
|
+
draftsDir: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Check if the current directory is a noslop project
|
|
38
|
+
* @param cwd - Working directory to check
|
|
39
|
+
* @returns True if drafts/ or posts/ directory exists
|
|
40
|
+
*/
|
|
41
|
+
export declare function isNoslopProject(cwd?: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Parse content from a directory into ContentItem objects
|
|
44
|
+
* @param dir - Directory containing content folders
|
|
45
|
+
* @param prefix - ID prefix ('D' for drafts, 'P' for posts)
|
|
46
|
+
* @returns Array of parsed content items, sorted alphabetically by folder name
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseContent(dir: string, prefix: string): ContentItem[];
|
|
49
|
+
/**
|
|
50
|
+
* Get all drafts from the drafts directory
|
|
51
|
+
* @param cwd - Working directory
|
|
52
|
+
* @returns Array of draft content items
|
|
53
|
+
*/
|
|
54
|
+
export declare function getDrafts(cwd?: string): ContentItem[];
|
|
55
|
+
/**
|
|
56
|
+
* Get all posts from the posts directory
|
|
57
|
+
* @param cwd - Working directory
|
|
58
|
+
* @returns Array of post content items
|
|
59
|
+
*/
|
|
60
|
+
export declare function getPosts(cwd?: string): ContentItem[];
|
|
61
|
+
/**
|
|
62
|
+
* Get all content (drafts + posts)
|
|
63
|
+
* @param cwd - Working directory
|
|
64
|
+
* @returns Object with drafts and posts arrays
|
|
65
|
+
*/
|
|
66
|
+
export declare function getAllContent(cwd?: string): {
|
|
67
|
+
drafts: ContentItem[];
|
|
68
|
+
posts: ContentItem[];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Find a content item by folder name or ID
|
|
72
|
+
* @param query - Folder name (exact or partial) or ID (D001, P001)
|
|
73
|
+
* @param cwd - Working directory
|
|
74
|
+
* @returns Matching content item or null if not found
|
|
75
|
+
* @throws Error if query contains path traversal patterns
|
|
76
|
+
* @example
|
|
77
|
+
* findItem('monday-motivation') // exact folder match
|
|
78
|
+
* findItem('monday') // partial folder match
|
|
79
|
+
* findItem('D001') // ID match
|
|
80
|
+
*/
|
|
81
|
+
export declare function findItem(query: string, cwd?: string): ContentItem | null;
|
|
82
|
+
/**
|
|
83
|
+
* Create a new draft with the given title
|
|
84
|
+
* @param title - Title for the draft (will be slugified for folder name)
|
|
85
|
+
* @param cwd - Working directory
|
|
86
|
+
* @returns The created ContentItem
|
|
87
|
+
* @throws Error if draft creation fails
|
|
88
|
+
*/
|
|
89
|
+
export declare function createDraft(title: string, cwd?: string): ContentItem;
|
|
90
|
+
/**
|
|
91
|
+
* Update the status of a content item
|
|
92
|
+
* @param item - Content item to update
|
|
93
|
+
* @param newStatus - New status ('draft' or 'ready')
|
|
94
|
+
* @throws Error if file operations fail
|
|
95
|
+
*/
|
|
96
|
+
export declare function updateStatus(item: ContentItem, newStatus: ContentStatus): void;
|
|
97
|
+
/**
|
|
98
|
+
* Update the scheduled date of a content item
|
|
99
|
+
* @param item - Content item to update
|
|
100
|
+
* @param datetime - Scheduled datetime string (YYYY-MM-DD HH:MM)
|
|
101
|
+
* @throws Error if file operations fail
|
|
102
|
+
*/
|
|
103
|
+
export declare function updateSchedule(item: ContentItem, datetime: string): void;
|
|
104
|
+
/**
|
|
105
|
+
* Move a draft to the posts folder
|
|
106
|
+
* @param item - Content item to move
|
|
107
|
+
* @param cwd - Working directory
|
|
108
|
+
* @throws Error if move operation fails
|
|
109
|
+
*/
|
|
110
|
+
export declare function moveToPosts(item: ContentItem, cwd?: string): void;
|
|
111
|
+
/**
|
|
112
|
+
* Move a post back to the drafts folder
|
|
113
|
+
* @param item - Content item to move
|
|
114
|
+
* @param cwd - Working directory
|
|
115
|
+
* @throws Error if move operation fails
|
|
116
|
+
*/
|
|
117
|
+
export declare function moveToDrafts(item: ContentItem, cwd?: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Add a published URL to a post
|
|
120
|
+
* @param item - Content item to update
|
|
121
|
+
* @param url - Published URL
|
|
122
|
+
* @throws Error if URL is invalid or file operations fail
|
|
123
|
+
*/
|
|
124
|
+
export declare function addPublishedUrl(item: ContentItem, url: string): void;
|
|
125
|
+
/**
|
|
126
|
+
* Delete a draft
|
|
127
|
+
* @param item - Content item to delete
|
|
128
|
+
* @throws Error if delete operation fails
|
|
129
|
+
*/
|
|
130
|
+
export declare function deleteDraft(item: ContentItem): void;
|
|
131
|
+
/**
|
|
132
|
+
* Format a date as YYYY-MM-DD HH:MM
|
|
133
|
+
* @param date - Date to format
|
|
134
|
+
* @returns Formatted date string
|
|
135
|
+
*/
|
|
136
|
+
export declare function formatDate(date: Date): string;
|
|
137
|
+
/**
|
|
138
|
+
* Sort content items by scheduled date (oldest first)
|
|
139
|
+
* Unscheduled items are placed at the end
|
|
140
|
+
* @param items - Array of content items to sort
|
|
141
|
+
* @returns New sorted array (does not mutate original)
|
|
142
|
+
*/
|
|
143
|
+
export declare function sortBySchedule(items: ContentItem[]): ContentItem[];
|