@hraness/dawg 0.6.1 → 0.7.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.
Files changed (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
package/src/media/cli.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { systemRunner, type CommandRunner } from "../auth/runner.ts";
18
18
  import { doctor, formatDoctor } from "./backend.ts";
19
+ import { analysisCacheStatus, analysisDoctorLine } from "../audio/analysis.ts";
19
20
  import { trackSlug } from "./paths.ts";
20
21
  import { MediaAbortError } from "./process.ts";
21
22
  import { findMediaTool } from "./registry.ts";
@@ -26,7 +27,7 @@ export type Output = { write(text: string): unknown };
26
27
 
27
28
  export const MEDIA_HELP = `dawg media — local media tools (see DAWG.md "Media tools")
28
29
 
29
- dawg media doctor backend + binaries + install commands
30
+ dawg media doctor backend + binaries + install commands + analysis cache
30
31
  dawg media download <url> [--name n] YouTube audio → tracks/<slug>/downloads/<n>.wav
31
32
  dawg media stems <file> six stems → <file>.stems/
32
33
  dawg media analyze <file> tempo, key, beat grid, peaks → <file>.analysis.json
@@ -68,6 +69,15 @@ export function parseMediaArgv(argv: readonly string[]): {
68
69
  json: boolean;
69
70
  help: boolean;
70
71
  } {
72
+ // `dawg media --help` (or `-h`): a leading help flag is not a verb.
73
+ if (argv[0] === "--help" || argv[0] === "-h")
74
+ return {
75
+ verb: undefined,
76
+ args: {},
77
+ track: "main",
78
+ json: false,
79
+ help: true,
80
+ };
71
81
  const [verb, ...rest] = argv;
72
82
  const args: Record<string, unknown> = {};
73
83
  const positional: string[] = [];
@@ -139,8 +149,15 @@ export async function runMediaCommand(
139
149
  ...host,
140
150
  ...(options.signal ? { signal: options.signal } : {}),
141
151
  });
142
- if (parsed.json) stdout.write(`${JSON.stringify(report, null, 2)}\n`);
143
- else stdout.write(`${formatDoctor(report).join("\n")}\n`);
152
+ const analysis = await analysisCacheStatus(cwd);
153
+ if (parsed.json)
154
+ stdout.write(
155
+ `${JSON.stringify({ ...report, analysisCache: analysis }, null, 2)}\n`,
156
+ );
157
+ else
158
+ stdout.write(
159
+ `${[...formatDoctor(report), await analysisDoctorLine(cwd)].join("\n")}\n`,
160
+ );
144
161
  return 0;
145
162
  }
146
163
  const spec = VERBS[parsed.verb];
@@ -38,6 +38,8 @@ export type ImportSampleArgs = Readonly<{
38
38
  end?: number;
39
39
  /** Root note name or MIDI number the sample is pitched at (default C4). */
40
40
  root?: string | number;
41
+ /** 1 folds to mono (0.7 audio clips); absent keeps today's stereo. */
42
+ channels?: 1 | 2;
41
43
  }>;
42
44
 
43
45
  export async function importSample(
@@ -68,7 +70,7 @@ export async function importSample(
68
70
  "-ar",
69
71
  String(SAMPLE_LIMITS.sampleRate),
70
72
  "-ac",
71
- "2",
73
+ String(args.channels ?? 2),
72
74
  "-c:a",
73
75
  "pcm_s16le",
74
76
  temp,
@@ -13,7 +13,9 @@ import {
13
13
  modalPairWarnings,
14
14
  } from "../../core/resonators.ts";
15
15
  import { plainSineWarnings } from "../audio/instrument-check.ts";
16
+ import { clipPinDiagnostics } from "./clip-pins.ts";
16
17
  import { isProject } from "./init.ts";
18
+ import { parseSimpleArgv } from "../argv.ts";
17
19
  import { typecheckProject } from "./typecheck.ts";
18
20
 
19
21
  export type CheckDeps = Readonly<{
@@ -41,12 +43,18 @@ export async function checkProject(
41
43
  (deps.typecheck ?? typecheckProject)(project),
42
44
  (deps.evaluate ?? evaluateProject)(project),
43
45
  ]);
46
+ // 0.7: a clip or take whose file no longer matches its pinned sha256
47
+ // fails like a type error.
48
+ const pins = evaluated.ok
49
+ ? await clipPinDiagnostics(project, evaluated.score)
50
+ : [];
44
51
  const diagnostics = [
45
52
  ...types.diagnostics,
46
53
  ...(evaluated.ok ? [] : evaluated.diagnostics),
54
+ ...pins,
47
55
  ];
48
56
  return Object.freeze({
49
- ok: types.ok && evaluated.ok,
57
+ ok: types.ok && evaluated.ok && pins.length === 0,
50
58
  diagnostics: Object.freeze(diagnostics),
51
59
  typesMs: types.ms,
52
60
  evalMs: evaluated.ms,
@@ -64,6 +72,9 @@ export async function checkProject(
64
72
  });
65
73
  }
66
74
 
75
+ export const CHECK_USAGE =
76
+ "usage: dawg check · typecheck and evaluate the project in this directory; exit 1 on problems";
77
+
67
78
  export async function runCheckCommand(
68
79
  argv: readonly string[],
69
80
  cwd: string,
@@ -71,6 +82,15 @@ export async function runCheckCommand(
71
82
  stderr: { write(text: string): unknown },
72
83
  deps: CheckDeps = {},
73
84
  ): Promise<number> {
85
+ const parsed = parseSimpleArgv(argv.slice(1), 0);
86
+ if (parsed.kind === "help") {
87
+ stdout.write(`${CHECK_USAGE}\n`);
88
+ return 0;
89
+ }
90
+ if (parsed.kind === "error") {
91
+ stderr.write(`${parsed.problem} · ${CHECK_USAGE}\n`);
92
+ return 2;
93
+ }
74
94
  const project = cwd;
75
95
  if (!(await isProject(project))) {
76
96
  stderr.write(
@@ -92,6 +112,5 @@ export async function runCheckCommand(
92
112
  }
93
113
  const count = report.diagnostics.length;
94
114
  stderr.write(`${count} problem${count === 1 ? "" : "s"}\n`);
95
- void argv;
96
115
  return 1;
97
116
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `dawg check` re-hashes every audio clip and take (0.7) and reports a file
3
+ * that is missing or no longer matches its pinned sha256, pointing at the
4
+ * track file the way a type error does.
5
+ */
6
+
7
+ import { createHash } from "node:crypto";
8
+ import { readFile, stat } from "node:fs/promises";
9
+ import { relative, resolve } from "node:path";
10
+ import type { Diagnostic } from "../../core/sdk/eval.ts";
11
+ import type { TrackScore } from "../../core/score.ts";
12
+ import { trackDirectories } from "../../core/sdk/print.ts";
13
+
14
+ const MAX_CLIP_FILE_BYTES = 50 * 1024 * 1024;
15
+
16
+ export async function clipPinDiagnostics(
17
+ project: string,
18
+ score: TrackScore,
19
+ ): Promise<Diagnostic[]> {
20
+ const out: Diagnostic[] = [];
21
+ const hashes = new Map<string, string | undefined>();
22
+ const hashOf = async (src: string): Promise<string | undefined> => {
23
+ if (hashes.has(src)) return hashes.get(src);
24
+ let hash: string | undefined;
25
+ const path = resolve(project, src);
26
+ if (!relative(project, path).startsWith("..")) {
27
+ try {
28
+ const info = await stat(path);
29
+ if (info.isFile() && info.size <= MAX_CLIP_FILE_BYTES)
30
+ hash = createHash("sha256")
31
+ .update(await readFile(path))
32
+ .digest("hex");
33
+ } catch {
34
+ hash = undefined;
35
+ }
36
+ }
37
+ hashes.set(src, hash);
38
+ return hash;
39
+ };
40
+ const dirs = trackDirectories(score);
41
+ for (const track of score.tracks) {
42
+ const file = `tracks/${dirs.get(track.id) ?? track.id}/track.ts`;
43
+ const items: [string, string, string][] = [
44
+ ...(track.takes ?? []).map(
45
+ (t) => [`take ${t.name}`, t.src, t.sha256] as [string, string, string],
46
+ ),
47
+ ...(track.clips ?? []).map(
48
+ (c) => [`clip ${c.id}`, c.src, c.sha256] as [string, string, string],
49
+ ),
50
+ ];
51
+ const usedTakes = new Set((track.clips ?? []).map((c) => c.take));
52
+ for (const [label, src, pinned] of items) {
53
+ const actual = await hashOf(src);
54
+ if (actual === pinned) continue;
55
+ // An unused take that is gone is only advice (sources.md 6).
56
+ if (
57
+ actual === undefined &&
58
+ label.startsWith("take ") &&
59
+ !usedTakes.has(label.slice(5))
60
+ )
61
+ continue;
62
+ out.push({
63
+ file,
64
+ message:
65
+ actual === undefined
66
+ ? `${label}: ${src} is missing (re-import it or remove the clip)`
67
+ : `${label}: ${src} does not match its sha256 pin (${pinned.slice(0, 8)} pinned, ${actual.slice(0, 8)} on disk); delete the sha256 line to re-pin`,
68
+ });
69
+ }
70
+ }
71
+ return out;
72
+ }
@@ -15,9 +15,11 @@
15
15
  */
16
16
 
17
17
  import { createHash } from "node:crypto";
18
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
18
+ import { mkdir, readFile } from "node:fs/promises";
19
19
  import { dirname, join, resolve } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
+ import { parseSimpleArgv } from "../argv.ts";
22
+ import { durableWrite } from "../fs/durable.ts";
21
23
 
22
24
  export const PROJECT_FILE = "dawg.json";
23
25
  export const PROJECT_FORMAT = "dawg.project/v1";
@@ -78,6 +80,7 @@ export default song({
78
80
  meter: [4, 4],
79
81
  bars: 4,
80
82
  tracks: [],
83
+ calibration: 1,
81
84
  });
82
85
  `;
83
86
 
@@ -210,6 +213,9 @@ export async function initProject(
210
213
  return Object.freeze({ wrote: Object.freeze(wrote), sdkVersion });
211
214
  }
212
215
 
216
+ export const INIT_USAGE =
217
+ "usage: dawg init [dir] · writes song.ts, dawg.json, tsconfig.json and .dawg/sdk (existing files are kept)";
218
+
213
219
  /** `dawg init [dir]`: creates the project and prints what it wrote. */
214
220
  export async function runInitCommand(
215
221
  argv: readonly string[],
@@ -217,8 +223,17 @@ export async function runInitCommand(
217
223
  stdout: { write(text: string): unknown },
218
224
  stderr: { write(text: string): unknown },
219
225
  ): Promise<number> {
220
- const target =
221
- argv[1] && !argv[1].startsWith("--") ? resolve(cwd, argv[1]) : cwd;
226
+ const parsed = parseSimpleArgv(argv.slice(1), 1);
227
+ if (parsed.kind === "help") {
228
+ stdout.write(`${INIT_USAGE}\n`);
229
+ return 0;
230
+ }
231
+ if (parsed.kind === "error") {
232
+ stderr.write(`${parsed.problem} · ${INIT_USAGE}\n`);
233
+ return 2;
234
+ }
235
+ const dir = parsed.positionals[0];
236
+ const target = dir ? resolve(cwd, dir) : cwd;
222
237
  try {
223
238
  const result = await initProject(target);
224
239
  if (result.wrote.length === 0)
@@ -241,13 +256,13 @@ async function readOptional(path: string): Promise<string | undefined> {
241
256
  }
242
257
  }
243
258
 
244
- /** Temp file + rename so a reader never sees a partial file. */
259
+ /**
260
+ * Temp file + fsync + rename + directory fsync, so a reader never sees a
261
+ * partial file and a crash never loses a write `.dawg/sync.json` vouches for.
262
+ */
245
263
  export async function writeAtomic(
246
264
  path: string,
247
265
  contents: string,
248
266
  ): Promise<void> {
249
- await mkdir(dirname(path), { recursive: true });
250
- const temporary = `${path}.${process.pid}.tmp`;
251
- await writeFile(temporary, contents, "utf8");
252
- await rename(temporary, path);
267
+ await durableWrite(path, contents);
253
268
  }