@telelabsai/ship 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/.claude/agents/git-ops.md +41 -0
- package/.claude/hooks/git-safety.cjs +66 -0
- package/.claude/rules/git-conventions.md +17 -0
- package/.claude/settings.json +16 -0
- package/.claude/skills/ship-version/SKILL.md +118 -0
- package/.claude/skills/ship-version/references/commit-standards.md +97 -0
- package/.claude/skills/ship-version/references/safety-protocols.md +41 -0
- package/.claude/skills/ship-version/references/workflow-changelog.md +95 -0
- package/.claude/skills/ship-version/references/workflow-commit.md +77 -0
- package/.claude/skills/ship-version/references/workflow-diff.md +74 -0
- package/.claude/skills/ship-version/references/workflow-pr.md +46 -0
- package/.claude/skills/ship-version/references/workflow-push.md +35 -0
- package/CLAUDE.md +17 -0
- package/README.md +239 -0
- package/cli/__tests__/cli.test.js +155 -0
- package/cli/bin.js +102 -0
- package/cli/commands/init.js +49 -0
- package/cli/commands/new.js +18 -0
- package/cli/commands/update.js +119 -0
- package/package.json +40 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Push Workflow
|
|
2
|
+
|
|
3
|
+
## Steps
|
|
4
|
+
|
|
5
|
+
### 1. Pre-check
|
|
6
|
+
```bash
|
|
7
|
+
git status
|
|
8
|
+
git branch --show-current
|
|
9
|
+
```
|
|
10
|
+
Warn if uncommitted changes exist.
|
|
11
|
+
|
|
12
|
+
### 2. Check Remote Tracking
|
|
13
|
+
```bash
|
|
14
|
+
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null
|
|
15
|
+
```
|
|
16
|
+
- If tracking exists → `git push`
|
|
17
|
+
- If no tracking → `git push -u origin <branch>`
|
|
18
|
+
|
|
19
|
+
### 3. Handle Errors
|
|
20
|
+
|
|
21
|
+
| Error | Action |
|
|
22
|
+
|-------|--------|
|
|
23
|
+
| Rejected (non-fast-forward) | Suggest `git pull --rebase` then push |
|
|
24
|
+
| Permission denied | Tell user to check remote access |
|
|
25
|
+
| Remote not found | Suggest `git remote add origin <url>` |
|
|
26
|
+
|
|
27
|
+
### 4. Confirm
|
|
28
|
+
```bash
|
|
29
|
+
git log --oneline origin/<branch>..HEAD
|
|
30
|
+
```
|
|
31
|
+
Show what was pushed.
|
|
32
|
+
|
|
33
|
+
## Safety
|
|
34
|
+
- Never force push (`--force` or `-f`)
|
|
35
|
+
- Never push to `main`/`master`/`production` without user confirmation
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project
|
|
4
|
+
Ship — AI-powered development toolkit by TeleLabs AI.
|
|
5
|
+
|
|
6
|
+
## Commands
|
|
7
|
+
- Lint: `npm run lint`
|
|
8
|
+
- Test: `npm test`
|
|
9
|
+
- Build: `npm run build`
|
|
10
|
+
|
|
11
|
+
## Skills
|
|
12
|
+
- `/ship-version` — Git version control (commit, push, pr, branch, diff, changelog)
|
|
13
|
+
|
|
14
|
+
## Rules
|
|
15
|
+
- Use conventional commits with optional ticket ID (--ticket for Jira, Trello, ClickUp, etc.)
|
|
16
|
+
- Never commit secrets or .env files
|
|
17
|
+
- Run lint before committing
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Ship
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@telelabsai/ship)
|
|
4
|
+
[](https://github.com/telelabs-ai/ship/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
**Ship helps you go from code to production.** AI-powered agents, skills, and guardrails that plug into Claude Code — so you spend less time on process and more time building.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
idea → plan → code → test → review → commit → push → PR → deploy
|
|
10
|
+
Ship handles this ──────────────────────────→
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @telelabsai/ship
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Add Ship to any project
|
|
23
|
+
ship init
|
|
24
|
+
|
|
25
|
+
# Or start fresh
|
|
26
|
+
ship new my-app
|
|
27
|
+
|
|
28
|
+
# Use from terminal (delegates to Claude Code)
|
|
29
|
+
ship version commit --ticket PROJ-42 "add auth"
|
|
30
|
+
|
|
31
|
+
# Or inside Claude Code directly
|
|
32
|
+
claude
|
|
33
|
+
/ship-version commit --ticket PROJ-42 "add auth"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Both paths use the same skill. One source of truth.
|
|
37
|
+
|
|
38
|
+
## CLI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Project setup
|
|
42
|
+
ship init Add Ship to current directory
|
|
43
|
+
ship init --dir ./my-app Add Ship to specific directory
|
|
44
|
+
ship new <name> Create new project with Ship config
|
|
45
|
+
ship update Update .claude/ to latest version
|
|
46
|
+
ship update --force Update without asking
|
|
47
|
+
|
|
48
|
+
# Skills (routes to Claude Code)
|
|
49
|
+
ship version commit Conventional commits with ticket tracking
|
|
50
|
+
ship version diff --pr Preview what a PR would contain
|
|
51
|
+
ship version push Push to remote safely
|
|
52
|
+
ship version pr Create pull request
|
|
53
|
+
ship version changelog Auto-generate changelog
|
|
54
|
+
ship version branch <name> Create and switch branch
|
|
55
|
+
ship plan "add auth" Plan implementation (coming soon)
|
|
56
|
+
ship test Run tests (coming soon)
|
|
57
|
+
ship review Code review (coming soon)
|
|
58
|
+
ship deploy staging Deploy (coming soon)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Any `ship <skill>` command translates to `claude -p "/ship-<skill>"`. When new skills are added, they work from the terminal immediately — no CLI update needed.
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
Ship adds a `.claude/` directory to your project with four layers:
|
|
66
|
+
|
|
67
|
+
| Layer | What it does | When it loads |
|
|
68
|
+
|-------|-------------|---------------|
|
|
69
|
+
| **Skills** | Step-by-step workflows you invoke | On demand (`/ship-version commit`) |
|
|
70
|
+
| **Agents** | Isolated AI subprocesses for heavy tasks | When needed (large diffs, PRs) |
|
|
71
|
+
| **Rules** | Conventions Claude always follows | Every message |
|
|
72
|
+
| **Hooks** | Code-level guardrails | Automatically (blocks secrets, force push) |
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
.claude/
|
|
76
|
+
├── skills/ Slash-command workflows
|
|
77
|
+
├── agents/ Subagent definitions
|
|
78
|
+
├── rules/ Always-loaded instructions
|
|
79
|
+
├── hooks/ Lifecycle automation
|
|
80
|
+
└── settings.json Configuration
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Skills
|
|
84
|
+
|
|
85
|
+
### ship-version — Git Version Control
|
|
86
|
+
|
|
87
|
+
Conventional commits, ticket tracking, security scanning, changelogs.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Commit
|
|
91
|
+
ship version commit # auto-detect type + scope
|
|
92
|
+
ship version commit "add login page" # custom message
|
|
93
|
+
ship version commit --type fix "null check" # explicit type
|
|
94
|
+
ship version commit --ticket PROJ-42 "user auth" # with ticket ref
|
|
95
|
+
|
|
96
|
+
# Diff
|
|
97
|
+
ship version diff # working tree changes
|
|
98
|
+
ship version diff --staged # staged only
|
|
99
|
+
ship version diff --pr # preview PR diff
|
|
100
|
+
ship version diff v1.0.0..v2.0.0 # between tags
|
|
101
|
+
|
|
102
|
+
# Changelog
|
|
103
|
+
ship version changelog # auto-detect from tags
|
|
104
|
+
ship version changelog v1.0.0 v2.0.0 # specific range
|
|
105
|
+
|
|
106
|
+
# Push / PR / Branch
|
|
107
|
+
ship version push # push current branch
|
|
108
|
+
ship version pr --ticket PROJ-42 "Add auth" # create PR with ticket
|
|
109
|
+
ship version branch feature/user-auth # create + switch
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Ticket Tracking
|
|
113
|
+
|
|
114
|
+
Works with any project management tool:
|
|
115
|
+
|
|
116
|
+
| Tool | Example |
|
|
117
|
+
|------|---------|
|
|
118
|
+
| Jira | `--ticket PROJ-123` |
|
|
119
|
+
| ClickUp | `--ticket CU-abc123` |
|
|
120
|
+
| Linear | `--ticket ENG-123` |
|
|
121
|
+
| GitHub Issues | `--ticket #42` |
|
|
122
|
+
| Trello | `--ticket #abc123` |
|
|
123
|
+
| Shortcut | `--ticket sc-12345` |
|
|
124
|
+
| Asana | `--ticket 1234567890` |
|
|
125
|
+
| Custom | `--ticket ANYTHING` |
|
|
126
|
+
|
|
127
|
+
Multiple tickets: `--ticket PROJ-42 --ticket #15`
|
|
128
|
+
|
|
129
|
+
### Commit Format
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
type(scope): description
|
|
133
|
+
|
|
134
|
+
[optional body]
|
|
135
|
+
|
|
136
|
+
Refs: TICKET-ID
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Types: `feat` | `fix` | `refactor` | `test` | `docs` | `chore` | `perf` | `style` | `ci` | `build`
|
|
140
|
+
|
|
141
|
+
## Safety
|
|
142
|
+
|
|
143
|
+
Ship includes automatic guardrails that can't be bypassed:
|
|
144
|
+
|
|
145
|
+
| Guardrail | What it prevents |
|
|
146
|
+
|-----------|-----------------|
|
|
147
|
+
| Secret detection | Blocks committing `.env`, API keys, credentials |
|
|
148
|
+
| Force push block | Prevents `git push --force` to any branch |
|
|
149
|
+
| Blind staging block | Prevents `git add .` / `git add -A` |
|
|
150
|
+
| Hard reset block | Prevents `git reset --hard` without confirmation |
|
|
151
|
+
| Branch protection | Prevents deleting main/master/production |
|
|
152
|
+
|
|
153
|
+
## Extending Ship
|
|
154
|
+
|
|
155
|
+
### Add a skill
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
mkdir -p .claude/skills/ship-deploy
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
# .claude/skills/ship-deploy/SKILL.md
|
|
163
|
+
---
|
|
164
|
+
name: ship-deploy
|
|
165
|
+
description: "Deploy to staging or production."
|
|
166
|
+
argument-hint: "<staging|production>"
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
# Instructions here...
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Now `ship deploy staging` works — both from terminal and inside Claude Code.
|
|
173
|
+
|
|
174
|
+
### Add an agent
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
# .claude/agents/my-agent.md
|
|
178
|
+
---
|
|
179
|
+
name: my-agent
|
|
180
|
+
description: "When to use this agent."
|
|
181
|
+
model: sonnet
|
|
182
|
+
tools: Bash, Read, Edit, Glob, Grep
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
Instructions here...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Add a rule
|
|
189
|
+
|
|
190
|
+
```markdown
|
|
191
|
+
# .claude/rules/my-rule.md
|
|
192
|
+
- Always use TypeScript strict mode
|
|
193
|
+
- Never use `any` type
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Rules load every message. Keep them short.
|
|
197
|
+
|
|
198
|
+
### Add a hook
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
// .claude/hooks/my-hook.cjs
|
|
202
|
+
const input = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
|
|
203
|
+
// Exit 0 = allow, Exit 2 = block (stdout = reason)
|
|
204
|
+
process.exit(0);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Wire in `.claude/settings.json`:
|
|
208
|
+
```json
|
|
209
|
+
{ "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [
|
|
210
|
+
{ "type": "command", "command": "node .claude/hooks/my-hook.cjs" }
|
|
211
|
+
]}]}}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Roadmap
|
|
215
|
+
|
|
216
|
+
- [x] `ship-version` — Git version control
|
|
217
|
+
- [ ] `ship-plan` — Implementation planning
|
|
218
|
+
- [ ] `ship-test` — Test runner with coverage
|
|
219
|
+
- [ ] `ship-review` — Code review
|
|
220
|
+
- [ ] `ship-fix` — Structured bug fixing
|
|
221
|
+
- [ ] `ship-deploy` — Deployment workflows
|
|
222
|
+
- [ ] `ship-docs` — Documentation management
|
|
223
|
+
- [ ] `ship-secure` — Security scanning
|
|
224
|
+
|
|
225
|
+
## Requirements
|
|
226
|
+
|
|
227
|
+
- [Node.js](https://nodejs.org) 18+
|
|
228
|
+
- [Claude Code](https://claude.ai/code) (for skill execution)
|
|
229
|
+
|
|
230
|
+
## Links
|
|
231
|
+
|
|
232
|
+
- **Website:** [telelabs.ai](https://telelabs.ai)
|
|
233
|
+
- **Email:** [tele@telelabs.ai](mailto:tele@telelabs.ai)
|
|
234
|
+
- **LinkedIn:** [linkedin.com/company/telelabsai](https://linkedin.com/company/telelabsai)
|
|
235
|
+
- **Issues:** [github.com/telelabs-ai/ship/issues](https://github.com/telelabs-ai/ship/issues)
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT — [TeleLabs AI](https://telelabs.ai)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const { describe, it, before, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { existsSync, rmSync, readFileSync } = require('fs');
|
|
5
|
+
const { join, resolve } = require('path');
|
|
6
|
+
|
|
7
|
+
const CLI = resolve(__dirname, '..', 'bin.js');
|
|
8
|
+
const TMP = resolve(__dirname, '..', '..', '.test-output');
|
|
9
|
+
|
|
10
|
+
// Helper: run CLI command and return stdout
|
|
11
|
+
function run(args) {
|
|
12
|
+
return execSync(`node ${CLI} ${args}`, { encoding: 'utf8', cwd: TMP });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('ship CLI', () => {
|
|
16
|
+
before(() => {
|
|
17
|
+
// Clean up any leftover test output
|
|
18
|
+
if (existsSync(TMP)) rmSync(TMP, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
after(() => {
|
|
22
|
+
if (existsSync(TMP)) rmSync(TMP, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('--help', () => {
|
|
26
|
+
it('shows help text', () => {
|
|
27
|
+
const out = execSync(`node ${CLI} --help`, { encoding: 'utf8' });
|
|
28
|
+
assert.match(out, /Ship/);
|
|
29
|
+
assert.match(out, /ship init/);
|
|
30
|
+
assert.match(out, /ship new/);
|
|
31
|
+
assert.match(out, /telelabs\.ai/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('shows help with -h alias', () => {
|
|
35
|
+
const out = execSync(`node ${CLI} -h`, { encoding: 'utf8' });
|
|
36
|
+
assert.match(out, /ship init/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('shows help when no args', () => {
|
|
40
|
+
const out = execSync(`node ${CLI}`, { encoding: 'utf8' });
|
|
41
|
+
assert.match(out, /ship init/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('--version', () => {
|
|
46
|
+
it('prints version number', () => {
|
|
47
|
+
const out = execSync(`node ${CLI} --version`, { encoding: 'utf8' }).trim();
|
|
48
|
+
assert.match(out, /^\d+\.\d+\.\d+$/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('works with -v alias', () => {
|
|
52
|
+
const out = execSync(`node ${CLI} -v`, { encoding: 'utf8' }).trim();
|
|
53
|
+
assert.match(out, /^\d+\.\d+\.\d+$/);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('skill delegation', () => {
|
|
58
|
+
it('delegates unknown commands to Claude Code as skills', () => {
|
|
59
|
+
// Any non-builtin command gets routed to Claude Code as /ship-<command>
|
|
60
|
+
// This will fail if Claude Code is not installed, which is expected in CI
|
|
61
|
+
try {
|
|
62
|
+
execSync(`node ${CLI} foobar`, { encoding: 'utf8', timeout: 5000 });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Expected: either Claude not installed or skill not found
|
|
65
|
+
assert.ok(
|
|
66
|
+
err.stderr.includes('Claude Code') || err.status !== 0,
|
|
67
|
+
'Should attempt to delegate to Claude Code'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('ship new', () => {
|
|
74
|
+
const projectDir = join(TMP, 'test-new-project');
|
|
75
|
+
|
|
76
|
+
before(() => {
|
|
77
|
+
if (existsSync(TMP)) rmSync(TMP, { recursive: true });
|
|
78
|
+
execSync(`mkdir -p ${TMP}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('creates project directory with .claude/', () => {
|
|
82
|
+
execSync(`node ${CLI} new ${projectDir}`, { encoding: 'utf8' });
|
|
83
|
+
assert.ok(existsSync(projectDir));
|
|
84
|
+
assert.ok(existsSync(join(projectDir, '.claude')));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('copies all expected files', () => {
|
|
88
|
+
// Agents
|
|
89
|
+
assert.ok(existsSync(join(projectDir, '.claude', 'agents', 'git-ops.md')));
|
|
90
|
+
// Skills
|
|
91
|
+
assert.ok(existsSync(join(projectDir, '.claude', 'skills', 'ship-version', 'SKILL.md')));
|
|
92
|
+
// Rules
|
|
93
|
+
assert.ok(existsSync(join(projectDir, '.claude', 'rules', 'git-conventions.md')));
|
|
94
|
+
// Hooks
|
|
95
|
+
assert.ok(existsSync(join(projectDir, '.claude', 'hooks', 'git-safety.cjs')));
|
|
96
|
+
// Settings
|
|
97
|
+
assert.ok(existsSync(join(projectDir, '.claude', 'settings.json')));
|
|
98
|
+
// CLAUDE.md
|
|
99
|
+
assert.ok(existsSync(join(projectDir, 'CLAUDE.md')));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('copies skill references', () => {
|
|
103
|
+
const refs = join(projectDir, '.claude', 'skills', 'ship-version', 'references');
|
|
104
|
+
assert.ok(existsSync(join(refs, 'commit-standards.md')));
|
|
105
|
+
assert.ok(existsSync(join(refs, 'workflow-commit.md')));
|
|
106
|
+
assert.ok(existsSync(join(refs, 'workflow-push.md')));
|
|
107
|
+
assert.ok(existsSync(join(refs, 'workflow-pr.md')));
|
|
108
|
+
assert.ok(existsSync(join(refs, 'workflow-diff.md')));
|
|
109
|
+
assert.ok(existsSync(join(refs, 'workflow-changelog.md')));
|
|
110
|
+
assert.ok(existsSync(join(refs, 'safety-protocols.md')));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('customizes CLAUDE.md with project name', () => {
|
|
114
|
+
const content = readFileSync(join(projectDir, 'CLAUDE.md'), 'utf8');
|
|
115
|
+
assert.match(content, /test-new-project/);
|
|
116
|
+
assert.match(content, /powered by Ship/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('fails if project already exists', () => {
|
|
120
|
+
assert.throws(
|
|
121
|
+
() => execSync(`node ${CLI} new ${projectDir}`, { encoding: 'utf8' }),
|
|
122
|
+
/already exists/
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('fails without project name', () => {
|
|
127
|
+
assert.throws(
|
|
128
|
+
() => execSync(`node ${CLI} new`, { encoding: 'utf8' }),
|
|
129
|
+
/project name required/
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('ship init', () => {
|
|
135
|
+
const initDir = join(TMP, 'test-init-project');
|
|
136
|
+
|
|
137
|
+
before(() => {
|
|
138
|
+
execSync(`mkdir -p ${initDir}`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('scaffolds .claude/ into existing directory', () => {
|
|
142
|
+
execSync(`node ${CLI} init --dir ${initDir}`, { encoding: 'utf8' });
|
|
143
|
+
assert.ok(existsSync(join(initDir, '.claude')));
|
|
144
|
+
assert.ok(existsSync(join(initDir, '.claude', 'settings.json')));
|
|
145
|
+
assert.ok(existsSync(join(initDir, 'CLAUDE.md')));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('fails if .claude/ already exists', () => {
|
|
149
|
+
assert.throws(
|
|
150
|
+
() => execSync(`node ${CLI} init --dir ${initDir}`, { encoding: 'utf8' }),
|
|
151
|
+
/already exists/
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
package/cli/bin.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { resolve } = require('path');
|
|
4
|
+
const { execSync, spawn } = require('child_process');
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0];
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
Ship — AI-powered development toolkit by TeleLabs AI
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
ship init [--dir <path>] Scaffold .claude/ into a project
|
|
13
|
+
ship new <name> Create new project with Ship config
|
|
14
|
+
ship update [--force] Update .claude/ to latest Ship version
|
|
15
|
+
ship <skill> [args] Run a Ship skill via Claude Code
|
|
16
|
+
ship --version Show version
|
|
17
|
+
ship --help Show this help
|
|
18
|
+
|
|
19
|
+
Skills (any ship-* skill installed in .claude/skills/):
|
|
20
|
+
ship version commit → /ship-version commit
|
|
21
|
+
ship version diff --pr → /ship-version diff --pr
|
|
22
|
+
ship plan "add auth" → /ship-plan add auth
|
|
23
|
+
ship deploy staging → /ship-deploy staging
|
|
24
|
+
ship test → /ship-test
|
|
25
|
+
ship review → /ship-review
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
ship init
|
|
29
|
+
ship new my-app
|
|
30
|
+
ship update
|
|
31
|
+
ship version commit --ticket PROJ-42 "add auth"
|
|
32
|
+
ship plan "implement user dashboard"
|
|
33
|
+
|
|
34
|
+
Requires: Claude Code (https://claude.ai/code)
|
|
35
|
+
https://telelabs.ai
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
if (!command || command === '--help' || command === '-h') {
|
|
39
|
+
console.log(HELP);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (command === '--version' || command === '-v') {
|
|
44
|
+
const pkg = require('../package.json');
|
|
45
|
+
console.log(pkg.version);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (command === 'init') {
|
|
50
|
+
const init = require('./commands/init.js');
|
|
51
|
+
const dirIdx = args.indexOf('--dir');
|
|
52
|
+
const targetDir = dirIdx !== -1 ? resolve(args[dirIdx + 1]) : process.cwd();
|
|
53
|
+
init(targetDir);
|
|
54
|
+
} else if (command === 'new') {
|
|
55
|
+
const newProject = require('./commands/new.js');
|
|
56
|
+
const projectName = args[1];
|
|
57
|
+
if (!projectName) {
|
|
58
|
+
console.error(' Error: project name required. Usage: ship new <name>');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
newProject(projectName);
|
|
62
|
+
} else if (command === 'update') {
|
|
63
|
+
const update = require('./commands/update.js');
|
|
64
|
+
const force = args.includes('--force');
|
|
65
|
+
update(process.cwd(), force);
|
|
66
|
+
} else {
|
|
67
|
+
// Generic skill routing: ship <skill> [args] → claude -p "/ship-<skill> [args]"
|
|
68
|
+
delegateToClaude(args);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Delegates any non-builtin command to Claude Code as a skill invocation.
|
|
73
|
+
* `ship version commit --ticket X` → `claude -p "/ship-version commit --ticket X"`
|
|
74
|
+
*
|
|
75
|
+
* @param {string[]} cliArgs - All arguments after `ship`
|
|
76
|
+
*/
|
|
77
|
+
function delegateToClaude(cliArgs) {
|
|
78
|
+
// Check Claude Code is installed
|
|
79
|
+
try {
|
|
80
|
+
execSync('claude --version', { stdio: 'pipe' });
|
|
81
|
+
} catch {
|
|
82
|
+
console.error(' Error: Claude Code is not installed.');
|
|
83
|
+
console.error(' Install it at: https://claude.ai/code');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build skill prompt: ship version commit → /ship-version commit
|
|
88
|
+
const skill = cliArgs[0];
|
|
89
|
+
const rest = cliArgs.slice(1).join(' ');
|
|
90
|
+
const prompt = `/ship-${skill}${rest ? ' ' + rest : ''}`;
|
|
91
|
+
|
|
92
|
+
const claude = spawn('claude', ['-p', prompt], {
|
|
93
|
+
stdio: 'inherit',
|
|
94
|
+
cwd: process.cwd(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
claude.on('close', (code) => process.exit(code || 0));
|
|
98
|
+
claude.on('error', (err) => {
|
|
99
|
+
console.error(` Error running Claude Code: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { resolve, join } = require('path');
|
|
2
|
+
const { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
3
|
+
|
|
4
|
+
// Source: .claude/ and CLAUDE.md from the package root (single source of truth)
|
|
5
|
+
const PKG_ROOT = resolve(__dirname, '..', '..');
|
|
6
|
+
const SOURCE_CLAUDE_DIR = join(PKG_ROOT, '.claude');
|
|
7
|
+
const SOURCE_CLAUDE_MD = join(PKG_ROOT, 'CLAUDE.md');
|
|
8
|
+
|
|
9
|
+
module.exports = function init(targetDir) {
|
|
10
|
+
const targetClaude = join(targetDir, '.claude');
|
|
11
|
+
const targetClaudeMd = join(targetDir, 'CLAUDE.md');
|
|
12
|
+
|
|
13
|
+
// Safety: warn if .claude/ already exists
|
|
14
|
+
if (existsSync(targetClaude)) {
|
|
15
|
+
console.error(' .claude/ already exists in this directory.');
|
|
16
|
+
console.error(' Use --force to overwrite (not yet implemented).');
|
|
17
|
+
console.error(' Skipping to avoid losing your customizations.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Ensure target directory exists
|
|
22
|
+
if (!existsSync(targetDir)) {
|
|
23
|
+
mkdirSync(targetDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Copy .claude/ directory
|
|
27
|
+
cpSync(SOURCE_CLAUDE_DIR, targetClaude, { recursive: true });
|
|
28
|
+
console.log(' .claude/ scaffolded');
|
|
29
|
+
|
|
30
|
+
// Copy CLAUDE.md (only if it doesn't exist)
|
|
31
|
+
if (!existsSync(targetClaudeMd)) {
|
|
32
|
+
// Read and customize the template CLAUDE.md
|
|
33
|
+
let claudeMd = readFileSync(SOURCE_CLAUDE_MD, 'utf8');
|
|
34
|
+
|
|
35
|
+
// Replace project name placeholder with directory name
|
|
36
|
+
const projectName = targetDir.split('/').pop();
|
|
37
|
+
claudeMd = claudeMd.replace(
|
|
38
|
+
'Ship — AI-powered development toolkit by TeleLabs AI.',
|
|
39
|
+
`${projectName} — powered by Ship (TeleLabs AI).`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
writeFileSync(targetClaudeMd, claudeMd);
|
|
43
|
+
console.log(' CLAUDE.md created');
|
|
44
|
+
} else {
|
|
45
|
+
console.log(' CLAUDE.md already exists, skipped');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('\n Ship initialized. Run "claude" to start.\n');
|
|
49
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { resolve, join } = require('path');
|
|
2
|
+
const { existsSync, mkdirSync } = require('fs');
|
|
3
|
+
const init = require('./init.js');
|
|
4
|
+
|
|
5
|
+
module.exports = function newProject(name) {
|
|
6
|
+
const targetDir = resolve(process.cwd(), name);
|
|
7
|
+
|
|
8
|
+
if (existsSync(targetDir)) {
|
|
9
|
+
console.error(` Error: "${name}" already exists.`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
mkdirSync(targetDir, { recursive: true });
|
|
14
|
+
console.log(`\n Creating ${name}/\n`);
|
|
15
|
+
|
|
16
|
+
// Scaffold Ship config into the new directory
|
|
17
|
+
init(targetDir);
|
|
18
|
+
};
|