orizu 0.5.49 → 0.5.50
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/account-directory.d.ts +2 -2
- package/dist/account-directory.d.ts.map +1 -1
- package/dist/account-directory.js +7 -32
- package/dist/auth-commands.d.ts.map +1 -1
- package/dist/auth-commands.js +22 -30
- package/dist/credentials.d.ts +5 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +84 -3
- package/dist/dataset-transfer-upload.d.ts.map +1 -1
- package/dist/dataset-transfer-upload.js +6 -1
- package/dist/directory-list-read.d.ts +37 -0
- package/dist/directory-list-read.d.ts.map +1 -0
- package/dist/directory-list-read.js +87 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +59 -41
- package/dist/hosted-harness-opencode.d.ts +31 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +45 -7
- package/dist/hosted-loop.d.ts +39 -2
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +127 -18
- package/dist/hosted-session-seed.d.ts +2 -0
- package/dist/hosted-session-seed.d.ts.map +1 -1
- package/dist/hosted-session-seed.js +27 -10
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +39 -234
- package/dist/keyboard-select.d.ts.map +1 -1
- package/dist/keyboard-select.js +23 -45
- package/dist/project-create-workflow.d.ts +18 -0
- package/dist/project-create-workflow.d.ts.map +1 -0
- package/dist/project-create-workflow.js +121 -0
- package/dist/self-update-command.d.ts +8 -0
- package/dist/self-update-command.d.ts.map +1 -0
- package/dist/self-update-command.js +390 -0
- package/dist/self-update.d.ts +29 -16
- package/dist/self-update.d.ts.map +1 -1
- package/dist/self-update.js +55 -244
- package/dist/setup-account.d.ts.map +1 -1
- package/dist/setup-account.js +4 -1
- package/dist/setup-and-skills-commands.d.ts.map +1 -1
- package/dist/setup-and-skills-commands.js +88 -102
- package/dist/skill-install-workflow.d.ts +23 -0
- package/dist/skill-install-workflow.d.ts.map +1 -0
- package/dist/skill-install-workflow.js +184 -0
- package/dist/skill-installer.d.ts +8 -3
- package/dist/skill-installer.d.ts.map +1 -1
- package/dist/skill-installer.js +26 -33
- package/dist/stored-session-requests.d.ts +27 -0
- package/dist/stored-session-requests.d.ts.map +1 -0
- package/dist/stored-session-requests.js +264 -0
- package/dist/terminal-key-session.d.ts +23 -0
- package/dist/terminal-key-session.d.ts.map +1 -0
- package/dist/terminal-key-session.js +74 -0
- package/package.json +1 -1
- package/src/account-directory.ts +8 -42
- package/src/auth-commands.ts +20 -29
- package/src/credentials.ts +83 -3
- package/src/dataset-transfer-upload.ts +7 -1
- package/src/directory-list-read.ts +123 -0
- package/src/hosted-boot.ts +65 -43
- package/src/hosted-harness-opencode.ts +65 -9
- package/src/hosted-loop.ts +152 -18
- package/src/hosted-session-seed.ts +25 -8
- package/src/http.ts +39 -317
- package/src/keyboard-select.ts +18 -55
- package/src/project-create-workflow.ts +139 -0
- package/src/self-update-command.ts +419 -0
- package/src/self-update.ts +70 -266
- package/src/setup-account.ts +4 -1
- package/src/setup-and-skills-commands.ts +89 -121
- package/src/skill-install-workflow.ts +215 -0
- package/src/skill-installer.ts +31 -38
- package/src/stored-session-requests.ts +316 -0
- package/src/terminal-key-session.ts +88 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// `orizu update` of an npm-global install (ORI-2424). self-update.ts loads
|
|
2
|
+
// this only once it has found such an install, so no other command, and not
|
|
3
|
+
// the daily background cache refresh, loads Effect. Effect owns the children
|
|
4
|
+
// this starts: each runs in the command's fiber, and a stop interrupts that
|
|
5
|
+
// fiber, which passes the signal on to the running child and waits for it
|
|
6
|
+
// within the stop cap.
|
|
7
|
+
import { spawn } from 'child_process'
|
|
8
|
+
import { stderr as errorOutput, stdout as output } from 'process'
|
|
9
|
+
|
|
10
|
+
import { Cause, Effect, Exit, Fiber, Schema } from 'effect'
|
|
11
|
+
|
|
12
|
+
import { sanitizeTerminalText } from './json-response.js'
|
|
13
|
+
import {
|
|
14
|
+
cliInvocation,
|
|
15
|
+
compareSemver,
|
|
16
|
+
MANUAL_INSTALL_COMMAND,
|
|
17
|
+
npmInvocation,
|
|
18
|
+
resolveLatest,
|
|
19
|
+
resolveUpdateRegistry,
|
|
20
|
+
tryWriteCache,
|
|
21
|
+
type GlobalInstall,
|
|
22
|
+
type SelfUpdateIo,
|
|
23
|
+
} from './self-update.js'
|
|
24
|
+
|
|
25
|
+
const CHILD_TIMEOUT_MS = 60_000
|
|
26
|
+
const STDERR_TAIL_CHARS = 4_000
|
|
27
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const
|
|
28
|
+
// The CLI's stop cap (ORI-2402) is 15 s. A stopped child gets 10 s to exit,
|
|
29
|
+
// then is killed; reading which version is installed gets the last 5 s.
|
|
30
|
+
const STOP_CHILD_WAIT_MS = 10_000
|
|
31
|
+
const STOP_VERSION_CHECK_MS = 5_000
|
|
32
|
+
const PIPE_GRACE_MS = 200
|
|
33
|
+
|
|
34
|
+
interface CommandResult { status: number; stderr: string; stdout: string; timedOut: boolean }
|
|
35
|
+
interface CaptureResult { status: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string; error: Error | null; timedOut: boolean }
|
|
36
|
+
|
|
37
|
+
// What the skills refresh child reports (`orizu skills update --json`).
|
|
38
|
+
const SkillUpdate = Schema.Struct({
|
|
39
|
+
path: Schema.String,
|
|
40
|
+
action: Schema.String,
|
|
41
|
+
backup: Schema.optionalKey(Schema.String),
|
|
42
|
+
warning: Schema.optionalKey(Schema.String),
|
|
43
|
+
error: Schema.optionalKey(Schema.String),
|
|
44
|
+
})
|
|
45
|
+
const SkillsReport = Schema.Struct({ updates: Schema.Array(SkillUpdate) })
|
|
46
|
+
type SkillUpdate = typeof SkillUpdate.Type
|
|
47
|
+
|
|
48
|
+
type SkillsRefreshStatus = 'none' | 'refreshed' | 'already-current'
|
|
49
|
+
|
|
50
|
+
interface SkillsRefreshResult { status: number; timedOut: boolean; skills: SkillsRefreshStatus; updates: readonly SkillUpdate[] | null }
|
|
51
|
+
|
|
52
|
+
interface UpdateResult {
|
|
53
|
+
installed: string; latest: string | null; action: string; prefix: string | null; skills: string
|
|
54
|
+
skillUpdates?: readonly SkillUpdate[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Where the command is, so a stop can say what it left behind.
|
|
58
|
+
type UpdateStep = 'installing' | 'verifying' | 'refreshing-skills'
|
|
59
|
+
interface Progress { step: UpdateStep | null; install: GlobalInstall | null; latest: string | null; installedNow: string }
|
|
60
|
+
|
|
61
|
+
// A stop: its signal, and when waiting for a child must end.
|
|
62
|
+
interface StopState { signal: NodeJS.Signals | null; deadline: number }
|
|
63
|
+
|
|
64
|
+
function testMs(name: string, fallback: number): number {
|
|
65
|
+
const override = process.env[name]
|
|
66
|
+
if (override === undefined) return fallback
|
|
67
|
+
const parsed = Number(override)
|
|
68
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const verifyTimeoutMs = () => testMs('ORIZU_UPDATE_TEST_VERIFY_TIMEOUT_MS', CHILD_TIMEOUT_MS)
|
|
72
|
+
|
|
73
|
+
// Stops reading a child's output, so a process it started that still holds
|
|
74
|
+
// the pipes can't keep this command waiting.
|
|
75
|
+
function releasePipes(child: ReturnType<typeof spawn>): void {
|
|
76
|
+
child.stdout?.destroy()
|
|
77
|
+
child.stderr?.destroy()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Passes the stop's signal on to a running child, waits for it to exit until
|
|
82
|
+
* the stop's deadline, then kills it. Runs when the command's fiber is
|
|
83
|
+
* interrupted while it waits for the child. It waits for the child itself
|
|
84
|
+
* ('exit'), not its output pipes ('close'): a process the child started can
|
|
85
|
+
* hold those open long after the child is gone.
|
|
86
|
+
*/
|
|
87
|
+
function stopChild(child: ReturnType<typeof spawn>, stop: StopState): Effect.Effect<void> {
|
|
88
|
+
return Effect.callback<void>(resume => {
|
|
89
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
90
|
+
releasePipes(child)
|
|
91
|
+
resume(Effect.void)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const kill = setTimeout(() => child.kill('SIGKILL'), Math.max(0, stop.deadline - Date.now()))
|
|
95
|
+
child.once('exit', () => {
|
|
96
|
+
clearTimeout(kill)
|
|
97
|
+
releasePipes(child)
|
|
98
|
+
resume(Effect.void)
|
|
99
|
+
})
|
|
100
|
+
child.kill(stop.signal ?? 'SIGTERM')
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runStreaming(
|
|
105
|
+
command: string,
|
|
106
|
+
args: string[],
|
|
107
|
+
io: SelfUpdateIo,
|
|
108
|
+
stop: StopState,
|
|
109
|
+
options: { timeoutMs?: number; captureStdout?: boolean } = {}
|
|
110
|
+
): Effect.Effect<CommandResult> {
|
|
111
|
+
return Effect.callback<CommandResult>(resume => {
|
|
112
|
+
let stderr = ''
|
|
113
|
+
let stdout = ''
|
|
114
|
+
let settled = false
|
|
115
|
+
let timedOut = false
|
|
116
|
+
let timer: NodeJS.Timeout | undefined
|
|
117
|
+
const finish = (status: number): void => {
|
|
118
|
+
if (settled) return
|
|
119
|
+
settled = true
|
|
120
|
+
if (timer) clearTimeout(timer)
|
|
121
|
+
resume(Effect.succeed({ status, stderr: stderr.slice(-STDERR_TAIL_CHARS), stdout, timedOut }))
|
|
122
|
+
}
|
|
123
|
+
const child = spawn(command, args, {
|
|
124
|
+
env: { ...process.env, ORIZU_NO_UPDATE_CHECK: '1' },
|
|
125
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
126
|
+
})
|
|
127
|
+
if (options.timeoutMs !== undefined) {
|
|
128
|
+
timer = setTimeout(() => {
|
|
129
|
+
timedOut = true
|
|
130
|
+
child.kill('SIGKILL')
|
|
131
|
+
}, options.timeoutMs)
|
|
132
|
+
}
|
|
133
|
+
child.stdout?.on('data', chunk => {
|
|
134
|
+
const text = sanitizeTerminalText(chunk)
|
|
135
|
+
if (options.captureStdout) {
|
|
136
|
+
stdout = `${stdout}${text}`
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if (io.json) errorOutput.write(text)
|
|
140
|
+
else output.write(text)
|
|
141
|
+
})
|
|
142
|
+
child.stderr?.on('data', chunk => {
|
|
143
|
+
const text = sanitizeTerminalText(chunk)
|
|
144
|
+
stderr = `${stderr}${text}`.slice(-STDERR_TAIL_CHARS)
|
|
145
|
+
errorOutput.write(text)
|
|
146
|
+
})
|
|
147
|
+
child.once('error', error => {
|
|
148
|
+
stderr = `${stderr}${sanitizeTerminalText(error.message)}`
|
|
149
|
+
finish(1)
|
|
150
|
+
})
|
|
151
|
+
child.once('close', code => finish(code ?? 1))
|
|
152
|
+
return Effect.suspend(() => {
|
|
153
|
+
if (timer) clearTimeout(timer)
|
|
154
|
+
return stopChild(child, stop)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Runs a child to completion and keeps all it said; used for version checks.
|
|
160
|
+
function runCaptured(command: string, args: string[], timeoutMs: number, stop: StopState | null): Effect.Effect<CaptureResult> {
|
|
161
|
+
return Effect.callback<CaptureResult>(resume => {
|
|
162
|
+
let stdout = ''
|
|
163
|
+
let stderr = ''
|
|
164
|
+
let error: Error | null = null
|
|
165
|
+
let timedOut = false
|
|
166
|
+
const child = spawn(command, args, {
|
|
167
|
+
env: { ...process.env, ORIZU_NO_UPDATE_CHECK: '1' },
|
|
168
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
|
+
})
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
timedOut = true
|
|
172
|
+
child.kill('SIGKILL')
|
|
173
|
+
}, timeoutMs)
|
|
174
|
+
child.stdout?.setEncoding('utf8')
|
|
175
|
+
child.stderr?.setEncoding('utf8')
|
|
176
|
+
child.stdout?.on('data', (chunk: string) => { stdout += chunk })
|
|
177
|
+
child.stderr?.on('data', (chunk: string) => { stderr += chunk })
|
|
178
|
+
let settled = false
|
|
179
|
+
let grace: NodeJS.Timeout | undefined
|
|
180
|
+
const settle = (status: number | null, signal: NodeJS.Signals | null): void => {
|
|
181
|
+
if (settled) return
|
|
182
|
+
settled = true
|
|
183
|
+
clearTimeout(timer)
|
|
184
|
+
if (grace) clearTimeout(grace)
|
|
185
|
+
releasePipes(child)
|
|
186
|
+
resume(Effect.succeed({ status, signal, stdout, stderr, error, timedOut }))
|
|
187
|
+
}
|
|
188
|
+
child.once('error', failure => {
|
|
189
|
+
error = failure
|
|
190
|
+
if (child.pid === undefined) settle(null, null)
|
|
191
|
+
})
|
|
192
|
+
child.once('close', (status, signal) => settle(status, signal))
|
|
193
|
+
// Once the process is gone, its output gets a moment to arrive; a process
|
|
194
|
+
// it started that holds the pipes doesn't extend the timeout.
|
|
195
|
+
child.once('exit', (status, signal) => { grace = setTimeout(() => settle(status, signal), PIPE_GRACE_MS) })
|
|
196
|
+
return Effect.suspend(() => {
|
|
197
|
+
clearTimeout(timer)
|
|
198
|
+
if (grace) clearTimeout(grace)
|
|
199
|
+
return stop ? stopChild(child, stop) : Effect.sync(() => { child.kill('SIGKILL'); releasePipes(child) })
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Refresh managed skills through the given CLI binary and classify the outcome
|
|
206
|
+
* from the child's --json report, never from its human text: an install with
|
|
207
|
+
* no skill targets reports 'none', the one state a caller of `update --json`
|
|
208
|
+
* most needs to see (ADR-001 coupling). The report's backups, warnings and
|
|
209
|
+
* failures are passed on, in human and JSON output alike.
|
|
210
|
+
*/
|
|
211
|
+
function refreshManagedSkills(command: string, args: string[], io: SelfUpdateIo, stop: StopState): Effect.Effect<SkillsRefreshResult> {
|
|
212
|
+
return Effect.gen(function* () {
|
|
213
|
+
const child = yield* runStreaming(command, [...args, 'skills', 'update', '--json'], io, stop, { timeoutMs: CHILD_TIMEOUT_MS, captureStdout: true })
|
|
214
|
+
const report = Schema.decodeUnknownOption(Schema.fromJsonString(SkillsReport))(child.stdout || '')
|
|
215
|
+
const updates = report._tag === 'Some' ? report.value.updates : null
|
|
216
|
+
if (updates !== null && !io.json) printSkillUpdates(io, updates, child.status === 0)
|
|
217
|
+
if (child.status !== 0) return { status: child.status, timedOut: child.timedOut, skills: 'none', updates }
|
|
218
|
+
if (updates === null) return { status: 1, timedOut: false, skills: 'none', updates }
|
|
219
|
+
if (updates.length === 0) return { status: 0, timedOut: false, skills: 'none', updates }
|
|
220
|
+
const refreshed = updates.some(update => update.action !== 'already-current')
|
|
221
|
+
return { status: 0, timedOut: false, skills: refreshed ? 'refreshed' : 'already-current', updates }
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The same lines `orizu skills update` prints for each install.
|
|
226
|
+
function printSkillUpdates(io: SelfUpdateIo, updates: readonly SkillUpdate[], succeeded: boolean): void {
|
|
227
|
+
if (updates.length === 0 && succeeded) io.printLine('No Orizu skill installs found. Run `orizu install-skill` first.')
|
|
228
|
+
for (const update of updates) {
|
|
229
|
+
const path = sanitizeTerminalText(update.path)
|
|
230
|
+
const backup = update.backup ? `; moved your earlier folder to ${sanitizeTerminalText(update.backup)}` : ''
|
|
231
|
+
if (update.action === 'already-current') io.printLine(`Current ${path}`)
|
|
232
|
+
else if (update.action === 'relinked') io.printLine(`Relinked ${path}`)
|
|
233
|
+
else if (update.action === 'rewrote') io.printLine(`Rewrote ${path} from the installed CLI skill${backup}`)
|
|
234
|
+
else if (update.action === 'failed') io.printLine(`Failed ${path}: ${sanitizeTerminalText(update.error ?? 'unknown error')}`)
|
|
235
|
+
else io.printLine(`Updated ${path}${backup}`)
|
|
236
|
+
if (update.warning) io.printLine(`Warning: ${sanitizeTerminalText(update.warning)}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function withSkillUpdates(result: UpdateResult, skills: SkillsRefreshResult): UpdateResult {
|
|
241
|
+
return skills.updates === null ? result : { ...result, skillUpdates: skills.updates }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function emit(io: SelfUpdateIo, result: UpdateResult): void {
|
|
245
|
+
if (io.json) io.printLine(JSON.stringify(result))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function fail(io: SelfUpdateIo, code: string, detail: string, result: UpdateResult): number {
|
|
249
|
+
io.printError(`${code}: ${detail}`)
|
|
250
|
+
emit(io, result)
|
|
251
|
+
return 1
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function selfUpdate(io: SelfUpdateIo, install: GlobalInstall, progress: Progress, stop: StopState): Effect.Effect<number> {
|
|
255
|
+
return Effect.gen(function* () {
|
|
256
|
+
progress.install = install
|
|
257
|
+
|
|
258
|
+
const checked = yield* Effect.promise(async () => {
|
|
259
|
+
try {
|
|
260
|
+
const registry = resolveUpdateRegistry()
|
|
261
|
+
const latest = await resolveLatest(registry)
|
|
262
|
+
tryWriteCache(latest)
|
|
263
|
+
return { ok: true as const, registry, latest }
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return { ok: false as const, error }
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
if (!checked.ok) {
|
|
269
|
+
const { error } = checked
|
|
270
|
+
return fail(io, 'update_check_failed', sanitizeTerminalText(error instanceof Error ? error.message : error), {
|
|
271
|
+
installed: io.installed, latest: null, action: 'check-failed', prefix: install.prefix, skills: 'not-run',
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
const { latest, registry } = checked
|
|
275
|
+
progress.latest = latest
|
|
276
|
+
|
|
277
|
+
if (compareSemver(io.installed, latest) >= 0) {
|
|
278
|
+
if (!io.json) io.printLine(`orizu ${io.installed} is already the latest.`)
|
|
279
|
+
if (io.dryRun) {
|
|
280
|
+
emit(io, { installed: io.installed, latest, action: 'already-latest', prefix: install.prefix, skills: 'not-run' })
|
|
281
|
+
return 0
|
|
282
|
+
}
|
|
283
|
+
progress.step = 'refreshing-skills'
|
|
284
|
+
const skills = yield* refreshManagedSkills(install.binPath, [], io, stop)
|
|
285
|
+
progress.step = null
|
|
286
|
+
if (skills.status !== 0) {
|
|
287
|
+
const detail = skills.timedOut
|
|
288
|
+
? 'orizu is already the latest, but its managed skills refresh timed out.'
|
|
289
|
+
: 'orizu is already the latest, but its managed skills could not be refreshed.'
|
|
290
|
+
return fail(io, 'update_skills_refresh_failed', detail, withSkillUpdates({
|
|
291
|
+
installed: io.installed, latest, action: 'already-latest', prefix: install.prefix, skills: 'refresh-failed',
|
|
292
|
+
}, skills))
|
|
293
|
+
}
|
|
294
|
+
emit(io, withSkillUpdates({ installed: io.installed, latest, action: 'already-latest', prefix: install.prefix, skills: skills.skills }, skills))
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (io.dryRun) {
|
|
299
|
+
if (!io.json) io.printLine(`Would update orizu ${io.installed} → ${latest} in ${install.prefix}.`)
|
|
300
|
+
emit(io, { installed: io.installed, latest, action: 'would-update', prefix: install.prefix, skills: 'would-refresh' })
|
|
301
|
+
return 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const npmCommand = npmInvocation()
|
|
305
|
+
const registryArgs = process.env.ORIZU_UPDATE_REGISTRY !== undefined
|
|
306
|
+
? ['--registry', registry]
|
|
307
|
+
: []
|
|
308
|
+
progress.step = 'installing'
|
|
309
|
+
const npm = yield* runStreaming(npmCommand.command, [...npmCommand.args,
|
|
310
|
+
'install', '-g', `orizu@${latest}`, '--prefix', install.prefix, '--no-audit', '--no-fund',
|
|
311
|
+
...registryArgs,
|
|
312
|
+
], io, stop)
|
|
313
|
+
progress.step = null
|
|
314
|
+
if (npm.status !== 0) {
|
|
315
|
+
const tail = npm.stderr.trim() ? ` npm stderr tail: ${npm.stderr.trim()}` : ''
|
|
316
|
+
return fail(io, 'update_install_failed', `${tail} Run \`${MANUAL_INSTALL_COMMAND}\`.`, {
|
|
317
|
+
installed: io.installed, latest, action: 'install-failed', prefix: install.prefix, skills: 'not-run',
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const verifyCommand = cliInvocation(install, ['--version'])
|
|
322
|
+
const verificationTimeoutMs = verifyTimeoutMs()
|
|
323
|
+
progress.step = 'verifying'
|
|
324
|
+
const verified = yield* runCaptured(verifyCommand.command, verifyCommand.args, verificationTimeoutMs, stop)
|
|
325
|
+
progress.step = null
|
|
326
|
+
const observed = sanitizeTerminalText(verified.stdout).trim()
|
|
327
|
+
if (verified.timedOut) {
|
|
328
|
+
return fail(io, 'update_verify_mismatch', `verification timed out after ${verificationTimeoutMs} ms.`, {
|
|
329
|
+
installed: io.installed, latest, action: 'verify-mismatch', prefix: install.prefix, skills: 'not-run',
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
if (verified.status !== 0 || observed !== `orizu ${latest}`) {
|
|
333
|
+
const launchError = verified.error ? ` launch error: ${sanitizeTerminalText(verified.error.message)}.` : ''
|
|
334
|
+
const exit = verified.status === null ? `signal ${verified.signal ?? 'unknown'}` : `exit ${verified.status}`
|
|
335
|
+
const errorTail = sanitizeTerminalText(verified.stderr).trim().split('\n').slice(-3).join(' ')
|
|
336
|
+
const detail = `expected orizu ${latest}, observed ${observed || '<no version>'} (${exit}).${launchError}${errorTail ? ` ${errorTail}` : ''}`
|
|
337
|
+
return fail(io, 'update_verify_mismatch', `${detail} Run \`${MANUAL_INSTALL_COMMAND}\` to reinstall.`, {
|
|
338
|
+
installed: io.installed, latest, action: 'verify-mismatch', prefix: install.prefix, skills: 'not-run',
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
progress.installedNow = latest
|
|
342
|
+
|
|
343
|
+
if (!io.json) io.printLine(`orizu ${latest} installed.`)
|
|
344
|
+
const skillsCommand = cliInvocation(install, [])
|
|
345
|
+
progress.step = 'refreshing-skills'
|
|
346
|
+
const skills = yield* refreshManagedSkills(skillsCommand.command, skillsCommand.args, io, stop)
|
|
347
|
+
progress.step = null
|
|
348
|
+
if (skills.status !== 0) {
|
|
349
|
+
const detail = skills.timedOut
|
|
350
|
+
? 'the CLI was updated, but its managed skills refresh timed out.'
|
|
351
|
+
: 'the CLI was updated, but its managed skills could not be refreshed.'
|
|
352
|
+
return fail(io, 'update_skills_refresh_failed', detail, withSkillUpdates({
|
|
353
|
+
installed: io.installed, latest, action: 'updated', prefix: install.prefix, skills: 'refresh-failed',
|
|
354
|
+
}, skills))
|
|
355
|
+
}
|
|
356
|
+
emit(io, withSkillUpdates({ installed: io.installed, latest, action: 'updated', prefix: install.prefix, skills: skills.skills }, skills))
|
|
357
|
+
return 0
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// After a stop: which orizu the global install runs now, read once in the stop cap's last 5 s.
|
|
362
|
+
async function installedVersionAfterStop(install: GlobalInstall): Promise<string | null> {
|
|
363
|
+
const command = cliInvocation(install, ['--version'])
|
|
364
|
+
const checked = await Effect.runPromise(runCaptured(command.command, command.args, STOP_VERSION_CHECK_MS, null))
|
|
365
|
+
const match = /^orizu (\S+)$/u.exec(sanitizeTerminalText(checked.stdout).trim())
|
|
366
|
+
return checked.status === 0 && match ? match[1]! : null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function reportStop(io: SelfUpdateIo, progress: Progress): Promise<void> {
|
|
370
|
+
const { step, install, latest } = progress
|
|
371
|
+
if (!step || !install || !latest) return
|
|
372
|
+
if (step === 'refreshing-skills') {
|
|
373
|
+
io.printError(`update_stopped: stopped while refreshing the managed skills. orizu ${progress.installedNow} is installed now. Run \`orizu skills update\` to finish the refresh.`)
|
|
374
|
+
emit(io, { installed: io.installed, latest, action: 'stopped', prefix: install.prefix, skills: 'refresh-stopped' })
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
const now = await installedVersionAfterStop(install)
|
|
378
|
+
const doing = step === 'installing' ? `npm was installing orizu ${latest}` : `checking the new orizu ${latest}`
|
|
379
|
+
const state = now ? `orizu ${now} is installed now.` : 'Could not tell which orizu is installed now.'
|
|
380
|
+
io.printError(`update_stopped: stopped while ${doing}. ${state} Run \`${MANUAL_INSTALL_COMMAND}\` to finish the update.`)
|
|
381
|
+
emit(io, { installed: io.installed, latest, action: 'stopped', prefix: install.prefix, skills: 'not-run' })
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Runs the update with stop handlers installed for exactly its duration. A
|
|
386
|
+
* stop interrupts it: the running child gets the same signal and the stop cap
|
|
387
|
+
* to exit, the command says what is installed now, then ends by that signal.
|
|
388
|
+
*/
|
|
389
|
+
export async function runSelfUpdate(io: SelfUpdateIo, install: GlobalInstall): Promise<number> {
|
|
390
|
+
const progress: Progress = { step: null, install: null, latest: null, installedNow: io.installed }
|
|
391
|
+
const stop: StopState = { signal: null, deadline: 0 }
|
|
392
|
+
// eslint-disable-next-line prefer-const -- assigned after the handlers that read it
|
|
393
|
+
let fiber: Fiber.Fiber<number>
|
|
394
|
+
const handlers = STOP_SIGNALS.map(signal => ({
|
|
395
|
+
signal,
|
|
396
|
+
handle: () => {
|
|
397
|
+
if (stop.signal) return
|
|
398
|
+
stop.signal = signal
|
|
399
|
+
stop.deadline = Date.now() + testMs('ORIZU_UPDATE_TEST_STOP_WAIT_MS', STOP_CHILD_WAIT_MS)
|
|
400
|
+
void Effect.runPromise(Fiber.interrupt(fiber))
|
|
401
|
+
},
|
|
402
|
+
}))
|
|
403
|
+
const removeHandlers = () => { for (const entry of handlers) process.off(entry.signal, entry.handle) }
|
|
404
|
+
for (const entry of handlers) process.on(entry.signal, entry.handle)
|
|
405
|
+
fiber = Effect.runFork(selfUpdate(io, install, progress, stop))
|
|
406
|
+
try {
|
|
407
|
+
const exit = await Effect.runPromise(Effect.exit(Fiber.join(fiber)))
|
|
408
|
+
if (stop.signal) {
|
|
409
|
+
await reportStop(io, progress)
|
|
410
|
+
removeHandlers()
|
|
411
|
+
process.kill(process.pid, stop.signal)
|
|
412
|
+
return await new Promise<never>(() => {})
|
|
413
|
+
}
|
|
414
|
+
if (Exit.isSuccess(exit)) return exit.value
|
|
415
|
+
throw Cause.squash(exit.cause)
|
|
416
|
+
} finally {
|
|
417
|
+
removeHandlers()
|
|
418
|
+
}
|
|
419
|
+
}
|