@frontera-sdk/cli 1.52.39 → 1.52.40

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 (225) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +48 -4
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
@@ -0,0 +1,141 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { SCAFFOLD_SAMPLE_ROWS } from './local-scaffold'
5
+
6
+ /**
7
+ * The rule set `frontera local data --synthetic` writes sample rows by,
8
+ * stamped into each dataset it writes (in its description, the only free
9
+ * text a dataset definition takes). Raise it whenever a change to the
10
+ * generator makes rows written before it break a rule it now keeps — a key
11
+ * that is another type's key taking each record once, a share staying
12
+ * between 0 and 100, times of day as HH:MM, a cancelled record carrying no
13
+ * resolution — so a project whose sample data is older is told to remake it.
14
+ *
15
+ * 1 (unstamped) everything before the stamp
16
+ * 2 1:1 key links, shares 0–100, HH:MM times, state-dependent fields, descriptions by kind
17
+ * 3 a link to a media-backed type's file holds a placeholder's path, so it resolves
18
+ * 4 a property without `required` is optional (state rules apply to it); an id named
19
+ * for another type takes its keys; counts, periods, fiscal years and keys are plausible
20
+ * 5 a property implementing a shared field takes the value of the linked record that
21
+ * implements it too (a work order's service region is its site's)
22
+ * 6 a field conditional on something its record has nothing for ("empty unless
23
+ * escalated", no `escalated` property or value) stays empty instead of random
24
+ * 7 a number keeps to the range its description gives ("(1 to 12)", "between 1 and 5"),
25
+ * a "How many" count stays up to 10, and a city is the large city a custom point is near
26
+ */
27
+ export const SAMPLE_DATA_RULES = 7
28
+
29
+ const COMMAND = '`frontera local data --synthetic`'
30
+ const MADE_BY = /made by `frontera local data --synthetic`(?: \(sample rules (\d+)\))?/
31
+
32
+ /** The words a sample dataset's description carries: who made it, by which rules. */
33
+ export function sampleStamp(rules = SAMPLE_DATA_RULES): string {
34
+ return `made by ${COMMAND} (sample rules ${rules})`
35
+ }
36
+
37
+ /** The description of a sample dataset, with the current stamp: the one it had, restamped, or a new one. */
38
+ export function stampedDescription(before: string | undefined, fresh: string): string {
39
+ if (!before) return fresh
40
+ if (MADE_BY.test(before)) return before.replace(MADE_BY, sampleStamp())
41
+ // A description of its own, on a dataset whose file had no rows yet: kept, and said to hold sample rows.
42
+ return `${before.replace(/\s+$/, '')} Its rows are sample records ${sampleStamp()}: not real records.`
43
+ }
44
+
45
+ /** The rule set a dataset's description says its rows were made by: 1 for an unstamped sample, null when not sample data. */
46
+ export function sampleRulesOf(description: unknown): number | null {
47
+ if (typeof description !== 'string') return null
48
+ const m = MADE_BY.exec(description)
49
+ return m ? (m[1] ? Number(m[1]) : 1) : null
50
+ }
51
+
52
+ /**
53
+ * Whether `frontera local data --synthetic` may write a dataset's rows: a
54
+ * dataset it made (its description says so), one with no rows yet, or the
55
+ * scaffold's sample rows unedited. Anything else — a dataset someone wrote,
56
+ * edited, loaded from a Source or brought in — is kept as it is.
57
+ */
58
+ export function isSampleDataset(root: string, dataset: string): boolean {
59
+ const definition = readJson(join(root, 'datasets', `${dataset}.json`))
60
+ const csvPath = join(root, 'datasets', `${dataset}.csv`)
61
+ if (!definition) return !existsSync(csvPath) || !hasRows(readFileSync(csvPath, 'utf8'))
62
+ if (sampleRulesOf(definition.description) !== null) return true
63
+ if (definition.kind !== undefined && definition.kind !== 'file') return false
64
+ const rel = typeof definition.path === 'string' ? definition.path.replace(/^\.\//, '') : `${dataset}.csv`
65
+ const csv = join(root, 'datasets', rel)
66
+ if (!existsSync(csv)) return true
67
+ const text = readFileSync(csv, 'utf8')
68
+ // Empty, or a header and nothing under it: no rows yet.
69
+ if (!hasRows(text)) return true
70
+ const scaffold = SCAFFOLD_SAMPLE_ROWS[dataset]
71
+ return scaffold !== undefined && text.replace(/\r\n/g, '\n') === scaffold
72
+ }
73
+
74
+ /**
75
+ * The sample datasets made by an older rule set than today's: their rows can
76
+ * break rules the generator keeps now. Each by its file, oldest rules first.
77
+ */
78
+ export function staleSampleData(root: string, written?: ReadonlySet<string>): Array<{ file: string; rules: number }> {
79
+ const dir = join(root, 'datasets')
80
+ if (!existsSync(dir)) return []
81
+ return readdirSync(dir)
82
+ // Only the datasets the generator writes (`sampleDatasetNames`): a media-backed type's is never remade.
83
+ .filter((f) => f.endsWith('.json') && (!written || written.has(f.slice(0, -'.json'.length))))
84
+ .flatMap((f) => {
85
+ const rules = sampleRulesOf(readJson(join(dir, f))?.description)
86
+ return rules !== null && rules < SAMPLE_DATA_RULES ? [{ file: `datasets/${f}`, rules }] : []
87
+ })
88
+ .sort((a, b) => a.rules - b.rules || a.file.localeCompare(b.file))
89
+ }
90
+
91
+ /** What `local dev` and `local status` say about stale sample data, or null when there is none. */
92
+ export function staleSampleDataLine(root: string, written?: ReadonlySet<string>): string | null {
93
+ const stale = staleSampleData(root, written)
94
+ if (!stale.length) return null
95
+ const names = stale.slice(0, 4).map((s) => s.file.replace(/^datasets\/|\.json$/g, '')).join(', ')
96
+ return `sample data made by an older generator (${stale.length} dataset${stale.length === 1 ? '' : 's'}: ${names}${stale.length > 4 ? ', …' : ''}): its rows can break rules the generator keeps now (links by a shared key or by an id's name, shares 0–100, HH:MM times, links to media files, fields empty by the record's state); \`frontera local dev --remake-sample-data\` or the next \`frontera local sync pull\` makes them again with the kept seed, rows and day, or ${COMMAND} remakes the sample datasets and leaves every other data file as it is`
97
+ }
98
+
99
+ /** Whether a CSV holds a record under its header: an empty file or a header alone holds none. */
100
+ function hasRows(text: string): boolean {
101
+ return readCsvRows(text).length > 0
102
+ }
103
+
104
+ function readJson(path: string): Record<string, unknown> | null {
105
+ try {
106
+ const doc = JSON.parse(readFileSync(path, 'utf8')) as unknown
107
+ return doc && typeof doc === 'object' && !Array.isArray(doc) ? (doc as Record<string, unknown>) : null
108
+ } catch {
109
+ return null
110
+ }
111
+ }
112
+
113
+ /** A CSV's rows by header (RFC 4180 quoting), for a dataset sample data keeps and links into. */
114
+ export function readCsvRows(text: string): Array<Record<string, string>> {
115
+ const records: string[][] = []
116
+ let field = ''
117
+ let record: string[] = []
118
+ let quoted = false
119
+ for (let i = 0; i < text.length; i++) {
120
+ const ch = text[i]!
121
+ if (quoted) {
122
+ if (ch === '"' && text[i + 1] === '"') { field += '"'; i++ }
123
+ else if (ch === '"') quoted = false
124
+ else field += ch
125
+ continue
126
+ }
127
+ if (ch === '"') quoted = true
128
+ else if (ch === ',') { record.push(field); field = '' }
129
+ else if (ch === '\n' || ch === '\r') {
130
+ if (ch === '\r' && text[i + 1] === '\n') i++
131
+ record.push(field); field = ''
132
+ if (record.some((v) => v !== '')) records.push(record)
133
+ record = []
134
+ } else field += ch
135
+ }
136
+ record.push(field)
137
+ if (record.some((v) => v !== '')) records.push(record)
138
+ const [header, ...rows] = records
139
+ if (!header) return []
140
+ return rows.map((r) => Object.fromEntries(header.map((h, i) => [h, r[i] ?? ''])))
141
+ }
@@ -0,0 +1,138 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import { SAMPLE_DATA_RULES, sampleRulesOf, staleSampleData } from './local-sample-data'
5
+ import { readLock } from './local-sync-plan'
6
+ import { generateSyntheticData, loadScenario, parseRegion, sampleDatasetNames, type GeneratedDataset } from './local-synthetic-data'
7
+
8
+ /**
9
+ * Sample data made again on its own: after a pull that brought Blueprint
10
+ * items (a type's properties or links may have changed), or when the sample
11
+ * datasets were made by an older generator (`staleSampleData`). Every sample
12
+ * dataset is made again together, so their links agree; only the datasets
13
+ * `isSampleDataset` lets the generator write (stamped by it, scaffold rows
14
+ * unedited, or empty), never a dataset someone wrote, edited, loaded or
15
+ * pulled. A project that has no generated sample data is left alone, and so
16
+ * are the Blueprint files (no backing is added, as `local data` adds one).
17
+ *
18
+ * The generator's options are kept in `.frontera/sample-data.json`, so a
19
+ * remake writes the same keys and the same rows as the last run did.
20
+ */
21
+
22
+ export const SAMPLE_OPTIONS_FILE = join('.frontera', 'sample-data.json')
23
+
24
+ export interface SampleOptions {
25
+ seed: number
26
+ rows: number
27
+ /** The day due dates are spread around, YYYY-MM-DD. */
28
+ today: string
29
+ /** `--region` as it was given, when it was. */
30
+ region?: string
31
+ /**
32
+ * What last made the sample data: `pull` (a `local sync pull`, or `local
33
+ * init --from-workspace`, wrote it with the pulled files) or `local` (`local
34
+ * data --synthetic`, or `local dev` making it again). Absent in a record
35
+ * from an older CLI.
36
+ */
37
+ madeBy?: 'pull' | 'local'
38
+ }
39
+
40
+ const DAY = /^\d{4}-\d{2}-\d{2}$/
41
+
42
+ export const isoDay = (at: Date) => at.toISOString().slice(0, 10)
43
+
44
+ export function readSampleOptions(root: string): SampleOptions | null {
45
+ try {
46
+ const doc = JSON.parse(readFileSync(join(root, SAMPLE_OPTIONS_FILE), 'utf8')) as Partial<SampleOptions>
47
+ if (!Number.isInteger(doc.seed) || !Number.isInteger(doc.rows) || typeof doc.today !== 'string' || !DAY.test(doc.today)) return null
48
+ return { seed: doc.seed!, rows: doc.rows!, today: doc.today, ...(typeof doc.region === 'string' && doc.region ? { region: doc.region } : {}), ...(doc.madeBy === 'pull' || doc.madeBy === 'local' ? { madeBy: doc.madeBy } : {}) }
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ export function writeSampleOptions(root: string, options: SampleOptions): void {
55
+ const path = join(root, SAMPLE_OPTIONS_FILE)
56
+ mkdirSync(dirname(path), { recursive: true })
57
+ writeFileSync(path, `${JSON.stringify(options, null, 2)}\n`)
58
+ }
59
+
60
+ /** Whether the project holds sample data the generator made (a stamped dataset), so it has some to remake. */
61
+ export function hasGeneratedSampleData(root: string, names: ReadonlySet<string> = sampleDatasetNames(root)): boolean {
62
+ for (const name of names) {
63
+ const path = join(root, 'datasets', `${name}.json`)
64
+ if (!existsSync(path)) continue
65
+ try {
66
+ if (sampleRulesOf((JSON.parse(readFileSync(path, 'utf8')) as { description?: unknown }).description) !== null) return true
67
+ } catch {
68
+ // Not a definition the generator wrote.
69
+ }
70
+ }
71
+ return false
72
+ }
73
+
74
+ export interface SampleRemake {
75
+ /** Why it was made again, in words. */
76
+ why: string
77
+ options: SampleOptions
78
+ written: GeneratedDataset[]
79
+ kept: GeneratedDataset[]
80
+ }
81
+
82
+ /**
83
+ * Make the project's sample data again with its kept options (seed 7, 200
84
+ * rows and today when none are kept, which are then kept). Null when the
85
+ * project has no generated sample data.
86
+ */
87
+ export async function remakeSampleData(root: string, why: string, now: Date = new Date(), madeBy: 'pull' | 'local' = 'local'): Promise<SampleRemake | null> {
88
+ const names = sampleDatasetNames(root)
89
+ if (!names.size || !hasGeneratedSampleData(root, names)) return null
90
+ const { madeBy: _before, ...kept } = readSampleOptions(root) ?? { seed: 7, rows: 200, today: isoDay(now) }
91
+ const options: SampleOptions = { ...kept, madeBy }
92
+ const region = options.region ? parseRegion(options.region) ?? undefined : undefined
93
+ const scenario = await loadScenario(root, options.seed)
94
+ const made = generateSyntheticData(root, { rows: options.rows, seed: options.seed, today: new Date(`${options.today}T00:00:00Z`), ...(region ? { region } : {}), ...(scenario ? { scenario } : {}) })
95
+ writeSampleOptions(root, options)
96
+ return { why, options, written: made.filter((m) => !m.kept), kept: made.filter((m) => m.kept) }
97
+ }
98
+
99
+ /**
100
+ * Why the project's sample data should be made again now, or null: the pull
101
+ * brought Blueprint items, or an older generator made it.
102
+ */
103
+ export function remakeReason(root: string, pulledBlueprint: boolean): string | null {
104
+ const stale = staleSampleData(root, sampleDatasetNames(root))
105
+ if (stale.length) return `made by an older generator (sample rules ${stale[0]!.rules})`
106
+ if (pulledBlueprint) return 'the pull brought Blueprint items'
107
+ return null
108
+ }
109
+
110
+ /**
111
+ * Whether a pulled project's sample data is the pull's own: the project was
112
+ * pulled (`frontera.lock`) and nothing recorded says the local generator made
113
+ * it since (`madeBy: local` in .frontera/sample-data.json, which `local data
114
+ * --synthetic` and a remake by `local dev` write). A record from an older CLI
115
+ * says neither, and counts as the pull's: the data predates this CLI's runs.
116
+ * `local dev` doesn't make such data again unasked; `local dev
117
+ * --remake-sample-data`, a pull, or `local data --synthetic` does.
118
+ */
119
+ export function pulledSampleData(root: string): boolean {
120
+ return readLock(root) !== null && readSampleOptions(root)?.madeBy !== 'local'
121
+ }
122
+
123
+ /** What `local dev` says instead of making the pull's sample data again: one line, with the command that does. */
124
+ export function pulledSampleDataNote(root: string): string {
125
+ const stale = staleSampleData(root, sampleDatasetNames(root))
126
+ const n = new Set(stale.map((s) => s.file)).size
127
+ return `Sample data: newer sample rules exist (${SAMPLE_DATA_RULES}; ${n} dataset${n === 1 ? '' : 's'} came with the pull at rules ${stale[0]?.rules ?? 'older'}), kept as pulled; \`frontera local dev --remake-sample-data\` makes it again with the kept seed, rows and day`
128
+ }
129
+
130
+ /** The lines a command says of a remake. */
131
+ export function remakeLines(r: SampleRemake): string[] {
132
+ const rows = r.written.reduce((n, d) => n + d.rows, 0)
133
+ return [
134
+ `Sample data made again (${r.why}): ${rows} rows for ${r.written.length} object type${r.written.length === 1 ? '' : 's'}, seed ${r.options.seed}, ${r.options.rows} rows a type, dates around ${r.options.today}${r.options.region ? `, region ${r.options.region}` : ''} (kept in ${SAMPLE_OPTIONS_FILE}); --keep-sample-data leaves it as it was`,
135
+ ...r.written.map((d) => ` ${d.objectType.padEnd(24)} ${String(d.rows).padStart(6)} ${d.csvPath}`),
136
+ ...(r.kept.length ? [` kept as they are (not sample data): ${r.kept.map((d) => d.csvPath).join(', ')}`] : []),
137
+ ]
138
+ }