@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
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFile, writeFile, stat, mkdir } from 'fs/promises';
|
|
3
|
+
import { join, dirname, basename, extname, relative } from 'path';
|
|
4
|
+
import { log } from '../lib/logger.js';
|
|
5
|
+
import { loadIgnore } from '../lib/ignore.js';
|
|
6
|
+
import { loadAppConfig, loadAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference } from '../lib/config.js';
|
|
7
|
+
import {
|
|
8
|
+
resolveDirective, resolveTemplateCols, assembleMetadata,
|
|
9
|
+
promptReferenceColumn, getTemplateCols, setTemplateCols,
|
|
10
|
+
loadMetadataSchema, saveMetadataSchema,
|
|
11
|
+
} from '../lib/metadata-schema.js';
|
|
12
|
+
import { loadStructureFile, findBinByPath, BINS_DIR } from '../lib/structure.js';
|
|
13
|
+
import { isMetadataFile } from '../lib/filenames.js';
|
|
14
|
+
import { findUnaddedFiles, MIME_TYPES, seedMetadataTemplate, detectDocumentationFile, detectManifestFile } from '../lib/insert.js';
|
|
15
|
+
import { runPendingMigrations } from '../lib/migrations.js';
|
|
16
|
+
|
|
17
|
+
export const adoptCommand = new Command('adopt')
|
|
18
|
+
.description('Create metadata companion for a file or directory (local only — use dbo push to insert)')
|
|
19
|
+
.argument('<path>', 'File path, directory path, or "." for current directory')
|
|
20
|
+
.option('-e, --entity <spec>', 'Entity and optional column: e.g. content, media, extension.widget, extension.String5')
|
|
21
|
+
.option('--into <metaPath>', 'Attach file to an existing metadata record as an additional companion column (single-file only)')
|
|
22
|
+
.option('-y, --yes', 'Skip all confirmation prompts and use defaults')
|
|
23
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
24
|
+
.option('--domain <host>', 'Override domain')
|
|
25
|
+
.option('--no-migrate', 'Skip pending migrations for this invocation')
|
|
26
|
+
.action(async (targetPath, options) => {
|
|
27
|
+
try {
|
|
28
|
+
await runPendingMigrations(options);
|
|
29
|
+
|
|
30
|
+
// --into is single-file only
|
|
31
|
+
if (options.into) {
|
|
32
|
+
if (targetPath === '.' || (await stat(targetPath)).isDirectory()) {
|
|
33
|
+
log.error('--into is not supported in directory mode');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
await adoptIntoRecord(targetPath, options.into, options.entity, options);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pathStat = await stat(targetPath).catch(() => null);
|
|
41
|
+
if (!pathStat) {
|
|
42
|
+
log.error(`Path not found: ${targetPath}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (pathStat.isDirectory()) {
|
|
47
|
+
await adoptDirectory(targetPath, options.entity, options);
|
|
48
|
+
} else {
|
|
49
|
+
await adoptSingleFile(targetPath, options.entity, options);
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
log.error(err.message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Disambiguate extension.widget (descriptor) from extension.String5 (column).
|
|
59
|
+
*/
|
|
60
|
+
function parseEntitySpec(spec, metadataSchema) {
|
|
61
|
+
if (!spec) return null;
|
|
62
|
+
const dotIdx = spec.indexOf('.');
|
|
63
|
+
if (dotIdx === -1) return { entity: spec, descriptor: null, column: null };
|
|
64
|
+
|
|
65
|
+
const entity = spec.slice(0, dotIdx);
|
|
66
|
+
const sub = spec.slice(dotIdx + 1);
|
|
67
|
+
|
|
68
|
+
if (entity === 'extension') {
|
|
69
|
+
// Check if `sub` is a known descriptor in metadata_schema.json
|
|
70
|
+
const extSchema = metadataSchema?.extension;
|
|
71
|
+
if (extSchema && typeof extSchema === 'object' && !Array.isArray(extSchema) && extSchema[sub] !== undefined) {
|
|
72
|
+
return { entity: 'extension', descriptor: sub, column: null };
|
|
73
|
+
}
|
|
74
|
+
// Otherwise treat as column name
|
|
75
|
+
return { entity: 'extension', descriptor: null, column: sub };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// e.g. "content.Content" — treat sub as explicit column
|
|
79
|
+
return { entity, descriptor: null, column: sub };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build entity-specific metadata for a file in or outside lib/bins/.
|
|
84
|
+
*/
|
|
85
|
+
async function buildBinMetadata(filePath, entity, appConfig, structure) {
|
|
86
|
+
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
87
|
+
const ext = extname(filePath).replace('.', '').toLowerCase();
|
|
88
|
+
const fileName = basename(filePath);
|
|
89
|
+
const base = basename(filePath, extname(filePath));
|
|
90
|
+
const fileDir = dirname(rel);
|
|
91
|
+
const bin = findBinByPath(fileDir, structure);
|
|
92
|
+
const binPath = bin?.path || '';
|
|
93
|
+
|
|
94
|
+
const metaPath = join(dirname(filePath), `${base}.metadata.json`);
|
|
95
|
+
|
|
96
|
+
if (entity === 'content') {
|
|
97
|
+
const contentPath = binPath
|
|
98
|
+
? `${binPath}/${base}.${ext}`
|
|
99
|
+
: `${base}.${ext}`;
|
|
100
|
+
const meta = {
|
|
101
|
+
_entity: 'content',
|
|
102
|
+
_companionReferenceColumns: ['Content'],
|
|
103
|
+
Name: base,
|
|
104
|
+
Content: `@${fileName}`,
|
|
105
|
+
Extension: ext.toUpperCase(),
|
|
106
|
+
Path: contentPath,
|
|
107
|
+
Active: 1,
|
|
108
|
+
Public: 0,
|
|
109
|
+
};
|
|
110
|
+
if (bin) meta.BinID = bin.binId;
|
|
111
|
+
if (appConfig.AppID) meta.AppID = appConfig.AppID;
|
|
112
|
+
return { meta, metaPath };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (entity === 'media') {
|
|
116
|
+
const meta = {
|
|
117
|
+
_entity: 'media',
|
|
118
|
+
_mediaFile: `@${fileName}`,
|
|
119
|
+
Name: base,
|
|
120
|
+
Filename: fileName,
|
|
121
|
+
Extension: ext,
|
|
122
|
+
Ownership: 'App',
|
|
123
|
+
};
|
|
124
|
+
if (bin) meta.BinID = bin.binId;
|
|
125
|
+
if (appConfig.AppID) meta.AppID = appConfig.AppID;
|
|
126
|
+
if (binPath) meta.Path = binPath;
|
|
127
|
+
const mimeType = MIME_TYPES[ext];
|
|
128
|
+
if (mimeType) meta.MimeType = mimeType;
|
|
129
|
+
if (appConfig.AppShortName) {
|
|
130
|
+
const parts = ['', 'media', appConfig.AppShortName, 'app'];
|
|
131
|
+
if (binPath) parts.push(binPath);
|
|
132
|
+
parts.push(fileName);
|
|
133
|
+
meta.FullPath = parts.join('/');
|
|
134
|
+
}
|
|
135
|
+
return { meta, metaPath };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null; // unsupported entity for bin metadata
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Adopt a single file — create a *.metadata.json companion (local only, no server call).
|
|
143
|
+
*/
|
|
144
|
+
async function adoptSingleFile(filePath, entityArg, options) {
|
|
145
|
+
const ig = await loadIgnore();
|
|
146
|
+
const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
147
|
+
if (ig.ignores(relPath)) {
|
|
148
|
+
log.dim(`Skipped (dboignored): ${relPath}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const dir = dirname(filePath);
|
|
153
|
+
const ext = extname(filePath);
|
|
154
|
+
const base = basename(filePath, ext);
|
|
155
|
+
const fileName = basename(filePath);
|
|
156
|
+
|
|
157
|
+
// Check for existing metadata
|
|
158
|
+
const metaPath = join(dir, `${base}.metadata.json`);
|
|
159
|
+
let existingMeta = null;
|
|
160
|
+
try {
|
|
161
|
+
existingMeta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
162
|
+
} catch { /* no file — that's fine */ }
|
|
163
|
+
|
|
164
|
+
if (existingMeta) {
|
|
165
|
+
if (existingMeta.UID || existingMeta._CreatedOn) {
|
|
166
|
+
log.warn(`"${fileName}" is already on the server (has UID/_CreatedOn) — skipping.`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Metadata exists but no server record
|
|
170
|
+
if (!options.yes) {
|
|
171
|
+
const inquirer = (await import('inquirer')).default;
|
|
172
|
+
const { overwrite } = await inquirer.prompt([{
|
|
173
|
+
type: 'confirm',
|
|
174
|
+
name: 'overwrite',
|
|
175
|
+
message: `Metadata already exists for "${fileName}" (no UID). Overwrite?`,
|
|
176
|
+
default: false,
|
|
177
|
+
}]);
|
|
178
|
+
if (!overwrite) return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const metadataSchema = await loadMetadataSchema() ?? {};
|
|
183
|
+
|
|
184
|
+
// --- Resolve entity from -e flag or directory inference ---
|
|
185
|
+
let entity, descriptor, column;
|
|
186
|
+
if (entityArg) {
|
|
187
|
+
const parsed = parseEntitySpec(entityArg, metadataSchema);
|
|
188
|
+
entity = parsed.entity;
|
|
189
|
+
descriptor = parsed.descriptor;
|
|
190
|
+
column = parsed.column;
|
|
191
|
+
} else {
|
|
192
|
+
// No -e: infer via resolveDirective
|
|
193
|
+
const directive = resolveDirective(filePath);
|
|
194
|
+
if (directive) {
|
|
195
|
+
entity = directive.entity;
|
|
196
|
+
descriptor = directive.descriptor;
|
|
197
|
+
} else {
|
|
198
|
+
// Fall back to interactive wizard
|
|
199
|
+
log.warn(`Cannot infer entity for "${fileName}". Use -e <entity> to specify.`);
|
|
200
|
+
await runInteractiveWizard(filePath, options);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const appConfig = await loadAppConfig();
|
|
206
|
+
|
|
207
|
+
// --- Special case: manifest.json ---
|
|
208
|
+
const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
209
|
+
if (rel.toLowerCase() === 'manifest.json') {
|
|
210
|
+
const manifestResult = await detectManifestFile(filePath);
|
|
211
|
+
if (manifestResult) {
|
|
212
|
+
log.success(`Created manifest metadata at ${manifestResult.metaPath}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- content or media entities: build entity-specific metadata ---
|
|
218
|
+
if ((entity === 'content' || entity === 'media') && !column) {
|
|
219
|
+
const structure = await loadStructureFile();
|
|
220
|
+
const result = await buildBinMetadata(filePath, entity, appConfig, structure);
|
|
221
|
+
if (result) {
|
|
222
|
+
await writeFile(result.metaPath, JSON.stringify(result.meta, null, 2) + '\n');
|
|
223
|
+
log.success(`Created ${entity} metadata: ${basename(result.metaPath)}`);
|
|
224
|
+
await seedMetadataTemplate();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- docs/ files with documentation directive ---
|
|
230
|
+
if (entity === 'extension' && descriptor === 'documentation') {
|
|
231
|
+
const docBase = base;
|
|
232
|
+
const companionDir = join(process.cwd(), 'lib', 'extension', 'documentation');
|
|
233
|
+
await mkdir(companionDir, { recursive: true });
|
|
234
|
+
const docMetaPath = join(companionDir, `${docBase}.metadata.json`);
|
|
235
|
+
|
|
236
|
+
const docMeta = {
|
|
237
|
+
_entity: 'extension',
|
|
238
|
+
Descriptor: 'documentation',
|
|
239
|
+
Name: docBase,
|
|
240
|
+
};
|
|
241
|
+
const contentCol = 'String10';
|
|
242
|
+
docMeta[contentCol] = `@/${relPath}`;
|
|
243
|
+
docMeta._companionReferenceColumns = [contentCol];
|
|
244
|
+
|
|
245
|
+
await writeFile(docMetaPath, JSON.stringify(docMeta, null, 2) + '\n');
|
|
246
|
+
log.success(`Created documentation metadata: ${basename(docMetaPath)}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- General path: use resolveTemplateCols + assembleMetadata ---
|
|
251
|
+
const appJson = await (async () => { try { return await loadAppJsonBaseline(); } catch { return null; } })();
|
|
252
|
+
const resolved = await resolveTemplateCols(entity, descriptor, appConfig, appJson);
|
|
253
|
+
|
|
254
|
+
if (resolved === null) {
|
|
255
|
+
// Malformed metadata_schema.json — fall through to wizard
|
|
256
|
+
await runInteractiveWizard(filePath, options);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let { cols, templates } = resolved;
|
|
261
|
+
|
|
262
|
+
// If an explicit column was provided, inject it as @reference
|
|
263
|
+
if (column) {
|
|
264
|
+
const hasCol = cols.some(c => c.startsWith(column + '=') || c === column);
|
|
265
|
+
if (hasCol) {
|
|
266
|
+
cols = cols.map(c => (c === column || c.startsWith(column + '=')) ? `${column}=@reference` : c);
|
|
267
|
+
} else {
|
|
268
|
+
cols.push(`${column}=@reference`);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
// Ensure @reference marker exists
|
|
272
|
+
const hasRefMarker = cols.some(c => c.includes('=@reference'));
|
|
273
|
+
if (!hasRefMarker) {
|
|
274
|
+
if (options.yes) {
|
|
275
|
+
log.warn(`No @reference column in template for ${entity}${descriptor ? '.' + descriptor : ''} — skipping content column.`);
|
|
276
|
+
} else {
|
|
277
|
+
const chosen = await promptReferenceColumn(cols, entity, descriptor);
|
|
278
|
+
if (chosen) {
|
|
279
|
+
cols = cols.map(c => c === chosen ? `${chosen}=@reference` : c);
|
|
280
|
+
templates = templates ?? metadataSchema;
|
|
281
|
+
setTemplateCols(templates, entity, descriptor, cols);
|
|
282
|
+
await saveMetadataSchema(templates);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { meta } = assembleMetadata(cols, filePath, entity, descriptor, appConfig);
|
|
289
|
+
const finalMetaPath = join(dirname(filePath), `${base}.metadata.json`);
|
|
290
|
+
|
|
291
|
+
await writeFile(finalMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
292
|
+
log.success(`Created metadata: ${basename(finalMetaPath)}`);
|
|
293
|
+
log.dim(` Run "dbo push" to insert this record on the server.`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Interactive wizard fallback — creates metadata by prompting the user.
|
|
298
|
+
* Does NOT submit to server (local-only).
|
|
299
|
+
*/
|
|
300
|
+
async function runInteractiveWizard(filePath, options) {
|
|
301
|
+
const inquirer = (await import('inquirer')).default;
|
|
302
|
+
const fileName = basename(filePath);
|
|
303
|
+
const base = basename(filePath, extname(filePath));
|
|
304
|
+
const metaPath = join(dirname(filePath), `${base}.metadata.json`);
|
|
305
|
+
|
|
306
|
+
log.plain('');
|
|
307
|
+
log.warn(`I cannot adopt "${fileName}" because I don't have enough metadata.`);
|
|
308
|
+
log.plain(`I have a few things that I need, and I can set that up for you,`);
|
|
309
|
+
log.plain(`or you can manually create "${base}.metadata.json" next to the file.`);
|
|
310
|
+
log.plain('');
|
|
311
|
+
|
|
312
|
+
if (!options.yes) {
|
|
313
|
+
const { proceed } = await inquirer.prompt([{
|
|
314
|
+
type: 'confirm',
|
|
315
|
+
name: 'proceed',
|
|
316
|
+
message: 'Want me to set that up for you?',
|
|
317
|
+
default: true,
|
|
318
|
+
}]);
|
|
319
|
+
|
|
320
|
+
if (!proceed) {
|
|
321
|
+
log.dim(`Skipping "${fileName}". Create "${base}.metadata.json" manually and try again.`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const appConfig = await loadAppConfig();
|
|
327
|
+
|
|
328
|
+
const answers = await inquirer.prompt([
|
|
329
|
+
{
|
|
330
|
+
type: 'input',
|
|
331
|
+
name: 'entity',
|
|
332
|
+
message: 'Entity name (e.g. content, template, style):',
|
|
333
|
+
default: 'content',
|
|
334
|
+
validate: v => v.trim() ? true : 'Entity is required',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
type: 'input',
|
|
338
|
+
name: 'contentColumn',
|
|
339
|
+
message: 'Column name for the file content (e.g. Content, Body, Template):',
|
|
340
|
+
default: 'Content',
|
|
341
|
+
validate: v => v.trim() ? true : 'Column name is required',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
type: 'input',
|
|
345
|
+
name: 'BinID',
|
|
346
|
+
message: 'BinID (optional, press Enter to skip):',
|
|
347
|
+
default: '',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: 'input',
|
|
351
|
+
name: 'SiteID',
|
|
352
|
+
message: 'SiteID (optional, press Enter to skip):',
|
|
353
|
+
default: '',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
type: 'input',
|
|
357
|
+
name: 'Path',
|
|
358
|
+
message: 'Path (optional, press Enter for auto-generated):',
|
|
359
|
+
default: relative(process.cwd(), filePath).replace(/\\/g, '/'),
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
// AppID: smart prompt when config has app info
|
|
364
|
+
if (appConfig.AppID) {
|
|
365
|
+
const { appIdChoice } = await inquirer.prompt([{
|
|
366
|
+
type: 'list',
|
|
367
|
+
name: 'appIdChoice',
|
|
368
|
+
message: `You're creating metadata without an AppID, but your config has information about the current App. Do you want me to add that Column information?`,
|
|
369
|
+
choices: [
|
|
370
|
+
{ name: `Yes, use AppID ${appConfig.AppID}`, value: 'use_config' },
|
|
371
|
+
{ name: 'No', value: 'none' },
|
|
372
|
+
{ name: 'Enter custom AppID', value: 'custom' },
|
|
373
|
+
],
|
|
374
|
+
}]);
|
|
375
|
+
if (appIdChoice === 'use_config') {
|
|
376
|
+
answers.AppID = String(appConfig.AppID);
|
|
377
|
+
} else if (appIdChoice === 'custom') {
|
|
378
|
+
const { customAppId } = await inquirer.prompt([{
|
|
379
|
+
type: 'input', name: 'customAppId',
|
|
380
|
+
message: 'Custom AppID:',
|
|
381
|
+
}]);
|
|
382
|
+
answers.AppID = customAppId;
|
|
383
|
+
} else {
|
|
384
|
+
answers.AppID = '';
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
const { appId } = await inquirer.prompt([{
|
|
388
|
+
type: 'input', name: 'appId',
|
|
389
|
+
message: 'AppID (optional, press Enter to skip):',
|
|
390
|
+
default: '',
|
|
391
|
+
}]);
|
|
392
|
+
answers.AppID = appId;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Build metadata object
|
|
396
|
+
const meta = {};
|
|
397
|
+
if (answers.AppID.trim()) meta.AppID = isFinite(answers.AppID) ? Number(answers.AppID) : answers.AppID;
|
|
398
|
+
if (answers.BinID.trim()) meta.BinID = isFinite(answers.BinID) ? Number(answers.BinID) : answers.BinID;
|
|
399
|
+
if (answers.SiteID.trim()) meta.SiteID = isFinite(answers.SiteID) ? Number(answers.SiteID) : answers.SiteID;
|
|
400
|
+
meta.Name = base;
|
|
401
|
+
if (answers.Path.trim()) meta.Path = answers.Path.trim();
|
|
402
|
+
meta[answers.contentColumn.trim()] = `@${fileName}`;
|
|
403
|
+
meta._entity = answers.entity.trim();
|
|
404
|
+
meta._companionReferenceColumns = [answers.contentColumn.trim()];
|
|
405
|
+
|
|
406
|
+
// Write metadata file — NO server submission
|
|
407
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
408
|
+
log.success(`Created ${basename(metaPath)}`);
|
|
409
|
+
log.dim(` Run "dbo push" to insert this record on the server.`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Attach a file to an existing metadata record as an additional companion column.
|
|
414
|
+
*/
|
|
415
|
+
async function adoptIntoRecord(filePath, intoPath, columnArg, options) {
|
|
416
|
+
if (!columnArg) {
|
|
417
|
+
log.error('-e <column> is required when using --into');
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Load target metadata
|
|
422
|
+
let targetMeta;
|
|
423
|
+
try {
|
|
424
|
+
targetMeta = JSON.parse(await readFile(intoPath, 'utf8'));
|
|
425
|
+
} catch {
|
|
426
|
+
log.error(`Target metadata not found: ${intoPath}`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const targetEntity = targetMeta._entity;
|
|
431
|
+
if (!targetEntity) {
|
|
432
|
+
log.error(`Target metadata has no _entity field: ${intoPath}`);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Parse column from -e (strip entity prefix if provided, validate if it doesn't match)
|
|
437
|
+
let column = columnArg;
|
|
438
|
+
const dotIdx = columnArg.indexOf('.');
|
|
439
|
+
if (dotIdx !== -1) {
|
|
440
|
+
const entityPrefix = columnArg.slice(0, dotIdx);
|
|
441
|
+
column = columnArg.slice(dotIdx + 1);
|
|
442
|
+
if (entityPrefix !== targetEntity) {
|
|
443
|
+
log.error(`Entity mismatch: -e specifies "${entityPrefix}" but target metadata has _entity "${targetEntity}"`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Validate column in metadata_schema.json
|
|
449
|
+
const metadataSchema = await loadMetadataSchema() ?? {};
|
|
450
|
+
const cols = getTemplateCols(metadataSchema, targetEntity, targetMeta.Descriptor ?? null);
|
|
451
|
+
if (cols && !cols.some(c => c === column || c.startsWith(column + '='))) {
|
|
452
|
+
log.warn(`Column "${column}" is not in the metadata schema for "${targetEntity}" — proceeding anyway.`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Check if column already set
|
|
456
|
+
if (targetMeta[column] !== undefined) {
|
|
457
|
+
if (!options.yes) {
|
|
458
|
+
const inquirer = (await import('inquirer')).default;
|
|
459
|
+
const { overwrite } = await inquirer.prompt([{
|
|
460
|
+
type: 'confirm',
|
|
461
|
+
name: 'overwrite',
|
|
462
|
+
message: `Column "${column}" already has value "${targetMeta[column]}" in ${basename(intoPath)}. Overwrite?`,
|
|
463
|
+
default: false,
|
|
464
|
+
}]);
|
|
465
|
+
if (!overwrite) return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Add column and update _companionReferenceColumns
|
|
470
|
+
const fileName = basename(filePath);
|
|
471
|
+
targetMeta[column] = `@${fileName}`;
|
|
472
|
+
const refs = targetMeta._companionReferenceColumns ?? [];
|
|
473
|
+
if (!refs.includes(column)) refs.push(column);
|
|
474
|
+
targetMeta._companionReferenceColumns = refs;
|
|
475
|
+
|
|
476
|
+
await writeFile(intoPath, JSON.stringify(targetMeta, null, 2) + '\n');
|
|
477
|
+
log.success(`Added ${column}: "@${fileName}" to ${basename(intoPath)}`);
|
|
478
|
+
log.dim(` _companionReferenceColumns: ${JSON.stringify(targetMeta._companionReferenceColumns)}`);
|
|
479
|
+
log.dim(` Run "dbo push" to insert/update the record on the server.`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Adopt all un-tracked files in a directory.
|
|
484
|
+
*/
|
|
485
|
+
async function adoptDirectory(dirPath, entityArg, options) {
|
|
486
|
+
if (!entityArg) {
|
|
487
|
+
log.error('The -e flag is required when adopting a directory.');
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Find files without metadata using the existing findUnaddedFiles logic
|
|
492
|
+
const ig = await loadIgnore();
|
|
493
|
+
const unadopted = await findUnaddedFiles(dirPath, ig);
|
|
494
|
+
|
|
495
|
+
if (unadopted.length === 0) {
|
|
496
|
+
log.info('Nothing to adopt — all files already have metadata.');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const metadataSchema = await loadMetadataSchema() ?? {};
|
|
501
|
+
const parsed = parseEntitySpec(entityArg, metadataSchema);
|
|
502
|
+
|
|
503
|
+
log.info(`Found ${unadopted.length} file(s) to adopt as "${entityArg}":`);
|
|
504
|
+
for (const f of unadopted) {
|
|
505
|
+
log.plain(` ${relative(process.cwd(), f)}`);
|
|
506
|
+
}
|
|
507
|
+
log.plain('');
|
|
508
|
+
|
|
509
|
+
if (!options.yes) {
|
|
510
|
+
const inquirer = (await import('inquirer')).default;
|
|
511
|
+
const { proceed } = await inquirer.prompt([{
|
|
512
|
+
type: 'confirm',
|
|
513
|
+
name: 'proceed',
|
|
514
|
+
message: `Create metadata for ${unadopted.length} file(s) as "${entityArg}"?`,
|
|
515
|
+
default: true,
|
|
516
|
+
}]);
|
|
517
|
+
if (!proceed) return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let succeeded = 0;
|
|
521
|
+
let skipped = 0;
|
|
522
|
+
|
|
523
|
+
for (const filePath of unadopted) {
|
|
524
|
+
try {
|
|
525
|
+
await adoptSingleFile(filePath, entityArg, { ...options, yes: true }); // suppress per-file prompts
|
|
526
|
+
succeeded++;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
log.error(`Failed: ${relative(process.cwd(), filePath)} — ${err.message}`);
|
|
529
|
+
skipped++;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
log.info(`Adopt complete: ${succeeded} created, ${skipped} failed/skipped.`);
|
|
534
|
+
}
|
package/src/commands/build.js
CHANGED
|
@@ -16,9 +16,9 @@ export const buildCommand = new Command('build')
|
|
|
16
16
|
|
|
17
17
|
if (!base && !local) {
|
|
18
18
|
if (targetPath) {
|
|
19
|
-
log.warn('No .
|
|
19
|
+
log.warn('No .app/scripts.json found — nothing to build');
|
|
20
20
|
} else {
|
|
21
|
-
log.info('No .
|
|
21
|
+
log.info('No .app/scripts.json found');
|
|
22
22
|
}
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
@@ -63,7 +63,7 @@ export const buildCommand = new Command('build')
|
|
|
63
63
|
if (targets.length === 0) return; // only global, done
|
|
64
64
|
}
|
|
65
65
|
if (targets.length === 0) {
|
|
66
|
-
log.info('No build hooks defined in .
|
|
66
|
+
log.info('No build hooks defined in .app/scripts.json targets');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
}
|