@lark-base-open/base-database-cli 1.1.1 → 1.3.7

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 (222) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +6 -0
  3. package/README.md +45 -27
  4. package/dist/ai-functions.d.ts +42 -0
  5. package/dist/ai-functions.d.ts.map +1 -0
  6. package/dist/ai-functions.js +327 -0
  7. package/dist/ai-functions.js.map +1 -0
  8. package/dist/archive.d.ts +21 -0
  9. package/dist/archive.d.ts.map +1 -1
  10. package/dist/archive.js +65 -10
  11. package/dist/archive.js.map +1 -1
  12. package/dist/auth.d.ts +5 -0
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +26 -3
  15. package/dist/auth.js.map +1 -1
  16. package/dist/check.d.ts +74 -0
  17. package/dist/check.d.ts.map +1 -0
  18. package/dist/check.js +283 -0
  19. package/dist/check.js.map +1 -0
  20. package/dist/dependency-policy.d.ts +10 -0
  21. package/dist/dependency-policy.d.ts.map +1 -0
  22. package/dist/dependency-policy.js +162 -0
  23. package/dist/dependency-policy.js.map +1 -0
  24. package/dist/deploy.d.ts +1 -0
  25. package/dist/deploy.d.ts.map +1 -1
  26. package/dist/deploy.js +122 -175
  27. package/dist/deploy.js.map +1 -1
  28. package/dist/environment-variables.d.ts +52 -0
  29. package/dist/environment-variables.d.ts.map +1 -0
  30. package/dist/environment-variables.js +563 -0
  31. package/dist/environment-variables.js.map +1 -0
  32. package/dist/environment.d.ts +1 -1
  33. package/dist/environment.js +1 -1
  34. package/dist/environment.js.map +1 -1
  35. package/dist/function-input-types.d.ts +10 -0
  36. package/dist/function-input-types.d.ts.map +1 -0
  37. package/dist/function-input-types.js +160 -0
  38. package/dist/function-input-types.js.map +1 -0
  39. package/dist/function-schema.d.ts.map +1 -1
  40. package/dist/function-schema.js +89 -36
  41. package/dist/function-schema.js.map +1 -1
  42. package/dist/init-field-retry.d.ts +17 -0
  43. package/dist/init-field-retry.d.ts.map +1 -0
  44. package/dist/init-field-retry.js +127 -0
  45. package/dist/init-field-retry.js.map +1 -0
  46. package/dist/init-journal.d.ts +98 -0
  47. package/dist/init-journal.d.ts.map +1 -0
  48. package/dist/init-journal.js +492 -0
  49. package/dist/init-journal.js.map +1 -0
  50. package/dist/init-schema.d.ts +8 -0
  51. package/dist/init-schema.d.ts.map +1 -0
  52. package/dist/init-schema.js +423 -0
  53. package/dist/init-schema.js.map +1 -0
  54. package/dist/init.d.ts +14 -0
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +883 -261
  57. package/dist/init.js.map +1 -1
  58. package/dist/lark-cli-error.d.ts +46 -0
  59. package/dist/lark-cli-error.d.ts.map +1 -0
  60. package/dist/lark-cli-error.js +171 -0
  61. package/dist/lark-cli-error.js.map +1 -0
  62. package/dist/lark-cli.d.ts +1 -1
  63. package/dist/lark-cli.d.ts.map +1 -1
  64. package/dist/lark-cli.js +66 -18
  65. package/dist/lark-cli.js.map +1 -1
  66. package/dist/mutation-schema.d.ts +146 -0
  67. package/dist/mutation-schema.d.ts.map +1 -0
  68. package/dist/mutation-schema.js +153 -0
  69. package/dist/mutation-schema.js.map +1 -0
  70. package/dist/mutation.d.ts +10 -0
  71. package/dist/mutation.d.ts.map +1 -0
  72. package/dist/mutation.js +131 -0
  73. package/dist/mutation.js.map +1 -0
  74. package/dist/npm.d.ts +1 -0
  75. package/dist/npm.d.ts.map +1 -1
  76. package/dist/npm.js +2 -2
  77. package/dist/npm.js.map +1 -1
  78. package/dist/preview-receipt.d.ts.map +1 -1
  79. package/dist/preview-receipt.js +15 -5
  80. package/dist/preview-receipt.js.map +1 -1
  81. package/dist/process.d.ts +3 -1
  82. package/dist/process.d.ts.map +1 -1
  83. package/dist/process.js +16 -4
  84. package/dist/process.js.map +1 -1
  85. package/dist/program.d.ts.map +1 -1
  86. package/dist/program.js +240 -41
  87. package/dist/program.js.map +1 -1
  88. package/dist/project-build.d.ts +25 -0
  89. package/dist/project-build.d.ts.map +1 -0
  90. package/dist/project-build.js +229 -0
  91. package/dist/project-build.js.map +1 -0
  92. package/dist/project-context.d.ts +344 -0
  93. package/dist/project-context.d.ts.map +1 -0
  94. package/dist/project-context.js +485 -0
  95. package/dist/project-context.js.map +1 -0
  96. package/dist/project-layout.d.ts +5 -0
  97. package/dist/project-layout.d.ts.map +1 -0
  98. package/dist/project-layout.js +24 -0
  99. package/dist/project-layout.js.map +1 -0
  100. package/dist/public-sql-error.d.ts +25 -0
  101. package/dist/public-sql-error.d.ts.map +1 -0
  102. package/dist/public-sql-error.js +81 -0
  103. package/dist/public-sql-error.js.map +1 -0
  104. package/dist/pull.d.ts.map +1 -1
  105. package/dist/pull.js +54 -15
  106. package/dist/pull.js.map +1 -1
  107. package/dist/site-readme.d.ts +3 -0
  108. package/dist/site-readme.d.ts.map +1 -0
  109. package/dist/site-readme.js +83 -0
  110. package/dist/site-readme.js.map +1 -0
  111. package/dist/workspace-fingerprint.d.ts +21 -1
  112. package/dist/workspace-fingerprint.d.ts.map +1 -1
  113. package/dist/workspace-fingerprint.js +81 -2
  114. package/dist/workspace-fingerprint.js.map +1 -1
  115. package/package.json +2 -1
  116. package/skills/lark-base-database/SKILL.md +66 -50
  117. package/skills/lark-base-database/references/ai-function.md +471 -0
  118. package/skills/lark-base-database/references/base-capability-routing.md +3 -3
  119. package/skills/lark-base-database/references/database-function-limitations.md +46 -0
  120. package/skills/lark-base-database/references/database-function.md +349 -0
  121. package/skills/lark-base-database/references/design/components/actions-fields.md +16 -0
  122. package/skills/lark-base-database/references/design/components/code/controls.css +122 -0
  123. package/skills/lark-base-database/references/design/components/code/controls.tsx +73 -0
  124. package/skills/lark-base-database/references/design/components/code/date-picker.tsx +42 -0
  125. package/skills/lark-base-database/references/design/components/code/floating.tsx +41 -0
  126. package/skills/lark-base-database/references/design/components/code/overlays.tsx +19 -0
  127. package/skills/lark-base-database/references/design/components/code/select.tsx +34 -0
  128. package/skills/lark-base-database/references/design/components/code/styles.d.ts +1 -0
  129. package/skills/lark-base-database/references/design/components/code/theme.css +49 -0
  130. package/skills/lark-base-database/references/design/components/code/usage.tsx +38 -0
  131. package/skills/lark-base-database/references/design/components/date-time-number.md +9 -0
  132. package/skills/lark-base-database/references/design/components/feedback-navigation.md +9 -0
  133. package/skills/lark-base-database/references/design/components/index.md +18 -0
  134. package/skills/lark-base-database/references/design/components/overlays.md +9 -0
  135. package/skills/lark-base-database/references/design/components/patterns/flat-business.md +7 -0
  136. package/skills/lark-base-database/references/design/components/selection.md +24 -0
  137. package/skills/lark-base-database/references/design/components.md +91 -0
  138. package/skills/lark-base-database/references/design/composition.md +90 -0
  139. package/skills/lark-base-database/references/design/control-recipes.md +107 -0
  140. package/skills/lark-base-database/references/design/examples.md +172 -0
  141. package/skills/lark-base-database/references/design/foundations.md +126 -0
  142. package/skills/lark-base-database/references/design/review.md +82 -0
  143. package/skills/lark-base-database/references/design/scenes/analysis-report.md +66 -0
  144. package/skills/lark-base-database/references/design/scenes/brand-showcase.md +70 -0
  145. package/skills/lark-base-database/references/design/scenes/business-operations.md +85 -0
  146. package/skills/lark-base-database/references/design/scenes/campaign-report.md +66 -0
  147. package/skills/lark-base-database/references/design/scenes/dashboard.md +77 -0
  148. package/skills/lark-base-database/references/design/scenes/festive-campaign.md +69 -0
  149. package/skills/lark-base-database/references/design/theme.md +27 -0
  150. package/skills/lark-base-database/references/design/typography.md +56 -0
  151. package/skills/lark-base-database/references/development.md +165 -32
  152. package/skills/lark-base-database/references/environment-variables.md +55 -0
  153. package/skills/lark-base-database/references/faas.md +41 -48
  154. package/skills/lark-base-database/references/file-transfer.md +25 -25
  155. package/skills/lark-base-database/references/frontend-design-guide.md +33 -77
  156. package/skills/lark-base-database/references/mutations.md +295 -0
  157. package/skills/lark-base-database/references/permission-design.md +24 -24
  158. package/skills/lark-base-database/references/release.md +51 -24
  159. package/skills/lark-base-database/references/style-guide.md +10 -67
  160. package/skills/lark-base-database/references/verification.md +15 -11
  161. package/skills/lark-base-database/references/workspace.md +46 -8
  162. package/templates/database-starter/README.md +34 -4
  163. package/templates/database-starter/database-functions/README.md +8 -0
  164. package/templates/database-starter/mutations/README.md +19 -0
  165. package/templates/database-starter/page/README.md +25 -19
  166. package/templates/database-starter/page/package-lock.json +8 -16
  167. package/templates/database-starter/page/package.json +2 -3
  168. package/templates/database-starter/page/src/App.tsx +23 -3
  169. package/templates/database-starter/page/src/components/UserSelectorEditor/index.tsx +32 -4
  170. package/templates/database-starter/page/src/components/UserSelectorPanel/index.tsx +104 -34
  171. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.test.ts +55 -11
  172. package/templates/database-starter/page/src/components/UserSelectorPanel/user-search.ts +68 -25
  173. package/templates/database-starter/page/src/components/database-runtime-ready.tsx +1 -1
  174. package/templates/database-starter/page/src/components/database-user-menu/index.tsx +4 -7
  175. package/templates/database-starter/page/src/components/feedback/page-state.tsx +40 -5
  176. package/templates/database-starter/page/src/components/floating-button/assets/edit-active.svg +3 -0
  177. package/templates/database-starter/page/src/components/floating-button/assets/edit-default.svg +3 -0
  178. package/templates/database-starter/page/src/components/floating-button/index.tsx +75 -112
  179. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.test.ts +107 -37
  180. package/templates/database-starter/page/src/components/floating-button/utils/project-meta.ts +13 -50
  181. package/templates/database-starter/page/src/components/ui-language.test.tsx +101 -0
  182. package/templates/database-starter/page/src/examples/ai-function/ImageTaskOperation.tsx +202 -0
  183. package/templates/database-starter/page/src/examples/ai-function/README.md +14 -0
  184. package/templates/database-starter/page/src/examples/ai-function/TextStreamOperation.tsx +184 -0
  185. package/templates/database-starter/page/src/examples/ai-function/ai-copy.ts +173 -0
  186. package/templates/database-starter/page/src/examples/ai-function/ai-delta.test.ts +30 -0
  187. package/templates/database-starter/page/src/examples/ai-function/ai-delta.ts +26 -0
  188. package/templates/database-starter/page/src/examples/ai-function/ai-language.test.tsx +86 -0
  189. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.test.ts +201 -0
  190. package/templates/database-starter/page/src/examples/ai-function/ai-media-panel.tsx +382 -0
  191. package/templates/database-starter/page/src/lib/attachment-image.tsx +144 -0
  192. package/templates/database-starter/page/src/lib/database-function-inputs.generated.ts +4 -0
  193. package/templates/database-starter/page/src/lib/database-runtime-attachment.test.ts +180 -76
  194. package/templates/database-starter/page/src/lib/database-runtime.test.ts +130 -10
  195. package/templates/database-starter/page/src/lib/database-runtime.ts +484 -51
  196. package/templates/database-starter/page/src/lib/date-time.ts +61 -0
  197. package/templates/database-starter/page/src/lib/ui-language.ts +86 -0
  198. package/templates/database-starter/page/src/main.tsx +1 -1
  199. package/templates/database-starter/page/tsconfig.json +2 -2
  200. package/templates/database-starter/page/vite.config.ts +1 -2
  201. package/templates/database-starter/prettierignore +1 -1
  202. package/templates/database-starter/server-functions/README.md +2 -2
  203. package/templates/database-starter/server-functions/package-lock.json +4 -4
  204. package/templates/database-starter/server-functions/package.json +1 -1
  205. package/dist/agent-guide.d.ts +0 -19
  206. package/dist/agent-guide.d.ts.map +0 -1
  207. package/dist/agent-guide.js +0 -31
  208. package/dist/agent-guide.js.map +0 -1
  209. package/dist/database-skill.d.ts +0 -24
  210. package/dist/database-skill.d.ts.map +0 -1
  211. package/dist/database-skill.js +0 -148
  212. package/dist/database-skill.js.map +0 -1
  213. package/skills/lark-base-database/references/base-sql-unsupported.md +0 -42
  214. package/skills/lark-base-database/references/database-capability-skill.md +0 -40
  215. package/skills/lark-base-database/references/sql-function.md +0 -289
  216. package/templates/database-capability/SKILL.md.tmpl +0 -58
  217. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.test.ts +0 -23
  218. package/templates/database-starter/page/src/components/floating-button/chatbot-entry.tsx +0 -873
  219. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.test.ts +0 -25
  220. package/templates/database-starter/page/src/components/floating-button/local-chat-panel.tsx +0 -197
  221. package/templates/database-starter/page/src/components/floating-button/utils/env.ts +0 -18
  222. package/templates/database-starter/sql/README.md +0 -8
package/dist/init.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
2
- import { basename, join, resolve } from 'node:path';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { InitJournal, InitFailureError, assertNoInitReceipt, } from './init-journal.js';
5
+ import { readInitSchema, InitSchemaError } from './init-schema.js';
6
+ import { createFieldsWithRetry } from './init-field-retry.js';
7
+ import { isRecord } from './lark-cli-error.js';
3
8
  import { starterRoot } from './assets.js';
4
- import { CONFIG_FILE_NAME, writeConfig } from './config.js';
9
+ import { DATABASE_FUNCTIONS_DIRECTORY, LEGACY_DATABASE_FUNCTIONS_DIRECTORY, databaseFunctionsDirectory, } from './project-layout.js';
10
+ import { CONFIG_FILE_NAME, readConfig, writeConfig } from './config.js';
5
11
  import { ONLINE_RUNTIME_PROFILE, resolveRuntimeProfile, } from './environment.js';
6
12
  import { responseData, runLarkCli, resolveLarkProfileEnvironment, } from './lark-cli.js';
7
13
  import { writePageTitle } from './page-title.js';
@@ -12,46 +18,6 @@ function requiredString(value, label) {
12
18
  }
13
19
  return value.trim();
14
20
  }
15
- function parseSchema(path) {
16
- const absolutePath = resolve(path);
17
- let parsed;
18
- try {
19
- parsed = JSON.parse(readFileSync(absolutePath, 'utf8'));
20
- }
21
- catch (error) {
22
- throw new Error(`Unable to read schema ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`);
23
- }
24
- if (!parsed || typeof parsed !== 'object') {
25
- throw new Error('Schema must be an object');
26
- }
27
- const tables = parsed.tables;
28
- if (!Array.isArray(tables) || tables.length === 0) {
29
- throw new Error('Schema must contain at least one table');
30
- }
31
- const names = new Set();
32
- for (const [tableIndex, table] of tables.entries()) {
33
- if (!table || typeof table !== 'object') {
34
- throw new Error(`schema.tables[${tableIndex}] must be an object`);
35
- }
36
- const candidate = table;
37
- const name = requiredString(candidate.name, `schema.tables[${tableIndex}].name`);
38
- if (names.has(name))
39
- throw new Error(`Duplicate table name: ${name}`);
40
- names.add(name);
41
- if (!Array.isArray(candidate.fields) || candidate.fields.length === 0) {
42
- throw new Error(`schema.tables[${tableIndex}].fields must not be empty`);
43
- }
44
- for (const [fieldIndex, field] of candidate.fields.entries()) {
45
- if (!field || typeof field !== 'object') {
46
- throw new Error(`schema.tables[${tableIndex}].fields[${fieldIndex}] must be an object`);
47
- }
48
- const candidateField = field;
49
- requiredString(candidateField.name, `schema.tables[${tableIndex}].fields[${fieldIndex}].name`);
50
- requiredString(candidateField.type, `schema.tables[${tableIndex}].fields[${fieldIndex}].type`);
51
- }
52
- }
53
- return parsed;
54
- }
55
21
  function tableId(response, label) {
56
22
  const data = responseData(response);
57
23
  const table = data.table;
@@ -76,6 +42,14 @@ function baseName(response) {
76
42
  ? value.trim()
77
43
  : undefined;
78
44
  }
45
+ function baseTimeZone(response) {
46
+ const data = responseData(response);
47
+ const base = data.base;
48
+ const value = base?.time_zone ?? base?.timeZone ?? data.time_zone ?? data.timeZone;
49
+ return typeof value === 'string' && value.trim() !== ''
50
+ ? value.trim()
51
+ : undefined;
52
+ }
79
53
  function existingTables(response) {
80
54
  const data = responseData(response);
81
55
  const value = data.tables ?? data.items;
@@ -185,29 +159,541 @@ function catalogBaseAppUrl(catalog) {
185
159
  const value = catalog.baseAppUrl;
186
160
  return typeof value === 'string' && value.trim() !== '' ? value : undefined;
187
161
  }
188
- export async function initializeProject(options) {
189
- const runtimeProfile = resolveRuntimeProfile(options.environment, options.runtimeProfile ?? ONLINE_RUNTIME_PROFILE);
190
- const workspace = resolve(options.directory);
191
- const databaseName = requiredString(options.name, '--name');
192
- for (const output of [
162
+ function validateApplication(value, token, name, description, baseTokenValue, tables, validateMetadata, validateBinding, exactBindings) {
163
+ if (!isRecord(value) || !isRecord(value.project))
164
+ throw new Error('Application readback does not contain project metadata');
165
+ const project = value.project;
166
+ if (project.token !== token)
167
+ throw new Error('Application token changed after initialization started');
168
+ if (validateMetadata &&
169
+ (project.name !== name || String(project.description ?? '') !== description))
170
+ throw new Error('Application metadata changed after initialization started');
171
+ if (!validateBinding)
172
+ return;
173
+ if (!Array.isArray(project.baseBindings))
174
+ throw new Error('Application readback does not contain Base bindings');
175
+ const binding = project.baseBindings.find((item) => isRecord(item) && item.baseToken === baseTokenValue);
176
+ if (!isRecord(binding) || !Array.isArray(binding.tableIds))
177
+ throw new Error('Application no longer contains the expected Base binding');
178
+ const actual = binding.tableIds.map(String);
179
+ const expected = tables.map((table) => table.tableId);
180
+ if (actual.length < expected.length ||
181
+ expected.some((id) => !actual.includes(id)) ||
182
+ (exactBindings &&
183
+ (project.baseBindings.length !== 1 || actual.length !== expected.length)))
184
+ throw new Error('Application Base bindings changed after initialization started');
185
+ }
186
+ function validateExactCatalog(value, baseTokenValue, tables) {
187
+ const catalog = validateCatalog(value, baseTokenValue, tables);
188
+ const actual = Array.isArray(catalog.tables) ? catalog.tables : [];
189
+ const expected = new Set(tables.map((table) => `${baseTokenValue}:${table.tableId}`));
190
+ if (actual.length !== expected.size ||
191
+ actual.some((table) => !isRecord(table) ||
192
+ !expected.has(`${String(table.baseToken)}:${String(table.id)}`)))
193
+ throw new Error('Application Catalog contains unexpected table bindings');
194
+ }
195
+ function recoveryBlocked(journal, phase, message) {
196
+ throw new InitFailureError(message, {
197
+ ok: false,
198
+ status: 'blocked',
199
+ phase,
200
+ journal: journal.path,
201
+ receipt: journal.receipt,
202
+ error: { message },
203
+ nextAction: 'Preserve the receipt and reconcile the reported resource or unknown outcome. No recovery write was sent.',
204
+ });
205
+ }
206
+ function existingFields(response) {
207
+ const data = responseData(response);
208
+ const value = data.fields ?? data.items;
209
+ if (!Array.isArray(value))
210
+ throw new Error('field-list response does not contain fields');
211
+ const fields = value.map((field, index) => {
212
+ if (!isRecord(field))
213
+ throw new Error(`field-list response fields[${index}] must be an object`);
214
+ return {
215
+ id: requiredString(field.id ?? field.field_id ?? field.fieldId, `field-list response fields[${index}] ID`),
216
+ name: requiredString(field.name, `field-list response fields[${index}] name`),
217
+ type: requiredString(field.type, `field-list response fields[${index}] type`),
218
+ raw: field,
219
+ };
220
+ });
221
+ return {
222
+ fields,
223
+ total: typeof data.total === 'number' && Number.isInteger(data.total)
224
+ ? data.total
225
+ : fields.length,
226
+ };
227
+ }
228
+ function listExistingFields(baseTokenValue, tableIdValue, env) {
229
+ const limit = 200;
230
+ const fields = [];
231
+ for (let offset = 0;; offset += limit) {
232
+ const page = existingFields(runLarkCli([
233
+ 'base',
234
+ '+field-list',
235
+ '--base-token',
236
+ baseTokenValue,
237
+ '--table-id',
238
+ tableIdValue,
239
+ '--limit',
240
+ String(limit),
241
+ '--offset',
242
+ String(offset),
243
+ ], env));
244
+ fields.push(...page.fields);
245
+ if (fields.length >= page.total)
246
+ return fields;
247
+ if (page.fields.length === 0)
248
+ throw new Error('field-list returned more fields without a page');
249
+ }
250
+ }
251
+ function primaryFieldId(baseTokenValue, tableIdValue, env) {
252
+ const data = responseData(runLarkCli([
253
+ 'base',
254
+ '+table-get',
255
+ '--base-token',
256
+ baseTokenValue,
257
+ '--table-id',
258
+ tableIdValue,
259
+ ], env));
260
+ const table = isRecord(data.table) ? data.table : {};
261
+ return requiredString(table.primary_field ?? table.primaryField, `table-get response primary field for ${tableIdValue}`);
262
+ }
263
+ function receiptTable(receipt, name) {
264
+ return receipt.resources.tables.find((table) => table.name === name);
265
+ }
266
+ function plainObject(value) {
267
+ return isRecord(value) ? value : {};
268
+ }
269
+ function normalizeStyle(type, value) {
270
+ const source = plainObject(value);
271
+ if (type === 'text')
272
+ return { type: source.type ?? 'plain', ...source };
273
+ if (type === 'number') {
274
+ const styleType = String(source.type ?? 'plain');
275
+ const defaults = {
276
+ plain: { precision: 2, percentage: false, thousands_separator: false },
277
+ currency: { precision: 2 },
278
+ progress: { percentage: true },
279
+ rating: { icon: 'star', min: 1, max: 5 },
280
+ };
281
+ return { type: styleType, ...(defaults[styleType] ?? {}), ...source };
282
+ }
283
+ if (['datetime', 'created_at', 'updated_at'].includes(type))
284
+ return { format: 'yyyy/MM/dd', ...source };
285
+ return Object.keys(source).length > 0 ? source : undefined;
286
+ }
287
+ function normalizeFieldConfig(source, receipt, remoteByTable) {
288
+ const type = String(source.type ?? '');
289
+ const result = {
290
+ name: source.name,
291
+ type,
292
+ description: source.description ?? null,
293
+ };
294
+ const style = normalizeStyle(type, source.style);
295
+ if (style)
296
+ result.style = style;
297
+ const supportsDefault = [
298
+ 'text',
299
+ 'number',
300
+ 'select',
301
+ 'datetime',
302
+ 'user',
303
+ ].includes(type);
304
+ if (supportsDefault && source.dynamic_options_source === undefined)
305
+ result.default_value = source.default_value ?? null;
306
+ if (type === 'select') {
307
+ result.multiple = source.multiple ?? false;
308
+ if (Array.isArray(source.options))
309
+ result.options = source.options.map((value) => {
310
+ const option = plainObject(value);
311
+ return {
312
+ name: option.name,
313
+ hue: option.hue ?? 'Blue',
314
+ lightness: option.lightness ?? 'Lighter',
315
+ };
316
+ });
317
+ if (isRecord(source.dynamic_options_source)) {
318
+ const dynamic = source.dynamic_options_source;
319
+ const tableReference = String(dynamic.table_id ?? '');
320
+ const table = receiptTable(receipt, tableReference);
321
+ const tableIdValue = table?.tableId ?? tableReference;
322
+ const remote = remoteByTable.get(tableIdValue) ?? [];
323
+ const fieldReference = String(dynamic.field_id ?? '');
324
+ const fieldIdValue = remote.find((field) => field.id === fieldReference || field.name === fieldReference)?.id ?? fieldReference;
325
+ result.dynamic_options_source = {
326
+ table_id: tableIdValue,
327
+ field_id: fieldIdValue,
328
+ };
329
+ }
330
+ }
331
+ if (type === 'user' || type === 'group_chat')
332
+ result.multiple = source.multiple ?? true;
333
+ if (type === 'link') {
334
+ const reference = String(source.link_table ?? '');
335
+ result.link_table = receiptTable(receipt, reference)?.tableId ?? reference;
336
+ result.bidirectional = source.bidirectional ?? false;
337
+ if (source.bidirectional_link_field_name !== undefined)
338
+ result.bidirectional_link_field_name =
339
+ source.bidirectional_link_field_name;
340
+ }
341
+ if (type === 'formula')
342
+ result.expression = source.expression;
343
+ if (type === 'lookup')
344
+ for (const key of ['from', 'select', 'where', 'aggregate'])
345
+ if (source[key] !== undefined)
346
+ result[key] = source[key];
347
+ if (type === 'auto_number' && source.style !== undefined)
348
+ result.style = source.style;
349
+ if (type === 'button')
350
+ result.button_config = source.button_config;
351
+ return result;
352
+ }
353
+ function configContains(actual, expected) {
354
+ if (Array.isArray(expected))
355
+ return (Array.isArray(actual) &&
356
+ actual.length === expected.length &&
357
+ expected.every((value, index) => configContains(actual[index], value)));
358
+ if (isRecord(expected))
359
+ return (isRecord(actual) &&
360
+ Object.entries(expected).every(([key, value]) => configContains(actual[key], value)));
361
+ return Object.is(actual, expected);
362
+ }
363
+ function stableJson(value) {
364
+ if (Array.isArray(value))
365
+ return value.map(stableJson);
366
+ if (isRecord(value))
367
+ return Object.fromEntries(Object.keys(value)
368
+ .sort()
369
+ .map((key) => [key, stableJson(value[key])]));
370
+ return value;
371
+ }
372
+ function assertRemoteField(journal, phase, remote, expected, remoteByTable, id) {
373
+ if (!id)
374
+ recoveryBlocked(journal, phase, `Receipt does not contain a confirmed field ID for ${String(expected.name)}`);
375
+ const actual = remote.find((field) => field.id === id);
376
+ const expectedConfig = normalizeFieldConfig(expected, journal.receipt, remoteByTable);
377
+ const actualConfig = actual
378
+ ? normalizeFieldConfig(actual.raw, journal.receipt, remoteByTable)
379
+ : undefined;
380
+ if (!actual ||
381
+ actual.name !== expected.name ||
382
+ actual.type !== expected.type ||
383
+ !configContains(actualConfig, expectedConfig))
384
+ recoveryBlocked(journal, phase, `Remote field drift detected for ${String(expected.name)} (${id})`);
385
+ }
386
+ function validateFieldItems(journal, step, fields, remote, remoteByTable) {
387
+ const items = step.items;
388
+ if (!items || items.length !== fields.length)
389
+ recoveryBlocked(journal, step.id, `Receipt does not contain a complete field result map for ${step.id}`);
390
+ const indexes = new Set();
391
+ const retry = [];
392
+ for (const item of items) {
393
+ if (!Number.isInteger(item.index) ||
394
+ item.index < 0 ||
395
+ item.index >= fields.length ||
396
+ indexes.has(item.index))
397
+ recoveryBlocked(journal, step.id, `Invalid field result index in ${step.id}`);
398
+ indexes.add(item.index);
399
+ const expected = fields[item.index];
400
+ if (!expected)
401
+ recoveryBlocked(journal, step.id, `Missing field plan for ${step.id}`);
402
+ if (item.status === 'created') {
403
+ assertRemoteField(journal, step.id, remote, expected, remoteByTable, item.field?.id);
404
+ }
405
+ else if (item.status === 'failed' || item.status === 'not_attempted') {
406
+ if (remote.some((field) => field.name === expected.name))
407
+ recoveryBlocked(journal, step.id, `Remote field ${String(expected.name)} exists without a confirmed receipt ID`);
408
+ retry.push(item.index);
409
+ }
410
+ else {
411
+ recoveryBlocked(journal, step.id, `Unsupported field result in ${step.id}`);
412
+ }
413
+ }
414
+ return retry;
415
+ }
416
+ function buildInitPlan(options, schema, resources) {
417
+ const steps = [];
418
+ if (schema) {
419
+ schema.tables.forEach((table, i) => steps.push({
420
+ id: i === 0 ? 'create-base' : `create-table:${i}`,
421
+ kind: 'write',
422
+ target: { tableIndex: i, tableName: table.name },
423
+ }));
424
+ schema.tables.forEach((table, i) => {
425
+ if (table.fields.length > 1)
426
+ steps.push({
427
+ id: `create-fields:${i}`,
428
+ kind: 'write',
429
+ target: { tableIndex: i, tableName: table.name, fieldOffset: 1 },
430
+ });
431
+ });
432
+ }
433
+ if (options.appToken)
434
+ steps.push({ id: 'update-app', kind: 'write' }, { id: 'bind-base', kind: 'write' });
435
+ else
436
+ steps.push({ id: 'create-app', kind: 'write' });
437
+ steps.push({ id: 'read-catalog', kind: 'read' }, { id: 'scaffold', kind: 'local' });
438
+ const planHash = createHash('sha256')
439
+ .update(JSON.stringify({
440
+ name: options.name,
441
+ description: options.description,
442
+ timeZone: options.timeZone,
443
+ schema,
444
+ baseToken: options.baseToken,
445
+ appToken: options.appToken,
446
+ }))
447
+ .digest('hex');
448
+ return {
449
+ steps,
450
+ planHash,
451
+ resources: {
452
+ ...resources,
453
+ baseSource: schema ? 'created' : 'existing',
454
+ appSource: options.appToken ? 'existing' : 'created',
455
+ ...(options.appToken ? { appToken: options.appToken.trim() } : {}),
456
+ },
457
+ };
458
+ }
459
+ function prepareResume(journal, options, schema, env) {
460
+ for (const step of journal.receipt.steps) {
461
+ if (step.kind === 'write' && ['running', 'unknown'].includes(step.status))
462
+ recoveryBlocked(journal, step.id, `Write outcome is unknown for ${step.id}; recovery cannot continue`);
463
+ if (step.id === 'create-app' && step.status === 'failed')
464
+ recoveryBlocked(journal, step.id, 'Application creation may have produced an application before returning an error');
465
+ if (step.status === 'partial' && !step.id.startsWith('create-fields:'))
466
+ recoveryBlocked(journal, step.id, `Unsupported partial step ${step.id}`);
467
+ if (step.kind === 'local' &&
468
+ !['not_attempted', 'succeeded'].includes(step.status))
469
+ recoveryBlocked(journal, step.id, 'Local scaffolding may be incomplete and will not be overwritten');
470
+ }
471
+ const receipt = journal.receipt;
472
+ const fieldRetries = new Map();
473
+ let remoteTables = [];
474
+ const needsBaseRead = receipt.resources.baseSource === 'existing' ||
475
+ receipt.steps.some((step) => (step.id === 'create-base' || step.id.startsWith('create-table:')) &&
476
+ step.status === 'succeeded');
477
+ if (needsBaseRead) {
478
+ const token = receipt.resources.baseToken;
479
+ if (!token)
480
+ recoveryBlocked(journal, 'resume-preflight', 'Receipt has no confirmed Base token');
481
+ const baseResponse = runLarkCli(['base', '+base-get', '--base-token', token], env);
482
+ if (receipt.resources.baseSource === 'created' &&
483
+ (baseName(baseResponse) !== requiredString(options.name, '--name') ||
484
+ baseTimeZone(baseResponse) !== options.timeZone))
485
+ recoveryBlocked(journal, 'resume-preflight', 'Created Base name or time zone changed after initialization started');
486
+ remoteTables = listExistingTables(token, env);
487
+ for (const table of receipt.resources.tables) {
488
+ const actual = remoteTables.find((item) => item.tableId === table.tableId);
489
+ if (!actual || actual.name !== table.name)
490
+ recoveryBlocked(journal, 'resume-preflight', `Remote table drift detected for ${table.name} (${table.tableId})`);
491
+ }
492
+ if (receipt.resources.baseSource === 'existing' &&
493
+ (remoteTables.length !== receipt.resources.tables.length ||
494
+ remoteTables.some((table) => !receipt.resources.tables.some((saved) => saved.tableId === table.tableId && saved.name === table.name))))
495
+ recoveryBlocked(journal, 'resume-preflight', 'Existing Base table set changed after initialization started');
496
+ }
497
+ const remoteByTable = new Map();
498
+ if (schema && receipt.resources.baseToken) {
499
+ for (const [tableIndex, table] of schema.tables.entries()) {
500
+ const step = journal.step(tableIndex === 0 ? 'create-base' : `create-table:${tableIndex}`);
501
+ const saved = receiptTable(receipt, table.name);
502
+ if (step.status === 'succeeded' && saved)
503
+ remoteByTable.set(saved.tableId, listExistingFields(receipt.resources.baseToken, saved.tableId, env));
504
+ }
505
+ }
506
+ if (schema) {
507
+ for (const [tableIndex, table] of schema.tables.entries()) {
508
+ const createStep = journal.step(tableIndex === 0 ? 'create-base' : `create-table:${tableIndex}`);
509
+ const savedTable = receiptTable(receipt, table.name);
510
+ if (createStep.status === 'succeeded') {
511
+ if (!savedTable)
512
+ recoveryBlocked(journal, createStep.id, `Receipt has no confirmed table ID for ${table.name}`);
513
+ const fields = remoteByTable.get(savedTable.tableId);
514
+ if (!fields)
515
+ recoveryBlocked(journal, createStep.id, `Remote fields were not available for ${table.name}`);
516
+ const primary = table.fields[0];
517
+ if (!primary)
518
+ recoveryBlocked(journal, createStep.id, `Primary field drift detected for ${table.name}`);
519
+ assertRemoteField(journal, createStep.id, fields, primary, remoteByTable, primaryFieldId(requiredString(receipt.resources.baseToken, 'receipt Base token'), savedTable.tableId, env));
520
+ const fieldStep = receipt.steps.find((step) => step.id === `create-fields:${tableIndex}`);
521
+ if (fieldStep && fieldStep.status !== 'not_attempted') {
522
+ const planned = table.fields.slice(1);
523
+ if (fieldStep.status === 'failed' &&
524
+ fieldStep.error?.kind === 'api' &&
525
+ !fieldStep.items) {
526
+ if (planned.some((expected) => fields.some((field) => field.name === expected.name)))
527
+ recoveryBlocked(journal, fieldStep.id, 'A failed field step has matching remote fields without confirmed IDs');
528
+ fieldRetries.set(fieldStep.id, planned.map((_, index) => index));
529
+ continue;
530
+ }
531
+ const retry = validateFieldItems(journal, fieldStep, planned, fields, remoteByTable);
532
+ if (fieldStep.status === 'succeeded') {
533
+ if (retry.length > 0)
534
+ recoveryBlocked(journal, fieldStep.id, `Succeeded field step contains incomplete items: ${fieldStep.id}`);
535
+ }
536
+ else if (fieldStep.status === 'partial' ||
537
+ fieldStep.status === 'failed') {
538
+ if (retry.length === 0)
539
+ journal.reconcile(fieldStep.id);
540
+ else
541
+ fieldRetries.set(fieldStep.id, retry);
542
+ }
543
+ else {
544
+ recoveryBlocked(journal, fieldStep.id, `Field recovery cannot continue from ${fieldStep.status}`);
545
+ }
546
+ }
547
+ }
548
+ else if (createStep.status === 'partial') {
549
+ recoveryBlocked(journal, createStep.id, `Unsupported partial step ${createStep.id}`);
550
+ }
551
+ else if (createStep.status === 'failed') {
552
+ if (createStep.id === 'create-base' || createStep.error?.kind !== 'api')
553
+ recoveryBlocked(journal, createStep.id, `Table creation is not safely retryable for ${createStep.id}`);
554
+ }
555
+ }
556
+ }
557
+ const createApp = receipt.steps.find((step) => step.id === 'create-app');
558
+ const updateApp = receipt.steps.find((step) => step.id === 'update-app');
559
+ const bindBase = receipt.steps.find((step) => step.id === 'bind-base');
560
+ if (createApp?.status === 'succeeded' ||
561
+ updateApp?.status === 'succeeded' ||
562
+ bindBase?.status === 'succeeded') {
563
+ const token = receipt.resources.appToken;
564
+ const baseTokenValue = receipt.resources.baseToken;
565
+ if (!token || !baseTokenValue)
566
+ recoveryBlocked(journal, 'resume-preflight', 'Receipt has no confirmed application or Base token');
567
+ const projectResponse = runLarkCli([
568
+ 'api',
569
+ 'GET',
570
+ `/open-apis/base/v3/sites/${encodeURIComponent(token)}/get`,
571
+ ], env);
572
+ try {
573
+ validateApplication(projectResponse.data, token, requiredString(options.name, '--name'), options.description ?? '', baseTokenValue, receipt.resources.tables, createApp?.status === 'succeeded' || updateApp?.status === 'succeeded', createApp?.status === 'succeeded' || bindBase?.status === 'succeeded', createApp?.status === 'succeeded');
574
+ if (createApp?.status === 'succeeded' ||
575
+ bindBase?.status === 'succeeded') {
576
+ const catalog = runLarkCli([
577
+ 'api',
578
+ 'GET',
579
+ `/open-apis/base/v3/sites/${encodeURIComponent(token)}/catalog`,
580
+ ], env);
581
+ if (createApp?.status === 'succeeded')
582
+ validateExactCatalog(catalog.data, baseTokenValue, receipt.resources.tables);
583
+ else
584
+ validateCatalog(catalog.data, baseTokenValue, receipt.resources.tables);
585
+ }
586
+ }
587
+ catch (error) {
588
+ recoveryBlocked(journal, 'resume-preflight', error instanceof Error ? error.message : 'Application drift detected');
589
+ }
590
+ }
591
+ for (const step of receipt.steps) {
592
+ if (step.kind === 'read') {
593
+ if (step.status === 'unknown')
594
+ recoveryBlocked(journal, step.id, `Read outcome is invalid for recovery: ${step.id}`);
595
+ if (step.status !== 'not_attempted')
596
+ journal.prepare(step.id);
597
+ }
598
+ else if (step.status === 'failed' &&
599
+ step.kind === 'write' &&
600
+ step.error?.kind === 'api' &&
601
+ !['create-base', 'create-app'].includes(step.id) &&
602
+ !step.id.startsWith('create-fields:'))
603
+ journal.prepare(step.id);
604
+ }
605
+ const scaffold = journal.step('scaffold');
606
+ const projectRoot = resolve(dirname(journal.path), '..', '..');
607
+ const scaffoldOutputs = [
193
608
  CONFIG_FILE_NAME,
194
609
  'catalog.json',
195
610
  'page',
196
611
  'server-functions',
197
- 'sql',
198
- ]) {
199
- if (existsSync(join(workspace, output))) {
200
- throw new Error(`Refusing to overwrite ${join(workspace, output)}`);
612
+ ];
613
+ if (scaffold.status === 'succeeded') {
614
+ for (const output of scaffoldOutputs)
615
+ if (!existsSync(join(projectRoot, output)))
616
+ recoveryBlocked(journal, 'scaffold', `Completed scaffold output is missing: ${output}`);
617
+ let databaseFunctions;
618
+ try {
619
+ databaseFunctions = databaseFunctionsDirectory(projectRoot);
620
+ }
621
+ catch (error) {
622
+ recoveryBlocked(journal, 'scaffold', error instanceof Error
623
+ ? error.message
624
+ : 'Function source roots are invalid');
625
+ }
626
+ if (!databaseFunctions) {
627
+ recoveryBlocked(journal, 'scaffold', `Completed scaffold output is missing: ${DATABASE_FUNCTIONS_DIRECTORY}`);
628
+ }
629
+ const config = readConfig(projectRoot);
630
+ const bindings = isRecord(config.bindings) ? config.bindings : {};
631
+ const main = isRecord(bindings.MAIN_BASE) ? bindings.MAIN_BASE : {};
632
+ const configuredTables = Array.isArray(main.tableIds)
633
+ ? main.tableIds.map(String)
634
+ : [];
635
+ const expectedTables = receipt.resources.tables.map((table) => table.tableId);
636
+ if (config.appToken !== receipt.resources.appToken ||
637
+ (config.larkProfile ?? undefined) !==
638
+ (receipt.larkProfile ?? undefined) ||
639
+ (config.runtimeProfile ?? ONLINE_RUNTIME_PROFILE.name) !==
640
+ receipt.environment ||
641
+ main.baseToken !== receipt.resources.baseToken ||
642
+ configuredTables.length !== expectedTables.length ||
643
+ configuredTables.some((id) => !expectedTables.includes(id)))
644
+ recoveryBlocked(journal, 'scaffold', 'Completed project configuration does not match the receipt');
645
+ }
646
+ else if (scaffold.status === 'not_attempted' &&
647
+ (scaffoldOutputs.some((output) => existsSync(join(projectRoot, output))) ||
648
+ existsSync(join(projectRoot, DATABASE_FUNCTIONS_DIRECTORY)) ||
649
+ existsSync(join(projectRoot, LEGACY_DATABASE_FUNCTIONS_DIRECTORY)))) {
650
+ recoveryBlocked(journal, 'scaffold', 'Project outputs exist before the recorded scaffold step');
651
+ }
652
+ return { fieldRetries };
653
+ }
654
+ export async function initializeProject(options) {
655
+ const runtimeProfile = resolveRuntimeProfile(options.environment, options.runtimeProfile ?? ONLINE_RUNTIME_PROFILE);
656
+ const workspace = resolve(options.directory);
657
+ const databaseName = requiredString(options.name, '--name');
658
+ if (!options.resume) {
659
+ for (const output of [
660
+ CONFIG_FILE_NAME,
661
+ 'catalog.json',
662
+ 'page',
663
+ 'server-functions',
664
+ DATABASE_FUNCTIONS_DIRECTORY,
665
+ LEGACY_DATABASE_FUNCTIONS_DIRECTORY,
666
+ ]) {
667
+ if (existsSync(join(workspace, output))) {
668
+ throw new Error(`Refusing to overwrite ${join(workspace, output)}`);
669
+ }
201
670
  }
671
+ assertNoInitReceipt(workspace);
202
672
  }
203
673
  const schemaPath = options.schema?.trim();
204
674
  const existingBaseToken = options.baseToken?.trim();
205
675
  const existingAppToken = options.appToken?.trim();
676
+ if (options.appToken !== undefined && !existingAppToken)
677
+ throw new Error('--app-token must be a non-empty string');
206
678
  if (Boolean(schemaPath) === Boolean(existingBaseToken)) {
207
679
  throw new Error('Provide exactly one of --schema or --base-token');
208
680
  }
681
+ let schema;
682
+ try {
683
+ schema = schemaPath ? readInitSchema(schemaPath) : undefined;
684
+ }
685
+ catch (error) {
686
+ if (!(error instanceof InitSchemaError))
687
+ throw error;
688
+ throw new InitFailureError(error.message, {
689
+ ok: false,
690
+ status: 'failed',
691
+ phase: 'preflight',
692
+ error: { message: error.message, path: error.path },
693
+ nextAction: 'Correct the schema at the reported path. No remote resources were created.',
694
+ });
695
+ }
209
696
  const { env, larkProfile } = resolveLarkProfileEnvironment(await runtimeProfile.commandEnvironment(), options.larkProfile, undefined, runtimeProfile);
210
- const attempts = runtimeProfile.initAttempts;
211
697
  let selectedBaseToken;
212
698
  let selectedBaseUrl;
213
699
  let selectedBaseName = databaseName;
@@ -215,227 +701,363 @@ export async function initializeProject(options) {
215
701
  const baseSource = existingBaseToken
216
702
  ? 'existing'
217
703
  : 'created';
218
- if (existingBaseToken) {
219
- selectedBaseToken = requiredString(existingBaseToken, '--base-token');
220
- process.stderr.write('Reading existing Base tables...\n');
221
- const existingBase = runLarkCli(['base', '+base-get', '--base-token', selectedBaseToken], env);
222
- selectedBaseUrl = baseUrl(existingBase);
223
- selectedBaseName = baseName(existingBase) ?? selectedBaseToken;
224
- tables = listExistingTables(selectedBaseToken, env);
225
- }
226
- else {
227
- const schema = parseSchema(requiredString(schemaPath, '--schema'));
228
- const [first, ...remaining] = schema.tables;
229
- if (!first)
230
- throw new Error('Schema must contain at least one table');
231
- process.stderr.write(`Creating Base ${databaseName}...\n`);
232
- const createdBase = await retry('base-create', attempts, () => runLarkCli([
233
- 'base',
234
- '+base-create',
235
- '--name',
236
- databaseName,
237
- '--time-zone',
238
- options.timeZone,
239
- '--table-name',
240
- first.name,
241
- '--fields',
242
- JSON.stringify(first.fields.slice(0, 1)),
243
- ], env));
244
- selectedBaseToken = baseToken(createdBase);
245
- selectedBaseUrl = baseUrl(createdBase);
246
- tables = [
247
- { name: first.name, tableId: tableId(createdBase, 'base-create') },
248
- ];
249
- for (const table of remaining) {
250
- await pause();
251
- process.stderr.write(`Creating table ${table.name}...\n`);
252
- const response = await retry(`table-create ${table.name}`, attempts, () => runLarkCli([
253
- 'base',
254
- '+table-create',
255
- '--base-token',
256
- selectedBaseToken,
257
- '--name',
258
- table.name,
259
- '--fields',
260
- JSON.stringify(table.fields.slice(0, 1)),
261
- ], env));
262
- tables.push({
263
- name: table.name,
264
- tableId: tableId(response, 'table-create'),
704
+ let journal;
705
+ let resumePreparation;
706
+ try {
707
+ if (options.resume) {
708
+ const plan = buildInitPlan(options, schema, {
709
+ ...(existingBaseToken ? { baseToken: existingBaseToken } : {}),
710
+ tables: [],
265
711
  });
712
+ journal = InitJournal.resume(workspace, {
713
+ environment: options.environment,
714
+ ...(larkProfile ? { larkProfile } : {}),
715
+ planHash: plan.planHash,
716
+ }, plan.steps, plan.resources, env);
717
+ resumePreparation = prepareResume(journal, options, schema, env);
266
718
  }
267
- for (const [tableIndex, table] of schema.tables.entries()) {
268
- const remainingFields = table.fields.slice(1);
269
- if (remainingFields.length === 0)
270
- continue;
271
- const createdTable = tables[tableIndex];
272
- if (!createdTable) {
273
- throw new Error(`Missing created table for schema table ${table.name}`);
719
+ if (existingBaseToken) {
720
+ selectedBaseToken = requiredString(existingBaseToken, '--base-token');
721
+ process.stderr.write('Reading existing Base tables...\n');
722
+ const existingBase = runLarkCli(['base', '+base-get', '--base-token', selectedBaseToken], env);
723
+ selectedBaseUrl = baseUrl(existingBase);
724
+ selectedBaseName = baseName(existingBase) ?? selectedBaseToken;
725
+ tables = listExistingTables(selectedBaseToken, env);
726
+ }
727
+ else {
728
+ if (!schema)
729
+ throw new Error('Schema is required');
730
+ const [first, ...remaining] = schema.tables;
731
+ journal ??= startJournal(workspace, options, larkProfile, env, schema, {
732
+ tables: [],
733
+ });
734
+ if (!first)
735
+ throw new Error('Schema must contain at least one table');
736
+ const baseStep = journal.step('create-base');
737
+ if (baseStep.status === 'succeeded') {
738
+ selectedBaseToken = requiredString(journal.receipt.resources.baseToken, 'receipt Base token');
739
+ const saved = receiptTable(journal.receipt, first.name);
740
+ if (!saved)
741
+ recoveryBlocked(journal, baseStep.id, `Receipt has no confirmed table ID for ${first.name}`);
742
+ const currentBase = runLarkCli(['base', '+base-get', '--base-token', selectedBaseToken], env);
743
+ selectedBaseUrl = baseUrl(currentBase);
744
+ selectedBaseName = baseName(currentBase) ?? databaseName;
745
+ tables = [saved];
746
+ }
747
+ else {
748
+ process.stderr.write(`Creating Base ${databaseName}...\n`);
749
+ const createdBase = await journal.run('create-base', () => runLarkCli([
750
+ 'base',
751
+ '+base-create',
752
+ '--name',
753
+ databaseName,
754
+ '--time-zone',
755
+ options.timeZone,
756
+ '--table-name',
757
+ first.name,
758
+ '--fields',
759
+ JSON.stringify(first.fields.slice(0, 1)),
760
+ ], env), (response) => {
761
+ baseToken(response);
762
+ tableId(response, 'base-create');
763
+ });
764
+ selectedBaseToken = baseToken(createdBase);
765
+ selectedBaseUrl = baseUrl(createdBase);
766
+ tables = [
767
+ { name: first.name, tableId: tableId(createdBase, 'base-create') },
768
+ ];
769
+ }
770
+ for (const [index, table] of remaining.entries()) {
771
+ const stepId = `create-table:${index + 1}`;
772
+ const step = journal.step(stepId);
773
+ if (step.status === 'succeeded') {
774
+ const saved = receiptTable(journal.receipt, table.name);
775
+ if (!saved)
776
+ recoveryBlocked(journal, stepId, `Receipt has no confirmed table ID for ${table.name}`);
777
+ tables.push(saved);
778
+ continue;
779
+ }
780
+ await pause();
781
+ process.stderr.write(`Creating table ${table.name}...\n`);
782
+ const response = await journal.run(stepId, () => runLarkCli([
783
+ 'base',
784
+ '+table-create',
785
+ '--base-token',
786
+ selectedBaseToken,
787
+ '--name',
788
+ table.name,
789
+ '--fields',
790
+ JSON.stringify(table.fields.slice(0, 1)),
791
+ ], env), (response) => {
792
+ tableId(response, 'table-create');
793
+ });
794
+ tables.push({
795
+ name: table.name,
796
+ tableId: tableId(response, 'table-create'),
797
+ });
798
+ }
799
+ for (const [tableIndex, table] of schema.tables.entries()) {
800
+ const remainingFields = table.fields.slice(1);
801
+ if (remainingFields.length === 0)
802
+ continue;
803
+ const stepId = `create-fields:${tableIndex}`;
804
+ const step = journal.step(stepId);
805
+ if (step.status === 'succeeded')
806
+ continue;
807
+ const createdTable = tables[tableIndex];
808
+ if (!createdTable) {
809
+ throw new Error(`Missing created table for schema table ${table.name}`);
810
+ }
811
+ await pause();
812
+ process.stderr.write(`Creating remaining fields for ${table.name}\n`);
813
+ const retryIndexes = resumePreparation?.fieldRetries.get(stepId);
814
+ if (retryIndexes)
815
+ journal.prepare(stepId);
816
+ const fieldJournal = journal;
817
+ await fieldJournal.run(stepId, () => createFieldsWithRetry({
818
+ journal: fieldJournal,
819
+ stepId,
820
+ fields: remainingFields,
821
+ initialIndexes: retryIndexes ?? remainingFields.map((_, index) => index),
822
+ env,
823
+ execute: (indexes) => runLarkCli([
824
+ 'base',
825
+ '+field-create',
826
+ '--base-token',
827
+ selectedBaseToken,
828
+ '--table-id',
829
+ createdTable.tableId,
830
+ '--json',
831
+ JSON.stringify(indexes.map((index) => remainingFields[index])),
832
+ ], env),
833
+ }));
274
834
  }
275
835
  await pause();
276
- process.stderr.write(`Creating remaining fields for ${table.name}\n`);
277
- await retry(`field-create ${table.name}`, attempts, () => runLarkCli([
278
- 'base',
279
- '+field-create',
280
- '--base-token',
281
- selectedBaseToken,
282
- '--table-id',
283
- createdTable.tableId,
284
- '--json',
285
- JSON.stringify(remainingFields),
286
- ], env));
287
836
  }
288
- await pause();
289
- }
290
- const binding = {
291
- baseToken: selectedBaseToken,
292
- tableIds: tables.map((table) => table.tableId),
293
- };
294
- let selectedAppToken;
295
- const databaseSource = existingAppToken
296
- ? 'existing'
297
- : 'created';
298
- if (existingAppToken) {
299
- selectedAppToken = requiredString(existingAppToken, '--app-token');
300
- process.stderr.write('Updating the existing application metadata...\n');
301
- runLarkCli([
302
- 'api',
303
- 'PATCH',
304
- `/open-apis/base/v3/sites/${encodeURIComponent(selectedAppToken)}/update`,
305
- '--data',
306
- JSON.stringify({
307
- name: options.name,
308
- description: options.description ?? '',
309
- }),
310
- ], env);
311
- process.stderr.write('Binding all tables to the existing application...\n');
312
- runLarkCli([
313
- 'api',
314
- 'POST',
315
- `/open-apis/base/v3/sites/${encodeURIComponent(selectedAppToken)}/bind_base`,
316
- '--data',
317
- JSON.stringify(binding),
318
- ], env);
319
- }
320
- else {
321
- process.stderr.write('Creating application and binding all tables...\n');
322
- const createdDatabase = runLarkCli([
323
- 'api',
324
- 'POST',
325
- '/open-apis/base/v3/sites/create',
326
- '--data',
327
- JSON.stringify({
328
- name: databaseName,
329
- description: options.description ?? '',
330
- baseBindings: [binding],
331
- }),
332
- ], env);
333
- selectedAppToken = appToken(createdDatabase);
334
- }
335
- const encodedToken = encodeURIComponent(selectedAppToken);
336
- const catalog = await retry('catalog', 5, () => {
337
- const response = runLarkCli(['api', 'GET', `/open-apis/base/v3/sites/${encodedToken}/catalog`], env);
338
- return validateCatalog(response.data, selectedBaseToken, tables);
339
- });
340
- mkdirSync(workspace, { recursive: true });
341
- for (const directory of ['page', 'server-functions', 'sql']) {
342
- cpSync(join(starterRoot, directory), join(workspace, directory), {
343
- recursive: true,
837
+ journal ??= startJournal(workspace, options, larkProfile, env, schema, {
838
+ baseToken: selectedBaseToken,
839
+ tables,
344
840
  });
345
- }
346
- writePageTitle(workspace, databaseName);
347
- runtimeProfile.prepareWorkspace?.(workspace);
348
- const readmePath = join(workspace, 'README.md');
349
- if (!existsSync(readmePath)) {
350
- cpSync(join(starterRoot, 'README.md'), readmePath);
351
- }
352
- mergeIgnoreFile(join(workspace, '.gitignore'), join(starterRoot, 'gitignore'));
353
- mergeIgnoreFile(join(workspace, '.prettierignore'), join(starterRoot, 'prettierignore'));
354
- writeFileSync(join(workspace, 'catalog.json'), `${JSON.stringify(catalog, null, 2)}\n`);
355
- const configPath = writeConfig(workspace, {
356
- configVersion: 1,
357
- ...(larkProfile ? { larkProfile } : {}),
358
- name: basename(workspace),
359
- ...(runtimeProfile.name === ONLINE_RUNTIME_PROFILE.name
360
- ? {}
361
- : { runtimeProfile: runtimeProfile.name }),
362
- appToken: selectedAppToken,
363
- bindings: {
364
- MAIN_BASE: {
365
- type: 'base',
366
- baseToken: selectedBaseToken,
367
- tableIds: tables.map((table) => table.tableId),
368
- },
369
- },
370
- });
371
- const base = {
372
- name: selectedBaseName,
373
- token: selectedBaseToken,
374
- ...(selectedBaseUrl ? { url: selectedBaseUrl } : {}),
375
- tables,
376
- };
377
- const baseAppUrl = catalogBaseAppUrl(catalog);
378
- return {
379
- ok: true,
380
- environment: runtimeProfile.name,
381
- workspace,
382
- ...(larkProfile ? { larkProfile } : {}),
383
- config: configPath,
384
- catalog: join(workspace, 'catalog.json'),
385
- baseSource,
386
- databaseSource,
387
- baseToken: selectedBaseToken,
388
- ...(selectedBaseUrl ? { baseUrl: selectedBaseUrl } : {}),
389
- tables,
390
- appToken: selectedAppToken,
391
- ...(baseAppUrl ? { baseAppUrl } : {}),
392
- ...(baseSource === 'existing' || databaseSource === 'existing'
393
- ? {
394
- reused: {
395
- ...(baseSource === 'existing' ? { base } : {}),
396
- ...(databaseSource === 'existing'
397
- ? {
398
- database: {
399
- name: databaseName,
400
- token: selectedAppToken,
401
- binding,
402
- },
403
- }
404
- : {}),
405
- },
841
+ const binding = {
842
+ baseToken: selectedBaseToken,
843
+ tableIds: tables.map((table) => table.tableId),
844
+ };
845
+ let selectedAppToken;
846
+ const databaseSource = existingAppToken
847
+ ? 'existing'
848
+ : 'created';
849
+ if (existingAppToken) {
850
+ selectedAppToken = requiredString(existingAppToken, '--app-token');
851
+ if (journal.step('update-app').status !== 'succeeded') {
852
+ process.stderr.write('Updating the existing application metadata...\n');
853
+ await journal.run('update-app', () => runLarkCli([
854
+ 'api',
855
+ 'PATCH',
856
+ `/open-apis/base/v3/sites/${encodeURIComponent(selectedAppToken)}/update`,
857
+ '--data',
858
+ JSON.stringify({
859
+ name: databaseName,
860
+ description: options.description ?? '',
861
+ }),
862
+ ], env));
406
863
  }
407
- : {}),
408
- created: {
409
- ...(baseSource === 'created' ? { base } : {}),
410
- ...(databaseSource === 'created'
411
- ? {
412
- database: {
864
+ if (journal.step('bind-base').status !== 'succeeded') {
865
+ process.stderr.write('Binding all tables to the existing application...\n');
866
+ await journal.run('bind-base', () => runLarkCli([
867
+ 'api',
868
+ 'POST',
869
+ `/open-apis/base/v3/sites/${encodeURIComponent(selectedAppToken)}/bind_base`,
870
+ '--data',
871
+ JSON.stringify(binding),
872
+ ], env));
873
+ }
874
+ }
875
+ else {
876
+ if (journal.step('create-app').status === 'succeeded') {
877
+ selectedAppToken = requiredString(journal.receipt.resources.appToken, 'receipt application token');
878
+ }
879
+ else {
880
+ process.stderr.write('Creating application and binding all tables...\n');
881
+ const createdDatabase = await journal.run('create-app', () => runLarkCli([
882
+ 'api',
883
+ 'POST',
884
+ '/open-apis/base/v3/sites/create',
885
+ '--data',
886
+ JSON.stringify({
413
887
  name: databaseName,
414
- token: selectedAppToken,
415
- binding,
888
+ description: options.description ?? '',
889
+ baseBindings: [binding],
890
+ }),
891
+ ], env), (response) => {
892
+ appToken(response);
893
+ });
894
+ selectedAppToken = appToken(createdDatabase);
895
+ }
896
+ }
897
+ const encodedToken = encodeURIComponent(selectedAppToken);
898
+ const catalog = await journal.run('read-catalog', () => retry('catalog', 5, () => {
899
+ const response = runLarkCli(['api', 'GET', `/open-apis/base/v3/sites/${encodedToken}/catalog`], env);
900
+ return validateCatalog(response.data, selectedBaseToken, tables);
901
+ }));
902
+ if (journal.step('scaffold').status === 'succeeded') {
903
+ let localCatalog;
904
+ try {
905
+ localCatalog = JSON.parse(readFileSync(join(workspace, 'catalog.json'), 'utf8'));
906
+ }
907
+ catch {
908
+ recoveryBlocked(journal, 'scaffold', 'Completed project Catalog is missing or invalid');
909
+ }
910
+ if (JSON.stringify(stableJson(localCatalog)) !==
911
+ JSON.stringify(stableJson(catalog)))
912
+ recoveryBlocked(journal, 'scaffold', 'Remote Catalog changed after local scaffolding completed');
913
+ }
914
+ const configPath = journal.step('scaffold').status === 'succeeded'
915
+ ? join(workspace, CONFIG_FILE_NAME)
916
+ : await journal.run('scaffold', () => {
917
+ mkdirSync(workspace, { recursive: true });
918
+ for (const directory of [
919
+ 'page',
920
+ 'server-functions',
921
+ 'mutations',
922
+ DATABASE_FUNCTIONS_DIRECTORY,
923
+ ]) {
924
+ cpSync(join(starterRoot, directory), join(workspace, directory), {
925
+ recursive: true,
926
+ });
927
+ }
928
+ writePageTitle(workspace, databaseName);
929
+ runtimeProfile.prepareWorkspace?.(workspace);
930
+ const readmePath = join(workspace, 'README.md');
931
+ if (!existsSync(readmePath)) {
932
+ cpSync(join(starterRoot, 'README.md'), readmePath);
933
+ }
934
+ mergeIgnoreFile(join(workspace, '.gitignore'), join(starterRoot, 'gitignore'));
935
+ mergeIgnoreFile(join(workspace, '.prettierignore'), join(starterRoot, 'prettierignore'));
936
+ writeFileSync(join(workspace, 'catalog.json'), `${JSON.stringify(catalog, null, 2)}\n`);
937
+ return writeConfig(workspace, {
938
+ configVersion: 1,
939
+ ...(larkProfile ? { larkProfile } : {}),
940
+ name: basename(workspace),
941
+ ...(runtimeProfile.name === ONLINE_RUNTIME_PROFILE.name
942
+ ? {}
943
+ : { runtimeProfile: runtimeProfile.name }),
944
+ appToken: selectedAppToken,
945
+ bindings: {
946
+ MAIN_BASE: {
947
+ type: 'base',
948
+ baseToken: selectedBaseToken,
949
+ tableIds: tables.map((table) => table.tableId),
950
+ },
951
+ },
952
+ });
953
+ });
954
+ journal.complete();
955
+ const base = {
956
+ name: selectedBaseName,
957
+ token: selectedBaseToken,
958
+ ...(selectedBaseUrl ? { url: selectedBaseUrl } : {}),
959
+ tables,
960
+ };
961
+ const baseAppUrl = catalogBaseAppUrl(catalog);
962
+ return {
963
+ ok: true,
964
+ initialization: {
965
+ journal: journal.path,
966
+ status: 'succeeded',
967
+ ...(options.resume ? { resumed: true } : {}),
968
+ },
969
+ environment: runtimeProfile.name,
970
+ workspace,
971
+ ...(larkProfile ? { larkProfile } : {}),
972
+ config: configPath,
973
+ catalog: join(workspace, 'catalog.json'),
974
+ baseSource,
975
+ databaseSource,
976
+ baseToken: selectedBaseToken,
977
+ ...(selectedBaseUrl ? { baseUrl: selectedBaseUrl } : {}),
978
+ tables,
979
+ appToken: selectedAppToken,
980
+ ...(baseAppUrl ? { baseAppUrl } : {}),
981
+ ...(baseSource === 'existing' || databaseSource === 'existing'
982
+ ? {
983
+ reused: {
984
+ ...(baseSource === 'existing' ? { base } : {}),
985
+ ...(databaseSource === 'existing'
986
+ ? {
987
+ database: {
988
+ name: databaseName,
989
+ token: selectedAppToken,
990
+ binding,
991
+ },
992
+ }
993
+ : {}),
416
994
  },
417
995
  }
418
996
  : {}),
419
- project: {
420
- directory: workspace,
421
- config: configPath,
422
- catalog: join(workspace, 'catalog.json'),
423
- source: workspace,
424
- },
425
- },
426
- nextSteps: [
427
- {
428
- action: 'develop',
429
- workingDirectory: workspace,
430
- description: 'Read catalog.json, then implement the complete Page, SQL Functions, and server-side FaaS Functions under page/, sql/, and server-functions/ in one pass.',
997
+ created: {
998
+ ...(baseSource === 'created' ? { base } : {}),
999
+ ...(databaseSource === 'created'
1000
+ ? {
1001
+ database: {
1002
+ name: databaseName,
1003
+ token: selectedAppToken,
1004
+ binding,
1005
+ },
1006
+ }
1007
+ : {}),
1008
+ project: {
1009
+ directory: workspace,
1010
+ config: configPath,
1011
+ catalog: join(workspace, 'catalog.json'),
1012
+ source: workspace,
1013
+ },
431
1014
  },
432
- {
433
- action: 'deployPreview',
434
- workingDirectory: workspace,
435
- command: runtimeProfile.deployCommand,
436
- description: 'Run this single command after implementation; it installs, builds, validates, uploads, publishes, and returns baseAppUrl.',
1015
+ developmentReadiness: {
1016
+ status: 'not_checked',
1017
+ dependencies: 'not_prepared',
1018
+ command: runtimeProfile.checkCommand,
1019
+ scope: 'local',
1020
+ sqlValidation: 'not_run',
1021
+ description: 'Initialization scaffolds source files but does not prepare Page/FaaS dependencies or run the local project check.',
437
1022
  },
438
- ],
439
- };
1023
+ nextSteps: [
1024
+ {
1025
+ action: 'develop',
1026
+ workingDirectory: workspace,
1027
+ description: 'Read catalog.json, then implement the complete Page, Database Functions, Mutation Functions, and server-side FaaS Functions under page/, database-functions/, mutations/, and server-functions/ in one pass.',
1028
+ },
1029
+ {
1030
+ action: 'checkLocal',
1031
+ workingDirectory: workspace,
1032
+ command: runtimeProfile.checkCommand,
1033
+ description: 'Run the supported local check after implementation. It regenerates Page Function input types from local metadata, may install Page/FaaS dependencies, and writes build output; it does not validate server-side Database Function queries or runtime behavior and does not deploy.',
1034
+ },
1035
+ {
1036
+ action: 'deployPreview',
1037
+ workingDirectory: workspace,
1038
+ command: runtimeProfile.deployCommand,
1039
+ description: 'Run this command after the local check passes; it repeats required build checks, validates server-side Database Function queries, uploads, publishes, and returns baseAppUrl.',
1040
+ },
1041
+ ],
1042
+ };
1043
+ }
1044
+ catch (error) {
1045
+ if (error instanceof InitFailureError)
1046
+ throw error;
1047
+ if (journal)
1048
+ throw journal.failure(error);
1049
+ throw error;
1050
+ }
1051
+ finally {
1052
+ journal?.close();
1053
+ }
1054
+ }
1055
+ function startJournal(workspace, options, larkProfile, env, schema, resources) {
1056
+ const plan = buildInitPlan(options, schema, resources);
1057
+ return new InitJournal(workspace, {
1058
+ environment: options.environment,
1059
+ ...(larkProfile ? { larkProfile } : {}),
1060
+ planHash: plan.planHash,
1061
+ }, plan.steps, plan.resources, env);
440
1062
  }
441
1063
  //# sourceMappingURL=init.js.map