@naxodev/apnea 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/SECURITY.md +36 -0
- package/briefs/orchestrator.md +4 -3
- package/dist/cli.js +8375 -15093
- package/docs/protocol/artifacts.md +18 -2
- package/docs/protocol/config.md +15 -3
- package/docs/protocol/manual-gate.md +8 -8
- package/docs/protocol/overview.md +17 -4
- package/extension/adapters/commit.ts +5 -1
- package/extension/adapters/dispatch.ts +9 -1
- package/extension/adapters/setup.ts +15 -1
- package/extension/adapters/start.ts +5 -1
- package/extension/adapters/status.ts +17 -2
- package/extension/adapters/wait.ts +6 -1
- package/extension/api.ts +7 -1
- package/extension/cli/main.ts +67 -7
- package/extension/cli/parse.ts +172 -5
- package/extension/domain/paths.ts +2 -11
- package/extension/domain/timeouts.ts +4 -0
- package/extension/domain/types.ts +65 -3
- package/extension/errors.ts +51 -16
- package/extension/operation-hooks.ts +6 -0
- package/extension/registry.ts +29 -15
- package/extension/run-tool.ts +19 -2
- package/extension/schema/config.ts +58 -16
- package/extension/schema/frontmatter.ts +57 -0
- package/extension/schema/state.ts +210 -13
- package/extension/services/app-live.ts +2 -1
- package/extension/services/config.ts +6 -4
- package/extension/services/file-system.ts +346 -75
- package/extension/services/herdr.ts +466 -260
- package/extension/services/operation-lock.ts +452 -0
- package/extension/services/process.ts +477 -0
- package/extension/services/run-store.ts +38 -16
- package/extension/services/vcs.ts +1258 -328
- package/extension/workflows/commit.ts +214 -13
- package/extension/workflows/dispatch.ts +305 -67
- package/extension/workflows/setup.ts +59 -32
- package/extension/workflows/start.ts +6 -4
- package/extension/workflows/status.ts +2 -2
- package/extension/workflows/wait.ts +62 -77
- package/package.json +2 -2
- package/schemas/config.schema.json +5 -1
- package/schemas/state.schema.json +165 -11
|
@@ -4,6 +4,8 @@ import { randomUUID } from "node:crypto"
|
|
|
4
4
|
import { Context, Effect, Layer } from "effect"
|
|
5
5
|
import { ConfigError } from "../errors.ts"
|
|
6
6
|
|
|
7
|
+
export const PERSISTED_INPUT_MAX_BYTES = 1024 * 1024
|
|
8
|
+
|
|
7
9
|
export interface FileSystemService {
|
|
8
10
|
readonly readFile: (path: string) => Effect.Effect<string>
|
|
9
11
|
readonly writeFile: (path: string, content: string) => Effect.Effect<void>
|
|
@@ -12,6 +14,38 @@ export interface FileSystemService {
|
|
|
12
14
|
destination: string,
|
|
13
15
|
content: string,
|
|
14
16
|
) => Effect.Effect<void, ConfigError>
|
|
17
|
+
readonly writeTrustedGlobalFile: (
|
|
18
|
+
accountHome: string,
|
|
19
|
+
destination: string,
|
|
20
|
+
content: string,
|
|
21
|
+
) => Effect.Effect<void, ConfigError>
|
|
22
|
+
readonly readTrustedGlobalFile: (
|
|
23
|
+
accountHome: string,
|
|
24
|
+
source: string,
|
|
25
|
+
limit?: number,
|
|
26
|
+
) => Effect.Effect<string, ConfigError>
|
|
27
|
+
readonly readProjectFile: (
|
|
28
|
+
root: string,
|
|
29
|
+
source: string,
|
|
30
|
+
limit?: number,
|
|
31
|
+
) => Effect.Effect<string, ConfigError>
|
|
32
|
+
readonly projectPathExists: (
|
|
33
|
+
root: string,
|
|
34
|
+
destination: string,
|
|
35
|
+
) => Effect.Effect<boolean, ConfigError>
|
|
36
|
+
readonly mkdirProject: (
|
|
37
|
+
root: string,
|
|
38
|
+
destination: string,
|
|
39
|
+
) => Effect.Effect<void, ConfigError>
|
|
40
|
+
readonly renameProjectFile: (
|
|
41
|
+
root: string,
|
|
42
|
+
from: string,
|
|
43
|
+
to: string,
|
|
44
|
+
) => Effect.Effect<void, ConfigError>
|
|
45
|
+
readonly removeProjectFile: (
|
|
46
|
+
root: string,
|
|
47
|
+
destination: string,
|
|
48
|
+
) => Effect.Effect<void, ConfigError>
|
|
15
49
|
readonly rename: (from: string, to: string) => Effect.Effect<void>
|
|
16
50
|
readonly exists: (path: string) => Effect.Effect<boolean>
|
|
17
51
|
readonly mkdir: (
|
|
@@ -31,6 +65,228 @@ export class FileSystem extends Context.Service<
|
|
|
31
65
|
FileSystemService
|
|
32
66
|
>()("apnea/FileSystem") {}
|
|
33
67
|
|
|
68
|
+
function secureProjectPath(root: string, destination: string): string {
|
|
69
|
+
const lexicalRoot = path.resolve(root)
|
|
70
|
+
const lexicalTarget = path.resolve(destination)
|
|
71
|
+
const relative = path.relative(lexicalRoot, lexicalTarget)
|
|
72
|
+
if (
|
|
73
|
+
relative === "" ||
|
|
74
|
+
relative === ".." ||
|
|
75
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
76
|
+
path.isAbsolute(relative)
|
|
77
|
+
) {
|
|
78
|
+
throw new ConfigError({
|
|
79
|
+
message: "project file destination must be below project root",
|
|
80
|
+
path: lexicalTarget,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const projectRoot = fs.realpathSync(lexicalRoot)
|
|
85
|
+
const target = path.join(projectRoot, relative)
|
|
86
|
+
let current = projectRoot
|
|
87
|
+
const components = relative.split(path.sep)
|
|
88
|
+
for (const [index, component] of components.entries()) {
|
|
89
|
+
current = path.join(current, component)
|
|
90
|
+
try {
|
|
91
|
+
const stat = fs.lstatSync(current)
|
|
92
|
+
if (stat.isSymbolicLink()) {
|
|
93
|
+
throw new ConfigError({
|
|
94
|
+
message: `refusing symlink below project root: ${current}`,
|
|
95
|
+
path: current,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (index < components.length - 1 && !stat.isDirectory()) {
|
|
99
|
+
throw new ConfigError({
|
|
100
|
+
message: `project path component is not a safe directory: ${current}`,
|
|
101
|
+
path: current,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return target
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createProjectDirectories(root: string, destination: string): void {
|
|
112
|
+
const target = secureProjectPath(root, destination)
|
|
113
|
+
const projectRoot = fs.realpathSync(path.resolve(root))
|
|
114
|
+
const relative = path.relative(projectRoot, target)
|
|
115
|
+
let current = projectRoot
|
|
116
|
+
for (const component of relative.split(path.sep)) {
|
|
117
|
+
current = path.join(current, component)
|
|
118
|
+
try {
|
|
119
|
+
const stat = fs.lstatSync(current)
|
|
120
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
121
|
+
throw new ConfigError({
|
|
122
|
+
message: `project path component is not a safe directory: ${current}`,
|
|
123
|
+
path: current,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
128
|
+
fs.mkdirSync(current, { mode: 0o700 })
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function trustedGlobalPath(accountHome: string, destination: string): string {
|
|
134
|
+
const lexicalHome = path.resolve(accountHome)
|
|
135
|
+
const lexicalTarget = path.resolve(destination)
|
|
136
|
+
const relative = path.relative(lexicalHome, lexicalTarget)
|
|
137
|
+
if (
|
|
138
|
+
relative === "" ||
|
|
139
|
+
relative === ".." ||
|
|
140
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
141
|
+
path.isAbsolute(relative)
|
|
142
|
+
) {
|
|
143
|
+
throw new ConfigError({
|
|
144
|
+
message: "trusted global file destination must be below account home",
|
|
145
|
+
path: lexicalTarget,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const home = fs.realpathSync(lexicalHome)
|
|
150
|
+
const target = path.join(home, relative)
|
|
151
|
+
let current = home
|
|
152
|
+
const components = relative.split(path.sep)
|
|
153
|
+
for (const [index, component] of components.entries()) {
|
|
154
|
+
current = path.join(current, component)
|
|
155
|
+
try {
|
|
156
|
+
const stat = fs.lstatSync(current)
|
|
157
|
+
if (stat.isSymbolicLink()) {
|
|
158
|
+
throw new ConfigError({
|
|
159
|
+
message: `refusing symlink below trusted account home: ${current}`,
|
|
160
|
+
path: current,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
if (index < components.length - 1 && !stat.isDirectory()) {
|
|
164
|
+
throw new ConfigError({
|
|
165
|
+
message: `trusted global path component is not a safe directory: ${current}`,
|
|
166
|
+
path: current,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return target
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createTrustedGlobalDirectories(
|
|
177
|
+
accountHome: string,
|
|
178
|
+
destination: string,
|
|
179
|
+
): void {
|
|
180
|
+
const target = trustedGlobalPath(accountHome, destination)
|
|
181
|
+
const home = fs.realpathSync(path.resolve(accountHome))
|
|
182
|
+
const relative = path.relative(home, target)
|
|
183
|
+
let current = home
|
|
184
|
+
for (const component of relative.split(path.sep)) {
|
|
185
|
+
current = path.join(current, component)
|
|
186
|
+
try {
|
|
187
|
+
const stat = fs.lstatSync(current)
|
|
188
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
189
|
+
throw new ConfigError({
|
|
190
|
+
message: `trusted global path component is not a safe directory: ${current}`,
|
|
191
|
+
path: current,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
196
|
+
fs.mkdirSync(current)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function projectEffect<A>(operation: () => A): Effect.Effect<A, ConfigError> {
|
|
202
|
+
return Effect.try({ try: operation, catch: (error) => error }).pipe(
|
|
203
|
+
Effect.catch((error) =>
|
|
204
|
+
error instanceof ConfigError ? Effect.fail(error) : Effect.die(error),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readRegularUtf8(target: string, limit: number): string {
|
|
210
|
+
if (
|
|
211
|
+
!Number.isSafeInteger(limit) ||
|
|
212
|
+
limit < 0 ||
|
|
213
|
+
limit > PERSISTED_INPUT_MAX_BYTES
|
|
214
|
+
) {
|
|
215
|
+
throw new ConfigError({
|
|
216
|
+
message: `persisted input byte limit must be between 0 and ${PERSISTED_INPUT_MAX_BYTES}`,
|
|
217
|
+
path: target,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
let descriptor: number | undefined
|
|
221
|
+
try {
|
|
222
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0
|
|
223
|
+
const nonBlocking = fs.constants.O_NONBLOCK ?? 0
|
|
224
|
+
descriptor = fs.openSync(
|
|
225
|
+
target,
|
|
226
|
+
fs.constants.O_RDONLY | noFollow | nonBlocking,
|
|
227
|
+
)
|
|
228
|
+
if (!fs.fstatSync(descriptor).isFile()) {
|
|
229
|
+
throw new ConfigError({
|
|
230
|
+
message: `persisted input is not a regular file: ${target}`,
|
|
231
|
+
path: target,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const bytes = Buffer.allocUnsafe(limit + 1)
|
|
236
|
+
let offset = 0
|
|
237
|
+
while (offset < bytes.length) {
|
|
238
|
+
const count = fs.readSync(
|
|
239
|
+
descriptor,
|
|
240
|
+
bytes,
|
|
241
|
+
offset,
|
|
242
|
+
bytes.length - offset,
|
|
243
|
+
null,
|
|
244
|
+
)
|
|
245
|
+
if (count === 0) break
|
|
246
|
+
offset += count
|
|
247
|
+
}
|
|
248
|
+
if (offset > limit) {
|
|
249
|
+
throw new ConfigError({
|
|
250
|
+
message: `persisted input exceeds ${limit} byte limit: ${target}`,
|
|
251
|
+
path: target,
|
|
252
|
+
details: { limit_bytes: limit },
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
257
|
+
bytes.subarray(0, offset),
|
|
258
|
+
)
|
|
259
|
+
} catch {
|
|
260
|
+
throw new ConfigError({
|
|
261
|
+
message: `persisted input is not valid UTF-8: ${target}`,
|
|
262
|
+
path: target,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function fsyncParentDirectory(target: string): void {
|
|
271
|
+
let descriptor: number | undefined
|
|
272
|
+
try {
|
|
273
|
+
descriptor = fs.openSync(path.dirname(target), fs.constants.O_RDONLY)
|
|
274
|
+
fs.fsyncSync(descriptor)
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
277
|
+
// POSIX permits EINVAL/ENOTSUP for unsupported directory fsync. Windows
|
|
278
|
+
// additionally rejects directory descriptors with EISDIR/EPERM.
|
|
279
|
+
const unsupported =
|
|
280
|
+
["EINVAL", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "") ||
|
|
281
|
+
(process.platform === "win32" && ["EISDIR", "EPERM"].includes(code ?? ""))
|
|
282
|
+
if (!unsupported) {
|
|
283
|
+
throw error
|
|
284
|
+
}
|
|
285
|
+
} finally {
|
|
286
|
+
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
34
290
|
export const FileSystemLive = Layer.succeed(
|
|
35
291
|
FileSystem,
|
|
36
292
|
FileSystem.of({
|
|
@@ -49,90 +305,105 @@ export const FileSystemLive = Layer.succeed(
|
|
|
49
305
|
}).pipe(Effect.orDie),
|
|
50
306
|
|
|
51
307
|
writeProjectFile: (root, destination, content) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
308
|
+
projectEffect(() => {
|
|
309
|
+
const target = secureProjectPath(root, destination)
|
|
310
|
+
if (path.resolve(path.dirname(destination)) !== path.resolve(root)) {
|
|
311
|
+
createProjectDirectories(root, path.dirname(destination))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const temporary = path.join(
|
|
315
|
+
path.dirname(target),
|
|
316
|
+
`.${path.basename(target)}.${randomUUID()}.tmp`,
|
|
317
|
+
)
|
|
318
|
+
let descriptor: number | undefined
|
|
319
|
+
try {
|
|
320
|
+
descriptor = fs.openSync(temporary, "wx", 0o600)
|
|
321
|
+
fs.writeFileSync(descriptor, content, "utf8")
|
|
322
|
+
fs.fsyncSync(descriptor)
|
|
323
|
+
fs.closeSync(descriptor)
|
|
324
|
+
descriptor = undefined
|
|
325
|
+
fs.renameSync(temporary, target)
|
|
326
|
+
fsyncParentDirectory(target)
|
|
327
|
+
} finally {
|
|
328
|
+
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
329
|
+
fs.rmSync(temporary, { force: true })
|
|
330
|
+
}
|
|
331
|
+
}),
|
|
332
|
+
|
|
333
|
+
writeTrustedGlobalFile: (accountHome, destination, content) =>
|
|
334
|
+
projectEffect(() => {
|
|
335
|
+
const target = trustedGlobalPath(accountHome, destination)
|
|
336
|
+
createTrustedGlobalDirectories(accountHome, path.dirname(destination))
|
|
337
|
+
|
|
338
|
+
let mode = 0o666
|
|
339
|
+
let preserveMode = false
|
|
340
|
+
try {
|
|
341
|
+
const existing = fs.lstatSync(target)
|
|
342
|
+
if (existing.isSymbolicLink() || !existing.isFile()) {
|
|
63
343
|
throw new ConfigError({
|
|
64
|
-
message:
|
|
344
|
+
message: `trusted global destination is not a safe regular file: ${target}`,
|
|
65
345
|
path: target,
|
|
66
346
|
})
|
|
67
347
|
}
|
|
348
|
+
mode = existing.mode & 0o777
|
|
349
|
+
preserveMode = true
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
352
|
+
}
|
|
68
353
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
89
|
-
throw error
|
|
90
|
-
fs.mkdirSync(current)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
354
|
+
const temporary = path.join(
|
|
355
|
+
path.dirname(target),
|
|
356
|
+
`.${path.basename(target)}.${randomUUID()}.tmp`,
|
|
357
|
+
)
|
|
358
|
+
let descriptor: number | undefined
|
|
359
|
+
try {
|
|
360
|
+
descriptor = fs.openSync(temporary, "wx", mode)
|
|
361
|
+
if (preserveMode) fs.fchmodSync(descriptor, mode)
|
|
362
|
+
fs.writeFileSync(descriptor, content, "utf8")
|
|
363
|
+
fs.fsyncSync(descriptor)
|
|
364
|
+
fs.closeSync(descriptor)
|
|
365
|
+
descriptor = undefined
|
|
366
|
+
fs.renameSync(temporary, target)
|
|
367
|
+
fsyncParentDirectory(target)
|
|
368
|
+
} finally {
|
|
369
|
+
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
370
|
+
fs.rmSync(temporary, { force: true })
|
|
371
|
+
}
|
|
372
|
+
}),
|
|
93
373
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
103
|
-
}
|
|
374
|
+
readTrustedGlobalFile: (
|
|
375
|
+
accountHome,
|
|
376
|
+
source,
|
|
377
|
+
limit = PERSISTED_INPUT_MAX_BYTES,
|
|
378
|
+
) =>
|
|
379
|
+
projectEffect(() =>
|
|
380
|
+
readRegularUtf8(trustedGlobalPath(accountHome, source), limit),
|
|
381
|
+
),
|
|
104
382
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
let descriptor: number | undefined
|
|
110
|
-
try {
|
|
111
|
-
descriptor = fs.openSync(
|
|
112
|
-
temporary,
|
|
113
|
-
fs.constants.O_WRONLY |
|
|
114
|
-
fs.constants.O_CREAT |
|
|
115
|
-
fs.constants.O_EXCL |
|
|
116
|
-
fs.constants.O_NOFOLLOW,
|
|
117
|
-
0o600,
|
|
118
|
-
)
|
|
119
|
-
fs.writeFileSync(descriptor, content, "utf8")
|
|
120
|
-
fs.fsyncSync(descriptor)
|
|
121
|
-
fs.closeSync(descriptor)
|
|
122
|
-
descriptor = undefined
|
|
123
|
-
fs.renameSync(temporary, target)
|
|
124
|
-
} finally {
|
|
125
|
-
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
126
|
-
fs.rmSync(temporary, { force: true })
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
catch: (e) => e,
|
|
130
|
-
}).pipe(
|
|
131
|
-
Effect.catch((error) =>
|
|
132
|
-
error instanceof ConfigError ? Effect.fail(error) : Effect.die(error),
|
|
133
|
-
),
|
|
383
|
+
readProjectFile: (root, source, limit = PERSISTED_INPUT_MAX_BYTES) =>
|
|
384
|
+
projectEffect(() =>
|
|
385
|
+
readRegularUtf8(secureProjectPath(root, source), limit),
|
|
134
386
|
),
|
|
135
387
|
|
|
388
|
+
projectPathExists: (root, destination) =>
|
|
389
|
+
projectEffect(() => fs.existsSync(secureProjectPath(root, destination))),
|
|
390
|
+
|
|
391
|
+
mkdirProject: (root, destination) =>
|
|
392
|
+
projectEffect(() => createProjectDirectories(root, destination)),
|
|
393
|
+
|
|
394
|
+
renameProjectFile: (root, from, to) =>
|
|
395
|
+
projectEffect(() => {
|
|
396
|
+
const source = secureProjectPath(root, from)
|
|
397
|
+
const destination = secureProjectPath(root, to)
|
|
398
|
+
createProjectDirectories(root, path.dirname(to))
|
|
399
|
+
fs.renameSync(source, destination)
|
|
400
|
+
}),
|
|
401
|
+
|
|
402
|
+
removeProjectFile: (root, destination) =>
|
|
403
|
+
projectEffect(() => {
|
|
404
|
+
fs.rmSync(secureProjectPath(root, destination), { force: true })
|
|
405
|
+
}),
|
|
406
|
+
|
|
136
407
|
rename: (from, to) =>
|
|
137
408
|
Effect.try({
|
|
138
409
|
try: () => {
|