motif-design 0.2.2 → 0.2.3

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/bin/cli.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { parseArgs, styleText } = require('node:util');
5
+ const path = require('node:path');
6
+
7
+ // ─── Subcommand registry ────────────────────────────────────────
8
+ const COMMANDS = {
9
+ init: './commands/init.js',
10
+ status: './commands/status.js',
11
+ update: './commands/update.js',
12
+ doctor: './commands/doctor.js',
13
+ list: './commands/list.js',
14
+ };
15
+
16
+ // ─── Parse top-level args ───────────────────────────────────────
17
+
18
+ const { values, positionals } = parseArgs({
19
+ args: process.argv.slice(2),
20
+ allowPositionals: true,
21
+ strict: false,
22
+ });
23
+
24
+ // ─── Handle --version / -v ──────────────────────────────────────
25
+
26
+ if (values.version || values.v) {
27
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
28
+ console.log(pkg.version);
29
+ process.exit(0);
30
+ }
31
+
32
+ // ─── Help text ──────────────────────────────────────────────────
33
+
34
+ function printHelp() {
35
+ console.log(`
36
+ ${styleText('bold', 'motif')} - Domain-intelligent design system for AI coding assistants
37
+
38
+ ${styleText('bold', 'USAGE')}
39
+ motif <command> [options]
40
+ npx motif-design@latest [options]
41
+
42
+ ${styleText('bold', 'COMMANDS')}
43
+ ${styleText('cyan', 'init')} Install Motif into the current project
44
+ ${styleText('cyan', 'status')} Show version, workflow phase, and screens composed
45
+ ${styleText('cyan', 'update')} Sync project files from updated global package
46
+ ${styleText('cyan', 'doctor')} Check installation integrity and configuration
47
+ ${styleText('cyan', 'list')} Show available design verticals
48
+ ${styleText('cyan', 'help')} Show this help message
49
+
50
+ ${styleText('bold', 'OPTIONS')}
51
+ -v, --version Show version number
52
+ -h, --help Show this help message
53
+
54
+ ${styleText('bold', 'EXAMPLES')}
55
+ motif init Install with auto-detected runtime
56
+ motif init --runtime claude-code Explicit runtime selection
57
+ motif status Check installation status
58
+ motif list Browse available verticals
59
+ motif doctor Diagnose installation issues
60
+ motif update Update to latest version
61
+ `);
62
+ }
63
+
64
+ // ─── Handle --help / -h / help subcommand ───────────────────────
65
+
66
+ if (values.help || values.h || positionals[0] === 'help') {
67
+ printHelp();
68
+ process.exit(0);
69
+ }
70
+
71
+ // ─── Route subcommand ───────────────────────────────────────────
72
+
73
+ const subcommand = positionals[0];
74
+
75
+ if (subcommand && COMMANDS[subcommand]) {
76
+ // Known subcommand: route to handler with remaining args
77
+ require(COMMANDS[subcommand]).run(process.argv.slice(3));
78
+ } else {
79
+ // No subcommand or unrecognized first arg: backward-compat init mode
80
+ // Passes all args through so `npx motif-design@latest --force` still works
81
+ require(COMMANDS.init).run(process.argv.slice(2));
82
+ }
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { parseArgs, styleText } = require('node:util');
6
+ const { findProjectRoot } = require('../lib/find-root.js');
7
+ const { readManifest, getPackageVersion, compareVersions, hashFile } = require('../lib/manifest.js');
8
+
9
+ // ─── Flag parsing ────────────────────────────────────────────────
10
+
11
+ function parseFlags(args) {
12
+ const { values } = parseArgs({
13
+ args,
14
+ options: {
15
+ help: { type: 'boolean', short: 'h', default: false },
16
+ },
17
+ strict: true,
18
+ });
19
+ return values;
20
+ }
21
+
22
+ function printHelp() {
23
+ console.log(`
24
+ ${styleText('bold', 'motif doctor')} - Check installation integrity and configuration
25
+
26
+ ${styleText('bold', 'USAGE')}
27
+ motif doctor [options]
28
+
29
+ ${styleText('bold', 'OPTIONS')}
30
+ -h, --help Show this help message
31
+
32
+ ${styleText('bold', 'CHECKS')}
33
+ File Integrity Verify all manifest files exist and match expected hashes
34
+ Hook Config Check CLAUDE.md sentinels and settings.json hooks
35
+ Version Compare project version with global package version
36
+ `);
37
+ }
38
+
39
+ // ─── Check result helpers ────────────────────────────────────────
40
+
41
+ function ok(msg) { return { status: 'OK', msg }; }
42
+ function warn(msg) { return { status: '!!', msg }; }
43
+ function fail(msg) { return { status: 'XX', msg }; }
44
+
45
+ function formatResult(result) {
46
+ const label = `[${result.status}]`;
47
+ if (result.status === 'OK') {
48
+ return ` ${styleText('green', label)} ${result.msg}`;
49
+ } else if (result.status === '!!') {
50
+ return ` ${styleText('yellow', label)} ${result.msg}`;
51
+ } else {
52
+ return ` ${styleText('red', label)} ${result.msg}`;
53
+ }
54
+ }
55
+
56
+ // ─── Checks ──────────────────────────────────────────────────────
57
+
58
+ function checkFileIntegrity(projectRoot, manifest) {
59
+ const results = [];
60
+ const files = manifest.files || {};
61
+ const fileKeys = Object.keys(files);
62
+
63
+ if (fileKeys.length === 0) {
64
+ results.push(warn('No files recorded in manifest'));
65
+ return results;
66
+ }
67
+
68
+ for (const relPath of fileKeys) {
69
+ const fullPath = path.join(projectRoot, relPath);
70
+ if (!fs.existsSync(fullPath)) {
71
+ results.push(fail(`Missing: ${relPath}`));
72
+ } else {
73
+ try {
74
+ const currentHash = hashFile(fullPath);
75
+ const expectedHash = files[relPath].hash || files[relPath];
76
+ if (currentHash === expectedHash) {
77
+ results.push(ok(`${relPath}`));
78
+ } else {
79
+ results.push(warn(`Modified: ${relPath}`));
80
+ }
81
+ } catch {
82
+ results.push(warn(`Could not hash: ${relPath}`));
83
+ }
84
+ }
85
+ }
86
+
87
+ return results;
88
+ }
89
+
90
+ function checkHookConfiguration(projectRoot) {
91
+ const results = [];
92
+
93
+ // Check CLAUDE.md sentinels
94
+ const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
95
+ if (fs.existsSync(claudeMdPath)) {
96
+ const content = fs.readFileSync(claudeMdPath, 'utf8');
97
+ const hasStart = content.includes('MOTIF-START');
98
+ const hasEnd = content.includes('MOTIF-END');
99
+ if (hasStart && hasEnd) {
100
+ results.push(ok('CLAUDE.md sentinels present'));
101
+ } else {
102
+ results.push(fail('CLAUDE.md missing MOTIF sentinel markers'));
103
+ }
104
+ } else {
105
+ results.push(fail('CLAUDE.md not found'));
106
+ }
107
+
108
+ // Check settings.json
109
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
110
+ if (fs.existsSync(settingsPath)) {
111
+ let settings;
112
+ try {
113
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
114
+ } catch {
115
+ results.push(fail('settings.json is invalid JSON'));
116
+ results.push(fail('PostToolUse hook not configured'));
117
+ results.push(fail('SessionStart hook not configured'));
118
+ results.push(fail('statusLine not configured'));
119
+ return results;
120
+ }
121
+
122
+ // PostToolUse hook
123
+ const postToolUse = settings.hooks?.PostToolUse;
124
+ if (Array.isArray(postToolUse) && postToolUse.some(h => (h.command || '').includes('motif'))) {
125
+ results.push(ok('PostToolUse hook configured'));
126
+ } else {
127
+ results.push(fail('PostToolUse hook not configured'));
128
+ }
129
+
130
+ // SessionStart hook
131
+ const sessionStart = settings.hooks?.SessionStart;
132
+ if (Array.isArray(sessionStart) && sessionStart.some(h => (h.command || '').includes('motif-session-start'))) {
133
+ results.push(ok('SessionStart hook configured'));
134
+ } else {
135
+ results.push(fail('SessionStart hook not configured'));
136
+ }
137
+
138
+ // statusLine
139
+ const statusLine = settings.statusLine;
140
+ if (statusLine && (statusLine.command || '').includes('motif-context-monitor')) {
141
+ results.push(ok('statusLine configured'));
142
+ } else {
143
+ results.push(fail('statusLine not configured'));
144
+ }
145
+ } else {
146
+ results.push(fail('settings.json not found'));
147
+ results.push(fail('PostToolUse hook not configured'));
148
+ results.push(fail('SessionStart hook not configured'));
149
+ results.push(fail('statusLine not configured'));
150
+ }
151
+
152
+ return results;
153
+ }
154
+
155
+ function checkVersionConsistency(manifest) {
156
+ const results = [];
157
+ const packageVersion = getPackageVersion();
158
+
159
+ if (!packageVersion) {
160
+ results.push(warn('Could not determine package version'));
161
+ return results;
162
+ }
163
+
164
+ const cmp = compareVersions(manifest.version, packageVersion);
165
+
166
+ if (cmp === 0) {
167
+ results.push(ok(`Versions match (v${packageVersion})`));
168
+ } else if (cmp === 1) {
169
+ results.push(warn(`Project version (v${manifest.version}) newer than package (v${packageVersion})`));
170
+ } else {
171
+ results.push(warn(`Update available: v${packageVersion} (installed: v${manifest.version})`));
172
+ }
173
+
174
+ return results;
175
+ }
176
+
177
+ // ─── Main ────────────────────────────────────────────────────────
178
+
179
+ function run(args) {
180
+ const flags = parseFlags(args);
181
+
182
+ if (flags.help) {
183
+ printHelp();
184
+ process.exit(0);
185
+ }
186
+
187
+ // Find project root
188
+ const projectRoot = findProjectRoot(process.cwd());
189
+ if (!projectRoot) {
190
+ console.error(styleText('red', 'Not inside a project directory.'));
191
+ console.error('Run this command from a directory that contains .git/ or package.json');
192
+ process.exit(1);
193
+ }
194
+
195
+ // Read manifest
196
+ const manifest = readManifest(projectRoot);
197
+ if (!manifest) {
198
+ console.error(styleText('red', 'No Motif installation found. Run \'motif init\' to install.'));
199
+ process.exit(1);
200
+ }
201
+
202
+ // Header
203
+ console.log('');
204
+ console.log(styleText('bold', 'Motif Doctor'));
205
+ console.log('\u2500'.repeat(40));
206
+
207
+ let totalOk = 0;
208
+ let totalWarn = 0;
209
+ let totalFail = 0;
210
+
211
+ function printCategory(name, results) {
212
+ console.log('');
213
+ console.log(styleText('bold', ` ${name}`));
214
+ for (const r of results) {
215
+ console.log(formatResult(r));
216
+ if (r.status === 'OK') totalOk++;
217
+ else if (r.status === '!!') totalWarn++;
218
+ else totalFail++;
219
+ }
220
+ }
221
+
222
+ // Run all checks
223
+ printCategory('File Integrity', checkFileIntegrity(projectRoot, manifest));
224
+ printCategory('Hook Configuration', checkHookConfiguration(projectRoot));
225
+ printCategory('Version Consistency', checkVersionConsistency(manifest));
226
+
227
+ // Summary
228
+ console.log('');
229
+ console.log('\u2500'.repeat(40));
230
+ const summaryParts = [];
231
+ summaryParts.push(`${totalOk} passed`);
232
+ if (totalWarn > 0) summaryParts.push(`${totalWarn} warnings`);
233
+ if (totalFail > 0) summaryParts.push(`${totalFail} failed`);
234
+ const summary = summaryParts.join(', ');
235
+
236
+ if (totalFail > 0) {
237
+ console.log(styleText('red', summary));
238
+ } else if (totalWarn > 0) {
239
+ console.log(styleText('yellow', summary));
240
+ } else {
241
+ console.log(styleText('green', summary));
242
+ }
243
+ console.log('');
244
+
245
+ process.exit(totalFail > 0 ? 1 : 0);
246
+ }
247
+
248
+ module.exports = { run };