@prasenjeet/shipli 1.0.0 → 1.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/README.md CHANGED
@@ -1,33 +1,36 @@
1
- # 🛡️ Shipli
1
+ # Shipli
2
2
 
3
- Store rejections cost days of development time. **Shipli** is a local CLI tool that statically analyzes your Flutter source code against the latest **Apple App Store** and **Google Play** guidelines using an LLM.
3
+ [![npm version](https://img.shields.io/npm/v/@prasenjeet/shipli)](https://www.npmjs.com/package/@prasenjeet/shipli)
4
+ [![license](https://img.shields.io/npm/l/@prasenjeet/shipli)](LICENSE)
4
5
 
5
- Catch missing permissions, policy violations, and compliance issues *before* you submit.
6
+ Store rejections cost days of development time. **Shipli** is a CLI tool that audits your Flutter source code against **Apple App Store** and **Google Play** guidelines using AI.
6
7
 
7
- ## Features
8
+ Catch missing permissions, policy violations, and compliance issues before you submit.
8
9
 
9
- * **Dual Store Support:** Audit against **Apple App Store** and **Google Play** — or both at once.
10
- * **Two Audit Modes:** `--mode store` for compliance, `--mode code` for quality, or both (default).
11
- * **Live Guidelines:** Fetches the latest store policies at runtime and caches them locally.
12
- * **Auto-Detection:** Detects project type (app/package) and platform (ios/android) automatically.
13
- * **Multi-Provider:** Choose between **Google Gemini** or **Anthropic Claude** as your AI backend.
14
- * **Zero-Setup Artifacts:** No `.ipa` or `.apk` needed — reads `lib/`, `pubspec.yaml`, `Info.plist`, and `AndroidManifest.xml` directly.
15
- * **CI-Friendly:** Exit code `1` on FAIL, `0` on PASS/WARNING.
10
+ ## Features
16
11
 
17
- ## 🚀 Installation
12
+ - **Dual Store Support** — Audit against Apple App Store and Google Play, together or individually.
13
+ - **Two Audit Modes** — Store compliance, code quality, or both in a single run.
14
+ - **AI-Powered** — Choose between Google Gemini or Anthropic Claude. Bring your own API key.
15
+ - **Up-to-Date Policies** — Ships with the latest Apple and Google Play store policies. Works offline.
16
+ - **Auto-Detection** — Automatically detects project type and target platform from your project structure.
17
+ - **Zero Setup** — No compiled artifacts needed. Point it at your Flutter project and run.
18
+ - **CI-Ready** — Designed for automation. Integrates with any CI/CD pipeline.
19
+
20
+ ## Installation
18
21
 
19
22
  ```bash
20
- npm install -g shipli
23
+ npm install -g @prasenjeet/shipli
21
24
  ```
22
25
 
23
- Requires **Node.js 18+**.
26
+ Requires Node.js 18 or later.
24
27
 
25
- ## 🛠️ Usage
28
+ ## Usage
26
29
 
27
30
  ### Quick Start
28
31
 
29
32
  ```bash
30
- # One-time setup
33
+ # One-time setup — select provider, model, and enter API key
31
34
  shipli init
32
35
 
33
36
  # Run full audit (auto-detects platform)
@@ -39,10 +42,18 @@ shipli --dir ./ --platform ios --mode store
39
42
  # Google Play only
40
43
  shipli --dir ./ --platform android --mode store
41
44
 
42
- # Code quality only (platform-agnostic)
45
+ # Code quality only
43
46
  shipli --dir ./ --mode code
44
47
  ```
45
48
 
49
+ ### Commands
50
+
51
+ | Command | Description |
52
+ |---------|-------------|
53
+ | `shipli init` | Create a `.shipli` config file interactively |
54
+ | `shipli config` | Update provider, model, or API key |
55
+ | `shipli --dir <path>` | Run the audit (default command) |
56
+
46
57
  ### Options
47
58
 
48
59
  | Flag | Description | Default |
@@ -50,12 +61,12 @@ shipli --dir ./ --mode code
50
61
  | `--dir <path>` | Path to Flutter project root | *required* |
51
62
  | `--key <key>` | API key | `.shipli` / env var |
52
63
  | `--provider <name>` | `gemini` or `claude` | `gemini` |
53
- | `--model <model>` | Model override | `gemini-2.5-flash` / `claude-sonnet-4-6` |
54
- | `--type <type>` | `app` or `package` | auto-detected from pubspec |
64
+ | `--model <model>` | Model override | per provider |
65
+ | `--type <type>` | `app` or `package` | auto-detected |
55
66
  | `--mode <mode>` | `store`, `code`, or `both` | `both` |
56
- | `--platform <platform>` | `ios`, `android`, or `both` | auto-detected from project |
67
+ | `--platform <platform>` | `ios`, `android`, or `both` | auto-detected |
57
68
 
58
- ### Configuration: `.shipli`
69
+ ### Configuration
59
70
 
60
71
  Run `shipli init` to create a `.shipli` config file, or create one manually:
61
72
 
@@ -69,74 +80,122 @@ Run `shipli init` to create a `.shipli` config file, or create one manually:
69
80
  ```
70
81
 
71
82
  The CLI looks for `.shipli` in two places:
83
+
72
84
  1. **Project-level** — in the `--dir` path (highest priority)
73
85
  2. **Global** — in your home directory `~/.shipli`
74
86
 
75
- **Resolution order:** CLI flags > project `.shipli` > global `~/.shipli` > env vars > defaults
87
+ Resolution order: CLI flags > project `.shipli` > global `~/.shipli` > env vars > defaults.
76
88
 
77
- > **Note:** Add `.shipli` to your `.gitignore` — it contains your API key.
89
+ > Add `.shipli` to your `.gitignore` — it contains your API key.
78
90
 
79
- ## 🧠 How it Works
91
+ Use `shipli config` to update settings anytime.
80
92
 
81
- 1. **Detects Project & Platform:** Auto-detects app vs package from `pubspec.yaml`, and iOS/Android from project structure.
82
- 2. **Extracts Evidence:** Scans `lib/` for Dart files, keeping only the architectural skeleton. For packages, also scans `example/`.
83
- 3. **Gathers Metadata:** Reads `Info.plist` (iOS) and/or `AndroidManifest.xml` (Android) permissions.
84
- 4. **Fetches Live Guidelines:** Downloads the latest Apple App Store Review Guidelines and/or Google Play Developer Policies (cached for 7 days).
85
- 5. **AI Audit:** Sends evidence + live guidelines to the LLM with a tailored system prompt.
86
- 6. **Actionable Report:** Outputs a Pass/Warning/Fail checklist with specific guideline citations.
93
+ ### Supported Models
87
94
 
88
- ## 📱 App Store Audit Categories (iOS)
95
+ | Provider | Models |
96
+ |----------|--------|
97
+ | Claude | opus-4-6, sonnet-4-6, opus-4-5, sonnet-4-5, haiku-4-5, opus-4-1, opus-4, sonnet-4, sonnet-3-7, haiku-3-5, sonnet-3-5 |
98
+ | Gemini | 3.1-pro, 3.1-flash-lite, 3-flash, 2.5-flash, 2.5-pro, 2.5-flash-lite, 1.5-pro, 1.5-flash |
99
+
100
+ ## Audit Categories
101
+
102
+ ### App Store (iOS)
89
103
 
90
104
  | Category | Examples |
91
105
  |----------|---------|
92
- | **Privacy & Permissions** | Missing Info.plist usage descriptions, undeclared APIs |
93
- | **Data Collection & Tracking** | ATT requirements, analytics SDKs |
94
- | **Content & Design** | Webview-only apps, minimum functionality |
95
- | **In-App Purchases** | StoreKit/IAP vs third-party payment |
96
- | **Legal & Compliance** | Privacy policy, encryption export, COPPA |
97
- | **Forbidden Patterns** | Code push, dynamic code loading |
106
+ | Privacy & Permissions | Undeclared sensitive APIs, missing usage descriptions |
107
+ | Data Collection & Tracking | Tracking transparency, analytics disclosure |
108
+ | Content & Design | Minimum functionality, UI compliance |
109
+ | In-App Purchases | Payment method compliance |
110
+ | Legal & Compliance | Privacy policy, export compliance |
111
+ | Prohibited Behaviors | Dynamic code loading, hot-patching |
98
112
 
99
- ## 🤖 Play Store Audit Categories (Android)
113
+ ### Play Store (Android)
100
114
 
101
115
  | Category | Examples |
102
116
  |----------|---------|
103
- | **Permissions & Data Safety** | Unnecessary permissions, Data Safety compliance |
104
- | **Data Collection & Privacy** | Privacy policy, User Data policy |
105
- | **Content & Behavior** | Restricted content, deceptive behavior |
106
- | **Billing & Monetization** | Google Play Billing Library requirements |
107
- | **Target API & Compatibility** | Minimum API level, Android version issues |
108
- | **Malware & Abuse** | Stalkerware, abusive notifications |
117
+ | Permissions & Data Safety | Unnecessary permissions, data safety declarations |
118
+ | Data Collection & Privacy | Privacy policy, user data handling |
119
+ | Content & Behavior | Restricted content, deceptive behavior |
120
+ | Billing & Monetization | Payment method compliance |
121
+ | API Level & Compatibility | Target API requirements, version compatibility |
122
+ | Security & Abuse | Malware patterns, abusive behavior |
109
123
 
110
- ## 🔧 Code Quality Categories
124
+ ### Code Quality
111
125
 
112
126
  | Category | Examples |
113
127
  |----------|---------|
114
- | **Security** | Hardcoded keys, insecure HTTP, injection |
115
- | **Architecture** | State management, separation of concerns |
116
- | **Error Handling** | try/catch, crash handling |
117
- | **Performance** | Widget rebuilds, memory leaks |
118
- | **Best Practices** | Dispose/lifecycle, null safety |
119
- | **Dependencies** | Outdated packages, version constraints |
128
+ | Security | Hardcoded credentials, insecure connections |
129
+ | Architecture | State management, separation of concerns |
130
+ | Error Handling | Exception coverage, crash handling |
131
+ | Performance | Memory leaks, unnecessary rebuilds |
132
+ | Best Practices | Lifecycle handling, null safety |
133
+ | Dependencies | Outdated packages, version constraints |
120
134
 
121
- ## 📦 Package Audit Categories
135
+ ### Package Auditing
122
136
 
123
137
  | Category | Examples |
124
138
  |----------|---------|
125
- | **API Surface & Documentation** | Export hygiene, clear entry points |
126
- | **Platform Declarations** | MethodChannel consistency |
127
- | **Consumer Permissions Guidance** | Undocumented permission requirements |
128
- | **Dependency Hygiene** | Constraint quality, misplaced deps |
129
- | **Example App Quality** | Missing example/, incomplete demos |
139
+ | API Surface & Documentation | Export hygiene, entry points |
140
+ | Platform Declarations | Channel consistency, missing platforms |
141
+ | Consumer Guidance | Undocumented permission requirements |
142
+ | Dependency Hygiene | Constraint quality, misplaced dependencies |
143
+ | Example App Quality | Missing or incomplete examples |
144
+
145
+ ## MCP Server
146
+
147
+ Shipli includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server, so AI coding assistants like Claude Code, Cursor, and Windsurf can run audits directly inside your editor.
148
+
149
+ ### Setup
150
+
151
+ Add to your MCP config (e.g. `~/.claude/.mcp.json`):
152
+
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "shipli": {
157
+ "command": "shipli-mcp",
158
+ "env": {
159
+ "SHIPLI_PROVIDER": "claude",
160
+ "SHIPLI_MODEL": "claude-haiku-4-5",
161
+ "ANTHROPIC_API_KEY": "your-api-key"
162
+ }
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ For Gemini, use `SHIPLI_PROVIDER: "gemini"` and set `GEMINI_API_KEY` instead.
169
+
170
+ ### Available Tools
171
+
172
+ | Tool | Description | Parameters |
173
+ |------|-------------|------------|
174
+ | `shipli_store_audit` | Run store compliance audit against Apple App Store and/or Google Play guidelines | `projectDir` (required), `platform` (optional: `ios`, `android`, `both`) |
175
+ | `shipli_code_review` | Run code quality and security review | `projectDir` (required) |
176
+
177
+ Both tools return structured JSON with PASS/WARNING/FAIL scores and specific guideline citations.
178
+
179
+ ### Usage
180
+
181
+ Once configured, ask your AI assistant:
182
+
183
+ ```
184
+ "Run a store audit on /path/to/my-flutter-app"
185
+ "Review the code quality of this Flutter project"
186
+ ```
187
+
188
+ The assistant will call the appropriate Shipli MCP tool and return the results inline.
130
189
 
131
- ## 🔄 CI Integration
190
+ ## CI Integration
132
191
 
133
192
  ```yaml
134
- # GitHub Actions example
193
+ # GitHub Actions
135
194
  - name: Shipli Audit
136
- run: npx shipli --dir ./ --platform both --provider claude --key ${{ secrets.ANTHROPIC_API_KEY }}
195
+ run: npx @prasenjeet/shipli --dir ./ --provider claude --key ${{ secrets.ANTHROPIC_API_KEY }}
137
196
  ```
138
197
 
139
- The CLI exits with code `1` if the audit result is **FAIL**, making it easy to gate deployments.
198
+ The CLI exits with code `1` on failure, making it easy to gate deployments.
140
199
 
141
200
  ## License
142
201
 
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@prasenjeet/shipli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered App Store review audit for Flutter projects. Catch rejections before you submit.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "shipli": "src/index.js"
7
+ "shipli": "src/index.js",
8
+ "shipli-mcp": "src/mcp-server.js"
8
9
  },
9
10
  "files": [
10
11
  "src",
@@ -32,11 +33,13 @@
32
33
  "url": ""
33
34
  },
34
35
  "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.27.1",
35
37
  "chalk": "^5.4.1",
36
38
  "commander": "^13.1.0",
37
39
  "fast-glob": "^3.3.3",
38
40
  "js-yaml": "^4.1.0",
39
41
  "ora": "^8.2.0",
40
- "plist": "^3.1.0"
42
+ "plist": "^3.1.0",
43
+ "zod": "^4.3.6"
41
44
  }
42
45
  }
@@ -0,0 +1,16 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export const PROVIDER_DEFAULTS = {
5
+ gemini: { model: 'gemini-2.5-flash', envKey: 'GEMINI_API_KEY' },
6
+ claude: { model: 'claude-sonnet-4-6', envKey: 'ANTHROPIC_API_KEY' },
7
+ };
8
+
9
+ export function detectPlatform(projectDir) {
10
+ const hasIos = existsSync(join(projectDir, 'ios'));
11
+ const hasAndroid = existsSync(join(projectDir, 'android'));
12
+ if (hasIos && hasAndroid) return 'both';
13
+ if (hasIos) return 'ios';
14
+ if (hasAndroid) return 'android';
15
+ return 'both';
16
+ }
package/src/index.js CHANGED
@@ -17,25 +17,12 @@ import { read as readPubspec } from './pubspec-reader.js';
17
17
  import { audit } from './auditor.js';
18
18
  import { fetchGuidelines } from './guidelines.js';
19
19
  import { print as printReport } from './reporter.js';
20
+ import { PROVIDER_DEFAULTS as DEFAULTS, detectPlatform } from './defaults.js';
20
21
 
21
22
  // Read package version
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
24
  const pkg = JSON.parse(await readFile(join(__dirname, '..', 'package.json'), 'utf-8'));
24
25
 
25
- const DEFAULTS = {
26
- gemini: { model: 'gemini-2.5-flash', envKey: 'GEMINI_API_KEY' },
27
- claude: { model: 'claude-sonnet-4-6', envKey: 'ANTHROPIC_API_KEY' },
28
- };
29
-
30
- function detectPlatform(projectDir) {
31
- const hasIos = existsSync(join(projectDir, 'ios'));
32
- const hasAndroid = existsSync(join(projectDir, 'android'));
33
- if (hasIos && hasAndroid) return 'both';
34
- if (hasIos) return 'ios';
35
- if (hasAndroid) return 'android';
36
- return 'both'; // default
37
- }
38
-
39
26
  const program = new Command();
40
27
 
41
28
  program
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { existsSync } from 'node:fs';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { resolve, join, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { scan } from './scanner.js';
11
+ import { read as readPubspec } from './pubspec-reader.js';
12
+ import { read as readPlist } from './plist-reader.js';
13
+ import { read as readManifest } from './manifest-reader.js';
14
+ import { fetchGuidelines } from './guidelines.js';
15
+ import { audit } from './auditor.js';
16
+ import { loadConfig } from './config.js';
17
+ import { PROVIDER_DEFAULTS, detectPlatform } from './defaults.js';
18
+
19
+ // ── Read package version ──
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const pkg = JSON.parse(await readFile(join(__dirname, '..', 'package.json'), 'utf-8'));
23
+
24
+ // ── Resolve provider config from env vars at startup ──
25
+
26
+ function resolveConfig(projectDir) {
27
+ const provider = (process.env.SHIPLI_PROVIDER || 'gemini').toLowerCase();
28
+ const defaults = PROVIDER_DEFAULTS[provider];
29
+ if (!defaults) {
30
+ throw new Error(`Unknown provider "${provider}". Set SHIPLI_PROVIDER to "gemini" or "claude".`);
31
+ }
32
+
33
+ const model = process.env.SHIPLI_MODEL || defaults.model;
34
+ const apiKey = process.env[defaults.envKey];
35
+
36
+ if (!apiKey) {
37
+ throw new Error(
38
+ `No API key found. Set ${defaults.envKey} in your MCP server env config.`
39
+ );
40
+ }
41
+
42
+ return { provider, model, apiKey };
43
+ }
44
+
45
+ // ── Validate project directory ──
46
+
47
+ function validateProject(projectDir) {
48
+ const resolved = resolve(projectDir);
49
+ if (!existsSync(resolved)) {
50
+ throw new Error(`Directory not found: ${resolved}`);
51
+ }
52
+ if (!existsSync(join(resolved, 'lib'))) {
53
+ throw new Error(`No lib/ directory found in ${resolved}. Is this a Flutter project?`);
54
+ }
55
+ return resolved;
56
+ }
57
+
58
+ // ── Create server ──
59
+
60
+ const server = new McpServer({
61
+ name: 'shipli',
62
+ version: pkg.version,
63
+ });
64
+
65
+ // ── Tool: shipli_store_audit ──
66
+
67
+ server.tool(
68
+ 'shipli_store_audit',
69
+ 'Run a store compliance audit on a Flutter project against Apple App Store and/or Google Play guidelines. Returns structured findings with PASS/WARNING/FAIL scores and specific guideline citations.',
70
+ {
71
+ projectDir: z.string().describe('Absolute path to the Flutter project root directory'),
72
+ platform: z.enum(['ios', 'android', 'both']).optional().describe('Target platform. Auto-detected from project structure if omitted.'),
73
+ },
74
+ async ({ projectDir, platform }) => {
75
+ try {
76
+ const dir = validateProject(projectDir);
77
+ const { provider, model, apiKey } = resolveConfig(dir);
78
+
79
+ const pubspec = await readPubspec(dir);
80
+ const projectType = pubspec.projectType;
81
+ const resolvedPlatform = platform || detectPlatform(dir);
82
+
83
+ const { files } = await scan(dir);
84
+
85
+ let exampleFiles = [];
86
+ if (projectType === 'package' && existsSync(join(dir, 'example', 'lib'))) {
87
+ const exampleResult = await scan(join(dir, 'example'));
88
+ exampleFiles = exampleResult.files;
89
+ }
90
+
91
+ let plistData = { found: false, permissions: {}, bundleId: null };
92
+ let manifestData = { found: false, permissions: [], packageName: null };
93
+
94
+ if (projectType === 'app' && (resolvedPlatform === 'ios' || resolvedPlatform === 'both')) {
95
+ plistData = await readPlist(dir);
96
+ }
97
+ if (projectType === 'app' && (resolvedPlatform === 'android' || resolvedPlatform === 'both')) {
98
+ manifestData = await readManifest(dir);
99
+ }
100
+
101
+ let appleGuidelines = null;
102
+ let googleGuidelines = null;
103
+
104
+ if (resolvedPlatform === 'ios' || resolvedPlatform === 'both') {
105
+ appleGuidelines = await fetchGuidelines('apple');
106
+ }
107
+ if (resolvedPlatform === 'android' || resolvedPlatform === 'both') {
108
+ googleGuidelines = await fetchGuidelines('google');
109
+ }
110
+
111
+ const result = await audit(
112
+ {
113
+ files,
114
+ exampleFiles,
115
+ permissions: plistData.permissions,
116
+ androidPermissions: manifestData.permissions,
117
+ pubspec,
118
+ plistFound: (resolvedPlatform === 'ios' || resolvedPlatform === 'both') ? plistData.found : undefined,
119
+ androidManifestFound: (resolvedPlatform === 'android' || resolvedPlatform === 'both') ? manifestData.found : undefined,
120
+ projectType,
121
+ appleGuidelines,
122
+ googleGuidelines,
123
+ },
124
+ { apiKey, model, provider, mode: 'store', platform: resolvedPlatform },
125
+ );
126
+
127
+ return {
128
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
129
+ };
130
+ } catch (err) {
131
+ return {
132
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
133
+ isError: true,
134
+ };
135
+ }
136
+ },
137
+ );
138
+
139
+ // ── Tool: shipli_code_review ──
140
+
141
+ server.tool(
142
+ 'shipli_code_review',
143
+ 'Run a code quality and security review on a Flutter project. Checks architecture, error handling, performance, dependencies, and security vulnerabilities.',
144
+ {
145
+ projectDir: z.string().describe('Absolute path to the Flutter project root directory'),
146
+ },
147
+ async ({ projectDir }) => {
148
+ try {
149
+ const dir = validateProject(projectDir);
150
+ const { provider, model, apiKey } = resolveConfig(dir);
151
+
152
+ const pubspec = await readPubspec(dir);
153
+ const projectType = pubspec.projectType;
154
+
155
+ const { files } = await scan(dir);
156
+
157
+ let exampleFiles = [];
158
+ if (projectType === 'package' && existsSync(join(dir, 'example', 'lib'))) {
159
+ const exampleResult = await scan(join(dir, 'example'));
160
+ exampleFiles = exampleResult.files;
161
+ }
162
+
163
+ const result = await audit(
164
+ {
165
+ files,
166
+ exampleFiles,
167
+ permissions: {},
168
+ androidPermissions: [],
169
+ pubspec,
170
+ plistFound: undefined,
171
+ androidManifestFound: undefined,
172
+ projectType,
173
+ appleGuidelines: null,
174
+ googleGuidelines: null,
175
+ },
176
+ { apiKey, model, provider, mode: 'code', platform: 'both' },
177
+ );
178
+
179
+ return {
180
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
181
+ };
182
+ } catch (err) {
183
+ return {
184
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
185
+ isError: true,
186
+ };
187
+ }
188
+ },
189
+ );
190
+
191
+ // ── Start server ──
192
+
193
+ const transport = new StdioServerTransport();
194
+ await server.connect(transport);