@gentbajko/slopify 0.8.4 → 1.0.1

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 (204) hide show
  1. package/README.md +28 -6
  2. package/dist/adapters/llm/claude-code.js +6 -3
  3. package/dist/adapters/llm/codex.js +102 -12
  4. package/dist/adapters/llm/gemini.js +11 -8
  5. package/dist/adapters/llm/run-cli.js +61 -6
  6. package/dist/adapters/tts/inworld-async.js +4 -4
  7. package/dist/adapters/tts/inworld.js +73 -68
  8. package/dist/catalog/registry.js +13 -73
  9. package/dist/edge/cli.js +5 -2
  10. package/dist/edge/events/hub.js +7 -2
  11. package/dist/edge/events/preview-cache.js +7 -4
  12. package/dist/edge/events/visibility.js +33 -0
  13. package/dist/edge/http/actions.js +74 -79
  14. package/dist/edge/http/app.js +39 -6
  15. package/dist/edge/http/audio-preview.js +9 -1
  16. package/dist/edge/http/checkpoints.js +95 -0
  17. package/dist/edge/http/diagnostics.js +31 -0
  18. package/dist/edge/http/draft-files.js +69 -0
  19. package/dist/edge/http/draft-problem.js +15 -0
  20. package/dist/edge/http/drafts.js +95 -0
  21. package/dist/edge/http/multipart.js +86 -0
  22. package/dist/edge/http/mutations.js +49 -0
  23. package/dist/edge/http/planning.js +12 -1
  24. package/dist/edge/http/project-templates.js +74 -0
  25. package/dist/edge/http/projects.js +19 -2
  26. package/dist/edge/http/revision-files.js +84 -0
  27. package/dist/edge/http/revisions.js +103 -0
  28. package/dist/edge/http/schedules.js +100 -0
  29. package/dist/edge/http/staging.js +11 -52
  30. package/dist/edge/http/storage.js +110 -0
  31. package/dist/edge/http/subtitles.js +9 -79
  32. package/dist/edge/http/tutorial.js +26 -0
  33. package/dist/edge/signal-shutdown.js +23 -0
  34. package/dist/kernel/audio-preview.js +11 -5
  35. package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
  36. package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
  37. package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
  38. package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
  39. package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
  40. package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
  41. package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
  42. package/dist/kernel/ports/model.js +5 -0
  43. package/dist/kernel/runner/attempt-repo.js +15 -0
  44. package/dist/kernel/runner/attempt.js +14 -2
  45. package/dist/kernel/runner/checkpoint-authority.js +23 -0
  46. package/dist/kernel/runner/index.js +35 -11
  47. package/dist/kernel/runner/providers.js +18 -8
  48. package/dist/kernel/runner/work-attempt.js +47 -0
  49. package/dist/kernel/runner/work-authority.js +37 -0
  50. package/dist/kernel/runner/work.js +23 -0
  51. package/dist/main.js +227 -69
  52. package/dist/slices/admission/repo.js +5 -56
  53. package/dist/slices/admission/schema.js +64 -0
  54. package/dist/slices/admission/start.js +13 -4
  55. package/dist/slices/article/continuation.js +9 -3
  56. package/dist/slices/article/provided-entries.js +7 -3
  57. package/dist/slices/article/run.js +20 -7
  58. package/dist/slices/article/segments.js +13 -4
  59. package/dist/slices/batch/index.js +8 -11
  60. package/dist/slices/cancel/index.js +78 -34
  61. package/dist/slices/checkpoints/change.js +250 -0
  62. package/dist/slices/checkpoints/fingerprint.js +33 -0
  63. package/dist/slices/checkpoints/index.js +2 -0
  64. package/dist/slices/checkpoints/model.js +1 -0
  65. package/dist/slices/checkpoints/recovery.js +116 -0
  66. package/dist/slices/checkpoints/repo.js +132 -0
  67. package/dist/slices/checkpoints/rules.js +30 -0
  68. package/dist/slices/checkpoints/schema.js +59 -0
  69. package/dist/slices/control/index.js +88 -22
  70. package/dist/slices/control/providers.js +3 -2
  71. package/dist/slices/control/revision-control-schema.js +7 -0
  72. package/dist/slices/control/revision-control.js +59 -0
  73. package/dist/slices/estimate/index.js +77 -59
  74. package/dist/slices/estimate/requests.js +102 -0
  75. package/dist/slices/images/run.js +14 -3
  76. package/dist/slices/library/slots.js +11 -11
  77. package/dist/slices/library/snapshot.js +33 -0
  78. package/dist/slices/narration/legacy-plan.js +58 -0
  79. package/dist/slices/narration/live.js +2 -2
  80. package/dist/slices/narration/plan.js +48 -54
  81. package/dist/slices/narration/run.js +21 -8
  82. package/dist/slices/play-drafts/convert.js +120 -0
  83. package/dist/slices/play-drafts/model.js +1 -0
  84. package/dist/slices/play-drafts/readiness.js +80 -0
  85. package/dist/slices/play-drafts/repo.js +76 -0
  86. package/dist/slices/play-drafts/review-inputs.js +150 -0
  87. package/dist/slices/play-drafts/review.js +109 -0
  88. package/dist/slices/play-drafts/schema.js +218 -0
  89. package/dist/slices/play-drafts/service.js +242 -0
  90. package/dist/slices/play-drafts/start-repo.js +113 -0
  91. package/dist/slices/play-drafts/start.js +148 -0
  92. package/dist/slices/play-drafts/uploads.js +67 -0
  93. package/dist/slices/project-templates/from-project.js +159 -0
  94. package/dist/slices/project-templates/model.js +1 -0
  95. package/dist/slices/project-templates/repo.js +41 -0
  96. package/dist/slices/project-templates/schema.js +37 -0
  97. package/dist/slices/project-templates/service.js +136 -0
  98. package/dist/slices/project-templates/setup.js +55 -0
  99. package/dist/slices/rebuild/admission-repo.js +120 -0
  100. package/dist/slices/rebuild/dependencies.js +33 -0
  101. package/dist/slices/rebuild/model.js +94 -0
  102. package/dist/slices/rebuild/narration-history.js +43 -0
  103. package/dist/slices/rebuild/narration-reuse.js +20 -0
  104. package/dist/slices/rebuild/preview-details.js +144 -0
  105. package/dist/slices/rebuild/preview-plan.js +223 -0
  106. package/dist/slices/rebuild/preview-retained.js +128 -0
  107. package/dist/slices/rebuild/provided-review.js +32 -0
  108. package/dist/slices/rebuild/recipe-audio.js +172 -0
  109. package/dist/slices/rebuild/recipe-build.js +17 -0
  110. package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
  111. package/dist/slices/rebuild/recipe-exports.js +65 -0
  112. package/dist/slices/rebuild/recipe-fixture.js +150 -0
  113. package/dist/slices/rebuild/recipe-input-schema.js +82 -0
  114. package/dist/slices/rebuild/recipe-legacy.js +135 -0
  115. package/dist/slices/rebuild/recipe-model.js +77 -0
  116. package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
  117. package/dist/slices/rebuild/recipe-save.js +221 -0
  118. package/dist/slices/rebuild/recipe-text.js +170 -0
  119. package/dist/slices/rebuild/recipe-validation.js +86 -0
  120. package/dist/slices/rebuild/recipe-visual.js +111 -0
  121. package/dist/slices/rebuild/recipe-work.js +212 -0
  122. package/dist/slices/rebuild/recipes.js +4 -0
  123. package/dist/slices/rebuild/repo.js +64 -0
  124. package/dist/slices/rebuild/runtime-actions.js +76 -0
  125. package/dist/slices/rebuild/runtime-admission.js +71 -0
  126. package/dist/slices/rebuild/runtime-article.js +117 -0
  127. package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
  128. package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
  129. package/dist/slices/rebuild/runtime-export.js +139 -0
  130. package/dist/slices/rebuild/runtime-instructions.js +51 -0
  131. package/dist/slices/rebuild/runtime-local.js +137 -0
  132. package/dist/slices/rebuild/runtime-materialize.js +105 -0
  133. package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
  134. package/dist/slices/rebuild/runtime-plan.js +103 -0
  135. package/dist/slices/rebuild/runtime-provider.js +184 -0
  136. package/dist/slices/rebuild/runtime-publication.js +172 -0
  137. package/dist/slices/rebuild/runtime-run.js +35 -0
  138. package/dist/slices/rebuild/runtime-store.js +166 -0
  139. package/dist/slices/rebuild/runtime-subtitles.js +202 -0
  140. package/dist/slices/rebuild/service-readiness.js +110 -0
  141. package/dist/slices/rebuild/service.js +149 -0
  142. package/dist/slices/rebuild/transition-repo.js +108 -0
  143. package/dist/slices/rebuild/work-records.js +44 -0
  144. package/dist/slices/reruns/index.js +5 -0
  145. package/dist/slices/research/run.js +33 -12
  146. package/dist/slices/revisions/adopt-assets.js +31 -0
  147. package/dist/slices/revisions/adopt-content.js +112 -0
  148. package/dist/slices/revisions/adopt.js +100 -0
  149. package/dist/slices/revisions/downloads.js +80 -0
  150. package/dist/slices/revisions/index.js +5 -0
  151. package/dist/slices/revisions/manifest-repo.js +125 -0
  152. package/dist/slices/revisions/model.js +1 -0
  153. package/dist/slices/revisions/mutation-assets.js +143 -0
  154. package/dist/slices/revisions/mutation-cues.js +95 -0
  155. package/dist/slices/revisions/mutation-prepare.js +182 -0
  156. package/dist/slices/revisions/mutation-request.js +58 -0
  157. package/dist/slices/revisions/mutation-work.js +47 -0
  158. package/dist/slices/revisions/mutations.js +194 -0
  159. package/dist/slices/revisions/projection.js +82 -0
  160. package/dist/slices/revisions/publication-model.js +1 -0
  161. package/dist/slices/revisions/publication-rules.js +166 -0
  162. package/dist/slices/revisions/publish.js +115 -0
  163. package/dist/slices/revisions/repo.js +83 -0
  164. package/dist/slices/revisions/restore.js +60 -0
  165. package/dist/slices/revisions/rules.js +99 -0
  166. package/dist/slices/revisions/schema.js +160 -0
  167. package/dist/slices/revisions/view.js +44 -0
  168. package/dist/slices/schedules/calendar.js +47 -0
  169. package/dist/slices/schedules/model.js +1 -0
  170. package/dist/slices/schedules/repo.js +281 -0
  171. package/dist/slices/schedules/scheduler.js +144 -0
  172. package/dist/slices/schedules/schema.js +80 -0
  173. package/dist/slices/schedules/service.js +178 -0
  174. package/dist/slices/settings/cli-paths.js +1 -1
  175. package/dist/slices/settings/cli-status.js +45 -5
  176. package/dist/slices/settings/tutorial-schema.js +39 -0
  177. package/dist/slices/settings/tutorial.js +66 -0
  178. package/dist/slices/storage/assets.js +108 -0
  179. package/dist/slices/storage/delete-project.js +2 -1
  180. package/dist/slices/storage/downloads.js +4 -1
  181. package/dist/slices/storage/portable.js +871 -0
  182. package/dist/slices/storage/prepare.js +67 -0
  183. package/dist/slices/storage/reconcile.js +25 -5
  184. package/dist/slices/storage/repo.js +2 -15
  185. package/dist/slices/storage/schema.js +35 -0
  186. package/dist/slices/storage/staging-refs.js +26 -0
  187. package/dist/slices/storage/staging.js +16 -11
  188. package/dist/slices/thumbnail/run.js +26 -9
  189. package/dist/slices/video/audio-export-args.js +33 -0
  190. package/dist/slices/video/audio-export.js +13 -36
  191. package/dist/slices/video/audio-inputs.js +4 -3
  192. package/dist/slices/video/ffmpeg.js +6 -0
  193. package/dist/slices/video/reuse-audio.js +11 -4
  194. package/dist/updater/install-flow.js +27 -2
  195. package/dist/updater/plan.js +28 -2
  196. package/dist/updater/service.js +27 -2
  197. package/dist/updater/worker.js +2 -1
  198. package/dist/web/assets/index-DX1_chKs.css +1 -0
  199. package/dist/web/assets/index-DwM-Y6nw.js +137 -0
  200. package/dist/web/index.html +2 -2
  201. package/package.json +2 -2
  202. package/dist/adapters/tts/inworld-text.js +0 -32
  203. package/dist/web/assets/index-Ce2HApus.css +0 -1
  204. package/dist/web/assets/index-xbD0MZOY.js +0 -130
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { cadenceSchema, validTimeZone } from "./calendar.js";
3
+ const id = z.uuid();
4
+ const keywordName = z
5
+ .string()
6
+ .min(1)
7
+ .max(200)
8
+ .refine((value) => value === value.trim(), "Keyword names cannot start or end with spaces.");
9
+ const item = z
10
+ .object({
11
+ title: z.string().trim().min(1).max(200),
12
+ values: z.record(keywordName, z.string().max(10000)).readonly(),
13
+ })
14
+ .strict()
15
+ .readonly();
16
+ const policy = z.enum(["skip", "run-once"]);
17
+ export const scheduleCreateSchema = z
18
+ .object({
19
+ id,
20
+ name: z.string().trim().min(1).max(200),
21
+ templateId: id,
22
+ templateVersion: z.number().int().positive(),
23
+ cadence: cadenceSchema,
24
+ timezone: z.string().min(1).max(100).refine(validTimeZone, "Choose a valid IANA timezone."),
25
+ missedPolicy: policy.default("skip"),
26
+ overlapPolicy: z.literal("skip").default("skip"),
27
+ spendLimitCents: z.number().int().nonnegative().nullable().default(null),
28
+ items: z.array(item).max(49).default([]).readonly(),
29
+ })
30
+ .strict()
31
+ .readonly();
32
+ export const scheduleUpdateSchema = scheduleCreateSchema
33
+ .unwrap()
34
+ .extend({ baseVersion: z.number().int().positive(), mutationId: id })
35
+ .strict()
36
+ .readonly();
37
+ export const scheduleDeleteSchema = z
38
+ .object({ id, baseVersion: z.number().int().positive() })
39
+ .strict()
40
+ .readonly();
41
+ export const scheduleControlSchema = z
42
+ .object({ id, baseVersion: z.number().int().positive() })
43
+ .strict()
44
+ .readonly();
45
+ export const scheduleSummarySchema = z
46
+ .object({
47
+ id,
48
+ name: z.string(),
49
+ templateId: id,
50
+ templateVersion: z.number().int().positive(),
51
+ cadence: cadenceSchema,
52
+ timezone: z.string(),
53
+ missedPolicy: policy,
54
+ overlapPolicy: z.literal("skip"),
55
+ spendLimitCents: z.number().int().nonnegative().nullable(),
56
+ items: z.array(item).readonly(),
57
+ status: z.enum(["active", "paused", "completed", "canceled"]),
58
+ version: z.number().int().positive(),
59
+ nextRunAt: z.string().datetime({ offset: true }).nullable(),
60
+ createdAt: z.string(),
61
+ updatedAt: z.string(),
62
+ deletedAt: z.string().datetime({ offset: true }).nullable(),
63
+ })
64
+ .strict()
65
+ .readonly();
66
+ export const scheduleRunSchema = z
67
+ .object({
68
+ id,
69
+ scheduleId: id,
70
+ scheduledFor: z.string().datetime({ offset: true }),
71
+ status: z.enum(["running", "succeeded", "failed", "skipped"]),
72
+ requestId: id.nullable(),
73
+ projectIds: z.array(z.string()).readonly(),
74
+ estimate: z.unknown().nullable(),
75
+ startedAt: z.string().datetime({ offset: true }),
76
+ endedAt: z.string().datetime({ offset: true }).nullable(),
77
+ error: z.string().nullable(),
78
+ })
79
+ .strict()
80
+ .readonly();
@@ -0,0 +1,178 @@
1
+ import { z } from "zod";
2
+ import { transact } from "../../kernel/db/tx.js";
3
+ import { requestHash } from "../play-drafts/repo.js";
4
+ import { nextOccurrence } from "./calendar.js";
5
+ import { insertSchedule, runsForSchedule, scheduleById, scheduleRows, setScheduleStatus, tombstoneScheduleRow, updateScheduleRow, } from "./repo.js";
6
+ import { scheduleControlSchema, scheduleCreateSchema, scheduleDeleteSchema, scheduleUpdateSchema, } from "./schema.js";
7
+ export function listSchedules(deps) {
8
+ return scheduleRows(deps.db);
9
+ }
10
+ export function listScheduleRuns(deps, id) {
11
+ if (!z.uuid().safeParse(id).success)
12
+ return { ok: false, reason: "invalid-input" };
13
+ const runs = runsForSchedule(deps.db, id);
14
+ if (!scheduleById(deps.db, id) && runs.length === 0)
15
+ return { ok: false, reason: "not-found" };
16
+ return { ok: true, value: runs };
17
+ }
18
+ export function createSchedule(deps, input) {
19
+ const parsed = scheduleCreateSchema.safeParse(input);
20
+ if (!parsed.success)
21
+ return { ok: false, reason: "invalid-input" };
22
+ const hash = requestHash(parsed.data);
23
+ return transact(deps.db, () => {
24
+ const old = deps.db
25
+ .prepare("SELECT creation_hash,deleted_at FROM schedules WHERE id=?")
26
+ .get(parsed.data.id);
27
+ if (old !== undefined) {
28
+ const saved = z
29
+ .object({ creation_hash: z.string(), deleted_at: z.string().nullable() })
30
+ .parse(old);
31
+ if (saved.deleted_at !== null)
32
+ return { ok: false, reason: "conflict" };
33
+ return saved.creation_hash === hash
34
+ ? { ok: true, value: scheduleById(deps.db, parsed.data.id) }
35
+ : { ok: false, reason: "conflict" };
36
+ }
37
+ const checked = checkTemplate(deps, parsed.data.templateId, parsed.data.templateVersion);
38
+ if (!checked.ok)
39
+ return checked;
40
+ const now = deps.clock.now();
41
+ const next = nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
42
+ if (next === null)
43
+ return { ok: false, reason: "not-due" };
44
+ const value = summary(parsed.data, next.toISOString(), now.toISOString());
45
+ insertSchedule(deps.db, value, hash);
46
+ return { ok: true, value };
47
+ });
48
+ }
49
+ export function updateSchedule(deps, input) {
50
+ const parsed = scheduleUpdateSchema.safeParse(input);
51
+ if (!parsed.success)
52
+ return { ok: false, reason: "invalid-input" };
53
+ const hash = requestHash(parsed.data);
54
+ return transact(deps.db, () => {
55
+ const previous = scheduleById(deps.db, parsed.data.id);
56
+ if (!previous)
57
+ return { ok: false, reason: "not-found" };
58
+ const row = deps.db
59
+ .prepare("SELECT mutation_id,mutation_hash FROM schedules WHERE id=?")
60
+ .get(previous.id);
61
+ if (row !== undefined) {
62
+ const saved = z
63
+ .object({ mutation_id: z.string().nullable(), mutation_hash: z.string().nullable() })
64
+ .parse(row);
65
+ if (saved.mutation_id === parsed.data.mutationId)
66
+ return saved.mutation_hash === hash
67
+ ? { ok: true, value: previous }
68
+ : { ok: false, reason: "conflict" };
69
+ }
70
+ if (previous.deletedAt !== null)
71
+ return { ok: false, reason: "conflict" };
72
+ if (previous.version !== parsed.data.baseVersion)
73
+ return { ok: false, reason: "conflict" };
74
+ if (previous.status === "canceled" || previous.status === "completed")
75
+ return { ok: false, reason: "conflict" };
76
+ const checked = checkTemplate(deps, parsed.data.templateId, parsed.data.templateVersion);
77
+ if (!checked.ok)
78
+ return checked;
79
+ const now = deps.clock.now();
80
+ const next = previous.status === "active"
81
+ ? nextOccurrence(parsed.data.cadence, parsed.data.timezone, now)
82
+ : previous.nextRunAt === null
83
+ ? null
84
+ : nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
85
+ const value = summary(parsed.data, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1);
86
+ if (!updateScheduleRow(deps.db, value, parsed.data.mutationId, hash, parsed.data.baseVersion))
87
+ return { ok: false, reason: "conflict" };
88
+ return { ok: true, value };
89
+ });
90
+ }
91
+ export function pauseSchedule(deps, input) {
92
+ return control(deps, input, "paused");
93
+ }
94
+ export function resumeSchedule(deps, input) {
95
+ const parsed = scheduleControlSchema.safeParse(input);
96
+ if (!parsed.success)
97
+ return { ok: false, reason: "invalid-input" };
98
+ const previous = scheduleById(deps.db, parsed.data.id);
99
+ if (!previous)
100
+ return { ok: false, reason: "not-found" };
101
+ if (previous.deletedAt !== null)
102
+ return { ok: false, reason: "conflict" };
103
+ if (previous.version !== parsed.data.baseVersion || previous.status !== "paused")
104
+ return { ok: false, reason: "conflict" };
105
+ const next = nextOccurrence(previous.cadence, previous.timezone, deps.clock.now());
106
+ const status = next === null ? "completed" : "active";
107
+ if (!setScheduleStatus(deps.db, previous.id, previous.version, status, next?.toISOString() ?? null, deps.clock.now().toISOString()))
108
+ return { ok: false, reason: "conflict" };
109
+ return { ok: true, value: scheduleById(deps.db, previous.id) };
110
+ }
111
+ export function cancelSchedule(deps, input) {
112
+ return control(deps, input, "canceled");
113
+ }
114
+ export function deleteSchedule(deps, input) {
115
+ const parsed = scheduleDeleteSchema.safeParse(input);
116
+ if (!parsed.success)
117
+ return { ok: false, reason: "invalid-input" };
118
+ const previous = scheduleById(deps.db, parsed.data.id);
119
+ if (!previous)
120
+ return { ok: false, reason: "not-found" };
121
+ if (previous.deletedAt !== null)
122
+ return { ok: true, value: { deleted: true } };
123
+ if (previous.version !== parsed.data.baseVersion)
124
+ return { ok: false, reason: "conflict" };
125
+ if (previous.status !== "completed" && previous.status !== "canceled")
126
+ return { ok: false, reason: "cancel-required" };
127
+ const deletedAt = deps.clock.now().toISOString();
128
+ return transact(deps.db, () => tombstoneScheduleRow(deps.db, previous.id, previous.version, deletedAt)
129
+ ? { ok: true, value: { deleted: true } }
130
+ : { ok: false, reason: "conflict" });
131
+ }
132
+ function control(deps, input, status) {
133
+ const parsed = scheduleControlSchema.safeParse(input);
134
+ if (!parsed.success)
135
+ return { ok: false, reason: "invalid-input" };
136
+ const previous = scheduleById(deps.db, parsed.data.id);
137
+ if (!previous)
138
+ return { ok: false, reason: "not-found" };
139
+ if (previous.deletedAt !== null)
140
+ return { ok: false, reason: "conflict" };
141
+ if (previous.version !== parsed.data.baseVersion ||
142
+ (previous.status !== "active" && !(status === "canceled" && previous.status === "paused")))
143
+ return { ok: false, reason: "conflict" };
144
+ const next = status === "canceled" ? null : previous.nextRunAt;
145
+ if (!setScheduleStatus(deps.db, previous.id, previous.version, status, next, deps.clock.now().toISOString()))
146
+ return { ok: false, reason: "conflict" };
147
+ return { ok: true, value: scheduleById(deps.db, previous.id) };
148
+ }
149
+ function checkTemplate(deps, id, version) {
150
+ const template = deps.template(id, version);
151
+ if (!template)
152
+ return { ok: false, reason: "missing-template" };
153
+ const sources = template.document.form.sources;
154
+ const mediaSources = ["audio", "images", "thumbnail"];
155
+ if (mediaSources.some((kind) => sources[kind] === "provide"))
156
+ return { ok: false, reason: "unsupported-media" };
157
+ return { ok: true, value: true };
158
+ }
159
+ function summary(input, nextRunAt, now, status = "active", version = 1) {
160
+ return {
161
+ id: input.id,
162
+ name: input.name,
163
+ templateId: input.templateId,
164
+ templateVersion: input.templateVersion,
165
+ cadence: input.cadence,
166
+ timezone: input.timezone,
167
+ missedPolicy: input.missedPolicy,
168
+ overlapPolicy: input.overlapPolicy,
169
+ spendLimitCents: input.spendLimitCents,
170
+ items: input.items,
171
+ status,
172
+ version,
173
+ nextRunAt,
174
+ createdAt: now,
175
+ updatedAt: now,
176
+ deletedAt: null,
177
+ };
178
+ }
@@ -77,7 +77,7 @@ async function save(deps, id, raw) {
77
77
  }
78
78
  const command = configured ?? provider.binary;
79
79
  const probed = await deps.probe(command, provider.versionArgs, cliProbeTimeoutMs);
80
- const readiness = readinessFromProbe(probed);
80
+ const readiness = readinessFromProbe(probed, provider);
81
81
  if (configured !== null && readiness.kind === "cli" && !readiness.installed)
82
82
  return {
83
83
  ok: false,
@@ -3,6 +3,9 @@ import { cliCommand } from "../../kernel/cli-command.js";
3
3
  // Gemini's Node startup can take several seconds on Windows. Providers are probed in
4
4
  // parallel; a hung process is bounded without treating ordinary startup as missing.
5
5
  export const cliProbeTimeoutMs = 15_000;
6
+ // The content adapter's isolation flags and feature gates were verified against this release.
7
+ // Older Codex binaries must fail before admission instead of failing every generation attempt.
8
+ export const minimumCodexCliVersion = "0.149.1";
6
9
  const probeOutputMax = 64 * 1024;
7
10
  // Known Windows package-manager shims are resolved to an executable plus fixed args;
8
11
  // user input never becomes a shell command. Missing/unsupported commands mean not ready.
@@ -25,16 +28,53 @@ export function nodeCliProbe(binary, args, timeoutMs) {
25
28
  });
26
29
  }
27
30
  export async function cliReadiness(probe, provider) {
28
- return readinessFromProbe(await probe(provider.binary, provider.versionArgs, cliProbeTimeoutMs));
31
+ return readinessFromProbe(await probe(provider.binary, provider.versionArgs, cliProbeTimeoutMs), provider);
29
32
  }
30
- export function readinessFromProbe(result) {
33
+ export function readinessFromProbe(result, provider) {
31
34
  if (!result.ran) {
32
35
  return { kind: "cli", installed: false };
33
36
  }
34
37
  const version = versionFrom(result.stdout);
35
- return version === undefined
36
- ? { kind: "cli", installed: true }
37
- : { kind: "cli", installed: true, version };
38
+ if (provider.id !== "codex")
39
+ return version === undefined
40
+ ? { kind: "cli", installed: true }
41
+ : { kind: "cli", installed: true, version };
42
+ if (version === undefined)
43
+ return {
44
+ kind: "cli",
45
+ installed: true,
46
+ issue: `Slopify could not verify this Codex CLI version. Install Codex CLI ${minimumCodexCliVersion} or newer, then try again.`,
47
+ };
48
+ if (!versionAtLeast(version, minimumCodexCliVersion))
49
+ return {
50
+ kind: "cli",
51
+ installed: true,
52
+ version,
53
+ issue: `Codex CLI ${minimumCodexCliVersion} or newer is required; version ${version} is installed. Update Codex CLI and try again.`,
54
+ };
55
+ return { kind: "cli", installed: true, version };
56
+ }
57
+ function versionAtLeast(version, minimum) {
58
+ const candidate = comparableVersion(version);
59
+ const floor = comparableVersion(minimum);
60
+ if (candidate === undefined || floor === undefined)
61
+ return false;
62
+ for (let index = 0; index < 3; index++) {
63
+ const current = candidate.parts[index] ?? 0n;
64
+ const required = floor.parts[index] ?? 0n;
65
+ if (current !== required)
66
+ return current > required;
67
+ }
68
+ return !(candidate.prerelease && !floor.prerelease);
69
+ }
70
+ function comparableVersion(version) {
71
+ const parsed = /^(\d+)\.(\d+)(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
72
+ if (parsed === null)
73
+ return undefined;
74
+ return {
75
+ parts: [BigInt(parsed[1] ?? "0"), BigInt(parsed[2] ?? "0"), BigInt(parsed[3] ?? "0")],
76
+ prerelease: parsed[4] !== undefined,
77
+ };
38
78
  }
39
79
  // `claude --version` answers "2.1.258 (Claude Code)" and `codex --version` answers
40
80
  // "codex-cli 0.149.1", so the version is the first dotted number on the output rather
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ export const tutorialSessionSchema = z
3
+ .object({
4
+ schemaVersion: z.literal(1),
5
+ active: z.boolean(),
6
+ stepId: z.enum([
7
+ "text-key",
8
+ "audio-key",
9
+ "image-key",
10
+ "voice",
11
+ "article-name",
12
+ "article-body",
13
+ "article-keywords",
14
+ "article-save",
15
+ "image-prompt",
16
+ "image-save",
17
+ "play-options",
18
+ "play-article",
19
+ "play-keywords",
20
+ "play-audio",
21
+ "play-images",
22
+ "play-video",
23
+ "play-subtitles",
24
+ "play-start",
25
+ "project",
26
+ "download",
27
+ ]),
28
+ articleId: z.string().optional(),
29
+ imageId: z.string().optional(),
30
+ projectId: z.string().optional(),
31
+ })
32
+ .strict();
33
+ export const tutorialWriteSchema = z
34
+ .object({
35
+ baseVersion: z.number().int().nonnegative(),
36
+ mutationId: z.uuid(),
37
+ session: tutorialSessionSchema,
38
+ })
39
+ .strict();
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ import { transact } from "../../kernel/db/tx.js";
3
+ import { fingerprint } from "../../kernel/runner/work.js";
4
+ import { readSetting, writeSetting } from "./repo.js";
5
+ import { tutorialSessionSchema, tutorialWriteSchema, } from "./tutorial-schema.js";
6
+ export { tutorialSessionSchema, tutorialWriteSchema, } from "./tutorial-schema.js";
7
+ const recordSchema = z
8
+ .object({
9
+ version: z.number().int().positive(),
10
+ session: tutorialSessionSchema,
11
+ mutationId: z.uuid(),
12
+ requestHash: z.string().regex(/^[a-f0-9]{64}$/),
13
+ })
14
+ .strict();
15
+ function readRecord(db) {
16
+ const raw = readSetting(db, "tutorial.session");
17
+ if (raw === undefined)
18
+ return undefined;
19
+ let value;
20
+ try {
21
+ value = JSON.parse(raw);
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof SyntaxError))
25
+ throw error;
26
+ return null;
27
+ }
28
+ const result = recordSchema.safeParse(value);
29
+ return result.success ? result.data : null;
30
+ }
31
+ export function readTutorial(db) {
32
+ const record = readRecord(db);
33
+ return {
34
+ version: record?.version ?? 0,
35
+ session: record?.session ?? { schemaVersion: 1, active: false, stepId: "text-key" },
36
+ readable: record !== null,
37
+ };
38
+ }
39
+ export function saveTutorial(db, input) {
40
+ const parsed = tutorialWriteSchema.parse(input);
41
+ const { baseVersion, session, mutationId } = parsed;
42
+ const requestHash = fingerprint(z.json().parse(JSON.parse(JSON.stringify(parsed))));
43
+ return transact(db, () => {
44
+ const current = readRecord(db);
45
+ if (current === null)
46
+ return { ok: false, reason: "unreadable" };
47
+ if (current?.mutationId === mutationId) {
48
+ return current.requestHash === requestHash
49
+ ? {
50
+ ok: true,
51
+ value: { version: current.version, session: current.session, readable: true },
52
+ }
53
+ : { ok: false, reason: "conflict" };
54
+ }
55
+ if ((current?.version ?? 0) !== baseVersion || baseVersion === Number.MAX_SAFE_INTEGER)
56
+ return { ok: false, reason: "conflict" };
57
+ const next = { version: baseVersion + 1, session, mutationId, requestHash };
58
+ writeSetting(db, "tutorial.session", JSON.stringify(next));
59
+ return { ok: true, value: { version: next.version, session, readable: true } };
60
+ });
61
+ }
62
+ export function resetTutorial(db) {
63
+ transact(db, () => {
64
+ db.prepare("DELETE FROM settings WHERE key = ?").run("tutorial.session");
65
+ });
66
+ }
@@ -0,0 +1,108 @@
1
+ import { closeSync, fchmodSync, fstatSync, fsyncSync, mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { basename, dirname, isAbsolute, relative, sep } from "node:path";
3
+ import { redact } from "../../kernel/log.js";
4
+ import { outputPath } from "./layout.js";
5
+ export function allocateAsset(deps, projectId, filename) {
6
+ requireBasename(filename);
7
+ const id = deps.ids.next();
8
+ requireBasename(id);
9
+ const path = `assets/${id}/${filename}`;
10
+ const absolutePath = outputPath(deps.paths, projectId, path);
11
+ const directory = outputPath(deps.paths, projectId, `assets/${id}`);
12
+ if (dirname(absolutePath) !== directory)
13
+ throw new Error("Asset filenames must be basenames.");
14
+ mkdirSync(outputPath(deps.paths, projectId, "assets"), { recursive: true, mode: 0o700 });
15
+ mkdirSync(directory, { mode: 0o700 });
16
+ return { id, projectId, path, absolutePath };
17
+ }
18
+ export function sealAsset(deps, pending) {
19
+ const file = openSync(pending.absolutePath, "r+");
20
+ try {
21
+ const stats = fstatSync(file);
22
+ if (!stats.isFile())
23
+ throw new Error("An asset must be a regular file.");
24
+ fchmodSync(file, 0o600);
25
+ fsyncSync(file);
26
+ return {
27
+ id: pending.id,
28
+ projectId: pending.projectId,
29
+ path: pending.path,
30
+ bytes: stats.size,
31
+ createdAt: deps.clock.now().toISOString(),
32
+ };
33
+ }
34
+ finally {
35
+ closeSync(file);
36
+ }
37
+ }
38
+ export function writeAsset(deps, projectId, filename, bytes) {
39
+ const pending = allocateAsset(deps, projectId, filename);
40
+ try {
41
+ writeFileSync(pending.absolutePath, bytes, { flag: "wx", mode: 0o600, flush: true });
42
+ return sealAsset(deps, pending);
43
+ }
44
+ catch (error) {
45
+ try {
46
+ rmSync(dirname(pending.absolutePath), { recursive: true, force: true });
47
+ }
48
+ catch (cleanupError) {
49
+ warnCleanup(deps, projectId, cleanupError);
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+ export function discardPreparedAssets(deps, assets) {
55
+ for (const asset of assets) {
56
+ try {
57
+ requireBasename(asset.id);
58
+ const filename = basename(asset.path);
59
+ requireBasename(filename);
60
+ if (asset.path !== `assets/${asset.id}/${filename}`) {
61
+ throw new Error("A prepared asset must belong to its allocated directory.");
62
+ }
63
+ const directory = outputPath(deps.paths, asset.projectId, `assets/${asset.id}`);
64
+ const registered = deps.db
65
+ .prepare("SELECT id,path FROM project_assets WHERE project_id=?")
66
+ .all(asset.projectId);
67
+ const retained = registered.some((row) => {
68
+ if (row.id === asset.id)
69
+ return true;
70
+ if (typeof row.path !== "string")
71
+ throw new Error("A registered asset must name a path.");
72
+ const inside = relative(directory, outputPath(deps.paths, asset.projectId, row.path));
73
+ return (inside === "" ||
74
+ (inside !== ".." && !inside.startsWith(`..${sep}`) && !isAbsolute(inside)));
75
+ });
76
+ if (!retained)
77
+ rmSync(directory, { recursive: true, force: true });
78
+ }
79
+ catch (error) {
80
+ warnCleanup(deps, asset.projectId, error);
81
+ }
82
+ }
83
+ }
84
+ function requireBasename(filename) {
85
+ if (filename === "" ||
86
+ filename === "." ||
87
+ filename === ".." ||
88
+ /[<>:"/\\|?*]/.test(filename) ||
89
+ [...filename].some((character) => {
90
+ const code = character.codePointAt(0) ?? 0;
91
+ return code < 0x20 || code === 0x7f;
92
+ }) ||
93
+ /[. ]$/.test(filename) ||
94
+ /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(filename))
95
+ throw new Error("Asset filenames must be portable basenames.");
96
+ }
97
+ function messageOf(error) {
98
+ return error instanceof Error ? error.message : String(error);
99
+ }
100
+ function warnCleanup(deps, projectId, error) {
101
+ try {
102
+ deps.log.write("warn", "asset.cleanup", { projectId, detail: messageOf(error) });
103
+ }
104
+ catch (loggingError) {
105
+ // A full disk can prevent both cleanup and file logging; preserve the write error.
106
+ process.emitWarning(redact(`asset.cleanup ${projectId}: ${messageOf(error)}; logging failed: ${messageOf(loggingError)}`));
107
+ }
108
+ }
@@ -6,7 +6,8 @@ export function deleteProject(deps, projectId) {
6
6
  if (!projectExists(deps.db, projectId)) {
7
7
  return { ok: false, reason: "no-project" };
8
8
  }
9
- if (derive(stagesOf(deps.db, projectId)) === "running") {
9
+ if (deps.hasInflight?.(projectId) === true ||
10
+ derive(stagesOf(deps.db, projectId)) === "running") {
10
11
  return { ok: false, reason: "running" };
11
12
  }
12
13
  // Files first, rows second. A folder that would not go leaves the project listed; rows
@@ -62,7 +62,7 @@ export function findDownload(deps, projectId, asset) {
62
62
  path,
63
63
  filename: downloadName(slugOf(title), output),
64
64
  bytes: stats,
65
- contentType: contentTypes[extname(output.path).toLowerCase()] ?? "application/octet-stream",
65
+ contentType: contentTypeOf(path),
66
66
  },
67
67
  };
68
68
  }
@@ -100,3 +100,6 @@ function sizeOf(path) {
100
100
  const stats = statSync(path, { throwIfNoEntry: false });
101
101
  return stats === undefined || !stats.isFile() ? undefined : stats.size;
102
102
  }
103
+ export function contentTypeOf(path) {
104
+ return contentTypes[extname(path).toLowerCase()] ?? "application/octet-stream";
105
+ }