@dboio/cli 0.16.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -138
- package/bin/dbo.js +2 -2
- package/package.json +1 -1
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +175 -138
- package/src/commands/adopt.js +534 -0
- package/src/commands/build.js +3 -3
- package/src/commands/clone.js +209 -75
- package/src/commands/deploy.js +3 -3
- package/src/commands/init.js +11 -11
- package/src/commands/install.js +3 -3
- package/src/commands/login.js +2 -2
- package/src/commands/mv.js +15 -15
- package/src/commands/pull.js +1 -1
- package/src/commands/push.js +194 -15
- package/src/commands/rm.js +2 -2
- package/src/commands/run.js +4 -4
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/config.js +186 -135
- package/src/lib/delta.js +119 -17
- package/src/lib/dependencies.js +51 -24
- package/src/lib/deploy-config.js +4 -4
- package/src/lib/domain-guard.js +8 -9
- package/src/lib/filenames.js +13 -2
- package/src/lib/ignore.js +2 -3
- package/src/{commands/add.js → lib/insert.js} +127 -472
- package/src/lib/metadata-schema.js +14 -20
- package/src/lib/metadata-templates.js +4 -4
- package/src/lib/migrations.js +1 -1
- package/src/lib/modify-key.js +1 -1
- package/src/lib/scaffold.js +5 -12
- package/src/lib/schema.js +67 -37
- package/src/lib/structure.js +6 -6
- package/src/lib/tagging.js +2 -2
- package/src/lib/ticketing.js +3 -7
- package/src/lib/toe-stepping.js +5 -5
- package/src/lib/transaction-key.js +1 -1
- package/src/migrations/004-rename-output-files.js +2 -2
- package/src/migrations/005-rename-output-metadata.js +2 -2
- package/src/migrations/006-remove-uid-companion-filenames.js +1 -1
- package/src/migrations/007-natural-entity-companion-filenames.js +1 -1
- package/src/migrations/008-metadata-uid-in-suffix.js +1 -1
- package/src/migrations/009-fix-media-collision-metadata-names.js +1 -1
- package/src/migrations/010-delete-paren-media-orphans.js +1 -1
- package/src/migrations/012-project-dir-restructure.js +211 -0
|
@@ -1,46 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, readdir, stat, writeFile, mkdir, unlink } from 'fs/promises';
|
|
1
|
+
import { readFile, readdir, writeFile, mkdir, unlink } from 'fs/promises';
|
|
3
2
|
import { join, dirname, basename, extname, relative } from 'path';
|
|
4
|
-
import { DboClient } from '
|
|
5
|
-
import { buildInputBody, checkSubmitErrors } from '
|
|
6
|
-
import { formatResponse
|
|
7
|
-
import { log } from '
|
|
8
|
-
import { shouldSkipColumn } from '
|
|
9
|
-
import { loadAppConfig, loadAppJsonBaseline, saveAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference, loadConfig } from '
|
|
10
|
-
import {
|
|
11
|
-
import { hasUidInFilename, buildMetaFilename, isMetadataFile } from '
|
|
12
|
-
import { setFileTimestamps } from '
|
|
13
|
-
import { checkStoredTicket, clearGlobalTicket } from '
|
|
14
|
-
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '
|
|
15
|
-
import { loadIgnore } from '
|
|
16
|
-
import { loadStructureFile, findBinByPath, BINS_DIR } from '
|
|
17
|
-
import {
|
|
18
|
-
import { upsertDeployEntry } from '../lib/deploy-config.js';
|
|
19
|
-
|
|
20
|
-
// ─── File type classification for auto-detection in bin directories ────────
|
|
21
|
-
|
|
22
|
-
/** Extensions treated as content entities (text-based, stored in content.Content) */
|
|
23
|
-
const CONTENT_EXTENSIONS = new Set([
|
|
24
|
-
'sh', 'js', 'json', 'html', 'txt', 'xml', 'css', 'md',
|
|
25
|
-
'htm', 'svg', 'sql', 'csv', 'yaml', 'yml', 'ts', 'jsx', 'tsx',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
/** Extensions treated as media entities (binary files: images, audio, video) */
|
|
29
|
-
const MEDIA_EXTENSIONS = new Set([
|
|
30
|
-
// Images
|
|
31
|
-
'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'bmp', 'tiff', 'tif', 'avif',
|
|
32
|
-
// Audio
|
|
33
|
-
'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a', 'wma',
|
|
34
|
-
// Video
|
|
35
|
-
'mp4', 'webm', 'mov', 'avi', 'mkv', 'wmv', 'flv',
|
|
36
|
-
// Fonts
|
|
37
|
-
'woff', 'woff2', 'ttf', 'otf', 'eot',
|
|
38
|
-
// Documents / archives (binary)
|
|
39
|
-
'pdf', 'zip', 'gz', 'tar',
|
|
40
|
-
]);
|
|
3
|
+
import { DboClient } from './client.js';
|
|
4
|
+
import { buildInputBody, checkSubmitErrors } from './input-parser.js';
|
|
5
|
+
import { formatResponse } from './formatter.js';
|
|
6
|
+
import { log } from './logger.js';
|
|
7
|
+
import { shouldSkipColumn } from './columns.js';
|
|
8
|
+
import { loadAppConfig, loadAppJsonBaseline, saveAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference, loadConfig } from './config.js';
|
|
9
|
+
import { loadMetadataSchema, saveMetadataSchema } from './metadata-schema.js';
|
|
10
|
+
import { hasUidInFilename, buildMetaFilename, isMetadataFile } from './filenames.js';
|
|
11
|
+
import { setFileTimestamps } from './timestamps.js';
|
|
12
|
+
import { checkStoredTicket, clearGlobalTicket } from './ticketing.js';
|
|
13
|
+
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from './modify-key.js';
|
|
14
|
+
import { loadIgnore } from './ignore.js';
|
|
15
|
+
import { loadStructureFile, findBinByPath, BINS_DIR } from './structure.js';
|
|
16
|
+
import { upsertDeployEntry } from './deploy-config.js';
|
|
41
17
|
|
|
42
18
|
/** Simple MIME type lookup for common media extensions */
|
|
43
|
-
const MIME_TYPES = {
|
|
19
|
+
export const MIME_TYPES = {
|
|
44
20
|
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
45
21
|
webp: 'image/webp', ico: 'image/x-icon', bmp: 'image/bmp', svg: 'image/svg+xml',
|
|
46
22
|
tiff: 'image/tiff', tif: 'image/tiff', avif: 'image/avif',
|
|
@@ -55,52 +31,66 @@ const MIME_TYPES = {
|
|
|
55
31
|
tar: 'application/x-tar',
|
|
56
32
|
};
|
|
57
33
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
34
|
+
/** Default template cols for core entity types */
|
|
35
|
+
const TEMPLATE_DEFAULTS = {
|
|
36
|
+
content: [
|
|
37
|
+
'AppID', 'BinID', 'Name', 'Path', 'Content=@reference', 'Extension',
|
|
38
|
+
'Type', 'Title', 'Active', 'Public=0', 'SiteID', 'Description',
|
|
39
|
+
'Keywords', 'Parameters', 'CacheControl', 'RequireSSL', 'EntityID',
|
|
40
|
+
],
|
|
41
|
+
media: [
|
|
42
|
+
'AppID', 'BinID', 'Name', 'Path', 'Filename', 'Extension',
|
|
43
|
+
'MimeType', 'FileSize', 'Ownership', 'Public', 'Description',
|
|
44
|
+
'Duration', 'ParentMediaID',
|
|
45
|
+
],
|
|
46
|
+
output: [
|
|
47
|
+
'AppID', 'BinID', 'Name', 'Type', 'CustomSQL=@reference',
|
|
48
|
+
'Active', 'Public', 'Title', 'Description', 'Parameters',
|
|
49
|
+
'Cache', 'Limit', 'MaxRows', 'RowsPerPage', 'ApplyLocks',
|
|
50
|
+
'AdhocFilters', 'BaseEntityID', 'DataSourceID',
|
|
51
|
+
'DefaultMediaID', 'TemplateContentID', 'children',
|
|
52
|
+
],
|
|
53
|
+
output_value: [
|
|
54
|
+
'OutputID', 'Title', 'ShortName', 'OutputType', 'OrderNumber',
|
|
55
|
+
'CustomSQL', 'Format', 'Aggregate', 'DatePart', 'Distinct',
|
|
56
|
+
'Hide', 'Shared', 'Summary', 'SortType', 'SortOrder',
|
|
57
|
+
'EnforceSecurity', 'ReferencedEntityColumnID',
|
|
58
|
+
'OutputValueEntityColumnRelID', 'children',
|
|
59
|
+
],
|
|
60
|
+
output_value_filter: [
|
|
61
|
+
'OutputID', 'OutputValueID', 'ShortName', 'FilterType', 'Operator',
|
|
62
|
+
'OrderNumber', 'CustomSQL', 'Prompted', 'Required', 'Inclusive',
|
|
63
|
+
'PrimaryValue', 'SecondaryValue', 'FilterPreset',
|
|
64
|
+
'ApplyToAggregate', 'ValuesByOutputID', 'children',
|
|
65
|
+
],
|
|
66
|
+
output_value_entity_column_rel: [
|
|
67
|
+
'OutputID', 'OutputValueID', 'JoinType', 'OrderNumber', 'CustomSQL',
|
|
68
|
+
'Alias', 'SourceJoinID', 'SourceEntityColumnID',
|
|
69
|
+
'ReferencedEntityColumnID', 'children',
|
|
70
|
+
],
|
|
71
|
+
};
|
|
99
72
|
|
|
100
73
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
74
|
+
* Seed metadata schema with default fields for the given entity
|
|
75
|
+
* (and any other missing core entities) if an entry doesn't already exist.
|
|
103
76
|
*/
|
|
77
|
+
export async function seedMetadataTemplate() {
|
|
78
|
+
const templates = await loadMetadataSchema() ?? {};
|
|
79
|
+
|
|
80
|
+
// Seed all missing core entities in one pass
|
|
81
|
+
const seeded = [];
|
|
82
|
+
for (const [name, cols] of Object.entries(TEMPLATE_DEFAULTS)) {
|
|
83
|
+
if (templates[name]) continue;
|
|
84
|
+
templates[name] = cols;
|
|
85
|
+
seeded.push(name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (seeded.length === 0) return;
|
|
89
|
+
|
|
90
|
+
await saveMetadataSchema(templates);
|
|
91
|
+
log.dim(` Seeded metadata_schema.json with ${seeded.join(', ')} fields`);
|
|
92
|
+
}
|
|
93
|
+
|
|
104
94
|
/**
|
|
105
95
|
* Detect whether a file lives in the project-root Documentation/ directory
|
|
106
96
|
* AND the ExtensionDocumentationMDPlacement preference is 'root'.
|
|
@@ -108,7 +98,7 @@ export const addCommand = new Command('add')
|
|
|
108
98
|
*
|
|
109
99
|
* @param {string} filePath - Path to the file being added
|
|
110
100
|
*/
|
|
111
|
-
async function detectDocumentationFile(filePath) {
|
|
101
|
+
export async function detectDocumentationFile(filePath) {
|
|
112
102
|
const placement = await loadExtensionDocumentationMDPlacement();
|
|
113
103
|
if (placement !== 'root') return null;
|
|
114
104
|
|
|
@@ -124,7 +114,7 @@ async function detectDocumentationFile(filePath) {
|
|
|
124
114
|
*
|
|
125
115
|
* Creates companion metadata in bins/app/ with pre-filled fields.
|
|
126
116
|
*/
|
|
127
|
-
async function detectManifestFile(filePath) {
|
|
117
|
+
export async function detectManifestFile(filePath) {
|
|
128
118
|
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
129
119
|
if (rel.toLowerCase() !== 'manifest.json') return null;
|
|
130
120
|
|
|
@@ -174,6 +164,26 @@ export async function detectBinFile(filePath) {
|
|
|
174
164
|
const ext = extname(filePath).replace('.', '').toLowerCase();
|
|
175
165
|
if (!ext) return null;
|
|
176
166
|
|
|
167
|
+
// Extensions treated as content entities (text-based, stored in content.Content)
|
|
168
|
+
const CONTENT_EXTENSIONS = new Set([
|
|
169
|
+
'sh', 'js', 'json', 'html', 'txt', 'xml', 'css', 'md',
|
|
170
|
+
'htm', 'svg', 'sql', 'csv', 'yaml', 'yml', 'ts', 'jsx', 'tsx',
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
// Extensions treated as media entities (binary files: images, audio, video)
|
|
174
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
175
|
+
// Images
|
|
176
|
+
'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'bmp', 'tiff', 'tif', 'avif',
|
|
177
|
+
// Audio
|
|
178
|
+
'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a', 'wma',
|
|
179
|
+
// Video
|
|
180
|
+
'mp4', 'webm', 'mov', 'avi', 'mkv', 'wmv', 'flv',
|
|
181
|
+
// Fonts
|
|
182
|
+
'woff', 'woff2', 'ttf', 'otf', 'eot',
|
|
183
|
+
// Documents / archives (binary)
|
|
184
|
+
'pdf', 'zip', 'gz', 'tar',
|
|
185
|
+
]);
|
|
186
|
+
|
|
177
187
|
const isContent = CONTENT_EXTENSIONS.has(ext);
|
|
178
188
|
const isMedia = MEDIA_EXTENSIONS.has(ext);
|
|
179
189
|
if (!isContent && !isMedia) return null;
|
|
@@ -256,314 +266,6 @@ export async function detectBinFile(filePath) {
|
|
|
256
266
|
return { meta, metaPath };
|
|
257
267
|
}
|
|
258
268
|
|
|
259
|
-
/** Default template cols for core entity types */
|
|
260
|
-
const TEMPLATE_DEFAULTS = {
|
|
261
|
-
content: [
|
|
262
|
-
'AppID', 'BinID', 'Name', 'Path', 'Content=@reference', 'Extension',
|
|
263
|
-
'Type', 'Title', 'Active', 'Public=0', 'SiteID', 'Description',
|
|
264
|
-
'Keywords', 'Parameters', 'CacheControl', 'RequireSSL', 'EntityID',
|
|
265
|
-
],
|
|
266
|
-
media: [
|
|
267
|
-
'AppID', 'BinID', 'Name', 'Path', 'Filename', 'Extension',
|
|
268
|
-
'MimeType', 'FileSize', 'Ownership', 'Public', 'Description',
|
|
269
|
-
'Duration', 'ParentMediaID',
|
|
270
|
-
],
|
|
271
|
-
output: [
|
|
272
|
-
'AppID', 'BinID', 'Name', 'Type', 'CustomSQL=@reference',
|
|
273
|
-
'Active', 'Public', 'Title', 'Description', 'Parameters',
|
|
274
|
-
'Cache', 'Limit', 'MaxRows', 'RowsPerPage', 'ApplyLocks',
|
|
275
|
-
'AdhocFilters', 'BaseEntityID', 'DataSourceID',
|
|
276
|
-
'DefaultMediaID', 'TemplateContentID', 'children',
|
|
277
|
-
],
|
|
278
|
-
output_value: [
|
|
279
|
-
'OutputID', 'Title', 'ShortName', 'OutputType', 'OrderNumber',
|
|
280
|
-
'CustomSQL', 'Format', 'Aggregate', 'DatePart', 'Distinct',
|
|
281
|
-
'Hide', 'Shared', 'Summary', 'SortType', 'SortOrder',
|
|
282
|
-
'EnforceSecurity', 'ReferencedEntityColumnID',
|
|
283
|
-
'OutputValueEntityColumnRelID', 'children',
|
|
284
|
-
],
|
|
285
|
-
output_value_filter: [
|
|
286
|
-
'OutputID', 'OutputValueID', 'ShortName', 'FilterType', 'Operator',
|
|
287
|
-
'OrderNumber', 'CustomSQL', 'Prompted', 'Required', 'Inclusive',
|
|
288
|
-
'PrimaryValue', 'SecondaryValue', 'FilterPreset',
|
|
289
|
-
'ApplyToAggregate', 'ValuesByOutputID', 'children',
|
|
290
|
-
],
|
|
291
|
-
output_value_entity_column_rel: [
|
|
292
|
-
'OutputID', 'OutputValueID', 'JoinType', 'OrderNumber', 'CustomSQL',
|
|
293
|
-
'Alias', 'SourceJoinID', 'SourceEntityColumnID',
|
|
294
|
-
'ReferencedEntityColumnID', 'children',
|
|
295
|
-
],
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Seed .dbo/metadata_schema.json with default fields for the given entity
|
|
300
|
-
* (and any other missing core entities) if an entry doesn't already exist.
|
|
301
|
-
*/
|
|
302
|
-
async function seedMetadataTemplate() {
|
|
303
|
-
const templates = await loadMetadataSchema() ?? {};
|
|
304
|
-
|
|
305
|
-
// Seed all missing core entities in one pass
|
|
306
|
-
const seeded = [];
|
|
307
|
-
for (const [name, cols] of Object.entries(TEMPLATE_DEFAULTS)) {
|
|
308
|
-
if (templates[name]) continue;
|
|
309
|
-
templates[name] = cols;
|
|
310
|
-
seeded.push(name);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (seeded.length === 0) return;
|
|
314
|
-
|
|
315
|
-
await saveMetadataSchema(templates);
|
|
316
|
-
log.dim(` Seeded metadata_schema.json with ${seeded.join(', ')} fields`);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function addSingleFile(filePath, client, options, batchDefaults) {
|
|
320
|
-
// Check .dboignore before doing any processing
|
|
321
|
-
const ig = await loadIgnore();
|
|
322
|
-
const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
323
|
-
if (ig.ignores(relPath)) {
|
|
324
|
-
log.dim(`Skipped (dboignored): ${relPath}`);
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const dir = dirname(filePath);
|
|
329
|
-
const ext = extname(filePath);
|
|
330
|
-
const base = basename(filePath, ext);
|
|
331
|
-
const fileName = basename(filePath);
|
|
332
|
-
const metaPath = join(dir, `${base}.metadata.json`);
|
|
333
|
-
|
|
334
|
-
// Step 1: Check for existing metadata
|
|
335
|
-
let meta = null;
|
|
336
|
-
try {
|
|
337
|
-
const raw = await readFile(metaPath, 'utf8');
|
|
338
|
-
if (raw.trim()) {
|
|
339
|
-
meta = JSON.parse(raw);
|
|
340
|
-
}
|
|
341
|
-
} catch {
|
|
342
|
-
// No metadata file — that's fine, we'll create one
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Step 2: If metadata exists with _CreatedOn, already on server
|
|
346
|
-
if (meta && meta._CreatedOn) {
|
|
347
|
-
log.warn(`"${fileName}" is already on the server (has _CreatedOn). Use "dbo push" to update.`);
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Step 3: If metadata exists without _CreatedOn, use it for insert
|
|
352
|
-
if (meta && meta._entity) {
|
|
353
|
-
log.info(`Found metadata for "${fileName}" — proceeding with insert`);
|
|
354
|
-
return await submitAdd(meta, metaPath, filePath, client, options);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Step 3b: Directive / template path
|
|
358
|
-
const directive = resolveDirective(filePath);
|
|
359
|
-
if (directive) {
|
|
360
|
-
const { entity, descriptor } = directive;
|
|
361
|
-
const appConfig = await loadAppConfig();
|
|
362
|
-
const appJson = await loadAppJsonBaseline().catch(() => null);
|
|
363
|
-
const resolved = await resolveTemplateCols(entity, descriptor, appConfig, appJson);
|
|
364
|
-
|
|
365
|
-
if (resolved !== null) {
|
|
366
|
-
let { cols, templates } = resolved;
|
|
367
|
-
|
|
368
|
-
// Check if @reference col is known
|
|
369
|
-
const hasRefMarker = cols.some(c => c.includes('=@reference'));
|
|
370
|
-
if (!hasRefMarker) {
|
|
371
|
-
if (options.yes) {
|
|
372
|
-
console.warn(`Warning: No @reference column in template for ${entity}${descriptor ? '.' + descriptor : ''} — skipping content column. Update .dbo/metadata_schema.json to add Key=@reference.`);
|
|
373
|
-
} else {
|
|
374
|
-
const chosen = await promptReferenceColumn(cols, entity, descriptor);
|
|
375
|
-
if (chosen) {
|
|
376
|
-
cols = cols.map(c => c === chosen ? `${chosen}=@reference` : c);
|
|
377
|
-
templates = templates ?? await loadMetadataSchema() ?? {};
|
|
378
|
-
setTemplateCols(templates, entity, descriptor, cols);
|
|
379
|
-
await saveMetadataSchema(templates);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const { meta } = assembleMetadata(cols, filePath, entity, descriptor, appConfig);
|
|
385
|
-
|
|
386
|
-
// Determine metadata path
|
|
387
|
-
let templateMetaPath;
|
|
388
|
-
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
389
|
-
if (rel.startsWith('docs/')) {
|
|
390
|
-
// docs/ files: companion metadata goes to extension/documentation/
|
|
391
|
-
const docDir = join(process.cwd(), 'extension', 'documentation');
|
|
392
|
-
await mkdir(docDir, { recursive: true });
|
|
393
|
-
templateMetaPath = join(docDir, `${basename(filePath, extname(filePath))}.metadata.json`);
|
|
394
|
-
} else {
|
|
395
|
-
templateMetaPath = join(dirname(filePath), `${basename(filePath, extname(filePath))}.metadata.json`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
await writeFile(templateMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
399
|
-
return await submitAdd(meta, templateMetaPath, filePath, client, options);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Step 3c: Auto-infer entity/descriptor for Documentation/ files when root placement is configured
|
|
404
|
-
const docInfo = await detectDocumentationFile(filePath);
|
|
405
|
-
if (docInfo) {
|
|
406
|
-
const filenameCol = await loadDescriptorFilenamePreference('documentation') || 'Name';
|
|
407
|
-
const docBase = basename(filePath, extname(filePath));
|
|
408
|
-
const companionDir = join(process.cwd(), 'extension', 'documentation');
|
|
409
|
-
await mkdir(companionDir, { recursive: true });
|
|
410
|
-
|
|
411
|
-
const docMeta = {
|
|
412
|
-
_entity: 'extension',
|
|
413
|
-
Descriptor: 'documentation',
|
|
414
|
-
[filenameCol]: docBase,
|
|
415
|
-
Name: docBase,
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// Find the content column name from saved preferences
|
|
419
|
-
const contentCol = filenameCol === 'Name' ? 'String10' : 'String10';
|
|
420
|
-
const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
421
|
-
docMeta[contentCol] = `@/${relPath}`;
|
|
422
|
-
docMeta._companionReferenceColumns = [contentCol];
|
|
423
|
-
|
|
424
|
-
const docMetaFile = join(companionDir, `${docBase}.metadata.json`);
|
|
425
|
-
await writeFile(docMetaFile, JSON.stringify(docMeta, null, 2) + '\n');
|
|
426
|
-
log.success(`Created companion metadata at ${docMetaFile}`);
|
|
427
|
-
return await submitAdd(docMeta, docMetaFile, filePath, client, options);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Step 3d: Auto-detect manifest.json at project root
|
|
431
|
-
const manifestMeta = await detectManifestFile(filePath);
|
|
432
|
-
if (manifestMeta) {
|
|
433
|
-
return await submitAdd(manifestMeta.meta, manifestMeta.metaPath, filePath, client, options);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Step 3e: Auto-detect content/media files in bin directories (lib/bins/)
|
|
437
|
-
const binMeta = await detectBinFile(filePath);
|
|
438
|
-
if (binMeta) {
|
|
439
|
-
return await submitAdd(binMeta.meta, binMeta.metaPath, filePath, client, options);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Step 4: No usable metadata — interactive wizard
|
|
443
|
-
const inquirer = (await import('inquirer')).default;
|
|
444
|
-
|
|
445
|
-
log.plain('');
|
|
446
|
-
log.warn(`I cannot add "${fileName}" to the server, because I don't have any metadata.`);
|
|
447
|
-
log.plain(`I have a few things that I need, and I can set that up for you,`);
|
|
448
|
-
log.plain(`or you manually add "${base}.metadata.json" next to the file.`);
|
|
449
|
-
log.plain('');
|
|
450
|
-
|
|
451
|
-
if (!options.yes) {
|
|
452
|
-
const { proceed } = await inquirer.prompt([{
|
|
453
|
-
type: 'confirm',
|
|
454
|
-
name: 'proceed',
|
|
455
|
-
message: 'Want me to set that up for you?',
|
|
456
|
-
default: true,
|
|
457
|
-
}]);
|
|
458
|
-
|
|
459
|
-
if (!proceed) {
|
|
460
|
-
log.dim(`Skipping "${fileName}". Create "${base}.metadata.json" manually and try again.`);
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Prompt for required fields (reuse batch defaults if available)
|
|
466
|
-
// Check if config has AppID for smart default
|
|
467
|
-
const appConfig = await loadAppConfig();
|
|
468
|
-
|
|
469
|
-
const answers = await inquirer.prompt([
|
|
470
|
-
{
|
|
471
|
-
type: 'input',
|
|
472
|
-
name: 'entity',
|
|
473
|
-
message: 'Entity name (e.g. content, template, style):',
|
|
474
|
-
default: batchDefaults?.entity || 'content',
|
|
475
|
-
validate: v => v.trim() ? true : 'Entity is required',
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
type: 'input',
|
|
479
|
-
name: 'contentColumn',
|
|
480
|
-
message: 'Column name for the file content (e.g. Content, Body, Template):',
|
|
481
|
-
default: batchDefaults?.contentColumn || 'Content',
|
|
482
|
-
validate: v => v.trim() ? true : 'Column name is required',
|
|
483
|
-
},
|
|
484
|
-
{
|
|
485
|
-
type: 'input',
|
|
486
|
-
name: 'BinID',
|
|
487
|
-
message: 'BinID (optional, press Enter to skip):',
|
|
488
|
-
default: batchDefaults?.BinID || '',
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
type: 'input',
|
|
492
|
-
name: 'SiteID',
|
|
493
|
-
message: 'SiteID (optional, press Enter to skip):',
|
|
494
|
-
default: batchDefaults?.SiteID || '',
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
type: 'input',
|
|
498
|
-
name: 'Path',
|
|
499
|
-
message: 'Path (optional, press Enter for auto-generated):',
|
|
500
|
-
default: batchDefaults?.Path || relative(process.cwd(), filePath).replace(/\\/g, '/'),
|
|
501
|
-
},
|
|
502
|
-
]);
|
|
503
|
-
|
|
504
|
-
// AppID: smart prompt when config has app info
|
|
505
|
-
if (batchDefaults?.AppID) {
|
|
506
|
-
answers.AppID = batchDefaults.AppID;
|
|
507
|
-
} else if (appConfig.AppID) {
|
|
508
|
-
const { appIdChoice } = await inquirer.prompt([{
|
|
509
|
-
type: 'list',
|
|
510
|
-
name: 'appIdChoice',
|
|
511
|
-
message: `You're submitting data without an AppID, but your config has information about the current App. Do you want me to add that Column information along with your submission?`,
|
|
512
|
-
choices: [
|
|
513
|
-
{ name: `Yes, use AppID ${appConfig.AppID}`, value: 'use_config' },
|
|
514
|
-
{ name: 'No', value: 'none' },
|
|
515
|
-
{ name: 'Enter custom AppID', value: 'custom' },
|
|
516
|
-
],
|
|
517
|
-
}]);
|
|
518
|
-
if (appIdChoice === 'use_config') {
|
|
519
|
-
answers.AppID = String(appConfig.AppID);
|
|
520
|
-
} else if (appIdChoice === 'custom') {
|
|
521
|
-
const { customAppId } = await inquirer.prompt([{
|
|
522
|
-
type: 'input', name: 'customAppId',
|
|
523
|
-
message: 'Custom AppID:',
|
|
524
|
-
}]);
|
|
525
|
-
answers.AppID = customAppId;
|
|
526
|
-
} else {
|
|
527
|
-
answers.AppID = '';
|
|
528
|
-
}
|
|
529
|
-
} else {
|
|
530
|
-
const { appId } = await inquirer.prompt([{
|
|
531
|
-
type: 'input', name: 'appId',
|
|
532
|
-
message: 'AppID (optional, press Enter to skip):',
|
|
533
|
-
default: '',
|
|
534
|
-
}]);
|
|
535
|
-
answers.AppID = appId;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Build metadata object
|
|
539
|
-
meta = {};
|
|
540
|
-
if (answers.AppID.trim()) meta.AppID = isFinite(answers.AppID) ? Number(answers.AppID) : answers.AppID;
|
|
541
|
-
if (answers.BinID.trim()) meta.BinID = isFinite(answers.BinID) ? Number(answers.BinID) : answers.BinID;
|
|
542
|
-
if (answers.SiteID.trim()) meta.SiteID = isFinite(answers.SiteID) ? Number(answers.SiteID) : answers.SiteID;
|
|
543
|
-
meta.Name = base;
|
|
544
|
-
if (answers.Path.trim()) meta.Path = answers.Path.trim();
|
|
545
|
-
meta[answers.contentColumn.trim()] = `@${fileName}`;
|
|
546
|
-
meta._entity = answers.entity.trim();
|
|
547
|
-
meta._companionReferenceColumns = [answers.contentColumn.trim()];
|
|
548
|
-
|
|
549
|
-
// Write metadata file
|
|
550
|
-
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
551
|
-
log.success(`Created ${basename(metaPath)}`);
|
|
552
|
-
|
|
553
|
-
// Submit the insert
|
|
554
|
-
const defaults = {
|
|
555
|
-
entity: answers.entity.trim(),
|
|
556
|
-
contentColumn: answers.contentColumn.trim(),
|
|
557
|
-
AppID: answers.AppID,
|
|
558
|
-
BinID: answers.BinID,
|
|
559
|
-
SiteID: answers.SiteID,
|
|
560
|
-
Path: answers.Path,
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
await submitAdd(meta, metaPath, filePath, client, options);
|
|
564
|
-
return defaults;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
269
|
/**
|
|
568
270
|
* Submit an insert to the server from a metadata object.
|
|
569
271
|
*/
|
|
@@ -716,7 +418,7 @@ export async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
|
716
418
|
return { entity: meta._entity };
|
|
717
419
|
}
|
|
718
420
|
|
|
719
|
-
async function updateBaselineWithNewRecord(meta, uid, lastUpdated) {
|
|
421
|
+
export async function updateBaselineWithNewRecord(meta, uid, lastUpdated) {
|
|
720
422
|
try {
|
|
721
423
|
const baseline = await loadAppJsonBaseline();
|
|
722
424
|
if (!baseline) return;
|
|
@@ -741,68 +443,30 @@ async function updateBaselineWithNewRecord(meta, uid, lastUpdated) {
|
|
|
741
443
|
}
|
|
742
444
|
|
|
743
445
|
/**
|
|
744
|
-
*
|
|
446
|
+
* Collect all @ companion file references from a metadata object,
|
|
447
|
+
* including references nested inside children (inline output hierarchy).
|
|
745
448
|
*/
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
log.info(`Found ${unadded.length} file(s) to add:`);
|
|
755
|
-
for (const f of unadded) {
|
|
756
|
-
log.plain(` ${relative(process.cwd(), f)}`);
|
|
757
|
-
}
|
|
758
|
-
log.plain('');
|
|
759
|
-
|
|
760
|
-
if (!options.yes) {
|
|
761
|
-
const inquirer = (await import('inquirer')).default;
|
|
762
|
-
const { proceed } = await inquirer.prompt([{
|
|
763
|
-
type: 'confirm',
|
|
764
|
-
name: 'proceed',
|
|
765
|
-
message: `Add ${unadded.length} file(s)?`,
|
|
766
|
-
default: true,
|
|
767
|
-
}]);
|
|
768
|
-
if (!proceed) return;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Pre-flight ticket validation
|
|
772
|
-
if (!options.ticket) {
|
|
773
|
-
const ticketCheck = await checkStoredTicket(options);
|
|
774
|
-
if (ticketCheck.cancel) {
|
|
775
|
-
log.info('Submission cancelled');
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
if (ticketCheck.clearTicket) {
|
|
779
|
-
await clearGlobalTicket();
|
|
780
|
-
log.dim(' Cleared stored ticket');
|
|
449
|
+
function collectCompanionRefs(meta, refs) {
|
|
450
|
+
for (const col of (meta._companionReferenceColumns || meta._contentColumns || [])) {
|
|
451
|
+
const val = meta[col];
|
|
452
|
+
if (typeof val === 'string' && val.startsWith('@') && !val.startsWith('@/')) {
|
|
453
|
+
refs.add(val.substring(1));
|
|
781
454
|
}
|
|
782
455
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
} catch (err) {
|
|
796
|
-
if (err.message === 'SKIP_ALL') {
|
|
797
|
-
log.info('Skipping remaining records');
|
|
798
|
-
break;
|
|
456
|
+
if (meta._mediaFile && typeof meta._mediaFile === 'string' && meta._mediaFile.startsWith('@')) {
|
|
457
|
+
refs.add(meta._mediaFile.substring(1));
|
|
458
|
+
}
|
|
459
|
+
// Recurse into inline output children (column, join, filter arrays)
|
|
460
|
+
if (meta.children && typeof meta.children === 'object' && !Array.isArray(meta.children)) {
|
|
461
|
+
for (const arr of Object.values(meta.children)) {
|
|
462
|
+
if (!Array.isArray(arr)) continue;
|
|
463
|
+
for (const child of arr) {
|
|
464
|
+
if (child && typeof child === 'object') {
|
|
465
|
+
collectCompanionRefs(child, refs);
|
|
466
|
+
}
|
|
799
467
|
}
|
|
800
|
-
log.error(`Failed: ${relative(process.cwd(), filePath)} — ${err.message}`);
|
|
801
|
-
failed++;
|
|
802
468
|
}
|
|
803
469
|
}
|
|
804
|
-
|
|
805
|
-
log.info(`Add complete: ${succeeded} succeeded, ${failed} failed`);
|
|
806
470
|
}
|
|
807
471
|
|
|
808
472
|
/**
|
|
@@ -835,15 +499,7 @@ export async function findUnaddedFiles(dir, ig, referencedFiles) {
|
|
|
835
499
|
const meta = JSON.parse(raw);
|
|
836
500
|
// Only count records that are on the server (have UID or _CreatedOn)
|
|
837
501
|
if (!meta.UID && !meta._CreatedOn) continue;
|
|
838
|
-
|
|
839
|
-
const val = meta[col];
|
|
840
|
-
if (typeof val === 'string' && val.startsWith('@') && !val.startsWith('@/')) {
|
|
841
|
-
localRefs.add(val.substring(1));
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
if (meta._mediaFile && typeof meta._mediaFile === 'string' && meta._mediaFile.startsWith('@')) {
|
|
845
|
-
localRefs.add(meta._mediaFile.substring(1));
|
|
846
|
-
}
|
|
502
|
+
collectCompanionRefs(meta, localRefs);
|
|
847
503
|
} catch { /* skip unreadable */ }
|
|
848
504
|
}
|
|
849
505
|
|
|
@@ -912,19 +568,18 @@ async function _scanMetadataRefs(dir, referenced) {
|
|
|
912
568
|
const meta = JSON.parse(raw);
|
|
913
569
|
if (!meta._CreatedOn && !meta.UID) continue; // only count server-confirmed records
|
|
914
570
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
571
|
+
// Collect all @ references (including from inline output children)
|
|
572
|
+
const allRefs = new Set();
|
|
573
|
+
collectCompanionRefs(meta, allRefs);
|
|
574
|
+
for (const refName of allRefs) {
|
|
575
|
+
if (refName.startsWith('/')) {
|
|
576
|
+
// @/path — absolute from project root
|
|
577
|
+
referenced.add(refName.substring(1));
|
|
578
|
+
} else {
|
|
579
|
+
// @filename — relative to metadata file's directory
|
|
580
|
+
const metaDir = relative(process.cwd(), dir).replace(/\\/g, '/');
|
|
581
|
+
const resolved = metaDir ? `${metaDir}/${refName}` : refName;
|
|
582
|
+
referenced.add(resolved);
|
|
928
583
|
}
|
|
929
584
|
}
|
|
930
585
|
|