@bprp/flockcode 0.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.
- package/package.json +45 -0
- package/src/app.ts +153 -0
- package/src/diagnose-stream.ts +305 -0
- package/src/env.ts +35 -0
- package/src/event-discovery.ts +355 -0
- package/src/event-driven-test.ts +72 -0
- package/src/index.ts +223 -0
- package/src/opencode.ts +278 -0
- package/src/prompt.ts +127 -0
- package/src/router/agents.ts +57 -0
- package/src/router/base.ts +10 -0
- package/src/router/commands.ts +57 -0
- package/src/router/context.ts +22 -0
- package/src/router/diffs.ts +46 -0
- package/src/router/index.ts +24 -0
- package/src/router/models.ts +55 -0
- package/src/router/permissions.ts +28 -0
- package/src/router/projects.ts +175 -0
- package/src/router/sessions.ts +316 -0
- package/src/router/snapshot.ts +9 -0
- package/src/server.ts +15 -0
- package/src/spawn-opencode.ts +166 -0
- package/src/sprite-configure-services.ts +302 -0
- package/src/sprite-sync.ts +413 -0
- package/src/sprites.ts +328 -0
- package/src/start-server.ts +49 -0
- package/src/state-stream.ts +711 -0
- package/src/transcribe.ts +100 -0
- package/src/types.ts +430 -0
- package/src/voice-prompt.ts +222 -0
- package/src/worktree-name.ts +62 -0
- package/src/worktree.ts +549 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn and manage an `opencode serve` child process.
|
|
3
|
+
*
|
|
4
|
+
* When no explicit OpenCode URL is provided, this module finds an available
|
|
5
|
+
* port, starts `opencode serve` on it, waits for it to become ready, and
|
|
6
|
+
* returns the URL. The child process is killed when the parent process exits.
|
|
7
|
+
*
|
|
8
|
+
* The managed port is persisted to the crust store so other processes can
|
|
9
|
+
* discover the running opencode instance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createStore, stateDir } from "@crustjs/store"
|
|
13
|
+
|
|
14
|
+
const OPENCODE_BIN = "opencode"
|
|
15
|
+
const DEFAULT_START_PORT = 4097
|
|
16
|
+
const MAX_PORT_ATTEMPTS = 100
|
|
17
|
+
const READY_TIMEOUT_MS = 30_000
|
|
18
|
+
const READY_POLL_INTERVAL_MS = 250
|
|
19
|
+
|
|
20
|
+
export const opencodeStore = createStore({
|
|
21
|
+
dirPath: stateDir("flockcode"),
|
|
22
|
+
name: "opencode",
|
|
23
|
+
fields: {
|
|
24
|
+
port: {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Port of the flock-managed opencode serve process",
|
|
27
|
+
validate: (v) => {
|
|
28
|
+
if (v < 1 || v > 65535) throw new Error("port must be 1–65535")
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** Result of ensuring an OpenCode server is available. */
|
|
35
|
+
export interface EnsureOpenCodeResult {
|
|
36
|
+
/** The base URL of the OpenCode server. */
|
|
37
|
+
url: string
|
|
38
|
+
/** The spawned subprocess, if one was started. `null` when connecting to an existing server. */
|
|
39
|
+
child: Bun.Subprocess | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a TCP port is available by trying to listen on it.
|
|
44
|
+
*/
|
|
45
|
+
async function isPortAvailable(port: number): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
const server = Bun.serve({
|
|
48
|
+
port,
|
|
49
|
+
fetch() {
|
|
50
|
+
return new Response("probe")
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
server.stop()
|
|
54
|
+
return true
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Find the first available port starting from `startFrom`.
|
|
62
|
+
*/
|
|
63
|
+
export async function findAvailablePort(startFrom: number = DEFAULT_START_PORT): Promise<number> {
|
|
64
|
+
for (let port = startFrom; port < startFrom + MAX_PORT_ATTEMPTS; port++) {
|
|
65
|
+
if (await isPortAvailable(port)) {
|
|
66
|
+
return port
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`No available port found in range ${startFrom}–${startFrom + MAX_PORT_ATTEMPTS - 1}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Poll the OpenCode server until it responds or the timeout expires.
|
|
74
|
+
*/
|
|
75
|
+
async function waitForReady(url: string, timeoutMs: number = READY_TIMEOUT_MS): Promise<void> {
|
|
76
|
+
const deadline = Date.now() + timeoutMs
|
|
77
|
+
const healthUrl = `${url}/health`
|
|
78
|
+
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(2000) })
|
|
82
|
+
if (res.ok) return
|
|
83
|
+
} catch {
|
|
84
|
+
// Not ready yet
|
|
85
|
+
}
|
|
86
|
+
await Bun.sleep(READY_POLL_INTERVAL_MS)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(`OpenCode server at ${url} did not become ready within ${timeoutMs}ms`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Spawn `opencode serve` as a child process on the given port.
|
|
94
|
+
*/
|
|
95
|
+
function spawnProcess(port: number): Bun.Subprocess {
|
|
96
|
+
const child = Bun.spawn([OPENCODE_BIN, "serve", "--port", String(port)], {
|
|
97
|
+
stdout: "pipe",
|
|
98
|
+
stderr: "pipe",
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Forward stderr so startup errors are visible
|
|
102
|
+
const reader = child.stderr.getReader()
|
|
103
|
+
const decoder = new TextDecoder()
|
|
104
|
+
child.exited.then(async () => {
|
|
105
|
+
while (true) {
|
|
106
|
+
const { done, value } = await reader.read()
|
|
107
|
+
if (done) break
|
|
108
|
+
const text = decoder.decode(value, { stream: true })
|
|
109
|
+
for (const line of text.split("\n")) {
|
|
110
|
+
if (line) console.error(`[opencode] ${line}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return child
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Ensure an OpenCode server is available.
|
|
120
|
+
*
|
|
121
|
+
* - If `explicitUrl` is a non-empty string, it is returned directly (assumes
|
|
122
|
+
* an OpenCode server is already running there).
|
|
123
|
+
* - Otherwise, finds an available port, spawns `opencode serve`, waits for
|
|
124
|
+
* it to become ready, and returns the URL plus the child process handle.
|
|
125
|
+
*/
|
|
126
|
+
export async function ensureOpenCode(explicitUrl?: string): Promise<EnsureOpenCodeResult> {
|
|
127
|
+
if (explicitUrl) {
|
|
128
|
+
return { url: explicitUrl, child: null }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const port = await findAvailablePort()
|
|
132
|
+
console.log(`Spawning opencode serve on port ${port}...`)
|
|
133
|
+
|
|
134
|
+
const child = spawnProcess(port)
|
|
135
|
+
const url = `http://localhost:${port}`
|
|
136
|
+
|
|
137
|
+
await waitForReady(url)
|
|
138
|
+
console.log(`OpenCode server ready at ${url}`)
|
|
139
|
+
|
|
140
|
+
await opencodeStore.patch({ port })
|
|
141
|
+
|
|
142
|
+
return { url, child }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Register signal handlers to kill a spawned child process on exit.
|
|
147
|
+
* Clears the stored port before exiting.
|
|
148
|
+
*/
|
|
149
|
+
export function cleanupOnExit(child: Bun.Subprocess): void {
|
|
150
|
+
const kill = async () => {
|
|
151
|
+
try {
|
|
152
|
+
child.kill()
|
|
153
|
+
} catch {
|
|
154
|
+
// Process may have already exited
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await opencodeStore.update(() => ({ port: undefined }))
|
|
158
|
+
} catch {
|
|
159
|
+
// Store write best-effort during shutdown
|
|
160
|
+
}
|
|
161
|
+
process.exit(0)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
process.on("SIGINT", kill)
|
|
165
|
+
process.on("SIGTERM", kill)
|
|
166
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprite configure-services — registers `opencode-serve` and `flock-server`
|
|
3
|
+
* services on the Fly Sprite so they auto-start on Sprite wake.
|
|
4
|
+
*
|
|
5
|
+
* Also writes a `.flockenv` file to the Sprite's home directory containing
|
|
6
|
+
* the environment variables needed by the flock server (auth token, API keys).
|
|
7
|
+
*
|
|
8
|
+
* Intended to be called independently from sync. Run `sync` first to clone
|
|
9
|
+
* repos, then `configure-services` to set up background services.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SpriteClient } from "./sprites"
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Result for a single service registration. */
|
|
19
|
+
export interface ServiceResult {
|
|
20
|
+
/** Whether the service was created fresh. */
|
|
21
|
+
created: boolean
|
|
22
|
+
/** Whether an existing service was updated. */
|
|
23
|
+
updated: boolean
|
|
24
|
+
/** Whether the service was already configured and left unchanged. */
|
|
25
|
+
unchanged: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Full result of a configure-services run. */
|
|
29
|
+
export interface ConfigureServicesResult {
|
|
30
|
+
/** Result for the `opencode-serve` service. */
|
|
31
|
+
opencodeServe: ServiceResult
|
|
32
|
+
/** Result for the `flock-server` service. */
|
|
33
|
+
flockServer: ServiceResult
|
|
34
|
+
/** Whether the `.flockenv` file was written to the Sprite. */
|
|
35
|
+
flockenvWritten: boolean
|
|
36
|
+
/** Whether the `.flockenv` file was already up-to-date. */
|
|
37
|
+
flockenvUnchanged: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Options for {@link configureServices}. */
|
|
41
|
+
export interface ConfigureServicesOptions {
|
|
42
|
+
/** If true, report what would happen without making changes. */
|
|
43
|
+
dryRun?: boolean
|
|
44
|
+
/**
|
|
45
|
+
* The port opencode should listen on inside the Sprite.
|
|
46
|
+
* @default 4096
|
|
47
|
+
*/
|
|
48
|
+
opencodePort?: number
|
|
49
|
+
/**
|
|
50
|
+
* Working directory for the opencode serve process on the Sprite.
|
|
51
|
+
* If not set, no `--dir` flag is passed (opencode uses its default).
|
|
52
|
+
*/
|
|
53
|
+
opencodeDir?: string
|
|
54
|
+
/**
|
|
55
|
+
* The port the flock server should listen on inside the Sprite.
|
|
56
|
+
* @default 3000
|
|
57
|
+
*/
|
|
58
|
+
flockServerPort?: number
|
|
59
|
+
/**
|
|
60
|
+
* Bearer token for authenticating mobile clients to the flock server.
|
|
61
|
+
* Written to `.flockenv` on the Sprite as `FLOCK_AUTH_TOKEN`.
|
|
62
|
+
*/
|
|
63
|
+
flockAuthToken?: string
|
|
64
|
+
/**
|
|
65
|
+
* Google / Gemini API key for audio transcription.
|
|
66
|
+
* Written to `.flockenv` on the Sprite as `GEMINI_API_KEY`.
|
|
67
|
+
*/
|
|
68
|
+
geminiApiKey?: string
|
|
69
|
+
/**
|
|
70
|
+
* Gemini model for audio transcription.
|
|
71
|
+
* Written to `.flockenv` on the Sprite as `TRANSCRIPTION_MODEL`.
|
|
72
|
+
*/
|
|
73
|
+
transcriptionModel?: string
|
|
74
|
+
/**
|
|
75
|
+
* Callback for progress messages. If not provided, messages are printed to
|
|
76
|
+
* stdout via `console.log`.
|
|
77
|
+
*/
|
|
78
|
+
onProgress?: (message: string) => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Constants
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const OPENCODE_SERVICE = "opencode-serve"
|
|
86
|
+
const FLOCK_SERVICE = "flock-server"
|
|
87
|
+
const FLOCKENV_FILENAME = ".flockenv"
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Core
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Configure services and environment on the Fly Sprite.
|
|
95
|
+
*
|
|
96
|
+
* 1. Writes a `.flockenv` file to `$HOME/.flockenv` with env vars the flock
|
|
97
|
+
* server needs (auth token, API keys, opencode URL).
|
|
98
|
+
* 2. Registers `opencode-serve` — runs `opencode serve --port <port>` as an
|
|
99
|
+
* internal service (no `http_port`, not externally proxied).
|
|
100
|
+
* 3. Registers `flock-server` — sources `.flockenv` and runs
|
|
101
|
+
* `bunx @bprp/flockcode start` as the externally-proxied HTTP service.
|
|
102
|
+
*/
|
|
103
|
+
export async function configureServices(
|
|
104
|
+
sprite: SpriteClient,
|
|
105
|
+
options: ConfigureServicesOptions = {},
|
|
106
|
+
): Promise<ConfigureServicesResult> {
|
|
107
|
+
const {
|
|
108
|
+
opencodePort = 4096,
|
|
109
|
+
opencodeDir,
|
|
110
|
+
flockServerPort = 3000,
|
|
111
|
+
flockAuthToken,
|
|
112
|
+
geminiApiKey,
|
|
113
|
+
dryRun = false,
|
|
114
|
+
} = options
|
|
115
|
+
const log = options.onProgress ?? console.log
|
|
116
|
+
|
|
117
|
+
const result: ConfigureServicesResult = {
|
|
118
|
+
opencodeServe: { created: false, updated: false, unchanged: false },
|
|
119
|
+
flockServer: { created: false, updated: false, unchanged: false },
|
|
120
|
+
flockenvWritten: false,
|
|
121
|
+
flockenvUnchanged: false,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// -- 1. Write .flockenv -------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const homeDir = await sprite.homeDir()
|
|
127
|
+
const flockenvPath = `${homeDir}/${FLOCKENV_FILENAME}`
|
|
128
|
+
const desiredEnv = buildFlockenv({
|
|
129
|
+
opencodePort,
|
|
130
|
+
flockAuthToken,
|
|
131
|
+
geminiApiKey,
|
|
132
|
+
transcriptionModel: options.transcriptionModel,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Check if the existing file already matches
|
|
136
|
+
let existingEnv: string | null = null
|
|
137
|
+
try {
|
|
138
|
+
const buf = await sprite.readFile(flockenvPath)
|
|
139
|
+
existingEnv = buf.toString("utf-8")
|
|
140
|
+
} catch {
|
|
141
|
+
// File doesn't exist yet
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (existingEnv === desiredEnv) {
|
|
145
|
+
log(` ${FLOCKENV_FILENAME} — already up-to-date`)
|
|
146
|
+
result.flockenvUnchanged = true
|
|
147
|
+
} else {
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
log(` ${FLOCKENV_FILENAME} — would write to ${flockenvPath}`)
|
|
150
|
+
} else {
|
|
151
|
+
log(` ${FLOCKENV_FILENAME} — writing to ${flockenvPath}...`)
|
|
152
|
+
await sprite.writeFile(flockenvPath, Buffer.from(desiredEnv, "utf-8"))
|
|
153
|
+
log(` ${FLOCKENV_FILENAME} — written`)
|
|
154
|
+
}
|
|
155
|
+
result.flockenvWritten = true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// -- 2. opencode-serve (internal, no http_port) -------------------------
|
|
159
|
+
|
|
160
|
+
log("")
|
|
161
|
+
const opencodeArgs = buildOpencodeArgs(opencodePort, opencodeDir)
|
|
162
|
+
result.opencodeServe = await ensureService(sprite, {
|
|
163
|
+
serviceName: OPENCODE_SERVICE,
|
|
164
|
+
desiredCmd: "opencode",
|
|
165
|
+
desiredArgs: opencodeArgs,
|
|
166
|
+
desiredHttpPort: null,
|
|
167
|
+
needs: [],
|
|
168
|
+
dryRun,
|
|
169
|
+
log,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// -- 3. flock-server (externally proxied) --------------------------------
|
|
173
|
+
|
|
174
|
+
log("")
|
|
175
|
+
const flockCmd = `source ${flockenvPath} && exec bunx @bprp/flockcode start --port ${flockServerPort}`
|
|
176
|
+
result.flockServer = await ensureService(sprite, {
|
|
177
|
+
serviceName: FLOCK_SERVICE,
|
|
178
|
+
desiredCmd: "bash",
|
|
179
|
+
desiredArgs: ["-c", flockCmd],
|
|
180
|
+
desiredHttpPort: flockServerPort,
|
|
181
|
+
needs: [OPENCODE_SERVICE],
|
|
182
|
+
dryRun,
|
|
183
|
+
log,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Helpers
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/** Build the `.flockenv` file contents. */
|
|
194
|
+
function buildFlockenv(opts: {
|
|
195
|
+
opencodePort: number
|
|
196
|
+
flockAuthToken?: string
|
|
197
|
+
geminiApiKey?: string
|
|
198
|
+
transcriptionModel?: string
|
|
199
|
+
}): string {
|
|
200
|
+
const lines: string[] = [
|
|
201
|
+
"# Flockcode environment — managed by `flock sprite configure-services`",
|
|
202
|
+
"# Do not edit manually; re-run configure-services to update.",
|
|
203
|
+
"",
|
|
204
|
+
`export OPENCODE_URL="http://localhost:${opts.opencodePort}"`,
|
|
205
|
+
]
|
|
206
|
+
if (opts.flockAuthToken) {
|
|
207
|
+
lines.push(`export FLOCK_AUTH_TOKEN="${opts.flockAuthToken}"`)
|
|
208
|
+
}
|
|
209
|
+
if (opts.geminiApiKey) {
|
|
210
|
+
lines.push(`export GEMINI_API_KEY="${opts.geminiApiKey}"`)
|
|
211
|
+
}
|
|
212
|
+
if (opts.transcriptionModel) {
|
|
213
|
+
lines.push(`export TRANSCRIPTION_MODEL="${opts.transcriptionModel}"`)
|
|
214
|
+
}
|
|
215
|
+
lines.push("") // trailing newline
|
|
216
|
+
return lines.join("\n")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Build the args array for the opencode serve command. */
|
|
220
|
+
function buildOpencodeArgs(port: number, dir?: string): string[] {
|
|
221
|
+
const args = ["serve", "--port", String(port)]
|
|
222
|
+
if (dir) {
|
|
223
|
+
args.push("--dir", dir)
|
|
224
|
+
}
|
|
225
|
+
return args
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Options for {@link ensureService}. */
|
|
229
|
+
interface EnsureServiceOptions {
|
|
230
|
+
serviceName: string
|
|
231
|
+
desiredCmd: string
|
|
232
|
+
desiredArgs: string[]
|
|
233
|
+
desiredHttpPort: number | null
|
|
234
|
+
needs: string[]
|
|
235
|
+
dryRun: boolean
|
|
236
|
+
log: (message: string) => void
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Ensure a single service is registered on the Sprite with the desired
|
|
241
|
+
* configuration. Creates, updates, or leaves it unchanged as appropriate.
|
|
242
|
+
*/
|
|
243
|
+
async function ensureService(
|
|
244
|
+
sprite: SpriteClient,
|
|
245
|
+
opts: EnsureServiceOptions,
|
|
246
|
+
): Promise<ServiceResult> {
|
|
247
|
+
const { serviceName, desiredCmd, desiredArgs, desiredHttpPort, needs, dryRun, log } = opts
|
|
248
|
+
const result: ServiceResult = { created: false, updated: false, unchanged: false }
|
|
249
|
+
|
|
250
|
+
const existing = await sprite.getService(serviceName)
|
|
251
|
+
|
|
252
|
+
const putConfig = {
|
|
253
|
+
cmd: desiredCmd,
|
|
254
|
+
args: desiredArgs,
|
|
255
|
+
...(desiredHttpPort != null ? { httpPort: desiredHttpPort } : {}),
|
|
256
|
+
...(needs.length > 0 ? { needs } : {}),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (existing) {
|
|
260
|
+
const matches =
|
|
261
|
+
existing.cmd === desiredCmd &&
|
|
262
|
+
arraysEqual(existing.args, desiredArgs) &&
|
|
263
|
+
(existing.http_port ?? null) === desiredHttpPort &&
|
|
264
|
+
arraysEqual(existing.needs, needs)
|
|
265
|
+
|
|
266
|
+
if (matches) {
|
|
267
|
+
log(` ${serviceName} — already configured (${existing.state?.status ?? "unknown"})`)
|
|
268
|
+
result.unchanged = true
|
|
269
|
+
} else {
|
|
270
|
+
if (dryRun) {
|
|
271
|
+
log(` ${serviceName} — would update (config changed)`)
|
|
272
|
+
log(` current: ${existing.cmd} ${existing.args.join(" ")} (port ${existing.http_port ?? "none"})`)
|
|
273
|
+
log(` desired: ${desiredCmd} ${desiredArgs.join(" ")} (port ${desiredHttpPort ?? "none"})`)
|
|
274
|
+
} else {
|
|
275
|
+
log(` ${serviceName} — updating...`)
|
|
276
|
+
await sprite.putService(serviceName, putConfig)
|
|
277
|
+
log(` ${serviceName} — updated`)
|
|
278
|
+
}
|
|
279
|
+
result.updated = true
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
if (dryRun) {
|
|
283
|
+
log(` ${serviceName} — would create: ${desiredCmd} ${desiredArgs.join(" ")} (port ${desiredHttpPort ?? "none"})`)
|
|
284
|
+
} else {
|
|
285
|
+
log(` ${serviceName} — creating...`)
|
|
286
|
+
await sprite.putService(serviceName, putConfig)
|
|
287
|
+
log(` ${serviceName} — created`)
|
|
288
|
+
}
|
|
289
|
+
result.created = true
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Shallow array equality. */
|
|
296
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
297
|
+
if (a.length !== b.length) return false
|
|
298
|
+
for (let i = 0; i < a.length; i++) {
|
|
299
|
+
if (a[i] !== b[i]) return false
|
|
300
|
+
}
|
|
301
|
+
return true
|
|
302
|
+
}
|