@saltcorn/copilot 0.8.2 → 0.8.3

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.
@@ -46,7 +46,13 @@ const task_tool = {
46
46
  type: "array",
47
47
  items: {
48
48
  type: "object",
49
- required: ["name", "description", "priority", "depends_on"],
49
+ required: [
50
+ "name",
51
+ "description",
52
+ "priority",
53
+ "depends_on",
54
+ "task_type",
55
+ ],
50
56
  additionalProperties: false,
51
57
  properties: {
52
58
  name: {
@@ -71,6 +77,12 @@ const task_tool = {
71
77
  type: "string",
72
78
  },
73
79
  },
80
+ task_type: {
81
+ type: "string",
82
+ enum: ["plugin", "data_model", "feature"],
83
+ description:
84
+ "plugin: specialized — installs a plugin from the Saltcorn plugin store. data_model: specialized — creates or modifies database tables/fields only. feature: broad catch-all — creates views, pages, triggers, workflows, or anything else not covered by the specialized types. Order: plugin tasks first, then data_model, then feature.",
85
+ },
74
86
  },
75
87
  },
76
88
  },
@@ -36,14 +36,20 @@ const {
36
36
  const { getState } = require("@saltcorn/data/db/state");
37
37
  const renderLayout = require("@saltcorn/markup/layout");
38
38
  const { viewname } = require("./common");
39
- const { requirementsList, req_routes } = require("./requirements");
40
- const { showSchema, schema_routes } = require("./schema");
41
- const { makeTaskList, task_routes } = require("./tasks");
42
- const { errorList, error_routes } = require("./errors");
39
+ const { showSchema, schema_routes, schemaStaticScript } = require("./schema");
40
+ const { task_routes } = require("./tasks");
41
+ const {
42
+ errorList,
43
+ doCreateErrorFixTask,
44
+ error_routes,
45
+ errTableStaticHtml,
46
+ } = require("./errors");
47
+ const { req_routes, requirementsStaticScript } = require("./requirements");
43
48
  const { feedbackList, feedback_routes } = require("./feedback");
44
- const { progressList, progress_routes } = require("./progress");
49
+ const { progress_routes } = require("./progress");
45
50
  const { runNextTask } = require("./run_task");
46
- const { makeTaskChart } = require("./taskchart");
51
+ const { researchPanel, research_routes } = require("./research");
52
+ const { phasesPanel, phasesStaticScript, phase_routes } = require("./phases");
47
53
 
48
54
  const get_state_fields = () => [];
49
55
 
@@ -56,37 +62,14 @@ const makeSpecForm = async (req) => {
56
62
  });
57
63
 
58
64
  return new Form({
59
- blurb: "Provide a high-level description of the application",
65
+ blurb: "Describe the application you want to build",
60
66
  fields: [
61
67
  {
62
- name: "description",
63
- label: "Description",
64
- type: "String",
65
- fieldview: "textarea",
66
- },
67
- {
68
- name: "audience",
69
- label: "Audience",
70
- type: "String",
71
- fieldview: "textarea",
72
- },
73
- {
74
- name: "core_features",
75
- label: "Core features",
76
- type: "String",
77
- fieldview: "textarea",
78
- },
79
- {
80
- name: "out_of_scope",
81
- label: "Out of scope",
82
- type: "String",
83
- fieldview: "textarea",
84
- },
85
- {
86
- name: "visual_style",
87
- label: "Visual style",
68
+ name: "specification",
69
+ label: "Specification",
88
70
  type: "String",
89
71
  fieldview: "textarea",
72
+ attributes: { rows: 10 },
90
73
  },
91
74
  ],
92
75
  xhrSubmit: true,
@@ -95,14 +78,95 @@ const makeSpecForm = async (req) => {
95
78
  });
96
79
  };
97
80
 
81
+ const specDepsModal = `
82
+ <div class="modal fade" id="specDepsModal" tabindex="-1" aria-hidden="true">
83
+ <div class="modal-dialog">
84
+ <div class="modal-content">
85
+ <div class="modal-header">
86
+ <h5 class="modal-title">Specification changed</h5>
87
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
88
+ </div>
89
+ <div class="modal-body">
90
+ <p>The following items were generated from the previous specification
91
+ and may now be outdated. Select any you want to clear:</p>
92
+ <div id="spec-deps-checks"></div>
93
+ </div>
94
+ <div class="modal-footer">
95
+ <button type="button" class="btn btn-secondary" id="specDepsKeepBtn">Save (keep all)</button>
96
+ <button type="button" class="btn btn-primary" id="specDepsSaveBtn">Clear selected &amp; save</button>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>`;
101
+
102
+ const specDepsScript = (vn) =>
103
+ script(
104
+ domReady(`
105
+ const _specVn = ${JSON.stringify(vn)};
106
+ const form = document.querySelector('form[action*="submit_specs"]');
107
+ if (!form) return;
108
+ const btn = form.querySelector('button[onclick*="ajaxSubmitForm"]');
109
+ if (!btn) return;
110
+
111
+ const doSaveSpec = () => {
112
+ const data = {};
113
+ for (const [k, v] of new FormData(form)) data[k] = v;
114
+ view_post(_specVn, 'submit_specs', data);
115
+ };
116
+
117
+ // Remove Saltcorn's inline onclick and replace with our own handler
118
+ btn.removeAttribute('onclick');
119
+ btn.addEventListener('click', (e) => {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ view_post(_specVn, 'check_spec_dependencies', {}, (deps) => {
123
+ const items = [];
124
+ if (deps.hasResearch) items.push({ key: 'clearResearch', label: 'Research questions & answers' });
125
+ if (deps.hasRequirements) items.push({ key: 'clearRequirements', label: 'Requirements' });
126
+ if (deps.hasSchema) items.push({ key: 'clearSchema', label: 'Schema' });
127
+ if (deps.hasPhases) items.push({ key: 'clearPhases', label: 'Phases' });
128
+ if (deps.hasTasks) items.push({ key: 'clearTasks', label: 'Tasks' });
129
+ if (!items.length) { doSaveSpec(); return; }
130
+ document.getElementById('spec-deps-checks').innerHTML = items.map((item) =>
131
+ '<div class="form-check">' +
132
+ '<input class="form-check-input" type="checkbox" id="dep_' + item.key +
133
+ '" value="' + item.key + '" checked>' +
134
+ '<label class="form-check-label" for="dep_' + item.key + '">' + item.label + '</label>' +
135
+ '</div>'
136
+ ).join('');
137
+ new bootstrap.Modal(document.getElementById('specDepsModal')).show();
138
+ });
139
+ });
140
+
141
+ document.getElementById('specDepsSaveBtn').addEventListener('click', () => {
142
+ const modal = bootstrap.Modal.getInstance(document.getElementById('specDepsModal'));
143
+ const clearData = {};
144
+ document.querySelectorAll('#spec-deps-checks input:checked').forEach((cb) => {
145
+ clearData[cb.value] = '1';
146
+ });
147
+ modal.hide();
148
+ if (Object.keys(clearData).length) {
149
+ const formData = {};
150
+ for (const [k, v] of new FormData(form)) formData[k] = v;
151
+ view_post(_specVn, 'clear_and_save_spec', Object.assign(formData, clearData));
152
+ } else {
153
+ doSaveSpec();
154
+ }
155
+ });
156
+
157
+ document.getElementById('specDepsKeepBtn').addEventListener('click', () => {
158
+ bootstrap.Modal.getInstance(document.getElementById('specDepsModal')).hide();
159
+ doSaveSpec();
160
+ });
161
+ `)
162
+ );
163
+
98
164
  const run = async (table_id, viewname, cfg, state, { req, res }) => {
99
165
  const specForm = await makeSpecForm(req);
100
- const reqList = await requirementsList(req);
101
- const taskList = await makeTaskList(req);
166
+ const research = await researchPanel(req);
167
+ const phases = await phasesPanel(req);
102
168
  const errList = await errorList(req);
103
169
  const feedbacks = await feedbackList(req);
104
- const progress = await progressList(req);
105
- const taskChart = await makeTaskChart(req);
106
170
  const schema = await showSchema(req);
107
171
  const layout = {
108
172
  type: "tabs",
@@ -111,26 +175,40 @@ const run = async (table_id, viewname, cfg, state, { req, res }) => {
111
175
  lazyLoadViews: true,
112
176
  titles: [
113
177
  "Specification",
114
- "Requirements",
178
+ "Research",
179
+ "Phases",
115
180
  "Schema",
116
- "Tasks",
117
- "Task chart",
118
- "Progress",
119
181
  "Feedback",
120
182
  "Errors",
121
183
  ],
122
184
  contents: [
123
185
  {
124
186
  type: "blank",
125
- contents: div({ class: "mt-2" }, renderForm(specForm, req.csrfToken())),
187
+ contents: div(
188
+ { class: "mt-2" },
189
+ renderForm(specForm, req.csrfToken()),
190
+ specDepsModal,
191
+ specDepsScript(viewname)
192
+ ),
193
+ },
194
+ { type: "blank", contents: research },
195
+ { type: "blank", contents: div(phasesStaticScript, phases) },
196
+ {
197
+ type: "blank",
198
+ contents: div(
199
+ schemaStaticScript,
200
+ div({ id: "schema-list-area" }, schema)
201
+ ),
126
202
  },
127
- { type: "blank", contents: reqList },
128
- { type: "blank", contents: schema },
129
- { type: "blank", contents: taskList },
130
- { type: "blank", contents: taskChart },
131
- { type: "blank", contents: progress },
132
203
  { type: "blank", contents: feedbacks },
133
- { type: "blank", contents: errList },
204
+ {
205
+ type: "blank",
206
+ contents: div(
207
+ requirementsStaticScript,
208
+ errTableStaticHtml,
209
+ div({ id: "err-list-area" }, errList)
210
+ ),
211
+ },
134
212
  ],
135
213
  deeplink: true,
136
214
  tabsStyle: "Tabs",
@@ -144,6 +222,119 @@ const run = async (table_id, viewname, cfg, state, { req, res }) => {
144
222
  });
145
223
  };
146
224
 
225
+ const check_spec_dependencies = async (
226
+ table_id,
227
+ viewname,
228
+ config,
229
+ body,
230
+ { req, res }
231
+ ) => {
232
+ const hasResearch =
233
+ !!(await MetaData.findOne({
234
+ type: "CopilotConstructMgr",
235
+ name: "research_questions",
236
+ })) ||
237
+ !!(await MetaData.findOne({
238
+ type: "CopilotConstructMgr",
239
+ name: "research_answers",
240
+ }));
241
+ const hasRequirements =
242
+ (
243
+ await MetaData.find({
244
+ type: "CopilotConstructMgr",
245
+ name: "requirement",
246
+ })
247
+ ).length > 0;
248
+ const hasSchema = !!(await MetaData.findOne({
249
+ type: "CopilotConstructMgr",
250
+ name: "schema",
251
+ }));
252
+ const hasPhases =
253
+ (
254
+ await MetaData.find({
255
+ type: "CopilotConstructMgr",
256
+ name: "phase",
257
+ })
258
+ ).length > 0;
259
+ const hasTasks =
260
+ (
261
+ await MetaData.find({
262
+ type: "CopilotConstructMgr",
263
+ name: "task",
264
+ })
265
+ ).length > 0;
266
+ return {
267
+ json: { hasResearch, hasRequirements, hasPhases, hasSchema, hasTasks },
268
+ };
269
+ };
270
+
271
+ // Clears selected dependencies, saves the spec, and reloads — all in one round trip
272
+ const clear_and_save_spec = async (
273
+ table_id,
274
+ viewname,
275
+ config,
276
+ body,
277
+ { req, res }
278
+ ) => {
279
+ const {
280
+ _csrf,
281
+ clearResearch,
282
+ clearRequirements,
283
+ clearSchema,
284
+ clearPhases,
285
+ clearTasks,
286
+ ...specBody
287
+ } = body;
288
+ if (clearResearch) {
289
+ for (const name of ["research_questions", "research_answers"]) {
290
+ const md = await MetaData.findOne({ type: "CopilotConstructMgr", name });
291
+ if (md) await md.delete();
292
+ }
293
+ }
294
+ if (clearRequirements) {
295
+ const rs = await MetaData.find({
296
+ type: "CopilotConstructMgr",
297
+ name: "requirement",
298
+ });
299
+ for (const r of rs) await r.delete();
300
+ }
301
+ if (clearSchema) {
302
+ const md = await MetaData.findOne({
303
+ type: "CopilotConstructMgr",
304
+ name: "schema",
305
+ });
306
+ if (md) await md.delete();
307
+ }
308
+ if (clearTasks) {
309
+ const ts = await MetaData.find({
310
+ type: "CopilotConstructMgr",
311
+ name: "task",
312
+ });
313
+ for (const t of ts) await t.delete();
314
+ }
315
+ if (clearPhases) {
316
+ const ps = await MetaData.find({
317
+ type: "CopilotConstructMgr",
318
+ name: "phase",
319
+ });
320
+ for (const ph of ps) await ph.delete();
321
+ }
322
+ const existing = await MetaData.findOne({
323
+ type: "CopilotConstructMgr",
324
+ name: "spec",
325
+ });
326
+ if (existing)
327
+ await db.update("_sc_metadata", { body: specBody }, existing.id);
328
+ else
329
+ await MetaData.create({
330
+ type: "CopilotConstructMgr",
331
+ name: "spec",
332
+ user_id: req.user?.id || undefined,
333
+ body: specBody,
334
+ });
335
+ return { json: { reload_page: true } };
336
+ };
337
+
147
338
  const submit_specs = async (table_id, viewname, config, body, { req, res }) => {
148
339
  const { _csrf, ...spec } = body;
149
340
  const existing = await MetaData.findOne({
@@ -168,6 +359,35 @@ const submit_specs = async (table_id, viewname, config, body, { req, res }) => {
168
359
  };
169
360
  };
170
361
 
362
+ // Creates fix tasks for application errors recorded while self-healing was off.
363
+ const healPendingErrors = async () => {
364
+ const settings = await MetaData.findOne({
365
+ type: "CopilotConstructMgr",
366
+ name: "settings",
367
+ });
368
+ if (!settings?.body?.error_heal) return;
369
+ const spec = await MetaData.findOne({
370
+ type: "CopilotConstructMgr",
371
+ name: "spec",
372
+ });
373
+ if (!spec) return;
374
+ const errors = await MetaData.find({
375
+ type: "CopilotConstructMgr",
376
+ name: "error",
377
+ });
378
+ for (const err of errors) {
379
+ if (
380
+ err.body.source !== "application" ||
381
+ err.body.fix_task_created ||
382
+ err.body.cannot_fix_reason
383
+ )
384
+ continue;
385
+ doCreateErrorFixTask(err, null).catch((e) =>
386
+ console.error("healPendingErrors failed", e)
387
+ );
388
+ }
389
+ };
390
+
171
391
  const virtual_triggers = () => {
172
392
  return [
173
393
  {
@@ -177,20 +397,49 @@ const virtual_triggers = () => {
177
397
  type: "CopilotConstructMgr",
178
398
  name: "error",
179
399
  });
180
- const messages = new Set(existing.map((m) => m.body?.error?.stack));
181
- if (!messages.has(row.stack))
182
- await MetaData.create({
400
+ // Allow a new record once a fix task exists — same error recurring after a fix gets another task.
401
+ const unfixedDuplicate =
402
+ row.stack &&
403
+ existing.find(
404
+ (m) =>
405
+ m.body?.error?.stack === row.stack && !m.body?.fix_task_created
406
+ );
407
+ if (unfixedDuplicate) return;
408
+
409
+ const source = (row.stack || "").includes("/app-constructor/")
410
+ ? "constructor"
411
+ : "application";
412
+
413
+ const errorMd = await MetaData.create({
414
+ type: "CopilotConstructMgr",
415
+ name: "error",
416
+ body: { status: "New", error: row, source },
417
+ user_id: null,
418
+ });
419
+
420
+ if (source === "application") {
421
+ const settings = await MetaData.findOne({
183
422
  type: "CopilotConstructMgr",
184
- name: "error",
185
- body: { status: "New", error: row },
186
- user_id: null,
423
+ name: "settings",
187
424
  });
425
+ if (settings?.body?.error_heal) {
426
+ const spec = await MetaData.findOne({
427
+ type: "CopilotConstructMgr",
428
+ name: "spec",
429
+ });
430
+ if (spec)
431
+ doCreateErrorFixTask(errorMd, null).catch((e) =>
432
+ console.error("auto error fix task failed", e)
433
+ );
434
+ }
435
+ }
188
436
  },
189
437
  },
190
438
  {
191
439
  when_trigger: "Often",
192
440
  run: async () => {
193
441
  await runNextTask();
442
+ await healPendingErrors();
194
443
  },
195
444
  },
196
445
  ];
@@ -205,12 +454,16 @@ module.exports = {
205
454
  run,
206
455
  routes: {
207
456
  submit_specs,
457
+ check_spec_dependencies,
458
+ clear_and_save_spec,
208
459
  ...req_routes,
460
+ ...research_routes,
209
461
  ...task_routes,
210
462
  ...error_routes,
211
463
  ...feedback_routes,
212
464
  ...progress_routes,
213
465
  ...schema_routes,
466
+ ...phase_routes,
214
467
  },
215
468
  virtual_triggers,
216
469
  };
package/builder-gen.js CHANGED
@@ -27,13 +27,9 @@ designated as ownership fields (set automatically from the logged-in user). \
27
27
  All other foreign key fields — including parent-context keys like trip_id — MUST appear as \
28
28
  selector inputs; Saltcorn auto-fills them from the URL state when opened in context.`;
29
29
 
30
- const EDIT_FIELDVIEW_SELECTION = `\
31
- For date fields always prefer fieldview "flatpickr" when available — it provides the best user experience \
32
- and works for both regular dates and day-only dates. \
33
- Only use fieldview "edit_day" as a fallback when the field has day_only=true and flatpickr is not installed. \
34
- For String fields that have an options attribute (a comma-separated list of fixed choices), \
35
- use fieldview "select" — this renders a dropdown with those options. \
36
- Do not use "select_by_code" for fields with fixed options.`;
30
+ const {
31
+ fieldview_selection_rules: EDIT_FIELDVIEW_SELECTION,
32
+ } = require("./app-constructor/prompts");
37
33
 
38
34
  const EDIT_LAYOUT_STRUCTURE = `\
39
35
  Every field MUST be preceded by a label. \
@@ -1309,9 +1305,16 @@ const buildContext = async (mode, tableName) => {
1309
1305
  if (!fieldviews.length && String(typeName).startsWith("Key")) {
1310
1306
  fieldviews = ["select", "show"];
1311
1307
  }
1308
+ // File fields store their fieldviews in state.fileviews, not on the type object
1309
+ let fileFvDefs = {};
1310
+ if (String(typeName) === "File") {
1311
+ fileFvDefs = getState().fileviews || {};
1312
+ fieldviews = Object.keys(fileFvDefs);
1313
+ }
1312
1314
  // editFieldviews: only fieldviews where isEdit is not explicitly false
1315
+ const effectiveFvDefs = String(typeName) === "File" ? fileFvDefs : fvDefs;
1313
1316
  const editFieldviews = fieldviews.filter(
1314
- (fv) => fvDefs[fv]?.isEdit !== false
1317
+ (fv) => effectiveFvDefs[fv]?.isEdit !== false
1315
1318
  );
1316
1319
 
1317
1320
  const isPkName =
package/builder-schema.js CHANGED
@@ -327,7 +327,13 @@ const buildBuilderSchema = ({ mode, ctx }) => {
327
327
  const actionDef = buildSegmentDef({
328
328
  type: "action",
329
329
  description: "Action button segment.",
330
+ required: ["rndid"],
330
331
  properties: {
332
+ rndid: {
333
+ type: "string",
334
+ description:
335
+ "Unique short identifier for this action (e.g. 'act1', 'del2'). REQUIRED — omitting it causes a server crash.",
336
+ },
331
337
  action_name: defs.action_name_enum
332
338
  ? { $ref: "#/$defs/action_name_enum", description: "Action name." }
333
339
  : { type: "string", description: "Action name." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -1,120 +0,0 @@
1
- const saltcorn_description = `This application will be implemented in Saltcorn, a database application development
2
- environment.
3
-
4
- Saltcorn applications contain the following entity types:
5
-
6
- * Tables: These are relational database tables and consists of fields of specified types and rows
7
- with a value for each field. Fields optionally can be required and/or unique. Every field has a name,
8
- which is a an identifier that is balid in both JavaScript and SQL, and a label, which is any short
9
- user-friendly string. Every table has a primary key
10
- (composite primary keys are not supported) which by default is an auto-incrementing integer with
11
- name \`id\` and label ID. Fields can also be of Key type (foreign key) referencing a primary key
12
- in another table, or its own table for a self-join. Tables can have
13
- calculated fields, which can be stored or non-stored. Both stored and non-stored fields are
14
- defined by a JavaScript expression, but only stored fields can reference other tables with join
15
- fields and aggregations.
16
-
17
- * Views: Views are elementary user interfaces into a database table. A view is defined by applying a
18
- view template (also sometime called a view pattern, the two are synonymous) to a table with a certain
19
- configuration. The view template defines the fundamental relationship between the UI and the table. For
20
- instance, the Show view template displays a single database row, the Edit view template is a form that
21
- can create a new row or edit an existing row, the List view template displays multiple rows in a grid.
22
- Views can embed views, for instance Show can embed another row through a Key field relationship, or
23
- some views are defined by an underlying view. For instance, the Feed view repeats an underlying view
24
- for multiple tables. New viewtemplates are provided by plugin modules.
25
-
26
- * Triggers: Triggers connect elementary actions (provided by plugin modules) to either a button in the
27
- user interface, or a periodic (hourly, daily etc) or table (for instance insert on specifc table) event.
28
- The elementary action each has a number of configuration fields that must be filled in after connecting
29
- the action to an event, table or button.
30
-
31
- * Page: A page has static content but can also embed views for synamic content. Pages can be either
32
- defined by a Saltcorn layout, for pages that can be edited with drag and drop, or by HTML for more
33
- flexible graphic designs. HTML pages should be used for landing pages.
34
-
35
- * Plugin modules: plugin modules can supply new field types, view templates or actions. Before they can be used,
36
- they need to be installed before they can be used. A plugin may also have a configuration that sets options
37
- for that plugin. Layout themes is Saltcorn are plugin modules.
38
- `;
39
-
40
- const existing_tables_list = (tables) => {
41
- const tableLines = [];
42
- tables.forEach((table) => {
43
- const fieldLines = table.fields.map(
44
- (f) =>
45
- ` * ${f.name} with type: ${f.pretty_type}.${
46
- f.description ? ` ${f.description}` : ""
47
- }`
48
- );
49
- tableLines.push(
50
- `${table.name}${
51
- table.description ? `: ${table.description}.` : "."
52
- } Contains the following fields:\n${fieldLines.join("\n")}`
53
- );
54
- });
55
- return `The database already contains the following tables:
56
-
57
- ${tableLines.join("\n\n")}`;
58
- };
59
-
60
- const existing_entities_list = ({ views, triggers, pages, tableById = {} }) => {
61
- const sections = [];
62
- if (views.length)
63
- sections.push(
64
- `The following views are already implemented — do NOT plan tasks to create them:\n` +
65
- views
66
- .map((v) => {
67
- const tablePart =
68
- v.table?.name ||
69
- (v.table_id && tableById[v.table_id]) ||
70
- v.exttable_name;
71
- return `- ${v.name} (${v.viewtemplate}${
72
- tablePart ? ` on ${tablePart}` : ""
73
- })`;
74
- })
75
- .join("\n")
76
- );
77
- if (triggers.length)
78
- sections.push(
79
- `The following triggers are already implemented — do NOT plan tasks to create them:\n` +
80
- triggers
81
- .map(
82
- (t) =>
83
- `- ${t.name} (${t.action}${
84
- t.when_trigger ? `, ${t.when_trigger}` : ""
85
- })`
86
- )
87
- .join("\n")
88
- );
89
- if (pages.length)
90
- sections.push(
91
- `The following pages are already implemented — do NOT plan tasks to create them:\n` +
92
- pages.map((p) => `- ${p.name}`).join("\n")
93
- );
94
- return sections.join("\n\n");
95
- };
96
-
97
- const available_plugins_list = (storePlugins, installedNames) => {
98
- const uninstalled = storePlugins.filter((p) => !installedNames.has(p.name));
99
- if (!uninstalled.length) return "";
100
- const lines = uninstalled.map((p) => {
101
- let line = `### ${p.name}`;
102
- if (p.description) line += `\n${p.description}`;
103
- if (p.contents) line += `\n${p.contents}`;
104
- return line;
105
- });
106
- return (
107
- `The following plugins are available in the Saltcorn store but not yet installed. ` +
108
- `If a task requires functionality provided by one of these plugins (e.g. a specific view template, field type, or action), ` +
109
- `include an explicit "Install plugin <name>" task before it with the exact plugin name as listed here. ` +
110
- `The executor will use that name directly without needing to look it up.\n\n` +
111
- lines.join("\n\n")
112
- );
113
- };
114
-
115
- module.exports = {
116
- saltcorn_description,
117
- existing_tables_list,
118
- existing_entities_list,
119
- available_plugins_list,
120
- };