@astrale-os/cli 1.0.0-beta.26 → 1.0.0-beta.28
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 +31 -5
- package/dist/astrale.js +1201 -358
- package/package.json +3 -2
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -0
- package/src/commands/__tests__/install-identity-override.test.ts +3 -3
- package/src/commands/domain/install.ts +19 -11
- package/src/commands/get.ts +1 -1
- package/src/commands/identity/register.ts +3 -4
- package/src/commands/logs.ts +2 -5
- package/src/commands/session/analyze.ts +26 -4
- package/src/commands/update.ts +61 -40
- package/src/lib/__tests__/skills.test.ts +809 -0
- package/src/lib/skills/lock.ts +117 -0
- package/src/lib/skills/sync.ts +814 -0
- package/src/lib/skills.ts +1 -52
- package/src/program/__tests__/program.test.ts +1 -1
- package/src/setup/steps/skills.ts +40 -38
- package/src/telemetry/__tests__/retention.test.ts +180 -0
- package/src/telemetry/__tests__/settings.test.ts +102 -0
- package/src/telemetry/__tests__/store-scan.test.ts +128 -0
- package/src/telemetry/__tests__/trigger.test.ts +33 -4
- package/src/telemetry/recorder.ts +6 -3
- package/src/telemetry/retention.ts +129 -0
- package/src/telemetry/session.ts +18 -12
- package/src/telemetry/settings.ts +64 -11
- package/src/telemetry/store.ts +87 -9
- package/src/telemetry/trigger.ts +16 -28
- package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
- package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
- package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
- package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
- package/studio/client/dist/index.html +3 -3
- package/studio/server/agent/prompts/anchors.test.ts +17 -4
- package/studio/server/agent/prompts/anchors.ts +3 -2
- package/studio/server/agent/prompts/system.test.ts +2 -3
- package/studio/server/agent/prompts/system.ts +3 -3
- package/studio/server/agent/run/preparation.ts +1 -1
- package/studio/server/api/context.ts +1 -1
- package/studio/server/api/deployment.ts +1 -1
- package/studio/server/api/views.ts +2 -2
- package/studio/server/api/workspace.ts +1 -1
- package/studio/server/cache.test.ts +3 -8
- package/studio/server/cache.ts +4 -45
- package/studio/server/cli-consumers.test.ts +31 -2
- package/studio/server/handoff/copy.ts +1 -1
- package/studio/server/introspect/canonical-schema.test.ts +154 -128
- package/studio/server/introspect/canonical-schema.ts +183 -302
- package/studio/server/introspect/core.ts +14 -21
- package/studio/server/introspect/extractor.ts +11 -15
- package/studio/server/introspect/overlay-tsmorph.test.ts +1 -5
- package/studio/server/introspect/overlay-tsmorph.ts +0 -1
- package/studio/server/introspect/overlay.ts +3 -24
- package/studio/server/introspect/revision.test.ts +24 -28
- package/studio/server/introspect/revision.ts +10 -21
- package/studio/server/introspect/runtime.test.ts +14 -14
- package/studio/server/introspect/runtime.ts +1 -46
- package/studio/server/lifecycle.ts +4 -1
- package/studio/server/state/documents.test.ts +69 -0
- package/studio/server/state/documents.ts +57 -10
- package/studio/server/workspace/updates.ts +23 -1
- package/studio/shared/contracts/runtime.ts +4 -0
- package/studio/shared/contracts/schema.ts +10 -26
- package/studio/shared/contracts/surface.test.ts +2 -2
- package/studio/shared/contracts/workspace.ts +3 -0
- package/studio/shared/schema/identity.ts +0 -1
- package/tsconfig.json +1 -1
- package/studio/client/dist/assets/index-C0FAnwNw.css +0 -2
- package/studio/client/dist/assets/index-OeJK1TjG.js +0 -81
- package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +0 -1
- package/studio/client/dist/assets/schema-studio-DRdfu_cQ.js +0 -8
- package/studio/server/introspect/core-extractor.ts +0 -30
- package/studio/server/introspect/overlay.test.ts +0 -44
- package/studio/server/introspect/source-overlay/annotations.ts +0 -14
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, rm, stat, unlink, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export type FileLockOptions = {
|
|
6
|
+
pollIntervalMs?: number
|
|
7
|
+
staleAfterMs?: number
|
|
8
|
+
timeoutMs?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Claim = { directory: string; pid: number; ticket: number | null; choosing: boolean }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serialize a cross-process transition with a filesystem bakery lock.
|
|
15
|
+
*
|
|
16
|
+
* Every contender owns a UUID directory for its entire lifetime. Dead claims are
|
|
17
|
+
* removed only by that unrepeatable name, so recovery never deletes a pathname a
|
|
18
|
+
* new live owner could have replaced between an ownership check and an unlink.
|
|
19
|
+
*/
|
|
20
|
+
export async function withFileLock<T>(
|
|
21
|
+
lockPath: string,
|
|
22
|
+
fn: () => Promise<T>,
|
|
23
|
+
opts: FileLockOptions = {},
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100
|
|
26
|
+
const staleAfterMs = opts.staleAfterMs ?? 30_000
|
|
27
|
+
const timeoutMs = opts.timeoutMs ?? 60_000
|
|
28
|
+
const deadline = Date.now() + timeoutMs
|
|
29
|
+
const claimsRoot = `${lockPath}.claims`
|
|
30
|
+
const token = randomUUID()
|
|
31
|
+
const claimDirectory = join(claimsRoot, token)
|
|
32
|
+
|
|
33
|
+
await mkdir(dirname(lockPath), { recursive: true })
|
|
34
|
+
await mkdir(claimsRoot, { recursive: true })
|
|
35
|
+
await mkdir(claimDirectory)
|
|
36
|
+
await writeFile(join(claimDirectory, 'owner.json'), JSON.stringify({ pid: process.pid }), {
|
|
37
|
+
mode: 0o600,
|
|
38
|
+
})
|
|
39
|
+
await writeFile(join(claimDirectory, 'choosing'), '', { mode: 0o600 })
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const claims = await liveClaims(claimsRoot, staleAfterMs)
|
|
43
|
+
const ticket = Math.max(0, ...claims.flatMap((claim) => claim.ticket ?? [])) + 1
|
|
44
|
+
await writeFile(join(claimDirectory, 'ticket'), String(ticket), { mode: 0o600 })
|
|
45
|
+
await unlink(join(claimDirectory, 'choosing'))
|
|
46
|
+
|
|
47
|
+
for (;;) {
|
|
48
|
+
const contenders = await liveClaims(claimsRoot, staleAfterMs)
|
|
49
|
+
const blocked = contenders.some(
|
|
50
|
+
(claim) =>
|
|
51
|
+
claim.directory !== claimDirectory &&
|
|
52
|
+
(claim.choosing ||
|
|
53
|
+
claim.ticket === null ||
|
|
54
|
+
claim.ticket < ticket ||
|
|
55
|
+
(claim.ticket === ticket && claim.directory < claimDirectory)),
|
|
56
|
+
)
|
|
57
|
+
if (!blocked) return await fn()
|
|
58
|
+
if (Date.now() >= deadline) throw new Error('Timed out waiting for another skill update')
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
await rm(claimDirectory, { recursive: true, force: true })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function liveClaims(root: string, staleAfterMs: number): Promise<Claim[]> {
|
|
67
|
+
const directories = (await readdir(root, { withFileTypes: true }).catch(() => []))
|
|
68
|
+
.filter((entry) => entry.isDirectory())
|
|
69
|
+
.map((entry) => join(root, entry.name))
|
|
70
|
+
const claims = await Promise.all(
|
|
71
|
+
directories.map(async (directory): Promise<Claim | null> => {
|
|
72
|
+
try {
|
|
73
|
+
const owner = JSON.parse(await readFile(join(directory, 'owner.json'), 'utf8')) as {
|
|
74
|
+
pid?: unknown
|
|
75
|
+
}
|
|
76
|
+
if (typeof owner.pid !== 'number') return await discardIfStale(directory, staleAfterMs)
|
|
77
|
+
if (!isPidAlive(owner.pid)) {
|
|
78
|
+
await rm(directory, { recursive: true, force: true })
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
const choosing = await lstat(join(directory, 'choosing'))
|
|
82
|
+
.then(() => true)
|
|
83
|
+
.catch(() => false)
|
|
84
|
+
const ticket = await readFile(join(directory, 'ticket'), 'utf8')
|
|
85
|
+
.then((value) => {
|
|
86
|
+
const parsed = Number(value)
|
|
87
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null
|
|
88
|
+
})
|
|
89
|
+
.catch(() => null)
|
|
90
|
+
return { directory, pid: owner.pid, ticket, choosing }
|
|
91
|
+
} catch {
|
|
92
|
+
return await discardIfStale(directory, staleAfterMs)
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
return claims.filter((claim): claim is Claim => claim !== null)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function discardIfStale(directory: string, staleAfterMs: number): Promise<null> {
|
|
100
|
+
try {
|
|
101
|
+
if (Date.now() - (await stat(directory)).mtimeMs > staleAfterMs) {
|
|
102
|
+
await rm(directory, { recursive: true, force: true })
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// A concurrently released unique claim is already gone.
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isPidAlive(pid: number): boolean {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(pid, 0)
|
|
113
|
+
return true
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return (error as { code?: string }).code === 'EPERM'
|
|
116
|
+
}
|
|
117
|
+
}
|