@assistkick/create 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/dist/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* migrate_to_turso — One-time migration tool that reads all existing local .md files
|
|
5
|
+
* (nodes/, sessions/) and graph.json/kanban.json, then inserts everything into the
|
|
6
|
+
* Turso database.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node tools/migrate_to_turso.js
|
|
10
|
+
* node tools/migrate_to_turso.js --dry-run # Report what would be migrated without writing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { program } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import matter from 'gray-matter';
|
|
16
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { getDb, getProjectRoot, closeDb } from '../lib/db.js';
|
|
19
|
+
import { nodes, edges, kanban, sessions, reviewMeta } from '../db/schema.js';
|
|
20
|
+
import { scoreNode } from '../lib/completeness.js';
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.option('--dry-run', 'Report what would be migrated without writing to database')
|
|
24
|
+
.parse();
|
|
25
|
+
|
|
26
|
+
const opts = program.opts();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a markdown body into sections map { sectionName: content }.
|
|
30
|
+
*/
|
|
31
|
+
const parseSections = (body) => {
|
|
32
|
+
const sectionMap = {};
|
|
33
|
+
const lines = body.split('\n');
|
|
34
|
+
let currentSection = null;
|
|
35
|
+
let currentLines = [];
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const match = line.match(/^## (.+)$/);
|
|
39
|
+
if (match) {
|
|
40
|
+
if (currentSection !== null) {
|
|
41
|
+
sectionMap[currentSection] = currentLines.join('\n').trim();
|
|
42
|
+
}
|
|
43
|
+
currentSection = match[1].trim();
|
|
44
|
+
currentLines = [];
|
|
45
|
+
} else if (currentSection !== null) {
|
|
46
|
+
currentLines.push(line);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (currentSection !== null) {
|
|
50
|
+
sectionMap[currentSection] = currentLines.join('\n').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return sectionMap;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract text under a ### subsection header from raw markdown content.
|
|
58
|
+
* Session files use h3 subsections (### Summary, ### Notes, etc.) within h2 sections.
|
|
59
|
+
*/
|
|
60
|
+
const extractSubsection = (content, name) => {
|
|
61
|
+
const regex = new RegExp(`^### ${name}\\s*$`, 'm');
|
|
62
|
+
const match = content.match(regex);
|
|
63
|
+
if (!match) return '';
|
|
64
|
+
|
|
65
|
+
const start = match.index + match[0].length;
|
|
66
|
+
const rest = content.slice(start);
|
|
67
|
+
// Find the next heading (## or ###)
|
|
68
|
+
const nextHeading = rest.search(/^#{2,3} /m);
|
|
69
|
+
const block = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
|
70
|
+
return block.trim();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Count open questions (unchecked checkboxes) in a sections map.
|
|
75
|
+
*/
|
|
76
|
+
const countOpenQuestions = (sectionMap) => {
|
|
77
|
+
const oq = sectionMap['Open Questions'] || '';
|
|
78
|
+
return oq.split('\n').filter(l => l.trim().startsWith('- [ ]')).length;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Migrate all node .md files from nodes/ directory.
|
|
83
|
+
*/
|
|
84
|
+
const migrateNodes = async (db, projectRoot, dryRun) => {
|
|
85
|
+
const nodesDir = join(projectRoot, 'nodes');
|
|
86
|
+
if (!existsSync(nodesDir)) {
|
|
87
|
+
console.log(chalk.yellow(' No nodes/ directory found. Skipping nodes migration.'));
|
|
88
|
+
return { migrated: 0, errors: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const files = readdirSync(nodesDir).filter(f => f.endsWith('.md')).sort();
|
|
92
|
+
const errors = [];
|
|
93
|
+
let migrated = 0;
|
|
94
|
+
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
const filepath = join(nodesDir, file);
|
|
97
|
+
try {
|
|
98
|
+
const raw = readFileSync(filepath, 'utf8');
|
|
99
|
+
const { data: fm, content } = matter(raw);
|
|
100
|
+
|
|
101
|
+
const sectionMap = parseSections(content);
|
|
102
|
+
const completeness = scoreNode(fm.type, fm, sectionMap);
|
|
103
|
+
const openQuestionsCount = countOpenQuestions(sectionMap);
|
|
104
|
+
|
|
105
|
+
const body = content.trim();
|
|
106
|
+
|
|
107
|
+
if (!dryRun) {
|
|
108
|
+
await db.insert(nodes).values({
|
|
109
|
+
id: fm.id,
|
|
110
|
+
type: fm.type,
|
|
111
|
+
name: fm.name,
|
|
112
|
+
status: fm.status || 'draft',
|
|
113
|
+
priority: fm.priority || 'medium',
|
|
114
|
+
completeness,
|
|
115
|
+
openQuestionsCount,
|
|
116
|
+
kind: fm.kind || null,
|
|
117
|
+
createdAt: fm.created_at || new Date().toISOString(),
|
|
118
|
+
updatedAt: fm.updated_at || new Date().toISOString(),
|
|
119
|
+
body,
|
|
120
|
+
}).onConflictDoUpdate({
|
|
121
|
+
target: nodes.id,
|
|
122
|
+
set: {
|
|
123
|
+
type: fm.type,
|
|
124
|
+
name: fm.name,
|
|
125
|
+
status: fm.status || 'draft',
|
|
126
|
+
priority: fm.priority || 'medium',
|
|
127
|
+
completeness,
|
|
128
|
+
openQuestionsCount,
|
|
129
|
+
kind: fm.kind || null,
|
|
130
|
+
updatedAt: fm.updated_at || new Date().toISOString(),
|
|
131
|
+
body,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
migrated++;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
errors.push({ file, error: err.message });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { migrated, errors };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Migrate edges from graph.json.
|
|
147
|
+
*/
|
|
148
|
+
const migrateEdges = async (db, projectRoot, dryRun) => {
|
|
149
|
+
const graphPath = join(projectRoot, 'graph.json');
|
|
150
|
+
if (!existsSync(graphPath)) {
|
|
151
|
+
console.log(chalk.yellow(' No graph.json found. Skipping edges migration.'));
|
|
152
|
+
return { migrated: 0, errors: [] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const graph = JSON.parse(readFileSync(graphPath, 'utf8'));
|
|
156
|
+
const edgeList = graph.edges || [];
|
|
157
|
+
const errors = [];
|
|
158
|
+
let migrated = 0;
|
|
159
|
+
|
|
160
|
+
for (const edge of edgeList) {
|
|
161
|
+
try {
|
|
162
|
+
if (!dryRun) {
|
|
163
|
+
await db.insert(edges).values({
|
|
164
|
+
fromId: edge.from,
|
|
165
|
+
relation: edge.relation,
|
|
166
|
+
toId: edge.to,
|
|
167
|
+
}).onConflictDoNothing();
|
|
168
|
+
}
|
|
169
|
+
migrated++;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
errors.push({ edge: `${edge.from} -${edge.relation}-> ${edge.to}`, error: err.message });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { migrated, errors };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Migrate kanban state from kanban.json.
|
|
180
|
+
*/
|
|
181
|
+
const migrateKanban = async (db, projectRoot, dryRun) => {
|
|
182
|
+
const kanbanPath = join(projectRoot, 'kanban.json');
|
|
183
|
+
if (!existsSync(kanbanPath)) {
|
|
184
|
+
console.log(chalk.yellow(' No kanban.json found. Skipping kanban migration.'));
|
|
185
|
+
return { migrated: 0, errors: [] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const kanbanData = JSON.parse(readFileSync(kanbanPath, 'utf8'));
|
|
189
|
+
const errors = [];
|
|
190
|
+
let migrated = 0;
|
|
191
|
+
let position = 0;
|
|
192
|
+
|
|
193
|
+
for (const [featureId, entry] of Object.entries(kanbanData)) {
|
|
194
|
+
try {
|
|
195
|
+
if (!dryRun) {
|
|
196
|
+
await db.insert(kanban).values({
|
|
197
|
+
nodeId: featureId,
|
|
198
|
+
columnName: entry.column,
|
|
199
|
+
position: position++,
|
|
200
|
+
}).onConflictDoUpdate({
|
|
201
|
+
target: kanban.nodeId,
|
|
202
|
+
set: {
|
|
203
|
+
columnName: entry.column,
|
|
204
|
+
position: position - 1,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Store extra metadata (rejection_count, notes, moved_at) in review_meta
|
|
209
|
+
const metaKey = `kanban:${featureId}`;
|
|
210
|
+
const metaValue = JSON.stringify({
|
|
211
|
+
rejection_count: entry.rejection_count || 0,
|
|
212
|
+
notes: entry.notes || [],
|
|
213
|
+
...(entry.dev_blocked ? { dev_blocked: true } : {}),
|
|
214
|
+
...(entry.moved_at ? { moved_at: entry.moved_at } : {}),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await db.insert(reviewMeta).values({
|
|
218
|
+
key: metaKey,
|
|
219
|
+
value: metaValue,
|
|
220
|
+
}).onConflictDoUpdate({
|
|
221
|
+
target: reviewMeta.key,
|
|
222
|
+
set: { value: metaValue },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
migrated++;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
errors.push({ featureId, error: err.message });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { migrated, errors };
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Migrate session .md files from sessions/ directory.
|
|
237
|
+
*/
|
|
238
|
+
const migrateSessions = async (db, projectRoot, dryRun) => {
|
|
239
|
+
const sessionsDir = join(projectRoot, 'sessions');
|
|
240
|
+
if (!existsSync(sessionsDir)) {
|
|
241
|
+
console.log(chalk.yellow(' No sessions/ directory found. Skipping sessions migration.'));
|
|
242
|
+
return { migrated: 0, errors: [] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const files = readdirSync(sessionsDir).filter(f => f.endsWith('.md')).sort();
|
|
246
|
+
const errors = [];
|
|
247
|
+
let migrated = 0;
|
|
248
|
+
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
const filepath = join(sessionsDir, file);
|
|
251
|
+
try {
|
|
252
|
+
const raw = readFileSync(filepath, 'utf8');
|
|
253
|
+
const { data: fm, content } = matter(raw);
|
|
254
|
+
|
|
255
|
+
const sessionNumber = fm.session;
|
|
256
|
+
if (sessionNumber == null) {
|
|
257
|
+
errors.push({ file, error: 'Missing session number in frontmatter' });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const nodesTouched = Array.isArray(fm.nodes_touched)
|
|
262
|
+
? fm.nodes_touched.join(',')
|
|
263
|
+
: (fm.nodes_touched || '');
|
|
264
|
+
|
|
265
|
+
// Extract summary — sessions use ### Summary (h3 subsections)
|
|
266
|
+
const summary = extractSubsection(content, 'Summary');
|
|
267
|
+
|
|
268
|
+
if (!dryRun) {
|
|
269
|
+
await db.insert(sessions).values({
|
|
270
|
+
sessionNumber,
|
|
271
|
+
startedAt: fm.started_at || new Date().toISOString(),
|
|
272
|
+
endedAt: fm.ended_at || null,
|
|
273
|
+
summary,
|
|
274
|
+
nodesTouched,
|
|
275
|
+
questionsResolved: fm.questions_resolved || 0,
|
|
276
|
+
body: content.trim(),
|
|
277
|
+
}).onConflictDoUpdate({
|
|
278
|
+
target: sessions.sessionNumber,
|
|
279
|
+
set: {
|
|
280
|
+
startedAt: fm.started_at || new Date().toISOString(),
|
|
281
|
+
endedAt: fm.ended_at || null,
|
|
282
|
+
summary,
|
|
283
|
+
nodesTouched,
|
|
284
|
+
questionsResolved: fm.questions_resolved || 0,
|
|
285
|
+
body: content.trim(),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
migrated++;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
errors.push({ file, error: err.message });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { migrated, errors };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Main migration entry point.
|
|
301
|
+
*/
|
|
302
|
+
const main = async () => {
|
|
303
|
+
const projectRoot = getProjectRoot();
|
|
304
|
+
const dryRun = opts.dryRun || false;
|
|
305
|
+
|
|
306
|
+
if (dryRun) {
|
|
307
|
+
console.log(chalk.cyan('=== DRY RUN — no database writes will occur ===\n'));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(chalk.bold('Migrating local files to Turso database...\n'));
|
|
311
|
+
|
|
312
|
+
const db = getDb();
|
|
313
|
+
|
|
314
|
+
// Step 1: Migrate nodes
|
|
315
|
+
console.log(chalk.blue('1. Migrating nodes...'));
|
|
316
|
+
const nodesResult = await migrateNodes(db, projectRoot, dryRun);
|
|
317
|
+
console.log(` ${chalk.green(nodesResult.migrated)} nodes migrated`);
|
|
318
|
+
if (nodesResult.errors.length > 0) {
|
|
319
|
+
for (const e of nodesResult.errors) {
|
|
320
|
+
console.log(` ${chalk.red('ERROR')} ${e.file}: ${e.error}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Step 2: Migrate edges
|
|
325
|
+
console.log(chalk.blue('\n2. Migrating edges...'));
|
|
326
|
+
const edgesResult = await migrateEdges(db, projectRoot, dryRun);
|
|
327
|
+
console.log(` ${chalk.green(edgesResult.migrated)} edges migrated`);
|
|
328
|
+
if (edgesResult.errors.length > 0) {
|
|
329
|
+
for (const e of edgesResult.errors) {
|
|
330
|
+
console.log(` ${chalk.red('ERROR')} ${e.edge}: ${e.error}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Step 3: Migrate kanban
|
|
335
|
+
console.log(chalk.blue('\n3. Migrating kanban state...'));
|
|
336
|
+
const kanbanResult = await migrateKanban(db, projectRoot, dryRun);
|
|
337
|
+
console.log(` ${chalk.green(kanbanResult.migrated)} kanban entries migrated`);
|
|
338
|
+
if (kanbanResult.errors.length > 0) {
|
|
339
|
+
for (const e of kanbanResult.errors) {
|
|
340
|
+
console.log(` ${chalk.red('ERROR')} ${e.featureId}: ${e.error}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Step 4: Migrate sessions
|
|
345
|
+
console.log(chalk.blue('\n4. Migrating sessions...'));
|
|
346
|
+
const sessionsResult = await migrateSessions(db, projectRoot, dryRun);
|
|
347
|
+
console.log(` ${chalk.green(sessionsResult.migrated)} sessions migrated`);
|
|
348
|
+
if (sessionsResult.errors.length > 0) {
|
|
349
|
+
for (const e of sessionsResult.errors) {
|
|
350
|
+
console.log(` ${chalk.red('ERROR')} ${e.file}: ${e.error}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Summary
|
|
355
|
+
const totalErrors = [
|
|
356
|
+
...nodesResult.errors,
|
|
357
|
+
...edgesResult.errors,
|
|
358
|
+
...kanbanResult.errors,
|
|
359
|
+
...sessionsResult.errors,
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
console.log(chalk.bold('\n=== Migration Summary ==='));
|
|
363
|
+
console.log(` Nodes: ${nodesResult.migrated}`);
|
|
364
|
+
console.log(` Edges: ${edgesResult.migrated}`);
|
|
365
|
+
console.log(` Kanban: ${kanbanResult.migrated}`);
|
|
366
|
+
console.log(` Sessions: ${sessionsResult.migrated}`);
|
|
367
|
+
|
|
368
|
+
if (totalErrors.length > 0) {
|
|
369
|
+
console.log(chalk.red(`\n Errors: ${totalErrors.length}`));
|
|
370
|
+
} else {
|
|
371
|
+
console.log(chalk.green('\n No errors.'));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (dryRun) {
|
|
375
|
+
console.log(chalk.cyan('\n=== DRY RUN complete — no changes were made ==='));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
closeDb();
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
main().catch((err) => {
|
|
382
|
+
console.error(chalk.red(`Migration failed: ${err.message}`));
|
|
383
|
+
closeDb();
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* move_card — Moves a feature card to a new column on the kanban board.
|
|
5
|
+
* Validates that the transition is allowed.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node tools/move_card.js feat_010 in_progress # todo → in_progress
|
|
9
|
+
* node tools/move_card.js feat_010 in_review # in_progress → in_review
|
|
10
|
+
* node tools/move_card.js feat_010 qa # in_review → qa (approve)
|
|
11
|
+
* node tools/move_card.js feat_010 todo --note "Missing tests" # in_review → todo (reject)
|
|
12
|
+
*
|
|
13
|
+
* Allowed transitions (AI via CLI):
|
|
14
|
+
* todo → in_progress (developer picks up feature)
|
|
15
|
+
* in_progress → in_review (developer submits for review)
|
|
16
|
+
* in_review → qa (reviewer approves)
|
|
17
|
+
* in_review → todo (reviewer rejects — increments rejection_count)
|
|
18
|
+
*
|
|
19
|
+
* User-only transitions:
|
|
20
|
+
* qa → done
|
|
21
|
+
* qa → todo (user QA rejection — increments rejection_count)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { program } from 'commander';
|
|
25
|
+
import chalk from 'chalk';
|
|
26
|
+
import { getKanbanEntry, saveKanbanEntry, kanbanExists } from '../lib/kanban.js';
|
|
27
|
+
|
|
28
|
+
const VALID_COLUMNS = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
|
|
29
|
+
|
|
30
|
+
// AI-allowed transitions (via CLI)
|
|
31
|
+
const ALLOWED_TRANSITIONS = {
|
|
32
|
+
'todo': ['in_progress'],
|
|
33
|
+
'in_progress': ['in_review'],
|
|
34
|
+
'in_review': ['qa', 'todo'],
|
|
35
|
+
'qa': ['todo'],
|
|
36
|
+
'done': [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.argument('<feature_id>', 'Feature ID (e.g., feat_010)')
|
|
41
|
+
.argument('<target_column>', `Target column (${VALID_COLUMNS.join(', ')})`)
|
|
42
|
+
.requiredOption('--project-id <id>', 'Project ID')
|
|
43
|
+
.option('--note <text>', 'Add a note (required for rejections)')
|
|
44
|
+
.option('--unblock', 'Remove dev_blocked flag from the feature')
|
|
45
|
+
.parse();
|
|
46
|
+
|
|
47
|
+
const [featureId, targetColumn] = program.args;
|
|
48
|
+
|
|
49
|
+
(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const opts = program.opts();
|
|
52
|
+
if (!(await kanbanExists(opts.projectId))) {
|
|
53
|
+
throw new Error('No kanban entries found. No features are being tracked yet.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!VALID_COLUMNS.includes(targetColumn)) {
|
|
57
|
+
throw new Error(`Invalid column "${targetColumn}". Valid columns: ${VALID_COLUMNS.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entry = await getKanbanEntry(featureId);
|
|
61
|
+
if (!entry) {
|
|
62
|
+
throw new Error(`Feature "${featureId}" not found on the kanban board.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentColumn = entry.column;
|
|
66
|
+
|
|
67
|
+
// Handle --unblock flag
|
|
68
|
+
if (opts.unblock) {
|
|
69
|
+
if (!entry.dev_blocked) {
|
|
70
|
+
console.log(chalk.yellow(`Feature "${featureId}" is not blocked.`));
|
|
71
|
+
} else {
|
|
72
|
+
delete entry.dev_blocked;
|
|
73
|
+
await saveKanbanEntry(featureId, entry);
|
|
74
|
+
console.log(chalk.green(`✓ Unblocked ${featureId}`));
|
|
75
|
+
}
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check dev_blocked
|
|
80
|
+
if (entry.dev_blocked && targetColumn === 'in_progress') {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Feature "${featureId}" is dev_blocked (failed 3 automated dev cycles). ` +
|
|
83
|
+
`Use --unblock flag to remove the block first.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (currentColumn === targetColumn) {
|
|
88
|
+
throw new Error(`Feature "${featureId}" is already in "${targetColumn}".`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const allowed = ALLOWED_TRANSITIONS[currentColumn] || [];
|
|
92
|
+
if (!allowed.includes(targetColumn)) {
|
|
93
|
+
const allowedStr = allowed.length > 0 ? allowed.join(', ') : 'none (terminal state)';
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Invalid transition: "${currentColumn}" → "${targetColumn}" is not allowed.\n` +
|
|
96
|
+
` Allowed transitions from "${currentColumn}": ${allowedStr}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle rejections (in_review → todo or qa → todo)
|
|
101
|
+
const isRejection = targetColumn === 'todo' && (currentColumn === 'in_review' || currentColumn === 'qa');
|
|
102
|
+
if (isRejection) {
|
|
103
|
+
if (!opts.note) {
|
|
104
|
+
throw new Error('Rejections require a --note explaining what needs to be fixed.');
|
|
105
|
+
}
|
|
106
|
+
entry.rejection_count = (entry.rejection_count || 0) + 1;
|
|
107
|
+
if (!entry.notes) entry.notes = [];
|
|
108
|
+
entry.notes.push({
|
|
109
|
+
from: currentColumn,
|
|
110
|
+
text: opts.note,
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
});
|
|
113
|
+
const count = entry.rejection_count;
|
|
114
|
+
const source = currentColumn === 'in_review' ? 'Review' : 'QA';
|
|
115
|
+
console.log(chalk.yellow(`⚠ ${source} rejection #${count} for ${featureId}`));
|
|
116
|
+
console.log(chalk.gray(` Note: ${opts.note}`));
|
|
117
|
+
if (count >= 3) {
|
|
118
|
+
console.log(chalk.red.bold(`⚠ WARNING: ${featureId} has been rejected ${count} times — needs attention`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Perform the transition
|
|
123
|
+
const previousColumn = entry.column;
|
|
124
|
+
entry.column = targetColumn;
|
|
125
|
+
entry.moved_at = new Date().toISOString();
|
|
126
|
+
|
|
127
|
+
await saveKanbanEntry(featureId, entry);
|
|
128
|
+
|
|
129
|
+
const arrow = `${previousColumn} → ${targetColumn}`;
|
|
130
|
+
console.log(chalk.green(`✓ Moved ${featureId}: ${arrow}`));
|
|
131
|
+
|
|
132
|
+
console.log('\n' + JSON.stringify({
|
|
133
|
+
feature_id: featureId,
|
|
134
|
+
previous_column: previousColumn,
|
|
135
|
+
new_column: targetColumn,
|
|
136
|
+
rejection_count: entry.rejection_count || 0,
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* rebuild_index — Regenerates the nodes table metadata by re-deriving completeness
|
|
5
|
+
* and status from each node's body content. With Turso as source of truth,
|
|
6
|
+
* this recalculates derived fields rather than rebuilding from .md files.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { program } from 'commander';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { readGraph, writeGraph } from '../lib/graph.js';
|
|
12
|
+
import { readNode, deriveMetadata } from '../lib/markdown.js';
|
|
13
|
+
import { getDb } from '../lib/db.js';
|
|
14
|
+
import { nodes } from '../db/schema.js';
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.requiredOption('--project-id <id>', 'Project ID')
|
|
18
|
+
.option('--dry-run', 'Show what would change without writing')
|
|
19
|
+
.parse();
|
|
20
|
+
|
|
21
|
+
const opts = program.opts();
|
|
22
|
+
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const db = getDb();
|
|
26
|
+
const allNodes = await db.select().from(nodes);
|
|
27
|
+
|
|
28
|
+
if (allNodes.length === 0) {
|
|
29
|
+
console.log(chalk.yellow('No nodes in the database. Nothing to rebuild.'));
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const oldGraph = await readGraph();
|
|
34
|
+
|
|
35
|
+
const newGraph = {
|
|
36
|
+
version: '1.0',
|
|
37
|
+
project: oldGraph.project,
|
|
38
|
+
nodes: [],
|
|
39
|
+
edges: oldGraph.edges,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const row of allNodes) {
|
|
43
|
+
const { frontmatter, sections } = await readNode(row.id);
|
|
44
|
+
const meta = deriveMetadata(frontmatter, sections, row.type);
|
|
45
|
+
|
|
46
|
+
const nodeEntry = {
|
|
47
|
+
id: row.id,
|
|
48
|
+
type: row.type,
|
|
49
|
+
name: row.name,
|
|
50
|
+
status: meta.status,
|
|
51
|
+
completeness: meta.completeness,
|
|
52
|
+
open_questions_count: meta.open_questions_count,
|
|
53
|
+
priority: row.priority,
|
|
54
|
+
created_at: row.createdAt,
|
|
55
|
+
updated_at: row.updatedAt,
|
|
56
|
+
...(row.kind ? { kind: row.kind } : {}),
|
|
57
|
+
body: row.body,
|
|
58
|
+
};
|
|
59
|
+
newGraph.nodes.push(nodeEntry);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (opts.dryRun) {
|
|
63
|
+
console.log(chalk.cyan('Dry run — would produce:'));
|
|
64
|
+
console.log(` Nodes: ${newGraph.nodes.length} (was ${oldGraph.nodes.length})`);
|
|
65
|
+
console.log(` Edges: ${newGraph.edges.length}`);
|
|
66
|
+
for (const node of newGraph.nodes) {
|
|
67
|
+
console.log(` ${node.id}: ${node.name} (${node.type}) — ${Math.round(node.completeness * 100)}%`);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
await writeGraph(newGraph, opts.projectId);
|
|
71
|
+
console.log(chalk.green(`✓ Rebuilt index: ${newGraph.nodes.length} nodes, ${newGraph.edges.length} edges`));
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* remove_edge — Removes a relationship between two nodes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { readGraph, removeEdge as graphRemoveEdge } from '../lib/graph.js';
|
|
10
|
+
import { readNode, writeNode } from '../lib/markdown.js';
|
|
11
|
+
import { assertNodeExists, assertValidRelation } from '../lib/validator.js';
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.argument('<from>', 'Source node ID')
|
|
15
|
+
.argument('<relation>', 'Relation type')
|
|
16
|
+
.argument('<to>', 'Target node ID')
|
|
17
|
+
.requiredOption('--project-id <id>', 'Project ID')
|
|
18
|
+
.parse();
|
|
19
|
+
|
|
20
|
+
const [from, relation, to] = program.args;
|
|
21
|
+
|
|
22
|
+
(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await assertNodeExists(from);
|
|
25
|
+
await assertNodeExists(to);
|
|
26
|
+
assertValidRelation(relation);
|
|
27
|
+
|
|
28
|
+
// Get node metadata before removing edge
|
|
29
|
+
const graph = await readGraph();
|
|
30
|
+
const fromNode = graph.nodes.find(n => n.id === from);
|
|
31
|
+
const toNode = graph.nodes.find(n => n.id === to);
|
|
32
|
+
|
|
33
|
+
// Remove edge from DB
|
|
34
|
+
await graphRemoveEdge(from, relation, to);
|
|
35
|
+
|
|
36
|
+
// Remove relation lines from body content
|
|
37
|
+
const fromData = await readNode(from);
|
|
38
|
+
const fromRelations = (fromData.sections['Relations'] || '').split('\n');
|
|
39
|
+
fromData.sections['Relations'] = fromRelations
|
|
40
|
+
.filter(l => !l.includes(`${relation} → ${to}`))
|
|
41
|
+
.join('\n')
|
|
42
|
+
.trim();
|
|
43
|
+
await writeNode(from, fromData.frontmatter, fromData.sections);
|
|
44
|
+
|
|
45
|
+
const toData = await readNode(to);
|
|
46
|
+
const toRelations = (toData.sections['Relations'] || '').split('\n');
|
|
47
|
+
toData.sections['Relations'] = toRelations
|
|
48
|
+
.filter(l => !l.includes(`${relation} ← ${from}`))
|
|
49
|
+
.join('\n')
|
|
50
|
+
.trim();
|
|
51
|
+
await writeNode(to, toData.frontmatter, toData.sections);
|
|
52
|
+
|
|
53
|
+
console.log(chalk.green(`✓ Removed edge: ${from} --${relation}--> ${to}`));
|
|
54
|
+
console.log(JSON.stringify({ from, relation, to }));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
})();
|