Skip to content

Commit 1d68064

Browse files
authored
Add AmpereContext, and use it in WatchCommand (#21)
1 parent 25dc51a commit 1d68064

8 files changed

Lines changed: 951 additions & 38 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package link.socket.ampere
2+
3+
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.SupervisorJob
7+
import kotlinx.coroutines.cancel
8+
import kotlinx.serialization.json.Json
9+
import link.socket.ampere.agents.events.EnvironmentService
10+
import link.socket.ampere.agents.events.relay.EventRelayService
11+
import link.socket.ampere.agents.events.utils.ConsoleEventLogger
12+
import link.socket.ampere.agents.events.utils.EventLogger
13+
import link.socket.ampere.data.DEFAULT_JSON
14+
import link.socket.ampere.db.Database
15+
import java.io.File
16+
17+
/**
18+
* Context that provides dependencies for CLI commands.
19+
*
20+
* AmpereContext handles all the initialization and wiring of the environment,
21+
* so commands just receive the services they need without worrying about
22+
* how they're created.
23+
*
24+
* This class:
25+
* - Initializes the database connection
26+
* - Creates the database schema
27+
* - Sets up the coroutine scope for async operations
28+
* - Creates the EnvironmentService with all orchestrators and repositories
29+
* - Provides access to services for CLI commands
30+
*
31+
* Usage:
32+
* ```kotlin
33+
* val context = AmpereContext()
34+
* context.start()
35+
* try {
36+
* // Use context.environmentService or context.eventRelayService
37+
* } finally {
38+
* context.close()
39+
* }
40+
* ```
41+
*/
42+
class AmpereContext(
43+
/**
44+
* Path to the SQLite database file.
45+
* Defaults to "ampere.db" in the user's home directory.
46+
*/
47+
databasePath: String = defaultDatabasePath(),
48+
49+
/**
50+
* JSON configuration for serialization.
51+
* Defaults to the standard Ampere JSON configuration.
52+
*/
53+
json: Json = DEFAULT_JSON,
54+
55+
/**
56+
* Event logger for system operations.
57+
* Defaults to console logging.
58+
*/
59+
logger: EventLogger = ConsoleEventLogger(),
60+
) {
61+
/**
62+
* Database driver for SQLite operations.
63+
*/
64+
private val driver: JdbcSqliteDriver = createDriver(databasePath)
65+
66+
/**
67+
* Database instance with all queries.
68+
*/
69+
private val database: Database = createDatabase(driver)
70+
71+
/**
72+
* Coroutine scope for async operations.
73+
* Uses Dispatchers.Default with a SupervisorJob for fault tolerance.
74+
*/
75+
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
76+
77+
/**
78+
* The environment service that provides access to all repositories,
79+
* orchestrators, and agent APIs.
80+
*/
81+
val environmentService: EnvironmentService = EnvironmentService.create(
82+
database = database,
83+
scope = scope,
84+
json = json,
85+
logger = logger,
86+
)
87+
88+
/**
89+
* Convenience accessor for the event relay service.
90+
* This is commonly needed by CLI commands that watch or query events.
91+
*/
92+
val eventRelayService: EventRelayService
93+
get() = environmentService.eventRelayService
94+
95+
/**
96+
* Start all orchestrator services.
97+
*
98+
* This must be called before using the context to ensure event routing
99+
* and other background operations are active.
100+
*/
101+
fun start() {
102+
environmentService.start()
103+
}
104+
105+
/**
106+
* Close all resources and stop background operations.
107+
*
108+
* This should be called when the CLI is shutting down to ensure
109+
* clean resource cleanup.
110+
*/
111+
fun close() {
112+
scope.cancel()
113+
driver.close()
114+
}
115+
116+
companion object {
117+
/**
118+
* Default database path in the user's home directory.
119+
*/
120+
private fun defaultDatabasePath(): String {
121+
val homeDir = System.getProperty("user.home")
122+
return File(homeDir, ".ampere/ampere.db").absolutePath
123+
}
124+
125+
/**
126+
* Create a JDBC SQLite driver with proper configuration.
127+
*/
128+
private fun createDriver(databasePath: String): JdbcSqliteDriver {
129+
// Ensure parent directory exists
130+
val dbFile = File(databasePath)
131+
dbFile.parentFile?.mkdirs()
132+
133+
return JdbcSqliteDriver("jdbc:sqlite:$databasePath")
134+
}
135+
136+
/**
137+
* Create the database instance and initialize the schema if needed.
138+
*/
139+
private fun createDatabase(driver: JdbcSqliteDriver): Database {
140+
// Check if the main table exists to determine if we need to create the schema
141+
val schemaExists = try {
142+
driver.executeQuery<Boolean>(
143+
identifier = null,
144+
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='EventStore'",
145+
mapper = { cursor ->
146+
app.cash.sqldelight.db.QueryResult.Value(cursor.next().value)
147+
},
148+
parameters = 0,
149+
binders = null
150+
).value
151+
} catch (e: Exception) {
152+
false
153+
}
154+
155+
if (!schemaExists) {
156+
// Schema doesn't exist, create it
157+
Database.Schema.create(driver)
158+
}
159+
160+
return Database(driver)
161+
}
162+
}
163+
}

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/Main.kt

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,49 @@ package link.socket.ampere
33
import com.github.ajalt.clikt.core.CliktCommand
44
import com.github.ajalt.clikt.core.subcommands
55

6-
fun main(args: Array<String>) = AmpereCommand()
7-
.subcommands(WatchCommand())
8-
.main(args)
6+
/**
7+
* Main entry point for the Ampere CLI.
8+
*
9+
* This function:
10+
* 1. Creates an AmpereContext to initialize all dependencies
11+
* 2. Starts the environment orchestrator
12+
* 3. Runs the CLI command
13+
* 4. Cleans up resources on exit
14+
*/
15+
fun main(args: Array<String>) {
16+
// Create context with all dependencies
17+
val context = AmpereContext()
918

10-
class AmpereCommand : CliktCommand(
19+
try {
20+
// Start all orchestrator services
21+
context.start()
22+
23+
// Run the CLI with injected dependencies
24+
AmpereCommand(context)
25+
.subcommands(
26+
WatchCommand(context.eventRelayService)
27+
)
28+
.main(args)
29+
} finally {
30+
// Clean up resources
31+
context.close()
32+
}
33+
}
34+
35+
/**
36+
* Root command for the Ampere CLI.
37+
*
38+
* @param context The application context providing access to services
39+
*/
40+
class AmpereCommand(
41+
private val context: AmpereContext,
42+
) : CliktCommand(
1143
name = "ampere",
1244
help = """
1345
Animated Multi-Agent (Prompting Technique) -> AniMA
1446
AniMA Model Protocol -> AMP
1547
AMP Example Runtime Environment -> AMPERE
16-
48+
1749
AMPERE is a tool for running AniMA simulations in a real-time, observable environment.
1850
""".trimIndent()
1951
) {

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/WatchCommand.kt

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package link.socket.ampere
22

3-
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
43
import com.github.ajalt.clikt.core.CliktCommand
54
import com.github.ajalt.clikt.parameters.options.multiple
65
import com.github.ajalt.clikt.parameters.options.option
@@ -10,15 +9,10 @@ import com.github.ajalt.mordant.rendering.TextColors.yellow
109
import com.github.ajalt.mordant.rendering.TextStyles.bold
1110
import com.github.ajalt.mordant.rendering.TextStyles.dim
1211
import com.github.ajalt.mordant.terminal.Terminal
13-
import kotlinx.coroutines.CoroutineScope
14-
import kotlinx.coroutines.Dispatchers
15-
import kotlinx.coroutines.SupervisorJob
16-
import kotlinx.coroutines.cancel
1712
import kotlinx.coroutines.runBlocking
18-
import link.socket.ampere.agents.events.EnvironmentService
1913
import link.socket.ampere.agents.events.EventSource
2014
import link.socket.ampere.agents.events.relay.EventRelayFilters
21-
import link.socket.ampere.db.Database
15+
import link.socket.ampere.agents.events.relay.EventRelayService
2216
import link.socket.ampere.renderer.EventRenderer
2317
import link.socket.ampere.util.EventTypeParser
2418

@@ -27,8 +21,12 @@ import link.socket.ampere.util.EventTypeParser
2721
*
2822
* This is the CLI's primary sensory interface - it lets you feel the pulse
2923
* of the organizational organism by streaming events as they occur.
24+
*
25+
* @param eventRelayService The service for subscribing to events (injected)
3026
*/
31-
class WatchCommand : CliktCommand(
27+
class WatchCommand(
28+
private val eventRelayService: EventRelayService,
29+
) : CliktCommand(
3230
name = "watch",
3331
help = """
3432
Watch events streaming from the AniMA substrate in real-time.
@@ -63,24 +61,7 @@ class WatchCommand : CliktCommand(
6361
terminal.println(dim("Connecting to event stream..."))
6462
terminal.println()
6563

66-
// Set up the event infrastructure
67-
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
68-
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
69-
7064
try {
71-
// Create database schema
72-
Database.Schema.create(driver)
73-
val database = Database(driver)
74-
75-
// Create environment service - this handles all infrastructure setup
76-
val environment = EnvironmentService.create(
77-
database = database,
78-
scope = scope,
79-
)
80-
81-
// Start the environment
82-
environment.start()
83-
8465
// Create event renderer
8566
val renderer = EventRenderer(terminal)
8667

@@ -94,16 +75,13 @@ class WatchCommand : CliktCommand(
9475
terminal.println(bold("Watching events... (Ctrl+C to stop)"))
9576
terminal.println()
9677

97-
environment.eventRelayService.subscribeToLiveEvents(filters)
78+
eventRelayService.subscribeToLiveEvents(filters)
9879
.collect { event ->
9980
renderer.render(event)
10081
}
10182
} catch (e: Exception) {
10283
terminal.println(red("Error: ${e.message}"))
10384
throw e
104-
} finally {
105-
driver.close()
106-
scope.cancel()
10785
}
10886
}
10987

0 commit comments

Comments
 (0)