@defrex/autobuild 0.8.0 → 0.10.0
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 +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem and note plumbing for `ab ticket attach` / `asset get` /
|
|
3
|
+
* `asset rm` (SPEC §8.8). The store owns the bytes and the validation
|
|
4
|
+
* (store/ticket-assets.ts); this module owns the two edges that touch the
|
|
5
|
+
* local filesystem and the one outward projection: the note left on the
|
|
6
|
+
* ticket in its source (SPEC §13).
|
|
7
|
+
*/
|
|
8
|
+
import { lstat, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
9
|
+
import { basename, dirname, join, resolve, sep } from 'node:path'
|
|
10
|
+
import {
|
|
11
|
+
ticketAssetEntriesError,
|
|
12
|
+
ticketAssetSizeError,
|
|
13
|
+
validateTicketAssetPath,
|
|
14
|
+
validateTicketAssetStructure,
|
|
15
|
+
type TicketAsset,
|
|
16
|
+
type TicketAssetEntryInput,
|
|
17
|
+
type TicketAssetInput,
|
|
18
|
+
type TicketAssetLayout,
|
|
19
|
+
type TicketAssetLimits,
|
|
20
|
+
type TicketAssetMeta,
|
|
21
|
+
} from '../store/ticket-assets'
|
|
22
|
+
|
|
23
|
+
export type TicketAssetNoteEvent = 'attached' | 'replaced' | 'removed'
|
|
24
|
+
|
|
25
|
+
export function formatAssetSize(bytes: number): string {
|
|
26
|
+
if (bytes < 1024) return `${bytes} B`
|
|
27
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
|
|
28
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function plural(count: number, word: string): string {
|
|
32
|
+
return `${count} ${word}${count === 1 ? '' : 's'}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The note left on the ticket in its source. Plain Markdown with backticks
|
|
37
|
+
* (no bold, no source-specific syntax) so every source and plugin renders it.
|
|
38
|
+
* It names the asset and how to fetch it, and states that the contents are in
|
|
39
|
+
* the Autobuild store, not in this ticket.
|
|
40
|
+
*/
|
|
41
|
+
export function ticketAssetNote(
|
|
42
|
+
event: TicketAssetNoteEvent,
|
|
43
|
+
meta: TicketAssetMeta,
|
|
44
|
+
cli = 'ab',
|
|
45
|
+
): string {
|
|
46
|
+
const fileCount = meta.entries.filter((entry) => entry.type === 'file').length
|
|
47
|
+
const label = `\`${meta.kind}/${meta.name}\``
|
|
48
|
+
const get = `${cli} ticket asset get ${meta.ticketId} ${meta.kind} ${meta.name} <dest>`
|
|
49
|
+
const contents = `${plural(fileCount, 'file')}, ${formatAssetSize(meta.size)}`
|
|
50
|
+
switch (event) {
|
|
51
|
+
case 'attached':
|
|
52
|
+
return [
|
|
53
|
+
`Ticket asset attached: ${label} (revision ${meta.revision}, ${contents}).`,
|
|
54
|
+
'',
|
|
55
|
+
'The contents live in the Autobuild store, not in this ticket.',
|
|
56
|
+
`Download the latest revision: \`${get}\``,
|
|
57
|
+
`Download this exact revision: \`${get} --rev ${meta.revision}\``,
|
|
58
|
+
].join('\n')
|
|
59
|
+
case 'replaced':
|
|
60
|
+
return [
|
|
61
|
+
`Ticket asset replaced: ${label} is now revision ${meta.revision} (${contents}).`,
|
|
62
|
+
'',
|
|
63
|
+
'The contents live in the Autobuild store, not in this ticket. Earlier revisions stay retrievable.',
|
|
64
|
+
`Download the latest revision: \`${get}\``,
|
|
65
|
+
`Download this exact revision: \`${get} --rev ${meta.revision}\``,
|
|
66
|
+
].join('\n')
|
|
67
|
+
case 'removed':
|
|
68
|
+
return [
|
|
69
|
+
`Ticket asset removed: ${label} is no longer one of this ticket's current assets (last revision ${meta.revision}).`,
|
|
70
|
+
'',
|
|
71
|
+
'The contents live in the Autobuild store, not in this ticket. That revision stays retrievable by number.',
|
|
72
|
+
`Download it: \`${get} --rev ${meta.revision}\``,
|
|
73
|
+
].join('\n')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AssetWalk {
|
|
78
|
+
layout: TicketAssetLayout
|
|
79
|
+
/** Default asset name: the path's basename. */
|
|
80
|
+
basename: string
|
|
81
|
+
files: { path: string; abs: string; size: number }[]
|
|
82
|
+
dirs: string[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Walk a file or folder for attachment. Symlinks and non-regular files are
|
|
87
|
+
* refused. The folder root is never an entry — it is represented only by the
|
|
88
|
+
* `tree` layout — so every entry path is relative, with no empty or `.`
|
|
89
|
+
* segment, and an empty folder walks to zero entries.
|
|
90
|
+
*/
|
|
91
|
+
export async function walkAssetPath(path: string): Promise<AssetWalk> {
|
|
92
|
+
const root = resolve(path)
|
|
93
|
+
const info = await lstat(root).catch((error: NodeJS.ErrnoException) => {
|
|
94
|
+
if (error.code === 'ENOENT') throw new Error(`${path}: no such file or directory`)
|
|
95
|
+
throw error
|
|
96
|
+
})
|
|
97
|
+
if (info.isSymbolicLink()) {
|
|
98
|
+
throw new Error(`${path}: symbolic links cannot be attached — attach the real file or folder`)
|
|
99
|
+
}
|
|
100
|
+
if (info.isFile()) {
|
|
101
|
+
return {
|
|
102
|
+
layout: 'file',
|
|
103
|
+
basename: basename(root),
|
|
104
|
+
files: [{ path: basename(root), abs: root, size: info.size }],
|
|
105
|
+
dirs: [],
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!info.isDirectory()) {
|
|
109
|
+
throw new Error(`${path}: only regular files and folders can be attached`)
|
|
110
|
+
}
|
|
111
|
+
const files: AssetWalk['files'] = []
|
|
112
|
+
const dirs: string[] = []
|
|
113
|
+
const visit = async (abs: string, rel: string): Promise<void> => {
|
|
114
|
+
const names = (await readdir(abs)).sort()
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
const childAbs = join(abs, name)
|
|
117
|
+
const childRel = rel === '' ? name : `${rel}/${name}`
|
|
118
|
+
const child = await lstat(childAbs)
|
|
119
|
+
if (child.isSymbolicLink()) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${childAbs}: symbolic links cannot be attached — remove it or attach its target`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
if (child.isDirectory()) {
|
|
125
|
+
dirs.push(childRel)
|
|
126
|
+
await visit(childAbs, childRel)
|
|
127
|
+
} else if (child.isFile()) {
|
|
128
|
+
files.push({ path: childRel, abs: childAbs, size: child.size })
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error(`${childAbs}: only regular files and folders can be attached`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await visit(root, '')
|
|
135
|
+
return { layout: 'tree', basename: basename(root), files, dirs }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build the put input from a walk. Everything the stat sizes can refuse —
|
|
140
|
+
* names, paths, structure, entry count, total bytes — is refused here, before
|
|
141
|
+
* any file's bytes are read.
|
|
142
|
+
*/
|
|
143
|
+
export async function buildAssetInput(
|
|
144
|
+
walk: AssetWalk,
|
|
145
|
+
asset: { kind: string; name: string },
|
|
146
|
+
limits: TicketAssetLimits,
|
|
147
|
+
): Promise<TicketAssetInput> {
|
|
148
|
+
const entryCount = walk.files.length + walk.dirs.length
|
|
149
|
+
if (entryCount > limits.maxEntries) throw ticketAssetEntriesError(entryCount, limits.maxEntries)
|
|
150
|
+
const total = walk.files.reduce((sum, file) => sum + file.size, 0)
|
|
151
|
+
if (total > limits.maxBytes) throw ticketAssetSizeError(total, limits.maxBytes)
|
|
152
|
+
validateTicketAssetStructure(
|
|
153
|
+
{
|
|
154
|
+
kind: asset.kind,
|
|
155
|
+
name: asset.name,
|
|
156
|
+
layout: walk.layout,
|
|
157
|
+
entries: [
|
|
158
|
+
...walk.dirs.map((path) => ({ type: 'dir' as const, path })),
|
|
159
|
+
...walk.files.map((file) => ({ type: 'file' as const, path: file.path, size: file.size })),
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
limits,
|
|
163
|
+
)
|
|
164
|
+
const entries: TicketAssetEntryInput[] = walk.dirs.map((path) => ({ type: 'dir', path }))
|
|
165
|
+
for (const file of walk.files) {
|
|
166
|
+
entries.push({
|
|
167
|
+
type: 'file',
|
|
168
|
+
path: file.path,
|
|
169
|
+
content: new Uint8Array(await readFile(file.abs)),
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
return { kind: asset.kind, name: asset.name, layout: walk.layout, entries }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Resolve a stored entry path under `root`, refusing anything that would land
|
|
176
|
+
* outside it. Stored paths are never trusted, even though upload validates. */
|
|
177
|
+
function guardedTarget(root: string, path: string): string {
|
|
178
|
+
validateTicketAssetPath(path)
|
|
179
|
+
const target = resolve(root, path)
|
|
180
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
181
|
+
throw new Error(`ticket asset path "${path}" escapes the destination`)
|
|
182
|
+
}
|
|
183
|
+
return target
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Refuse to write through a symlink: every existing component of `target`
|
|
187
|
+
* below `root`, and `target` itself, must not be one. Containment is checked
|
|
188
|
+
* against the real filesystem, since mkdir and writeFile follow links. */
|
|
189
|
+
async function assertNoSymlinks(root: string, target: string): Promise<void> {
|
|
190
|
+
const rel = target === root ? '' : target.slice(root.length + 1)
|
|
191
|
+
let current = root
|
|
192
|
+
const parts = rel === '' ? [] : rel.split(sep)
|
|
193
|
+
for (const part of ['', ...parts]) {
|
|
194
|
+
if (part !== '') current = join(current, part)
|
|
195
|
+
if (current === root) continue
|
|
196
|
+
const info = await lstat(current).catch(() => null)
|
|
197
|
+
if (info === null) return
|
|
198
|
+
if (info.isSymbolicLink()) {
|
|
199
|
+
throw new Error(`${current}: refusing to write through a symbolic link in the destination`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Write a downloaded asset to `dest`. A `file` asset becomes the file `dest`
|
|
206
|
+
* (or `dest/<filename>` when `dest` is an existing directory); a `tree` asset
|
|
207
|
+
* becomes the directory `dest` itself — created even when empty — with every
|
|
208
|
+
* `dir` entry made, then every file written.
|
|
209
|
+
*/
|
|
210
|
+
export async function writeAssetTo(asset: TicketAsset, dest: string): Promise<string> {
|
|
211
|
+
const root = resolve(dest)
|
|
212
|
+
if (asset.meta.layout === 'file') {
|
|
213
|
+
const file = asset.entries.find((entry) => entry.type === 'file')
|
|
214
|
+
if (file === undefined || file.type !== 'file') {
|
|
215
|
+
throw new Error(`ticket asset ${asset.meta.kind}/${asset.meta.name} holds no file`)
|
|
216
|
+
}
|
|
217
|
+
const isDir = await stat(root)
|
|
218
|
+
.then((info) => info.isDirectory())
|
|
219
|
+
.catch(() => false)
|
|
220
|
+
const target = isDir ? guardedTarget(root, file.path) : root
|
|
221
|
+
if (isDir) await assertNoSymlinks(root, target)
|
|
222
|
+
else {
|
|
223
|
+
const existing = await lstat(target).catch(() => null)
|
|
224
|
+
if (existing?.isSymbolicLink()) {
|
|
225
|
+
throw new Error(`${target}: refusing to write through a symbolic link`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await mkdir(dirname(target), { recursive: true })
|
|
229
|
+
await writeFile(target, file.content)
|
|
230
|
+
return target
|
|
231
|
+
}
|
|
232
|
+
await mkdir(root, { recursive: true })
|
|
233
|
+
for (const entry of asset.entries) {
|
|
234
|
+
if (entry.type !== 'dir') continue
|
|
235
|
+
const target = guardedTarget(root, entry.path)
|
|
236
|
+
await assertNoSymlinks(root, target)
|
|
237
|
+
await mkdir(target, { recursive: true })
|
|
238
|
+
}
|
|
239
|
+
for (const entry of asset.entries) {
|
|
240
|
+
if (entry.type !== 'file') continue
|
|
241
|
+
const target = guardedTarget(root, entry.path)
|
|
242
|
+
await assertNoSymlinks(root, target)
|
|
243
|
+
await mkdir(dirname(target), { recursive: true })
|
|
244
|
+
await writeFile(target, entry.content)
|
|
245
|
+
}
|
|
246
|
+
return root
|
|
247
|
+
}
|