orizu 0.5.39 → 0.5.41
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/README.md +77 -0
- package/dist/dataset-clone-workflow.d.ts +11 -0
- package/dist/dataset-clone-workflow.d.ts.map +1 -0
- package/dist/dataset-clone-workflow.js +100 -0
- package/dist/dataset-commands.d.ts +3 -1
- package/dist/dataset-commands.d.ts.map +1 -1
- package/dist/dataset-commands.js +107 -91
- package/dist/dataset-delete-workflow.d.ts +4 -0
- package/dist/dataset-delete-workflow.d.ts.map +1 -0
- package/dist/dataset-delete-workflow.js +75 -0
- package/dist/dataset-lock-workflow.d.ts +12 -0
- package/dist/dataset-lock-workflow.d.ts.map +1 -0
- package/dist/dataset-lock-workflow.js +76 -0
- package/dist/dataset-readme-workflow.d.ts +13 -0
- package/dist/dataset-readme-workflow.d.ts.map +1 -0
- package/dist/dataset-readme-workflow.js +82 -0
- package/dist/dataset-row-workflow.d.ts +14 -0
- package/dist/dataset-row-workflow.d.ts.map +1 -0
- package/dist/dataset-row-workflow.js +202 -0
- package/dist/dataset-split-reads.d.ts +28 -0
- package/dist/dataset-split-reads.d.ts.map +1 -0
- package/dist/dataset-split-reads.js +47 -0
- package/dist/dataset-transfer-upload.d.ts +2 -6
- package/dist/dataset-transfer-upload.d.ts.map +1 -1
- package/dist/dataset-transfer-upload.js +9 -2
- package/dist/dataset-upload-snapshot.d.ts.map +1 -1
- package/dist/dataset-upload-snapshot.js +7 -1
- package/dist/dataset-version-workflow.d.ts +24 -0
- package/dist/dataset-version-workflow.d.ts.map +1 -0
- package/dist/dataset-version-workflow.js +99 -0
- package/dist/feedback-cli.js +3 -3
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +29 -3
- package/dist/hosted-boot.d.ts +1 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +17 -0
- package/dist/hosted-harness-opencode.d.ts +5 -6
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +73 -59
- package/dist/hosted-loop-lifecycle.d.ts +6 -11
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop-lifecycle.js +6 -11
- package/dist/hosted-loop.d.ts +9 -0
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/instruction-delivery.d.ts +14 -0
- package/dist/instruction-delivery.d.ts.map +1 -0
- package/dist/instruction-delivery.js +103 -0
- package/dist/instruction-set-sync/index.d.ts +17 -0
- package/dist/instruction-set-sync/index.d.ts.map +1 -1
- package/dist/instruction-set-sync/index.js +44 -17
- package/dist/instruction-set-update/index.d.ts +2 -2
- package/dist/instruction-set-update/index.d.ts.map +1 -1
- package/dist/instruction-set-update/index.js +2 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +19 -27
- package/dist/json-response.d.ts +1 -0
- package/dist/json-response.d.ts.map +1 -1
- package/dist/json-response.js +7 -0
- package/dist/last-error-record.d.ts +4 -2
- package/dist/last-error-record.d.ts.map +1 -1
- package/dist/last-error-record.js +10 -68
- package/dist/last-error-workflow.d.ts +4 -0
- package/dist/last-error-workflow.d.ts.map +1 -0
- package/dist/last-error-workflow.js +78 -0
- package/dist/preview-runtime.d.ts +2 -2
- package/dist/preview-runtime.d.ts.map +1 -1
- package/dist/preview-runtime.js +37 -7
- package/dist/task-commands.d.ts.map +1 -1
- package/dist/task-commands.js +9 -16
- package/dist/task-export-workflow.d.ts +4 -0
- package/dist/task-export-workflow.d.ts.map +1 -0
- package/dist/task-export-workflow.js +46 -0
- package/dist/task-report-workflow.d.ts +47 -0
- package/dist/task-report-workflow.d.ts.map +1 -0
- package/dist/task-report-workflow.js +160 -0
- package/dist/workbench-cli.js +1 -1
- package/package.json +1 -1
- package/src/dataset-clone-workflow.ts +100 -0
- package/src/dataset-commands.ts +95 -142
- package/src/dataset-delete-workflow.ts +72 -0
- package/src/dataset-lock-workflow.ts +74 -0
- package/src/dataset-readme-workflow.ts +80 -0
- package/src/dataset-row-workflow.ts +248 -0
- package/src/dataset-split-reads.ts +43 -0
- package/src/dataset-transfer-upload.ts +12 -5
- package/src/dataset-upload-snapshot.ts +8 -1
- package/src/dataset-version-workflow.ts +105 -0
- package/src/feedback-cli.ts +4 -4
- package/src/help.ts +29 -3
- package/src/hosted-boot.ts +16 -1
- package/src/hosted-harness-opencode.ts +74 -72
- package/src/hosted-loop-lifecycle.ts +6 -11
- package/src/hosted-loop.ts +20 -10
- package/src/hosted-model-cache.d.mts +2 -0
- package/src/hosted-model-cache.mjs +53 -0
- package/src/index.ts +12 -2
- package/src/instruction-delivery.ts +123 -0
- package/src/instruction-set-sync/index.ts +61 -14
- package/src/instruction-set-update/index.ts +4 -3
- package/src/instruction-sets-cli.ts +19 -27
- package/src/json-response.ts +8 -0
- package/src/last-error-record.ts +11 -85
- package/src/last-error-workflow.ts +121 -0
- package/src/preview-runtime.ts +40 -10
- package/src/task-commands.ts +9 -43
- package/src/task-export-workflow.ts +56 -0
- package/src/task-report-workflow.ts +179 -0
- package/src/workbench-cli.ts +1 -1
- package/vendor/skills/orizu/references/cli-reference.md +34 -0
- package/vendor/skills/orizu/references/dataset-design.md +1 -1
- package/dist/dataset-edit-rows.d.ts +0 -10
- package/dist/dataset-edit-rows.d.ts.map +0 -1
- package/dist/dataset-edit-rows.js +0 -90
- package/src/dataset-edit-rows.ts +0 -124
|
@@ -509,17 +509,25 @@ function removeObservedSyncLock(lockPath: string, observed: ObservedSyncLock): b
|
|
|
509
509
|
return true
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
export
|
|
512
|
+
export interface SyncLockClaim { lockPath: string; metadata: string; deadline: number }
|
|
513
|
+
|
|
514
|
+
export function startSyncLockClaim(appRoot: string): SyncLockClaim {
|
|
513
515
|
mkdirSync(appRoot, { recursive: true })
|
|
514
|
-
const lockPath = join(appRoot, '.orizu.lock.json.lock')
|
|
515
|
-
const metadata = `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`
|
|
516
516
|
const lockWaitMs = processTestMilliseconds(
|
|
517
517
|
'ORIZU_TEST_SYNC_LOCK_WAIT_MS',
|
|
518
518
|
100,
|
|
519
519
|
SYNC_LOCK_WAIT_MS
|
|
520
520
|
) ?? SYNC_LOCK_WAIT_MS
|
|
521
|
-
|
|
521
|
+
return {
|
|
522
|
+
lockPath: join(appRoot, '.orizu.lock.json.lock'),
|
|
523
|
+
metadata: `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
|
|
524
|
+
deadline: Date.now() + lockWaitMs,
|
|
525
|
+
}
|
|
526
|
+
}
|
|
522
527
|
|
|
528
|
+
// One claim attempt: returns the release function, or how long to wait before
|
|
529
|
+
// trying again. Waiting is the caller's job so it can be blocking or not.
|
|
530
|
+
export function tryClaimSyncLock({ lockPath, metadata, deadline }: SyncLockClaim): (() => void) | number {
|
|
523
531
|
while (true) {
|
|
524
532
|
try {
|
|
525
533
|
writeFileSync(lockPath, metadata, { encoding: 'utf8', flag: 'wx', mode: 0o600 })
|
|
@@ -540,17 +548,26 @@ export function withSyncLock<T>(appRoot: string, operation: () => T): T {
|
|
|
540
548
|
if (Date.now() >= deadline) {
|
|
541
549
|
throw new Error(`instruction_set_sync_lock_busy${busyPid === undefined ? '' : `:${busyPid}`}`)
|
|
542
550
|
}
|
|
543
|
-
|
|
551
|
+
return Math.min(SYNC_LOCK_RETRY_MS, Math.max(1, deadline - Date.now()))
|
|
544
552
|
}
|
|
545
553
|
}
|
|
546
554
|
|
|
547
|
-
|
|
555
|
+
return () => {
|
|
548
556
|
try {
|
|
549
557
|
if (readFileSync(lockPath, 'utf8') === metadata) rmSync(lockPath)
|
|
550
558
|
} catch (error) {
|
|
551
559
|
if (!(isNodeError(error) && error.code === 'ENOENT')) throw error
|
|
552
560
|
}
|
|
553
561
|
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function withSyncLock<T>(appRoot: string, operation: () => T): T {
|
|
565
|
+
const claim = startSyncLockClaim(appRoot)
|
|
566
|
+
let release = tryClaimSyncLock(claim)
|
|
567
|
+
while (typeof release === 'number') {
|
|
568
|
+
sleepSync(release)
|
|
569
|
+
release = tryClaimSyncLock(claim)
|
|
570
|
+
}
|
|
554
571
|
let result: T
|
|
555
572
|
try {
|
|
556
573
|
result = operation()
|
|
@@ -600,6 +617,16 @@ export interface SyncDiskOptions {
|
|
|
600
617
|
platform?: NodeJS.Platform
|
|
601
618
|
}
|
|
602
619
|
|
|
620
|
+
export interface StagedSync {
|
|
621
|
+
appRoot: string
|
|
622
|
+
destination: string
|
|
623
|
+
wasPresent: boolean
|
|
624
|
+
/** Publishes the staged version; the caller must hold the folder lock. Returns warnings. */
|
|
625
|
+
publish: () => string[]
|
|
626
|
+
/** Removes the staged version if it was not published. Safe to call more than once. */
|
|
627
|
+
discard: () => void
|
|
628
|
+
}
|
|
629
|
+
|
|
603
630
|
export function syncPayloadToDisk(
|
|
604
631
|
out: string,
|
|
605
632
|
project: string,
|
|
@@ -607,6 +634,22 @@ export function syncPayloadToDisk(
|
|
|
607
634
|
payload: SyncPayload,
|
|
608
635
|
options: SyncDiskOptions = {}
|
|
609
636
|
): SyncDiskResult {
|
|
637
|
+
const staged = stageSyncPayload(out, project, plan, payload, options)
|
|
638
|
+
try {
|
|
639
|
+
const warnings = withSyncLock(staged.appRoot, staged.publish)
|
|
640
|
+
return { destination: staged.destination, wasPresent: staged.wasPresent, warnings }
|
|
641
|
+
} finally {
|
|
642
|
+
staged.discard()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function stageSyncPayload(
|
|
647
|
+
out: string,
|
|
648
|
+
project: string,
|
|
649
|
+
plan: SyncRequestPlan,
|
|
650
|
+
payload: SyncPayload,
|
|
651
|
+
options: SyncDiskOptions = {}
|
|
652
|
+
): StagedSync {
|
|
610
653
|
const target = parseSyncTarget(options.target)
|
|
611
654
|
const now = options.now ?? (() => new Date())
|
|
612
655
|
const set = payload.instructionSet
|
|
@@ -717,9 +760,11 @@ export function syncPayloadToDisk(
|
|
|
717
760
|
assertExistingVersionMatches(destination, componentHashes, manifestBytes, generatedBytes)
|
|
718
761
|
}
|
|
719
762
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
763
|
+
return {
|
|
764
|
+
appRoot,
|
|
765
|
+
destination,
|
|
766
|
+
wasPresent,
|
|
767
|
+
publish: () => {
|
|
723
768
|
holdForProcessTest('ORIZU_TEST_SYNC_HOLD_BEFORE_PUBLISH_MS')
|
|
724
769
|
const currentLock = readLock(appRoot, project)
|
|
725
770
|
holdForProcessTest('ORIZU_TEST_SYNC_HOLD_LOCK_MS')
|
|
@@ -786,8 +831,9 @@ export function syncPayloadToDisk(
|
|
|
786
831
|
}
|
|
787
832
|
stagedTemp = null
|
|
788
833
|
}
|
|
789
|
-
warnings = emitManagedArtifacts(out, appRoot, currentLock, options.forceHelpers ?? false, target, journal)
|
|
834
|
+
const warnings = emitManagedArtifacts(out, appRoot, currentLock, options.forceHelpers ?? false, target, journal)
|
|
790
835
|
journal.write(join(appRoot, 'orizu.lock.json'), serializeLock(currentLock))
|
|
836
|
+
return warnings
|
|
791
837
|
} catch (error) {
|
|
792
838
|
journal.rollback()
|
|
793
839
|
currentLock.helpers = priorHelpers
|
|
@@ -802,9 +848,10 @@ export function syncPayloadToDisk(
|
|
|
802
848
|
}
|
|
803
849
|
throw error
|
|
804
850
|
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
|
|
851
|
+
},
|
|
852
|
+
discard: () => {
|
|
853
|
+
if (stagedTemp) rmSync(stagedTemp, { recursive: true, force: true })
|
|
854
|
+
stagedTemp = null
|
|
855
|
+
},
|
|
808
856
|
}
|
|
809
|
-
return { destination, wasPresent, warnings }
|
|
810
857
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type InstructionSetLockV1,
|
|
12
12
|
} from '../instruction-set-lock/index.js'
|
|
13
13
|
import { assertOutputConfined, reconcileGeneratedIndex } from '../instruction-set-sync/helpers.js'
|
|
14
|
-
import { profileSlug, recordResolvedPayloadInLock, syncPayloadToDisk, withSyncLock, type SyncPayload } from '../instruction-set-sync/index.js'
|
|
14
|
+
import { profileSlug, recordResolvedPayloadInLock, syncPayloadToDisk, withSyncLock, type SyncDiskResult, type SyncPayload } from '../instruction-set-sync/index.js'
|
|
15
15
|
import { verifyInstructionSetTree, verifyMaterializedVersionAgainstLock } from '../instruction-set-verify/index.js'
|
|
16
16
|
|
|
17
17
|
export interface UpdateResolution {
|
|
@@ -222,7 +222,8 @@ export async function applyUpdate(
|
|
|
222
222
|
project: string,
|
|
223
223
|
plan: UpdatePlan,
|
|
224
224
|
noSync: boolean,
|
|
225
|
-
now: () => Date = () => new Date()
|
|
225
|
+
now: () => Date = () => new Date(),
|
|
226
|
+
sync: (...args: Parameters<typeof syncPayloadToDisk>) => SyncDiskResult | Promise<SyncDiskResult> = syncPayloadToDisk
|
|
226
227
|
): Promise<UpdateResult> {
|
|
227
228
|
const absent: string[] = []
|
|
228
229
|
const warnings: string[] = []
|
|
@@ -271,7 +272,7 @@ export async function applyUpdate(
|
|
|
271
272
|
let lock = plan.lock
|
|
272
273
|
for (const resolution of plan.resolutions) {
|
|
273
274
|
const set = resolution.payload.instructionSet
|
|
274
|
-
const result =
|
|
275
|
+
const result = await sync(out, project, {
|
|
275
276
|
parsed: { set: set.slug, profile: resolution.profileIdentity },
|
|
276
277
|
path: '',
|
|
277
278
|
lock,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { authedFetch } from './http.js'
|
|
2
2
|
import { INSTRUCTION_SET_COMPONENT_KEY } from './instruction-set-lock/index.js'
|
|
3
3
|
import { loadInstructionSetManifest, MAX_INSTRUCTION_SET_COMPONENT_BYTES, type InstructionSetManifest } from './instruction-set-manifest.js'
|
|
4
|
-
import { sanitizeHumanInlineText, sanitizeTerminalText } from './json-response.js'
|
|
5
|
-
import { parseSyncTarget, planSyncRequest,
|
|
6
|
-
import { applyPrune,
|
|
4
|
+
import { responsePayload, sanitizeHumanInlineText, sanitizeTerminalText } from './json-response.js'
|
|
5
|
+
import { parseSyncTarget, planSyncRequest, type SyncTarget } from './instruction-set-sync/index.js'
|
|
6
|
+
import { applyPrune, planPrune, PruneKeepUnresolvedError } from './instruction-set-update/index.js'
|
|
7
7
|
import { printVerifyReport, verifyInstructionSetTree } from './instruction-set-verify/index.js'
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { join } from 'node:path'
|
|
@@ -74,12 +74,8 @@ function lifecycleOptionValues(args: string[], command: 'update' | 'prune', flag
|
|
|
74
74
|
return values
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
throw new Error(`${action} failed (${response.status}): ${typeof payload.error === 'string' ? payload.error : response.statusText}`)
|
|
81
|
-
}
|
|
82
|
-
return payload
|
|
77
|
+
function printInterrupted(io: InstructionSetsCommandIo, message: string) {
|
|
78
|
+
io.print(io.json ? JSON.stringify({ error: message }) : message)
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/u
|
|
@@ -340,23 +336,14 @@ export async function instructionSetsCommand(args: string[], io: InstructionSets
|
|
|
340
336
|
const project = await io.resolveProjectSlug(projectOption)
|
|
341
337
|
if (subcommand === 'update') {
|
|
342
338
|
const output = lifecycleOptionValues(args, 'update', '--out')[0] ?? '.'
|
|
343
|
-
const lock = readUpdateLock(output, project)
|
|
344
|
-
const payloads: SyncPayload[] = []
|
|
345
|
-
for (const [setSlug, set] of Object.entries(lock.instructionSets)) {
|
|
346
|
-
const barePath = `/api/cli/instruction-sets/${encodeURIComponent(setSlug)}/sync?project=${encodeURIComponent(project)}`
|
|
347
|
-
payloads.push(await responsePayload(await authedFetch(barePath, { method: 'GET' }), 'Instruction sets update') as unknown as SyncPayload)
|
|
348
|
-
for (const profileSlugValue of Object.keys(set.profiles)) {
|
|
349
|
-
const identity = lockedProfileIdentity(output, setSlug, profileSlugValue)
|
|
350
|
-
const query = new URLSearchParams({ project, profile: identity })
|
|
351
|
-
const path = `/api/cli/instruction-sets/${encodeURIComponent(setSlug)}/sync?${query.toString()}`
|
|
352
|
-
payloads.push(await responsePayload(await authedFetch(path, { method: 'GET' }), 'Instruction sets update') as unknown as SyncPayload)
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
const plan = makeUpdatePlan(project, lock, payloads)
|
|
356
339
|
const isApproved = args.includes('--yes')
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
340
|
+
const { updateInstructionSets } = await import('./instruction-delivery.js')
|
|
341
|
+
const updated = await updateInstructionSets(output, project, isApproved, args.includes('--no-sync'))
|
|
342
|
+
if ('interrupted' in updated) {
|
|
343
|
+
printInterrupted(io, 'Instruction sets update interrupted; some sets may already be updated. Run orizu instructions update again, then orizu instructions verify.')
|
|
344
|
+
return updated.interrupted
|
|
345
|
+
}
|
|
346
|
+
const { plan, result } = updated
|
|
360
347
|
if (io.json) io.print(JSON.stringify({ plan: plan.lines, applied: isApproved, absent: result.absent, warnings: result.warnings }))
|
|
361
348
|
else {
|
|
362
349
|
for (const line of plan.lines) io.print(line)
|
|
@@ -527,11 +514,16 @@ export async function instructionSetsCommand(args: string[], io: InstructionSets
|
|
|
527
514
|
if (unknownOption) throw new Error(`instruction_set_sync_option_unknown:${unknownOption}`)
|
|
528
515
|
const output = requiredOptionValue(args, subcommand, '--out') ?? '.'
|
|
529
516
|
const plan = planSyncRequest(reference, requiredOptionValue(args, subcommand, '--version'), output, project)
|
|
530
|
-
const
|
|
531
|
-
const
|
|
517
|
+
const { syncInstructionSet } = await import('./instruction-delivery.js')
|
|
518
|
+
const delivered = await syncInstructionSet(output, project, plan, {
|
|
532
519
|
forceHelpers: args.includes('--force-helpers'),
|
|
533
520
|
target: syncTarget!,
|
|
534
521
|
})
|
|
522
|
+
if ('interrupted' in delivered) {
|
|
523
|
+
printInterrupted(io, 'Instruction set sync interrupted; any unpublished staged version was removed. Run orizu instructions sync again.')
|
|
524
|
+
return delivered.interrupted
|
|
525
|
+
}
|
|
526
|
+
const { payload, result } = delivered
|
|
535
527
|
if (io.json) io.print(JSON.stringify({ ...payload, warnings: result.warnings }))
|
|
536
528
|
else {
|
|
537
529
|
io.print(`Synced ${reference} to ${result.destination}`)
|
package/src/json-response.ts
CHANGED
|
@@ -35,3 +35,11 @@ export async function parseJsonResponse<T>(
|
|
|
35
35
|
)
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
export async function responsePayload(response: Response, action: string): Promise<Record<string, unknown>> {
|
|
40
|
+
const payload = await response.json().catch(() => ({})) as Record<string, unknown>
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`${action} failed (${response.status}): ${typeof payload.error === 'string' ? payload.error : response.statusText}`)
|
|
43
|
+
}
|
|
44
|
+
return payload
|
|
45
|
+
}
|
package/src/last-error-record.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'crypto'
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
closeSync,
|
|
5
|
-
fsyncSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
openSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
renameSync,
|
|
10
|
-
rmSync,
|
|
11
|
-
statSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from 'fs'
|
|
14
1
|
import { homedir } from 'os'
|
|
15
2
|
import { join } from 'path'
|
|
16
3
|
|
|
@@ -235,7 +222,7 @@ function resolveHomeDir(): string | null {
|
|
|
235
222
|
}
|
|
236
223
|
}
|
|
237
224
|
|
|
238
|
-
function isStringWithinCap(
|
|
225
|
+
export function isStringWithinCap(
|
|
239
226
|
value: unknown,
|
|
240
227
|
maximumCharacters: number,
|
|
241
228
|
wasTruncated: boolean
|
|
@@ -247,45 +234,12 @@ function isStringWithinCap(
|
|
|
247
234
|
&& value.endsWith(LAST_ERROR_TRUNCATION_MARKER)
|
|
248
235
|
}
|
|
249
236
|
|
|
250
|
-
function boundedServerBaseUrl(value: string | null | undefined): string | null {
|
|
237
|
+
export function boundedServerBaseUrl(value: string | null | undefined): string | null {
|
|
251
238
|
return value && Buffer.byteLength(value, 'utf8') <= LAST_ERROR_SERVER_BASE_URL_MAX_BYTES
|
|
252
239
|
? value
|
|
253
240
|
: null
|
|
254
241
|
}
|
|
255
242
|
|
|
256
|
-
function isLastErrorRecord(value: unknown): value is LastErrorRecord {
|
|
257
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
258
|
-
const record = value as Partial<LastErrorRecord>
|
|
259
|
-
if (record.version !== LAST_ERROR_RECORD_VERSION) return false
|
|
260
|
-
if (typeof record.recordedAt !== 'string' || !Number.isFinite(Date.parse(record.recordedAt))) return false
|
|
261
|
-
if (typeof record.cliVersion !== 'string' && record.cliVersion !== null) return false
|
|
262
|
-
if (record.code !== undefined && record.code !== null && (
|
|
263
|
-
typeof record.code !== 'string'
|
|
264
|
-
|| Array.from(record.code).length > LAST_ERROR_CODE_MAX_CHARS
|
|
265
|
-
)) return false
|
|
266
|
-
if (record.serverBaseUrl !== undefined && typeof record.serverBaseUrl !== 'string' && record.serverBaseUrl !== null) return false
|
|
267
|
-
if (record.teamSlug !== undefined && typeof record.teamSlug !== 'string' && record.teamSlug !== null) return false
|
|
268
|
-
if (typeof record.argvTruncated !== 'boolean') return false
|
|
269
|
-
const argvTruncated = record.argvTruncated
|
|
270
|
-
if (record.command !== null && !isStringWithinCap(
|
|
271
|
-
record.command,
|
|
272
|
-
LAST_ERROR_ARG_MAX_CHARS,
|
|
273
|
-
argvTruncated
|
|
274
|
-
)) return false
|
|
275
|
-
if (!Array.isArray(record.argv) || record.argv.length > LAST_ERROR_MAX_ARGS) return false
|
|
276
|
-
if (!record.argv.every(argument => isStringWithinCap(
|
|
277
|
-
argument,
|
|
278
|
-
LAST_ERROR_ARG_MAX_CHARS,
|
|
279
|
-
argvTruncated
|
|
280
|
-
))) return false
|
|
281
|
-
if (typeof record.messageTruncated !== 'boolean') return false
|
|
282
|
-
return isStringWithinCap(
|
|
283
|
-
record.message,
|
|
284
|
-
LAST_ERROR_MESSAGE_MAX_CHARS,
|
|
285
|
-
record.messageTruncated
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
243
|
export function buildLastErrorRecord(input: LastErrorRecordInput): LastErrorRecord {
|
|
290
244
|
const context: ScrubContext = {
|
|
291
245
|
homeDir: input.homeDir ?? null,
|
|
@@ -350,15 +304,12 @@ export function getLastErrorRecordPath(configDir?: string): string {
|
|
|
350
304
|
return join(configDir ?? getConfigDir(), LAST_ERROR_FILENAME)
|
|
351
305
|
}
|
|
352
306
|
|
|
353
|
-
export function writeLastErrorRecord(
|
|
307
|
+
export async function writeLastErrorRecord(
|
|
354
308
|
input: LastErrorRecordInput,
|
|
355
309
|
options: WriteLastErrorRecordOptions = {}
|
|
356
|
-
): void {
|
|
310
|
+
): Promise<void> {
|
|
357
311
|
try {
|
|
358
312
|
const configDirectory = options.configDir ?? getConfigDir()
|
|
359
|
-
mkdirSync(configDirectory, { recursive: true, mode: 0o700 })
|
|
360
|
-
chmodSync(configDirectory, 0o700)
|
|
361
|
-
|
|
362
313
|
const record = buildLastErrorRecord({
|
|
363
314
|
...input,
|
|
364
315
|
homeDir: input.homeDir === undefined ? resolveHomeDir() : input.homeDir,
|
|
@@ -371,44 +322,19 @@ export function writeLastErrorRecord(
|
|
|
371
322
|
const serialized = serializeRecord(record)
|
|
372
323
|
if (Buffer.byteLength(serialized, 'utf8') > LAST_ERROR_FILE_MAX_BYTES) return
|
|
373
324
|
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
configDirectory,
|
|
377
|
-
`.${LAST_ERROR_FILENAME}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
try {
|
|
381
|
-
const fileDescriptor = openSync(temporaryPath, 'wx', 0o600)
|
|
382
|
-
try {
|
|
383
|
-
writeFileSync(fileDescriptor, serialized, 'utf8')
|
|
384
|
-
fsyncSync(fileDescriptor)
|
|
385
|
-
} finally {
|
|
386
|
-
closeSync(fileDescriptor)
|
|
387
|
-
}
|
|
388
|
-
chmodSync(temporaryPath, 0o600)
|
|
389
|
-
renameSync(temporaryPath, path)
|
|
390
|
-
chmodSync(path, 0o600)
|
|
391
|
-
} finally {
|
|
392
|
-
rmSync(temporaryPath, { force: true })
|
|
393
|
-
}
|
|
325
|
+
const { publishLastErrorRecord } = await import('./last-error-workflow.js')
|
|
326
|
+
await publishLastErrorRecord(configDirectory, serialized)
|
|
394
327
|
} catch {
|
|
395
|
-
// A diagnostic record is best effort and must never replace the original CLI failure
|
|
328
|
+
// A diagnostic record is best effort and must never replace the original CLI failure,
|
|
329
|
+
// including when Effect cannot load (a packed CLI without its dependencies).
|
|
396
330
|
}
|
|
397
331
|
}
|
|
398
332
|
|
|
399
|
-
export function readLastErrorRecord(configDir?: string): LastErrorRecord | null {
|
|
333
|
+
export async function readLastErrorRecord(configDir?: string): Promise<LastErrorRecord | null> {
|
|
400
334
|
try {
|
|
401
335
|
const path = getLastErrorRecordPath(configDir)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return isLastErrorRecord(record)
|
|
405
|
-
? {
|
|
406
|
-
...record,
|
|
407
|
-
code: record.code ?? null,
|
|
408
|
-
serverBaseUrl: boundedServerBaseUrl(record.serverBaseUrl),
|
|
409
|
-
teamSlug: record.teamSlug ?? null,
|
|
410
|
-
}
|
|
411
|
-
: null
|
|
336
|
+
const { loadLastErrorRecord } = await import('./last-error-workflow.js')
|
|
337
|
+
return await loadLastErrorRecord(path)
|
|
412
338
|
} catch {
|
|
413
339
|
return null
|
|
414
340
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto'
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
import { Effect, Schema } from 'effect'
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
LAST_ERROR_ARG_MAX_CHARS,
|
|
19
|
+
LAST_ERROR_CODE_MAX_CHARS,
|
|
20
|
+
LAST_ERROR_FILE_MAX_BYTES,
|
|
21
|
+
LAST_ERROR_MAX_ARGS,
|
|
22
|
+
LAST_ERROR_MESSAGE_MAX_CHARS,
|
|
23
|
+
} from './last-error-caps.js'
|
|
24
|
+
import {
|
|
25
|
+
boundedServerBaseUrl,
|
|
26
|
+
isStringWithinCap,
|
|
27
|
+
LAST_ERROR_FILENAME,
|
|
28
|
+
LAST_ERROR_RECORD_VERSION,
|
|
29
|
+
type LastErrorRecord,
|
|
30
|
+
} from './last-error-record.js'
|
|
31
|
+
|
|
32
|
+
// ORI-2450: Effect owns the last-error file's publication and reading.
|
|
33
|
+
// last-error-record.ts loads this module with a literal dynamic import, so the
|
|
34
|
+
// npm CLI only loads Effect on a failure or a feedback read, and the source
|
|
35
|
+
// bundle still includes it.
|
|
36
|
+
|
|
37
|
+
class LastErrorFileFailure extends Error {
|
|
38
|
+
constructor(readonly operation: string) {
|
|
39
|
+
super(`Could not ${operation}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const file = <A>(operation: string, action: () => A) => Effect.try({
|
|
44
|
+
try: action,
|
|
45
|
+
catch: () => new LastErrorFileFailure(operation),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const NullableString = Schema.NullOr(Schema.String)
|
|
49
|
+
|
|
50
|
+
const LastErrorRecordSchema = Schema.Struct({
|
|
51
|
+
version: Schema.Literal(LAST_ERROR_RECORD_VERSION),
|
|
52
|
+
recordedAt: Schema.String.check(Schema.makeFilter(value => Number.isFinite(Date.parse(value)))),
|
|
53
|
+
cliVersion: NullableString,
|
|
54
|
+
code: Schema.optionalKey(NullableString.check(Schema.makeFilter(value =>
|
|
55
|
+
value === null || Array.from(value).length <= LAST_ERROR_CODE_MAX_CHARS))),
|
|
56
|
+
serverBaseUrl: Schema.optionalKey(NullableString),
|
|
57
|
+
teamSlug: Schema.optionalKey(NullableString),
|
|
58
|
+
command: NullableString,
|
|
59
|
+
argv: Schema.Array(Schema.String),
|
|
60
|
+
argvTruncated: Schema.Boolean,
|
|
61
|
+
message: Schema.String,
|
|
62
|
+
messageTruncated: Schema.Boolean,
|
|
63
|
+
}).check(Schema.makeFilter(record =>
|
|
64
|
+
(record.command === null || isStringWithinCap(record.command, LAST_ERROR_ARG_MAX_CHARS, record.argvTruncated))
|
|
65
|
+
&& record.argv.length <= LAST_ERROR_MAX_ARGS
|
|
66
|
+
&& record.argv.every(argument => isStringWithinCap(argument, LAST_ERROR_ARG_MAX_CHARS, record.argvTruncated))
|
|
67
|
+
&& isStringWithinCap(record.message, LAST_ERROR_MESSAGE_MAX_CHARS, record.messageTruncated)))
|
|
68
|
+
|
|
69
|
+
const publication = (configDirectory: string, serialized: string) => Effect.gen(function*() {
|
|
70
|
+
yield* file('create directory', () => mkdirSync(configDirectory, { recursive: true, mode: 0o700 }))
|
|
71
|
+
yield* file('protect directory', () => chmodSync(configDirectory, 0o700))
|
|
72
|
+
const path = join(configDirectory, LAST_ERROR_FILENAME)
|
|
73
|
+
// Write a private temporary file, then rename it over the record, so a
|
|
74
|
+
// reader never sees a half-written record and a failed write keeps the old one.
|
|
75
|
+
yield* Effect.acquireUseRelease(
|
|
76
|
+
Effect.sync(() => join(
|
|
77
|
+
configDirectory,
|
|
78
|
+
`.${LAST_ERROR_FILENAME}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`
|
|
79
|
+
)),
|
|
80
|
+
temporaryPath => Effect.gen(function*() {
|
|
81
|
+
yield* Effect.acquireUseRelease(
|
|
82
|
+
file('open temporary record', () => openSync(temporaryPath, 'wx', 0o600)),
|
|
83
|
+
descriptor => Effect.gen(function*() {
|
|
84
|
+
yield* file('write temporary record', () => writeFileSync(descriptor, serialized, 'utf8'))
|
|
85
|
+
yield* file('flush temporary record', () => fsyncSync(descriptor))
|
|
86
|
+
}),
|
|
87
|
+
descriptor => Effect.sync(() => closeSync(descriptor)),
|
|
88
|
+
)
|
|
89
|
+
yield* file('protect temporary record', () => chmodSync(temporaryPath, 0o600))
|
|
90
|
+
yield* file('publish record', () => renameSync(temporaryPath, path))
|
|
91
|
+
yield* file('protect published record', () => chmodSync(path, 0o600))
|
|
92
|
+
}),
|
|
93
|
+
temporaryPath => Effect.sync(() => rmSync(temporaryPath, { force: true })),
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export function publishLastErrorRecord(configDirectory: string, serialized: string): Promise<void> {
|
|
98
|
+
return Effect.runPromise(publication(configDirectory, serialized))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const loading = (path: string) => Effect.gen(function*() {
|
|
102
|
+
const stat = yield* file('inspect record', () => statSync(path))
|
|
103
|
+
if (stat.size > LAST_ERROR_FILE_MAX_BYTES) return null
|
|
104
|
+
const text = yield* file('read record', () => readFileSync(path, 'utf8'))
|
|
105
|
+
const raw = yield* file('parse record', () => JSON.parse(text) as unknown)
|
|
106
|
+
const record = yield* Schema.decodeUnknownEffect(LastErrorRecordSchema)(raw)
|
|
107
|
+
// The Schema checks the known fields; the reader has always kept any others.
|
|
108
|
+
const admitted: LastErrorRecord = {
|
|
109
|
+
...(raw as Record<string, unknown>),
|
|
110
|
+
...record,
|
|
111
|
+
argv: [...record.argv],
|
|
112
|
+
code: record.code ?? null,
|
|
113
|
+
serverBaseUrl: boundedServerBaseUrl(record.serverBaseUrl),
|
|
114
|
+
teamSlug: record.teamSlug ?? null,
|
|
115
|
+
}
|
|
116
|
+
return admitted
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export function loadLastErrorRecord(path: string): Promise<LastErrorRecord | null> {
|
|
120
|
+
return Effect.runPromise(loading(path).pipe(Effect.orElseSucceed(() => null)))
|
|
121
|
+
}
|
package/src/preview-runtime.ts
CHANGED
|
@@ -112,8 +112,8 @@ interface PreviewPage {
|
|
|
112
112
|
screenshot: (options: { path: string; fullPage: boolean }) => Promise<unknown>
|
|
113
113
|
waitForTimeout: (timeout: number) => Promise<unknown>
|
|
114
114
|
once?: (event: string, listener: () => void) => unknown
|
|
115
|
-
on?: (event: string, listener: () => void) => unknown
|
|
116
|
-
off?: (event: string, listener: () => void) => unknown
|
|
115
|
+
on?: (event: string, listener: (error: Error) => void) => unknown
|
|
116
|
+
off?: (event: string, listener: (error: Error) => void) => unknown
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
@@ -256,19 +256,32 @@ export async function runLocalAppPreview(options: AppPreviewOptions): Promise<Ap
|
|
|
256
256
|
headless: !options.headed,
|
|
257
257
|
...(chromiumExecutablePath ? { executablePath: chromiumExecutablePath } : {}),
|
|
258
258
|
})
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
let page: PreviewPage | null = null
|
|
260
|
+
let handleRenderError: ((error: Error) => void) | undefined
|
|
261
|
+
let hasFinishedKeepingOpen = false
|
|
261
262
|
try {
|
|
262
|
-
await
|
|
263
|
-
|
|
263
|
+
page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
|
264
|
+
const renderError = new Promise<never>((_, reject) => {
|
|
265
|
+
handleRenderError = error => reject(new Error(`App preview render failed: ${error.message}`))
|
|
266
|
+
page?.on?.('pageerror', handleRenderError)
|
|
267
|
+
})
|
|
268
|
+
// Listen before navigation: React can fail before the ready marker mounts.
|
|
269
|
+
await Promise.race([
|
|
270
|
+
page.goto(url, { waitUntil: 'networkidle' }).then(() =>
|
|
271
|
+
page!.waitForSelector('[data-orizu-preview-ready="true"]', { timeout: 15_000 })
|
|
272
|
+
),
|
|
273
|
+
renderError,
|
|
274
|
+
])
|
|
264
275
|
if (options.screenshotPath) {
|
|
265
276
|
await page.screenshot({ path: options.screenshotPath, fullPage: true })
|
|
266
277
|
}
|
|
267
278
|
if (options.keepOpen) {
|
|
268
279
|
await waitForPreviewClose(page, browser)
|
|
280
|
+
hasFinishedKeepingOpen = true
|
|
269
281
|
}
|
|
270
282
|
} finally {
|
|
271
|
-
if (
|
|
283
|
+
if (handleRenderError) page?.off?.('pageerror', handleRenderError)
|
|
284
|
+
if (!hasFinishedKeepingOpen) {
|
|
272
285
|
await browser.close()
|
|
273
286
|
}
|
|
274
287
|
}
|
|
@@ -826,7 +839,8 @@ export default TextContent
|
|
|
826
839
|
import React from 'react'
|
|
827
840
|
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
828
841
|
export function CodeBlock({ code, children, className, ...props }) {
|
|
829
|
-
|
|
842
|
+
const codeText = Array.isArray(code) ? code.map(line => line.content).join('\\n') : code
|
|
843
|
+
return <pre {...props} className={cn('overflow-auto rounded-md border bg-muted p-3 font-mono text-xs', className)}><code>{children ?? codeText}</code></pre>
|
|
830
844
|
}
|
|
831
845
|
export default CodeBlock
|
|
832
846
|
`
|
|
@@ -843,8 +857,24 @@ export default Prose
|
|
|
843
857
|
const contentRendererSource = `
|
|
844
858
|
import React from 'react'
|
|
845
859
|
function cn(...values) { return values.filter(Boolean).join(' ') }
|
|
846
|
-
export function ContentRenderer({ content,
|
|
847
|
-
|
|
860
|
+
export function ContentRenderer({ contentType, content, maxHeight = '400px', customRenderer, className }) {
|
|
861
|
+
let renderedContent
|
|
862
|
+
switch (contentType) {
|
|
863
|
+
case 'image':
|
|
864
|
+
renderedContent = <div className="flex items-center justify-center p-2"><img src={content} alt="Content" width={800} height={600} className="max-w-full object-contain" style={{ maxHeight, height: 'auto', width: 'auto' }} /></div>
|
|
865
|
+
break
|
|
866
|
+
case 'video':
|
|
867
|
+
renderedContent = <div className="flex h-full justify-center p-2"><video src={content} controls className="max-h-full max-w-full" /></div>
|
|
868
|
+
break
|
|
869
|
+
case 'custom':
|
|
870
|
+
renderedContent = customRenderer ? customRenderer({ content }) : <p className="text-destructive">Custom renderer required for custom content type</p>
|
|
871
|
+
break
|
|
872
|
+
default:
|
|
873
|
+
renderedContent = <p>Invalid content type</p>
|
|
874
|
+
}
|
|
875
|
+
return maxHeight
|
|
876
|
+
? <div className={cn('overflow-auto', className)} style={{ maxHeight }}>{renderedContent}</div>
|
|
877
|
+
: renderedContent
|
|
848
878
|
}
|
|
849
879
|
export default ContentRenderer
|
|
850
880
|
`
|