@openweave/weave-cli 1.0.2

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.
@@ -0,0 +1,239 @@
1
+ import { join } from "path";
2
+ import { CLIArgs, CommandResult, CliCommand } from "../types.js";
3
+ import { IWeaveProvider, JsonProvider, MemoryProvider } from "@openweave/weave-provider";
4
+
5
+ /**
6
+ * MigrateCommand
7
+ *
8
+ * Migrate data between any two registered OpenWeave providers.
9
+ *
10
+ * Usage:
11
+ * weave migrate --from json --to sqlite
12
+ * weave migrate --from sqlite --to json --data-dir /my/data
13
+ * weave migrate --from json --to sqlite --dry-run
14
+ *
15
+ * Supported built-in providers (no external service needed):
16
+ * json — JSON files in --data-dir (default: .weave)
17
+ * sqlite — SQLite file via node:sqlite (requires @openweave/weave-provider-sqlite)
18
+ * memory — In-memory (useful for --dry-run inspection)
19
+ *
20
+ * Extended providers (require running services + extra packages):
21
+ * mongodb — requires @openweave/weave-provider-mongodb + WEAVE_MONGODB_URI
22
+ * postgres — requires @openweave/weave-provider-postgres + WEAVE_POSTGRES_URL
23
+ * mysql — requires @openweave/weave-provider-mysql + WEAVE_MYSQL_URI
24
+ */
25
+ export const migrateCommand: CliCommand = {
26
+ name: "migrate",
27
+ description: "Migrate data between OpenWeave storage providers",
28
+ usage: "weave migrate --from <source> --to <destination> [options]",
29
+ flags: {
30
+ from: {
31
+ short: "f",
32
+ description: "Source provider (json | sqlite | memory | mongodb | postgres | mysql)",
33
+ default: "json",
34
+ },
35
+ to: {
36
+ short: "t",
37
+ description: "Target provider (json | sqlite | memory | mongodb | postgres | mysql)",
38
+ default: "sqlite",
39
+ },
40
+ "data-dir": {
41
+ short: "d",
42
+ description: "Data directory for file-based providers (default: .weave)",
43
+ default: ".weave",
44
+ },
45
+ "db-file": {
46
+ description: "SQLite database file path (default: <data-dir>/weave.db)",
47
+ default: "",
48
+ },
49
+ prefix: {
50
+ short: "p",
51
+ description: "Migrate only keys matching this prefix (e.g. graph:)",
52
+ default: "",
53
+ },
54
+ "dry-run": {
55
+ description: "Preview what would be migrated without writing to destination",
56
+ default: false,
57
+ },
58
+ verbose: {
59
+ short: "v",
60
+ description: "Verbose output",
61
+ default: false,
62
+ },
63
+ },
64
+
65
+ async execute(args: CLIArgs): Promise<CommandResult> {
66
+ const fromName = (args.flags["from"] as string) || "json";
67
+ const toName = (args.flags["to"] as string) || "sqlite";
68
+ const dataDir = (args.flags["data-dir"] as string) || ".weave";
69
+ const dbFile = (args.flags["db-file"] as string) || join(dataDir, "weave.db");
70
+ const prefix = (args.flags["prefix"] as string) || undefined;
71
+ const dryRun = !!args.flags["dry-run"];
72
+ const verbose = !!args.flags["verbose"];
73
+
74
+ if (fromName === toName) {
75
+ return { success: false, message: "Source and destination providers must differ." };
76
+ }
77
+
78
+ const log = (msg: string) => { if (verbose) console.error(` ${msg}`); };
79
+
80
+ let source: IWeaveProvider<unknown>;
81
+ let destination: IWeaveProvider<unknown>;
82
+
83
+ try {
84
+ source = await buildProvider(fromName, dataDir, dbFile, log);
85
+ } catch (err) {
86
+ return {
87
+ success: false,
88
+ message: `Failed to open source provider "${fromName}": ${(err as Error).message}`,
89
+ };
90
+ }
91
+
92
+ try {
93
+ if (!dryRun) {
94
+ destination = await buildProvider(toName, dataDir, dbFile + ".migrated", log);
95
+ } else {
96
+ destination = new MemoryProvider<unknown>();
97
+ }
98
+ } catch (err) {
99
+ await source.close().catch(() => {});
100
+ return {
101
+ success: false,
102
+ message: `Failed to open destination provider "${toName}": ${(err as Error).message}`,
103
+ };
104
+ }
105
+
106
+ try {
107
+ const keys = await source.list(prefix);
108
+
109
+ if (keys.length === 0) {
110
+ return {
111
+ success: true,
112
+ message: `No keys found${prefix ? ` matching prefix "${prefix}"` : ""}. Nothing to migrate.`,
113
+ };
114
+ }
115
+
116
+ log(`Found ${keys.length} key(s) to migrate...`);
117
+
118
+ let migrated = 0;
119
+ let failed = 0;
120
+ const errors: string[] = [];
121
+
122
+ for (const key of keys) {
123
+ try {
124
+ const value = await source.get(key);
125
+ if (value !== null) {
126
+ if (!dryRun) await destination.set(key, value);
127
+ migrated++;
128
+ log(`✓ ${key}`);
129
+ }
130
+ } catch (err) {
131
+ failed++;
132
+ errors.push(` ${key}: ${(err as Error).message}`);
133
+ log(`✗ ${key}: ${(err as Error).message}`);
134
+ }
135
+ }
136
+
137
+ const dryRunNote = dryRun ? " [DRY RUN — no data written]" : "";
138
+ const summary = [
139
+ `Migration complete${dryRunNote}`,
140
+ ` From: ${fromName}`,
141
+ ` To: ${dryRun ? "memory (dry-run)" : toName}`,
142
+ ` Keys: ${keys.length} found`,
143
+ ` Migrated: ${migrated}`,
144
+ ...(failed > 0 ? [` Failed: ${failed}`, ...errors] : []),
145
+ ].join("\n");
146
+
147
+ return { success: failed === 0, message: summary };
148
+ } finally {
149
+ await source.close().catch(() => {});
150
+ if (!dryRun) await destination.close().catch(() => {});
151
+ }
152
+ },
153
+ };
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Factory: create a provider by name
157
+ // ---------------------------------------------------------------------------
158
+
159
+ async function buildProvider(
160
+ name: string,
161
+ dataDir: string,
162
+ dbFile: string,
163
+ log: (msg: string) => void
164
+ ): Promise<IWeaveProvider<unknown>> {
165
+ log(`Opening "${name}" provider...`);
166
+
167
+ switch (name) {
168
+ case "json":
169
+ return new JsonProvider(dataDir);
170
+
171
+ case "memory":
172
+ return new MemoryProvider<unknown>();
173
+
174
+ case "sqlite": {
175
+ // Dynamically import to avoid hard dep; package must be installed
176
+ const { SqliteProvider } = await import(
177
+ "@openweave/weave-provider-sqlite" as string
178
+ ).catch(() => {
179
+ throw new Error(
180
+ '"sqlite" provider requires @openweave/weave-provider-sqlite to be installed.'
181
+ );
182
+ });
183
+ return new (SqliteProvider as new (path: string) => IWeaveProvider<unknown>)(dbFile);
184
+ }
185
+
186
+ case "mongodb": {
187
+ const { MongoProvider } = await import(
188
+ "@openweave/weave-provider-mongodb" as string
189
+ ).catch(() => {
190
+ throw new Error(
191
+ '"mongodb" provider requires @openweave/weave-provider-mongodb to be installed.'
192
+ );
193
+ });
194
+ const uri = process.env["WEAVE_MONGODB_URI"] ?? "mongodb://localhost:27017";
195
+ return (MongoProvider as { connect(o: object): Promise<IWeaveProvider<unknown>> }).connect({
196
+ uri,
197
+ });
198
+ }
199
+
200
+ case "postgres": {
201
+ const { PostgresProvider } = await import(
202
+ "@openweave/weave-provider-postgres" as string
203
+ ).catch(() => {
204
+ throw new Error(
205
+ '"postgres" provider requires @openweave/weave-provider-postgres to be installed.'
206
+ );
207
+ });
208
+ const connectionString =
209
+ process.env["WEAVE_POSTGRES_URL"] ??
210
+ process.env["DATABASE_URL"] ??
211
+ "postgresql://postgres:postgres@localhost:5432/openweave";
212
+ return (
213
+ PostgresProvider as { connect(o: object): Promise<IWeaveProvider<unknown>> }
214
+ ).connect({ connectionString });
215
+ }
216
+
217
+ case "mysql": {
218
+ const { MysqlProvider } = await import(
219
+ "@openweave/weave-provider-mysql" as string
220
+ ).catch(() => {
221
+ throw new Error(
222
+ '"mysql" provider requires @openweave/weave-provider-mysql to be installed.'
223
+ );
224
+ });
225
+ const uri =
226
+ process.env["WEAVE_MYSQL_URI"] ??
227
+ process.env["DATABASE_URL"] ??
228
+ "mysql://root:root@localhost:3306/openweave";
229
+ return (MysqlProvider as { connect(o: object): Promise<IWeaveProvider<unknown>> }).connect({
230
+ uri,
231
+ });
232
+ }
233
+
234
+ default:
235
+ throw new Error(
236
+ `Unknown provider "${name}". Supported: json, sqlite, memory, mongodb, postgres, mysql.`
237
+ );
238
+ }
239
+ }
@@ -0,0 +1,249 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { CLIArgs, CommandResult, CliCommand } from '../types';
4
+ import { resolveProjectRoot } from '../utils';
5
+
6
+ interface Milestone {
7
+ id: string;
8
+ name: string;
9
+ status: 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'deferred';
10
+ priority: 'critical' | 'high' | 'medium' | 'low';
11
+ start_date?: string;
12
+ end_date?: string;
13
+ completion_percentage: number;
14
+ subtasks: {
15
+ id: string;
16
+ title: string;
17
+ status: 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'deferred';
18
+ completed_date?: string;
19
+ }[];
20
+ }
21
+
22
+ /**
23
+ * Milestones Command - View project milestones and tasks
24
+ */
25
+ export class MilestonesCommand implements CliCommand {
26
+ name = 'milestones';
27
+ description = 'List all milestones and sub-tasks';
28
+ usage = 'weave milestones [--filter=active] [--sort=priority]';
29
+ flags = {
30
+ filter: {
31
+ short: 'f',
32
+ description:
33
+ 'Filter by status: all, active, completed, blocked (default: all)',
34
+ default: 'all',
35
+ },
36
+ sort: {
37
+ short: 's',
38
+ description: 'Sort by: priority, date, name (default: priority)',
39
+ default: 'priority',
40
+ },
41
+ json: {
42
+ short: 'j',
43
+ description: 'Output as JSON',
44
+ default: false,
45
+ },
46
+ };
47
+
48
+ async execute(args: CLIArgs): Promise<CommandResult> {
49
+ try {
50
+ const projectRoot = resolveProjectRoot();
51
+ const weaveDir = join(projectRoot, '.weave');
52
+ const roadmapPath = join(weaveDir, 'ROADMAP.md');
53
+
54
+ if (!existsSync(roadmapPath)) {
55
+ return {
56
+ success: false,
57
+ message: 'Error: ROADMAP.md not found',
58
+ error: 'Please run "weave init <project-name>" first',
59
+ };
60
+ }
61
+
62
+ // Mock milestones for demonstration
63
+ const mockMilestones: Milestone[] = [
64
+ {
65
+ id: 'm1',
66
+ name: 'Setup Development Environment',
67
+ status: 'completed',
68
+ priority: 'critical',
69
+ completion_percentage: 100,
70
+ subtasks: [
71
+ {
72
+ id: 'st1',
73
+ title: 'Install dependencies',
74
+ status: 'completed',
75
+ completed_date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
76
+ .toISOString()
77
+ .split('T')[0],
78
+ },
79
+ {
80
+ id: 'st2',
81
+ title: 'Configure TypeScript',
82
+ status: 'completed',
83
+ completed_date: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000)
84
+ .toISOString()
85
+ .split('T')[0],
86
+ },
87
+ {
88
+ id: 'st3',
89
+ title: 'Setup testing framework',
90
+ status: 'completed',
91
+ completed_date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000)
92
+ .toISOString()
93
+ .split('T')[0],
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ id: 'm2',
99
+ name: 'Core Feature Implementation',
100
+ status: 'in_progress',
101
+ priority: 'critical',
102
+ start_date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)
103
+ .toISOString()
104
+ .split('T')[0],
105
+ completion_percentage: 65,
106
+ subtasks: [
107
+ {
108
+ id: 'st4',
109
+ title: 'Implement knowledge graph',
110
+ status: 'completed',
111
+ completed_date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000)
112
+ .toISOString()
113
+ .split('T')[0],
114
+ },
115
+ {
116
+ id: 'st5',
117
+ title: 'Add orphan detection',
118
+ status: 'in_progress',
119
+ },
120
+ {
121
+ id: 'st6',
122
+ title: 'Implement milestone planner',
123
+ status: 'not_started',
124
+ },
125
+ {
126
+ id: 'st7',
127
+ title: 'Create MCP server',
128
+ status: 'not_started',
129
+ },
130
+ ],
131
+ },
132
+ {
133
+ id: 'm3',
134
+ name: 'CLI Development',
135
+ status: 'not_started',
136
+ priority: 'high',
137
+ completion_percentage: 0,
138
+ subtasks: [
139
+ {
140
+ id: 'st8',
141
+ title: 'Design CLI commands',
142
+ status: 'not_started',
143
+ },
144
+ {
145
+ id: 'st9',
146
+ title: 'Implement command handlers',
147
+ status: 'not_started',
148
+ },
149
+ {
150
+ id: 'st10',
151
+ title: 'Add help and documentation',
152
+ status: 'not_started',
153
+ },
154
+ ],
155
+ },
156
+ ];
157
+
158
+ const filter = (args.flags.filter as string) || 'all';
159
+ const filtered = this.filterMilestones(mockMilestones, filter);
160
+
161
+ if (args.flags.json) {
162
+ return {
163
+ success: true,
164
+ message: JSON.stringify(filtered, null, 2),
165
+ data: filtered,
166
+ };
167
+ }
168
+
169
+ let output = '\n📋 Project Milestones\n' + '='.repeat(60) + '\n';
170
+
171
+ for (const milestone of filtered) {
172
+ output += this.formatMilestone(milestone);
173
+ }
174
+
175
+ output += '\n' + '='.repeat(60) + '\n';
176
+ output += `Total: ${filtered.length} milestone(s)\n`;
177
+
178
+ return {
179
+ success: true,
180
+ message: output,
181
+ data: filtered,
182
+ };
183
+ } catch (error) {
184
+ return {
185
+ success: false,
186
+ message: 'Error reading milestones',
187
+ error: error instanceof Error ? error.message : String(error),
188
+ };
189
+ }
190
+ }
191
+
192
+ private filterMilestones(
193
+ milestones: Milestone[],
194
+ filter: string
195
+ ): Milestone[] {
196
+ if (filter === 'all') return milestones;
197
+ if (filter === 'active')
198
+ return milestones.filter((m) => m.status === 'in_progress');
199
+ if (filter === 'completed')
200
+ return milestones.filter((m) => m.status === 'completed');
201
+ if (filter === 'blocked')
202
+ return milestones.filter((m) => m.status === 'blocked');
203
+ return milestones;
204
+ }
205
+
206
+ private formatMilestone(milestone: Milestone): string {
207
+ const statusIcon = {
208
+ completed: '✅',
209
+ in_progress: '🔄',
210
+ not_started: '🔜',
211
+ blocked: '⛔',
212
+ deferred: '⏸️',
213
+ }[milestone.status];
214
+
215
+ const priorityEmoji = {
216
+ critical: '🔴',
217
+ high: '🟠',
218
+ medium: '🟡',
219
+ low: '🟢',
220
+ }[milestone.priority];
221
+
222
+ let output = `\n${statusIcon} ${milestone.name} [${priorityEmoji} ${milestone.priority}]\n`;
223
+ output += ` Progress: ${this.progressBar(milestone.completion_percentage)} ${milestone.completion_percentage}%\n`;
224
+
225
+ if (milestone.subtasks && milestone.subtasks.length > 0) {
226
+ output += ' Sub-tasks:\n';
227
+ for (const subtask of milestone.subtasks) {
228
+ const subIcon = {
229
+ completed: '✓',
230
+ in_progress: '→',
231
+ not_started: '○',
232
+ blocked: '✗',
233
+ deferred: '–',
234
+ }[subtask.status];
235
+ output += ` [${subIcon}] ${subtask.title}\n`;
236
+ }
237
+ }
238
+
239
+ return output;
240
+ }
241
+
242
+ private progressBar(percentage: number, width: number = 20): string {
243
+ const filled = Math.round((percentage / 100) * width);
244
+ const empty = width - filled;
245
+ return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
246
+ }
247
+ }
248
+
249
+ export const milestonesCommand = new MilestonesCommand();
@@ -0,0 +1,210 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { CLIArgs, CommandResult, CliCommand } from '../types';
4
+ import { resolveProjectRoot } from '../utils';
5
+
6
+ interface OrphanEntity {
7
+ id: string;
8
+ name: string;
9
+ type: string;
10
+ file: string;
11
+ line: number;
12
+ severity: 'critical' | 'high' | 'medium' | 'low';
13
+ description: string;
14
+ }
15
+
16
+ /**
17
+ * Orphans Command - Detect unused code
18
+ */
19
+ export class OrphansCommand implements CliCommand {
20
+ name = 'orphans';
21
+ description = 'Analyze code for orphaned (unused) entities';
22
+ usage = 'weave orphans [--severity=all] [--type=all]';
23
+ flags = {
24
+ severity: {
25
+ short: 's',
26
+ description: 'Filter by severity: all, critical, high, medium, low',
27
+ default: 'all',
28
+ },
29
+ type: {
30
+ short: 't',
31
+ description: 'Filter by entity type: all, function, class, variable',
32
+ default: 'all',
33
+ },
34
+ 'include-tests': {
35
+ short: 'i',
36
+ description: 'Include test files in analysis',
37
+ default: false,
38
+ },
39
+ json: {
40
+ short: 'j',
41
+ description: 'Output as JSON',
42
+ default: false,
43
+ },
44
+ };
45
+
46
+ async execute(args: CLIArgs): Promise<CommandResult> {
47
+ try {
48
+ const projectRoot = resolveProjectRoot();
49
+ const weaveDir = join(projectRoot, '.weave');
50
+ const configPath = join(weaveDir, 'config.json');
51
+
52
+ if (!existsSync(configPath)) {
53
+ return {
54
+ success: false,
55
+ message: 'Error: .weave directory not found',
56
+ error: 'Please run "weave init <project-name>" first',
57
+ };
58
+ }
59
+
60
+ // Mock orphans data for demonstration
61
+ const mockOrphans: OrphanEntity[] = [
62
+ {
63
+ id: 'o1',
64
+ name: 'legacyParser',
65
+ type: 'function',
66
+ file: 'src/utils/parser.ts',
67
+ line: 42,
68
+ severity: 'high',
69
+ description: 'Parser function with no external references. Possibly replaced by new implementation.',
70
+ },
71
+ {
72
+ id: 'o2',
73
+ name: 'validateConfig',
74
+ type: 'function',
75
+ file: 'src/validators/config.ts',
76
+ line: 15,
77
+ severity: 'medium',
78
+ description: 'Exported function but only used internally in test files.',
79
+ },
80
+ {
81
+ id: 'o3',
82
+ name: 'DeprecatedClass',
83
+ type: 'class',
84
+ file: 'src/core/deprecated.ts',
85
+ line: 8,
86
+ severity: 'critical',
87
+ description: 'Class marked as exported but has no usages outside module. Consider removing.',
88
+ },
89
+ {
90
+ id: 'o4',
91
+ name: 'tempCache',
92
+ type: 'variable',
93
+ file: 'src/state/cache.ts',
94
+ line: 101,
95
+ severity: 'low',
96
+ description: 'Module-level variable possibly being phased out.',
97
+ },
98
+ ];
99
+
100
+ const severity = (args.flags.severity as string) || 'all';
101
+ const typeFilter = (args.flags.type as string) || 'all';
102
+
103
+ let results = mockOrphans;
104
+
105
+ if (severity !== 'all') {
106
+ results = results.filter((o) => o.severity === severity);
107
+ }
108
+
109
+ if (typeFilter !== 'all') {
110
+ results = results.filter(
111
+ (o) => o.type.toLowerCase() === typeFilter.toLowerCase()
112
+ );
113
+ }
114
+
115
+ if (args.flags.json) {
116
+ return {
117
+ success: true,
118
+ message: JSON.stringify(results, null, 2),
119
+ data: {
120
+ total: results.length,
121
+ by_severity: this.groupBySeverity(results),
122
+ orphans: results,
123
+ },
124
+ };
125
+ }
126
+
127
+ let output = '\n🔎 Code Analysis: Orphaned Entities\n';
128
+ output += '='.repeat(60) + '\n';
129
+
130
+ if (results.length === 0) {
131
+ output += '✨ No orphaned entities detected!\n';
132
+ } else {
133
+ const bySeverity = this.groupBySeverity(results);
134
+
135
+ for (const sev of ['critical', 'high', 'medium', 'low']) {
136
+ if (bySeverity[sev] && bySeverity[sev].length > 0) {
137
+ output += `\n${this.getSeverityIcon(sev as any)} ${sev.toUpperCase()} (${bySeverity[sev].length})\n`;
138
+ output += '-'.repeat(40) + '\n';
139
+
140
+ for (const orphan of bySeverity[sev]) {
141
+ output += this.formatOrphan(orphan);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ output += '\n' + '='.repeat(60) + '\n';
148
+ output += `Total orphaned entities: ${results.length}\n`;
149
+ output += 'Recommendation: Remove or refactor unused code regularly.\n';
150
+
151
+ return {
152
+ success: true,
153
+ message: output,
154
+ data: {
155
+ total: results.length,
156
+ by_severity: this.groupBySeverity(results),
157
+ orphans: results,
158
+ },
159
+ };
160
+ } catch (error) {
161
+ return {
162
+ success: false,
163
+ message: 'Error analyzing orphans',
164
+ error: error instanceof Error ? error.message : String(error),
165
+ };
166
+ }
167
+ }
168
+
169
+ private groupBySeverity(orphans: OrphanEntity[]): Record<string, OrphanEntity[]> {
170
+ return {
171
+ critical: orphans.filter((o) => o.severity === 'critical'),
172
+ high: orphans.filter((o) => o.severity === 'high'),
173
+ medium: orphans.filter((o) => o.severity === 'medium'),
174
+ low: orphans.filter((o) => o.severity === 'low'),
175
+ };
176
+ }
177
+
178
+ private getSeverityIcon(
179
+ severity: 'critical' | 'high' | 'medium' | 'low'
180
+ ): string {
181
+ const icons = {
182
+ critical: '🔴',
183
+ high: '🟠',
184
+ medium: '🟡',
185
+ low: '🟢',
186
+ };
187
+ return icons[severity];
188
+ }
189
+
190
+ private getTypeIcon(type: string): string {
191
+ const icons: Record<string, string> = {
192
+ function: '📌',
193
+ class: '🏛️',
194
+ variable: '📦',
195
+ interface: '📋',
196
+ enum: '📊',
197
+ default: '❓',
198
+ };
199
+ return icons[type.toLowerCase()] || icons.default;
200
+ }
201
+
202
+ private formatOrphan(orphan: OrphanEntity): string {
203
+ let output = ` ${this.getTypeIcon(orphan.type)} ${orphan.name}\n`;
204
+ output += ` File: ${orphan.file}:${orphan.line}\n`;
205
+ output += ` ${orphan.description}\n`;
206
+ return output;
207
+ }
208
+ }
209
+
210
+ export const orphansCommand = new OrphansCommand();