@ainyc/canonry 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.
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs'
2
+ import { parse } from 'yaml'
3
+ import { loadConfig } from '../config.js'
4
+ import { ApiClient } from '../client.js'
5
+
6
+ export async function applyConfig(filePath: string): Promise<void> {
7
+ if (!fs.existsSync(filePath)) {
8
+ throw new Error(`File not found: ${filePath}`)
9
+ }
10
+
11
+ const content = fs.readFileSync(filePath, 'utf-8')
12
+ const config = parse(content) as object
13
+
14
+ const clientConfig = loadConfig()
15
+ const client = new ApiClient(clientConfig.apiUrl, clientConfig.apiKey)
16
+
17
+ const result = await client.apply(config) as {
18
+ id: string
19
+ name: string
20
+ displayName: string
21
+ configRevision: number
22
+ }
23
+
24
+ console.log(`Applied config for "${result.name}" (revision ${result.configRevision})`)
25
+ }
@@ -0,0 +1,36 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ function getClient(): ApiClient {
5
+ const config = loadConfig()
6
+ return new ApiClient(config.apiUrl, config.apiKey)
7
+ }
8
+
9
+ export async function addCompetitors(project: string, domains: string[]): Promise<void> {
10
+ // First get existing competitors, then put the combined list
11
+ const client = getClient()
12
+ const existing = await client.listCompetitors(project) as Array<{ domain: string }>
13
+ const existingDomains = existing.map(c => c.domain)
14
+ const allDomains = [...new Set([...existingDomains, ...domains])]
15
+ await client.putCompetitors(project, allDomains)
16
+ console.log(`Added ${domains.length} competitor(s) to "${project}".`)
17
+ }
18
+
19
+ export async function listCompetitors(project: string): Promise<void> {
20
+ const client = getClient()
21
+ const comps = await client.listCompetitors(project) as Array<{
22
+ id: string
23
+ domain: string
24
+ createdAt: string
25
+ }>
26
+
27
+ if (comps.length === 0) {
28
+ console.log(`No competitors found for "${project}".`)
29
+ return
30
+ }
31
+
32
+ console.log(`Competitors for "${project}" (${comps.length}):\n`)
33
+ for (const c of comps) {
34
+ console.log(` ${c.domain}`)
35
+ }
36
+ }
@@ -0,0 +1,41 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ type TimelineEntry = {
5
+ keyword: string
6
+ runs: {
7
+ runId: string
8
+ createdAt: string
9
+ citationState: string
10
+ transition: string
11
+ }[]
12
+ }
13
+
14
+ function getClient(): ApiClient {
15
+ const config = loadConfig()
16
+ return new ApiClient(config.apiUrl, config.apiKey)
17
+ }
18
+
19
+ export async function showEvidence(project: string): Promise<void> {
20
+ const client = getClient()
21
+ const timeline = await client.getTimeline(project) as TimelineEntry[]
22
+
23
+ if (timeline.length === 0) {
24
+ console.log('No keyword evidence yet. Trigger a run first with "canonry run".')
25
+ return
26
+ }
27
+
28
+ console.log(`Evidence: ${project}\n`)
29
+
30
+ for (const entry of timeline) {
31
+ const latest = entry.runs[entry.runs.length - 1]
32
+ if (!latest) continue
33
+ const state = latest.citationState === 'cited' ? '✓ cited' : '✗ not-cited'
34
+ const transition = latest.transition !== latest.citationState ? ` (${latest.transition})` : ''
35
+ console.log(` ${state}${transition} ${entry.keyword}`)
36
+ }
37
+
38
+ console.log(`\n Keywords: ${timeline.length}`)
39
+ const cited = timeline.filter(e => e.runs[e.runs.length - 1]?.citationState === 'cited').length
40
+ console.log(` Cited: ${cited} / ${timeline.length}`)
41
+ }
@@ -0,0 +1,40 @@
1
+ import { stringify } from 'yaml'
2
+ import { loadConfig } from '../config.js'
3
+ import { ApiClient } from '../client.js'
4
+
5
+ export async function exportProject(
6
+ project: string,
7
+ opts: { includeResults?: boolean },
8
+ ): Promise<void> {
9
+ const config = loadConfig()
10
+ const client = new ApiClient(config.apiUrl, config.apiKey)
11
+
12
+ const data = await client.getExport(project) as {
13
+ apiVersion: string
14
+ kind: string
15
+ metadata: { name: string; labels: Record<string, string> }
16
+ spec: {
17
+ displayName: string
18
+ canonicalDomain: string
19
+ country: string
20
+ language: string
21
+ keywords: string[]
22
+ competitors: string[]
23
+ }
24
+ }
25
+
26
+ if (opts.includeResults) {
27
+ // Fetch latest run data and include as annotation
28
+ try {
29
+ const runs = await client.listRuns(project) as Array<{ id: string }>
30
+ if (runs.length > 0) {
31
+ const latestRun = await client.getRun(runs[runs.length - 1]!.id)
32
+ ;(data as Record<string, unknown>).results = latestRun
33
+ }
34
+ } catch {
35
+ // Results not available, skip
36
+ }
37
+ }
38
+
39
+ console.log(stringify(data))
40
+ }
@@ -0,0 +1,41 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ function getClient(): ApiClient {
5
+ const config = loadConfig()
6
+ return new ApiClient(config.apiUrl, config.apiKey)
7
+ }
8
+
9
+ export async function showHistory(project: string): Promise<void> {
10
+ const client = getClient()
11
+
12
+ try {
13
+ const entries = await client.getHistory(project) as Array<{
14
+ id: string
15
+ actor: string
16
+ action: string
17
+ entityType: string
18
+ entityId: string | null
19
+ createdAt: string
20
+ }>
21
+
22
+ if (entries.length === 0) {
23
+ console.log(`No audit history for "${project}".`)
24
+ return
25
+ }
26
+
27
+ console.log(`Audit history for "${project}" (${entries.length}):\n`)
28
+ console.log(' TIMESTAMP ACTION ENTITY TYPE ACTOR')
29
+ console.log(' ─────────────────────── ────────────────── ─────────── ─────')
30
+
31
+ for (const entry of entries) {
32
+ console.log(
33
+ ` ${entry.createdAt.padEnd(23)} ${entry.action.padEnd(18)} ${entry.entityType.padEnd(11)} ${entry.actor}`,
34
+ )
35
+ }
36
+ } catch (err: unknown) {
37
+ const message = err instanceof Error ? err.message : String(err)
38
+ console.error(`Failed to fetch history: ${message}`)
39
+ process.exit(1)
40
+ }
41
+ }
@@ -0,0 +1,122 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import readline from 'node:readline'
4
+ import { getConfigDir, getConfigPath, configExists, saveConfig } from '../config.js'
5
+ import type { CanonryConfig, ProviderConfigEntry } from '../config.js'
6
+ import { createClient, migrate } from '@ainyc/canonry-db'
7
+ import { apiKeys } from '@ainyc/canonry-db'
8
+ import path from 'node:path'
9
+
10
+ function prompt(question: string): Promise<string> {
11
+ const rl = readline.createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout,
14
+ })
15
+ return new Promise(resolve => {
16
+ rl.question(question, answer => {
17
+ rl.close()
18
+ resolve(answer.trim())
19
+ })
20
+ })
21
+ }
22
+
23
+ const DEFAULT_QUOTA = {
24
+ maxConcurrency: 2,
25
+ maxRequestsPerMinute: 10,
26
+ maxRequestsPerDay: 500,
27
+ }
28
+
29
+ export async function initCommand(): Promise<void> {
30
+ console.log('Initializing canonry...\n')
31
+
32
+ if (configExists()) {
33
+ console.log(`Config already exists at ${getConfigPath()}`)
34
+ console.log('To reinitialize, delete the config file first.')
35
+ return
36
+ }
37
+
38
+ // Create config directory
39
+ const configDir = getConfigDir()
40
+ if (!fs.existsSync(configDir)) {
41
+ fs.mkdirSync(configDir, { recursive: true })
42
+ }
43
+
44
+ // Prompt for provider API keys
45
+ const providers: CanonryConfig['providers'] = {}
46
+
47
+ console.log('Configure AI providers (at least one required):\n')
48
+
49
+ // Gemini
50
+ const geminiApiKey = await prompt('Gemini API key (press Enter to skip): ')
51
+ if (geminiApiKey) {
52
+ const geminiModel = await prompt(' Gemini model [gemini-2.5-flash]: ') || 'gemini-2.5-flash'
53
+ providers.gemini = { apiKey: geminiApiKey, model: geminiModel, quota: DEFAULT_QUOTA }
54
+ }
55
+
56
+ // OpenAI
57
+ const openaiApiKey = await prompt('OpenAI API key (press Enter to skip): ')
58
+ if (openaiApiKey) {
59
+ const openaiModel = await prompt(' OpenAI model [gpt-4o]: ') || 'gpt-4o'
60
+ providers.openai = { apiKey: openaiApiKey, model: openaiModel, quota: DEFAULT_QUOTA }
61
+ }
62
+
63
+ // Claude
64
+ const claudeApiKey = await prompt('Anthropic API key (press Enter to skip): ')
65
+ if (claudeApiKey) {
66
+ const claudeModel = await prompt(' Claude model [claude-sonnet-4-6]: ') || 'claude-sonnet-4-6'
67
+ providers.claude = { apiKey: claudeApiKey, model: claudeModel, quota: DEFAULT_QUOTA }
68
+ }
69
+
70
+ // Local LLM
71
+ console.log('\nLocal LLM (Ollama, LM Studio, llama.cpp, vLLM — any OpenAI-compatible API):')
72
+ const localBaseUrl = await prompt('Local LLM base URL (press Enter to skip, e.g. http://localhost:11434/v1): ')
73
+ if (localBaseUrl) {
74
+ const localModel = await prompt(' Model name [llama3]: ') || 'llama3'
75
+ const localApiKey = await prompt(' API key (press Enter if not needed): ') || undefined
76
+ providers.local = { baseUrl: localBaseUrl, apiKey: localApiKey, model: localModel, quota: DEFAULT_QUOTA }
77
+ }
78
+
79
+ // Validate at least one provider
80
+ const hasProvider = providers.gemini || providers.openai || providers.claude || providers.local
81
+ if (!hasProvider) {
82
+ console.error('\nAt least one provider is required.')
83
+ process.exit(1)
84
+ }
85
+
86
+ // Generate random API key for the local server
87
+ const rawApiKey = `cnry_${crypto.randomBytes(16).toString('hex')}`
88
+ const keyHash = crypto.createHash('sha256').update(rawApiKey).digest('hex')
89
+ const keyPrefix = rawApiKey.slice(0, 9)
90
+
91
+ // Database path
92
+ const databasePath = path.join(configDir, 'data.db')
93
+
94
+ // Create and migrate database
95
+ const db = createClient(databasePath)
96
+ migrate(db)
97
+
98
+ // Insert the API key
99
+ db.insert(apiKeys).values({
100
+ id: crypto.randomUUID(),
101
+ name: 'default',
102
+ keyHash,
103
+ keyPrefix,
104
+ scopes: '["*"]',
105
+ createdAt: new Date().toISOString(),
106
+ }).run()
107
+
108
+ // Save config
109
+ saveConfig({
110
+ apiUrl: 'http://localhost:4100',
111
+ database: databasePath,
112
+ apiKey: rawApiKey,
113
+ providers,
114
+ })
115
+
116
+ const providerNames = Object.keys(providers).join(', ')
117
+ console.log(`\nConfig saved to ${getConfigPath()}`)
118
+ console.log(`Database created at ${databasePath}`)
119
+ console.log(`API key: ${rawApiKey}`)
120
+ console.log(`Providers: ${providerNames}`)
121
+ console.log('\nRun "canonry serve" to start the server.')
122
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs'
2
+ import { loadConfig } from '../config.js'
3
+ import { ApiClient } from '../client.js'
4
+
5
+ function getClient(): ApiClient {
6
+ const config = loadConfig()
7
+ return new ApiClient(config.apiUrl, config.apiKey)
8
+ }
9
+
10
+ export async function addKeywords(project: string, keywords: string[]): Promise<void> {
11
+ const client = getClient()
12
+ await client.appendKeywords(project, keywords)
13
+ console.log(`Added ${keywords.length} keyword(s) to "${project}".`)
14
+ }
15
+
16
+ export async function listKeywords(project: string): Promise<void> {
17
+ const client = getClient()
18
+ const kws = await client.listKeywords(project) as Array<{
19
+ id: string
20
+ keyword: string
21
+ createdAt: string
22
+ }>
23
+
24
+ if (kws.length === 0) {
25
+ console.log(`No keywords found for "${project}".`)
26
+ return
27
+ }
28
+
29
+ console.log(`Keywords for "${project}" (${kws.length}):\n`)
30
+ for (const kw of kws) {
31
+ console.log(` ${kw.keyword}`)
32
+ }
33
+ }
34
+
35
+ export async function importKeywords(project: string, filePath: string): Promise<void> {
36
+ if (!fs.existsSync(filePath)) {
37
+ throw new Error(`File not found: ${filePath}`)
38
+ }
39
+
40
+ const content = fs.readFileSync(filePath, 'utf-8')
41
+ const keywords = content
42
+ .split('\n')
43
+ .map(line => line.trim())
44
+ .filter(line => line.length > 0 && !line.startsWith('#'))
45
+
46
+ if (keywords.length === 0) {
47
+ console.log('No keywords found in file.')
48
+ return
49
+ }
50
+
51
+ const client = getClient()
52
+ await client.appendKeywords(project, keywords)
53
+ console.log(`Imported ${keywords.length} keyword(s) to "${project}".`)
54
+ }
@@ -0,0 +1,70 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ function getClient(): ApiClient {
5
+ const config = loadConfig()
6
+ return new ApiClient(config.apiUrl, config.apiKey)
7
+ }
8
+
9
+ interface NotificationResponse {
10
+ id: string
11
+ projectId: string
12
+ channel: string
13
+ url: string
14
+ events: string[]
15
+ enabled: boolean
16
+ }
17
+
18
+ export async function addNotification(project: string, opts: {
19
+ webhook: string
20
+ events: string[]
21
+ }): Promise<void> {
22
+ const client = getClient()
23
+ const result = await client.createNotification(project, {
24
+ channel: 'webhook',
25
+ url: opts.webhook,
26
+ events: opts.events,
27
+ }) as NotificationResponse
28
+
29
+ console.log(`Notification created for "${project}":`)
30
+ printNotification(result)
31
+ }
32
+
33
+ export async function listNotifications(project: string): Promise<void> {
34
+ const client = getClient()
35
+ const results = await client.listNotifications(project) as NotificationResponse[]
36
+ if (results.length === 0) {
37
+ console.log(`No notifications configured for "${project}"`)
38
+ return
39
+ }
40
+
41
+ console.log(`Notifications for "${project}":\n`)
42
+ for (const n of results) {
43
+ printNotification(n)
44
+ console.log()
45
+ }
46
+ }
47
+
48
+ export async function removeNotification(project: string, id: string): Promise<void> {
49
+ const client = getClient()
50
+ await client.deleteNotification(project, id)
51
+ console.log(`Notification ${id} removed from "${project}"`)
52
+ }
53
+
54
+ export async function testNotification(project: string, id: string): Promise<void> {
55
+ const client = getClient()
56
+ const result = await client.testNotification(project, id) as { status: number; ok: boolean }
57
+ if (result.ok) {
58
+ console.log(`Test webhook delivered successfully (HTTP ${result.status})`)
59
+ } else {
60
+ console.error(`Test webhook failed: HTTP ${result.status}`)
61
+ }
62
+ }
63
+
64
+ function printNotification(n: NotificationResponse): void {
65
+ console.log(` ID: ${n.id}`)
66
+ console.log(` Channel: ${n.channel}`)
67
+ console.log(` URL: ${n.url}`)
68
+ console.log(` Events: ${n.events.join(', ')}`)
69
+ console.log(` Enabled: ${n.enabled ? 'yes' : 'no'}`)
70
+ }
@@ -0,0 +1,89 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ function getClient(): ApiClient {
5
+ const config = loadConfig()
6
+ return new ApiClient(config.apiUrl, config.apiKey)
7
+ }
8
+
9
+ export async function createProject(
10
+ name: string,
11
+ opts: { domain: string; country: string; language: string; displayName: string },
12
+ ): Promise<void> {
13
+ const client = getClient()
14
+ const result = await client.putProject(name, {
15
+ displayName: opts.displayName,
16
+ canonicalDomain: opts.domain,
17
+ country: opts.country,
18
+ language: opts.language,
19
+ }) as { id: string; name: string }
20
+ console.log(`Project created: ${result.name} (${result.id})`)
21
+ }
22
+
23
+ export async function listProjects(): Promise<void> {
24
+ const client = getClient()
25
+ const projects = await client.listProjects() as Array<{
26
+ name: string
27
+ canonicalDomain: string
28
+ country: string
29
+ language: string
30
+ }>
31
+
32
+ if (projects.length === 0) {
33
+ console.log('No projects found.')
34
+ return
35
+ }
36
+
37
+ console.log('Projects:\n')
38
+ const nameWidth = Math.max(4, ...projects.map(p => p.name.length))
39
+ const domainWidth = Math.max(6, ...projects.map(p => p.canonicalDomain.length))
40
+
41
+ console.log(
42
+ ` ${'NAME'.padEnd(nameWidth)} ${'DOMAIN'.padEnd(domainWidth)} COUNTRY LANGUAGE`,
43
+ )
44
+ console.log(` ${'─'.repeat(nameWidth)} ${'─'.repeat(domainWidth)} ─────── ────────`)
45
+
46
+ for (const p of projects) {
47
+ console.log(
48
+ ` ${p.name.padEnd(nameWidth)} ${p.canonicalDomain.padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`,
49
+ )
50
+ }
51
+ }
52
+
53
+ export async function showProject(name: string): Promise<void> {
54
+ const client = getClient()
55
+ const project = await client.getProject(name) as {
56
+ id: string
57
+ name: string
58
+ displayName: string
59
+ canonicalDomain: string
60
+ country: string
61
+ language: string
62
+ tags: string[]
63
+ labels: Record<string, string>
64
+ configSource: string
65
+ configRevision: number
66
+ createdAt: string
67
+ updatedAt: string
68
+ }
69
+
70
+ console.log(`Project: ${project.displayName}\n`)
71
+ console.log(` Name: ${project.name}`)
72
+ console.log(` ID: ${project.id}`)
73
+ console.log(` Domain: ${project.canonicalDomain}`)
74
+ console.log(` Country: ${project.country}`)
75
+ console.log(` Language: ${project.language}`)
76
+ console.log(` Config source: ${project.configSource}`)
77
+ console.log(` Config revision: ${project.configRevision}`)
78
+ console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(', ') : '(none)'}`)
79
+ const labelEntries = Object.entries(project.labels)
80
+ console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(', ') : '(none)'}`)
81
+ console.log(` Created: ${project.createdAt}`)
82
+ console.log(` Updated: ${project.updatedAt}`)
83
+ }
84
+
85
+ export async function deleteProject(name: string): Promise<void> {
86
+ const client = getClient()
87
+ await client.deleteProject(name)
88
+ console.log(`Project deleted: ${name}`)
89
+ }
@@ -0,0 +1,54 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ function getClient(): ApiClient {
5
+ const config = loadConfig()
6
+ return new ApiClient(config.apiUrl, config.apiKey)
7
+ }
8
+
9
+ export async function triggerRun(project: string, opts?: { provider?: string }): Promise<void> {
10
+ const client = getClient()
11
+ const body: Record<string, unknown> = {}
12
+ if (opts?.provider) {
13
+ body.providers = [opts.provider]
14
+ }
15
+ const run = await client.triggerRun(project, body) as {
16
+ id: string
17
+ status: string
18
+ kind: string
19
+ }
20
+ console.log(`Run created: ${run.id}`)
21
+ console.log(` Kind: ${run.kind}`)
22
+ console.log(` Status: ${run.status}`)
23
+ if (opts?.provider) {
24
+ console.log(` Provider: ${opts.provider}`)
25
+ }
26
+ }
27
+
28
+ export async function listRuns(project: string): Promise<void> {
29
+ const client = getClient()
30
+ const runs = await client.listRuns(project) as Array<{
31
+ id: string
32
+ status: string
33
+ kind: string
34
+ trigger: string
35
+ startedAt: string | null
36
+ finishedAt: string | null
37
+ createdAt: string
38
+ }>
39
+
40
+ if (runs.length === 0) {
41
+ console.log(`No runs found for "${project}".`)
42
+ return
43
+ }
44
+
45
+ console.log(`Runs for "${project}" (${runs.length}):\n`)
46
+ console.log(' ID STATUS KIND TRIGGER CREATED')
47
+ console.log(' ──────────────────────────────────── ────────── ────────────────── ───────── ───────────────────────')
48
+
49
+ for (const run of runs) {
50
+ console.log(
51
+ ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`,
52
+ )
53
+ }
54
+ }
@@ -0,0 +1,90 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { ApiClient } from '../client.js'
3
+
4
+ function getClient(): ApiClient {
5
+ const config = loadConfig()
6
+ return new ApiClient(config.apiUrl, config.apiKey)
7
+ }
8
+
9
+ interface ScheduleResponse {
10
+ id: string
11
+ projectId: string
12
+ cronExpr: string
13
+ preset: string | null
14
+ timezone: string
15
+ enabled: boolean
16
+ providers: string[]
17
+ lastRunAt: string | null
18
+ nextRunAt: string | null
19
+ }
20
+
21
+ export async function setSchedule(project: string, opts: {
22
+ preset?: string
23
+ cron?: string
24
+ timezone?: string
25
+ providers?: string[]
26
+ }): Promise<void> {
27
+ const client = getClient()
28
+ const body: Record<string, unknown> = {}
29
+ if (opts.preset) body.preset = opts.preset
30
+ if (opts.cron) body.cron = opts.cron
31
+ if (opts.timezone) body.timezone = opts.timezone
32
+ if (opts.providers?.length) body.providers = opts.providers
33
+
34
+ const result = await client.putSchedule(project, body) as ScheduleResponse
35
+ console.log(`Schedule set for "${project}":`)
36
+ printSchedule(result)
37
+ }
38
+
39
+ export async function showSchedule(project: string): Promise<void> {
40
+ const client = getClient()
41
+ const result = await client.getSchedule(project) as ScheduleResponse
42
+ printSchedule(result)
43
+ }
44
+
45
+ export async function enableSchedule(project: string): Promise<void> {
46
+ const client = getClient()
47
+ const current = await client.getSchedule(project) as ScheduleResponse
48
+ const body: Record<string, unknown> = { timezone: current.timezone }
49
+ if (current.preset) body.preset = current.preset
50
+ else body.cron = current.cronExpr
51
+ if (current.providers.length) body.providers = current.providers
52
+
53
+ await client.putSchedule(project, body)
54
+ console.log(`Schedule enabled for "${project}"`)
55
+ }
56
+
57
+ export async function disableSchedule(project: string): Promise<void> {
58
+ const client = getClient()
59
+ const current = await client.getSchedule(project) as ScheduleResponse
60
+ const body: Record<string, unknown> = { timezone: current.timezone, enabled: false }
61
+ if (current.preset) body.preset = current.preset
62
+ else body.cron = current.cronExpr
63
+ if (current.providers.length) body.providers = current.providers
64
+
65
+ await client.putSchedule(project, body)
66
+ console.log(`Schedule disabled for "${project}"`)
67
+ }
68
+
69
+ export async function removeSchedule(project: string): Promise<void> {
70
+ const client = getClient()
71
+ await client.deleteSchedule(project)
72
+ console.log(`Schedule removed for "${project}"`)
73
+ }
74
+
75
+ function printSchedule(s: ScheduleResponse): void {
76
+ const label = s.preset ?? s.cronExpr
77
+ console.log(` Schedule: ${label}`)
78
+ console.log(` Cron: ${s.cronExpr}`)
79
+ console.log(` Timezone: ${s.timezone}`)
80
+ console.log(` Enabled: ${s.enabled ? 'yes' : 'no'}`)
81
+ if (s.providers.length) {
82
+ console.log(` Providers: ${s.providers.join(', ')}`)
83
+ }
84
+ if (s.lastRunAt) {
85
+ console.log(` Last run: ${s.lastRunAt}`)
86
+ }
87
+ if (s.nextRunAt) {
88
+ console.log(` Next run: ${s.nextRunAt}`)
89
+ }
90
+ }
@@ -0,0 +1,24 @@
1
+ import { loadConfig } from '../config.js'
2
+ import { createClient, migrate } from '@ainyc/canonry-db'
3
+ import { createServer } from '../server.js'
4
+
5
+ export async function serveCommand(): Promise<void> {
6
+ const config = loadConfig()
7
+ const port = parseInt(process.env.CANONRY_PORT ?? '4100', 10)
8
+
9
+ // Create DB client and run migrations
10
+ const db = createClient(config.database)
11
+ migrate(db)
12
+
13
+ // Create and start server
14
+ const app = await createServer({ config, db })
15
+
16
+ try {
17
+ await app.listen({ host: '0.0.0.0', port })
18
+ console.log(`\nCanonry server running at http://localhost:${port}`)
19
+ console.log('Press Ctrl+C to stop.\n')
20
+ } catch (err) {
21
+ app.log.error(err)
22
+ process.exit(1)
23
+ }
24
+ }