@dboio/cli 0.19.7 → 0.20.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.
- package/README.md +18 -12
- package/bin/dbo.js +5 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +39 -12
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +18 -12
- package/plugins/claude/dbo/docs/dual-platform-maintenance.md +135 -0
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +13 -3
- package/plugins/claude/dbo/skills/white-paper/SKILL.md +49 -8
- package/plugins/claude/dbo/skills/white-paper/references/api-reference.md +1 -1
- package/plugins/claude/track/.claude-plugin/plugin.json +1 -1
- package/src/commands/adopt.js +22 -19
- package/src/commands/clone.js +412 -57
- package/src/commands/init.js +2 -2
- package/src/commands/input.js +2 -2
- package/src/commands/login.js +3 -3
- package/src/commands/push.js +142 -43
- package/src/commands/status.js +15 -7
- package/src/lib/config.js +117 -11
- package/src/lib/dependencies.js +11 -9
- package/src/lib/filenames.js +54 -66
- package/src/lib/ignore.js +3 -0
- package/src/lib/input-parser.js +2 -6
- package/src/lib/insert.js +34 -49
- package/src/lib/structure.js +23 -8
- package/src/lib/ticketing.js +66 -9
- package/src/lib/toe-stepping.js +103 -3
- package/src/migrations/008-metadata-uid-in-suffix.js +4 -2
- package/src/migrations/009-fix-media-collision-metadata-names.js +9 -3
- package/src/migrations/013-remove-uid-from-meta-filenames.js +117 -0
- package/src/migrations/014-entity-dir-to-data-source.js +68 -0
package/src/lib/filenames.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Filename convention helpers for the metadata
|
|
3
|
-
* Metadata files: name.metadata
|
|
4
|
-
* Companion files: natural names
|
|
2
|
+
* Filename convention helpers for the metadata convention.
|
|
3
|
+
* Metadata files: name.metadata.json (UID is stored inside the JSON, not in the filename)
|
|
4
|
+
* Companion files: natural names.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { rename, unlink, writeFile, readFile, readdir } from 'fs/promises';
|
|
@@ -15,6 +15,8 @@ import { join, dirname, basename, extname } from 'path';
|
|
|
15
15
|
* @param {string} uid - Server-assigned UID
|
|
16
16
|
* @param {string} [ext] - File extension WITHOUT leading dot (e.g. 'css', 'png', '')
|
|
17
17
|
* @returns {string} - e.g. "colors~abc123.css" or "abc123.css" or "colors~abc123"
|
|
18
|
+
* @deprecated Use buildMetaFilename() for metadata files. This function is retained
|
|
19
|
+
* only for legacy detection/migration code.
|
|
18
20
|
*/
|
|
19
21
|
export function buildUidFilename(name, uid, ext = '') {
|
|
20
22
|
const base = (name === uid) ? uid : `${name}~${uid}`;
|
|
@@ -100,6 +102,7 @@ export function stripUidFromFilename(localName, uid) {
|
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
104
|
* Check whether a filename already contains ~<uid>.
|
|
105
|
+
* @deprecated Only used by legacy migration code.
|
|
103
106
|
*/
|
|
104
107
|
export function hasUidInFilename(filename, uid) {
|
|
105
108
|
return typeof filename === 'string' && typeof uid === 'string'
|
|
@@ -114,25 +117,28 @@ export function hasUidInFilename(filename, uid) {
|
|
|
114
117
|
* Returns { name, uid } or null.
|
|
115
118
|
*/
|
|
116
119
|
export function detectLegacyDotUid(filename) {
|
|
117
|
-
const match = filename.match(/^(.+)\.([a-z0-
|
|
120
|
+
const match = filename.match(/^(.+)\.([a-z0-9_]{10,})\.metadata\.json$/);
|
|
118
121
|
return match ? { name: match[1], uid: match[2] } : null;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/**
|
|
122
|
-
* Build
|
|
123
|
-
* Format: <naturalBase>.metadata
|
|
125
|
+
* Build a metadata filename for a record.
|
|
126
|
+
* Format: <naturalBase>.metadata.json
|
|
127
|
+
* The UID is stored inside the JSON, not in the filename.
|
|
124
128
|
*
|
|
125
|
-
* For content records:
|
|
126
|
-
* For media records:
|
|
127
|
-
* For output records:
|
|
129
|
+
* For content records: naturalBase = "colors" → "colors.metadata.json"
|
|
130
|
+
* For media records: naturalBase = "logo.png" → "logo.png.metadata.json"
|
|
131
|
+
* For output records: naturalBase = "Sales" → "Sales.metadata.json"
|
|
132
|
+
*
|
|
133
|
+
* Collision avoidance (when two records share a name) is handled by the caller
|
|
134
|
+
* via numbered suffixes: "colors-1.metadata.json", "colors-2.metadata.json", etc.
|
|
128
135
|
*
|
|
129
136
|
* @param {string} naturalBase - Name without any ~uid (may include media extension)
|
|
130
|
-
* @param {string} uid - Server-assigned UID
|
|
131
137
|
* @returns {string}
|
|
132
138
|
*/
|
|
133
|
-
export function buildMetaFilename(naturalBase
|
|
139
|
+
export function buildMetaFilename(naturalBase) {
|
|
134
140
|
// Guard: strip any trailing .metadata suffix(es) and ~uid fragments from naturalBase
|
|
135
|
-
// to prevent double-metadata filenames (e.g., "app.metadata.metadata
|
|
141
|
+
// to prevent double-metadata filenames (e.g., "app.metadata.metadata.json")
|
|
136
142
|
let base = naturalBase;
|
|
137
143
|
const metaParsed = parseMetaFilename(base + '.json');
|
|
138
144
|
if (metaParsed) {
|
|
@@ -142,34 +148,43 @@ export function buildMetaFilename(naturalBase, uid) {
|
|
|
142
148
|
while (base.endsWith('.metadata')) {
|
|
143
149
|
base = base.substring(0, base.length - 9);
|
|
144
150
|
}
|
|
145
|
-
return `${base}.metadata
|
|
151
|
+
return `${base}.metadata.json`;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
/**
|
|
149
155
|
* Test whether a filename is a metadata file (new or legacy format).
|
|
150
156
|
*
|
|
151
|
-
* New format: name.metadata
|
|
152
|
-
* Legacy format: name~uid.
|
|
157
|
+
* New format: name.metadata.json (UID in JSON content, not filename)
|
|
158
|
+
* Legacy format: name.metadata~uid.json (also accepted during migration)
|
|
153
159
|
*
|
|
154
160
|
* @param {string} filename
|
|
155
161
|
* @returns {boolean}
|
|
156
162
|
*/
|
|
157
163
|
export function isMetadataFile(filename) {
|
|
158
|
-
return
|
|
159
|
-
||
|
|
160
|
-
|| /\.[a-z0-9]{10,}\.metadata\.json$/i.test(filename);
|
|
164
|
+
return filename.endsWith('.metadata.json') // new format + legacy pre-UID temp files
|
|
165
|
+
|| /\.metadata~[a-z0-9_]+\.json$/i.test(filename) // legacy tilde-uid suffix format
|
|
166
|
+
|| /\.[a-z0-9]{10,}\.metadata\.json$/i.test(filename); // legacy dot-separated: name.uid.metadata.json
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
/**
|
|
164
|
-
* Parse a
|
|
165
|
-
*
|
|
170
|
+
* Parse a metadata filename into its components.
|
|
171
|
+
*
|
|
172
|
+
* New format: name.metadata.json → { naturalBase: "name" }
|
|
173
|
+
* Legacy format: name.metadata~uid.json → { naturalBase: "name", uid: "uid" }
|
|
166
174
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
175
|
+
* Returns null for non-metadata filenames.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} filename
|
|
178
|
+
* @returns {{ naturalBase: string, uid?: string } | null}
|
|
169
179
|
*/
|
|
170
180
|
export function parseMetaFilename(filename) {
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
// Legacy format with uid in suffix (migration target for 008, source for 013)
|
|
182
|
+
const legacy = filename.match(/^(.+)\.metadata~([a-z0-9_]+)\.json$/i);
|
|
183
|
+
if (legacy) return { naturalBase: legacy[1], uid: legacy[2] };
|
|
184
|
+
|
|
185
|
+
// New format: name.metadata.json
|
|
186
|
+
const m = filename.match(/^(.+)\.metadata\.json$/i);
|
|
187
|
+
return m ? { naturalBase: m[1] } : null;
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
/**
|
|
@@ -181,11 +196,11 @@ export function parseMetaFilename(filename) {
|
|
|
181
196
|
*/
|
|
182
197
|
export function detectLegacyTildeMetadata(filename) {
|
|
183
198
|
// Case 1: name~uid.metadata.json (content/entity metadata)
|
|
184
|
-
const m1 = filename.match(/^(.+)~([a-z0-
|
|
199
|
+
const m1 = filename.match(/^(.+)~([a-z0-9_]{10,})\.metadata\.json$/);
|
|
185
200
|
if (m1) return { naturalBase: m1[1], uid: m1[2] };
|
|
186
201
|
|
|
187
202
|
// Case 2: name~uid.ext.metadata.json (media metadata)
|
|
188
|
-
const m2 = filename.match(/^(.+)~([a-z0-
|
|
203
|
+
const m2 = filename.match(/^(.+)~([a-z0-9_]{10,})\.([a-z0-9]+)\.metadata\.json$/);
|
|
189
204
|
if (m2) return { naturalBase: `${m2[1]}.${m2[3]}`, uid: m2[2] };
|
|
190
205
|
|
|
191
206
|
return null;
|
|
@@ -210,7 +225,7 @@ export async function findMetadataForCompanion(companionPath) {
|
|
|
210
225
|
let entries;
|
|
211
226
|
try { entries = await readdir(dir); } catch { return null; }
|
|
212
227
|
|
|
213
|
-
// 1. Fast path: match naturalBase of
|
|
228
|
+
// 1. Fast path: match naturalBase of metadata files
|
|
214
229
|
for (const entry of entries) {
|
|
215
230
|
const parsed = parseMetaFilename(entry);
|
|
216
231
|
if (parsed && (parsed.naturalBase === base || parsed.naturalBase === companionName)) {
|
|
@@ -218,7 +233,7 @@ export async function findMetadataForCompanion(companionPath) {
|
|
|
218
233
|
}
|
|
219
234
|
}
|
|
220
235
|
|
|
221
|
-
// 2. Scan all metadata files
|
|
236
|
+
// 2. Scan all metadata files for @reference match
|
|
222
237
|
for (const entry of entries) {
|
|
223
238
|
if (!isMetadataFile(entry)) continue;
|
|
224
239
|
const metaPath = join(dir, entry);
|
|
@@ -241,8 +256,10 @@ export async function findMetadataForCompanion(companionPath) {
|
|
|
241
256
|
}
|
|
242
257
|
|
|
243
258
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
259
|
+
* Update the UID field in a metadata file in-place.
|
|
260
|
+
* Previously renamed the file to include ~uid in the name; now the UID lives
|
|
261
|
+
* only inside the JSON content, so no rename is needed.
|
|
262
|
+
*
|
|
246
263
|
* Restores file timestamps from _LastUpdated.
|
|
247
264
|
*
|
|
248
265
|
* @param {Object} meta - Current metadata object
|
|
@@ -250,57 +267,28 @@ export async function findMetadataForCompanion(companionPath) {
|
|
|
250
267
|
* @param {string} uid - Newly assigned UID from server
|
|
251
268
|
* @param {string} lastUpdated - Server _LastUpdated value
|
|
252
269
|
* @param {string} serverTz - Timezone for timestamp conversion
|
|
253
|
-
* @returns {Promise<{ newMetaPath: string, newFilePath:
|
|
270
|
+
* @returns {Promise<{ newMetaPath: string, newFilePath: null, updatedMeta: Object }>}
|
|
254
271
|
*/
|
|
255
272
|
export async function renameToUidConvention(meta, metaPath, uid, lastUpdated, serverTz) {
|
|
256
|
-
const metaDir = dirname(metaPath);
|
|
257
|
-
const metaFilename = basename(metaPath);
|
|
258
|
-
|
|
259
|
-
// Already in new format — nothing to do
|
|
260
|
-
if (parseMetaFilename(metaFilename)?.uid === uid) {
|
|
261
|
-
return { newMetaPath: metaPath, newFilePath: null, updatedMeta: meta };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Determine naturalBase from the temp/old metadata filename
|
|
265
|
-
// Temp format from adopt.js: "colors.metadata.json" → naturalBase = "colors"
|
|
266
|
-
// Old tilde format: "colors~uid.metadata.json" → naturalBase = "colors"
|
|
267
|
-
let naturalBase;
|
|
268
|
-
const legacyParsed = detectLegacyTildeMetadata(metaFilename);
|
|
269
|
-
if (legacyParsed) {
|
|
270
|
-
naturalBase = legacyParsed.naturalBase;
|
|
271
|
-
} else if (metaFilename.endsWith('.metadata.json')) {
|
|
272
|
-
naturalBase = metaFilename.slice(0, -'.metadata.json'.length);
|
|
273
|
-
} else {
|
|
274
|
-
naturalBase = basename(metaFilename, '.json');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const newMetaPath = join(metaDir, buildMetaFilename(naturalBase, uid));
|
|
278
|
-
|
|
279
|
-
// Update only the UID field; @reference values are UNCHANGED (companions keep natural names)
|
|
280
273
|
const updatedMeta = { ...meta, UID: uid };
|
|
281
274
|
|
|
282
|
-
// Write updated metadata to
|
|
283
|
-
await writeFile(
|
|
284
|
-
|
|
285
|
-
// Remove old metadata file (new content already written to newMetaPath above)
|
|
286
|
-
if (metaPath !== newMetaPath) {
|
|
287
|
-
try { await unlink(metaPath); } catch { /* old file already gone */ }
|
|
288
|
-
}
|
|
275
|
+
// Write updated metadata to same path (no rename — UID is in the JSON, not the filename)
|
|
276
|
+
await writeFile(metaPath, JSON.stringify(updatedMeta, null, 2) + '\n');
|
|
289
277
|
|
|
290
|
-
// Restore timestamps for metadata file and companions
|
|
278
|
+
// Restore timestamps for metadata file and companions
|
|
291
279
|
if (serverTz && lastUpdated) {
|
|
292
280
|
const { setFileTimestamps } = await import('./timestamps.js');
|
|
293
|
-
try { await setFileTimestamps(
|
|
281
|
+
try { await setFileTimestamps(metaPath, lastUpdated, lastUpdated, serverTz); } catch {}
|
|
294
282
|
const contentCols = [...(meta._companionReferenceColumns || meta._contentColumns || [])];
|
|
295
283
|
if (meta._mediaFile) contentCols.push('_mediaFile');
|
|
296
284
|
for (const col of contentCols) {
|
|
297
285
|
const ref = updatedMeta[col];
|
|
298
286
|
if (ref && String(ref).startsWith('@')) {
|
|
299
|
-
const fp = join(
|
|
287
|
+
const fp = join(dirname(metaPath), String(ref).substring(1));
|
|
300
288
|
try { await setFileTimestamps(fp, lastUpdated, lastUpdated, serverTz); } catch {}
|
|
301
289
|
}
|
|
302
290
|
}
|
|
303
291
|
}
|
|
304
292
|
|
|
305
|
-
return { newMetaPath, newFilePath: null, updatedMeta };
|
|
293
|
+
return { newMetaPath: metaPath, newFilePath: null, updatedMeta };
|
|
306
294
|
}
|
package/src/lib/ignore.js
CHANGED
package/src/lib/input-parser.js
CHANGED
|
@@ -37,12 +37,8 @@ export async function buildInputBody(dataExpressions, extraParams = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
for (const expr of dataExpressions) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (const op of ops) {
|
|
43
|
-
const encoded = await encodeInputExpression(op.trim());
|
|
44
|
-
parts.push(encoded);
|
|
45
|
-
}
|
|
40
|
+
const encoded = await encodeInputExpression(expr.trim());
|
|
41
|
+
parts.push(encoded);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
return parts.join('&');
|
package/src/lib/insert.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, readdir, writeFile, mkdir
|
|
1
|
+
import { readFile, readdir, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname, basename, extname, relative } from 'path';
|
|
3
3
|
import { DboClient } from './client.js';
|
|
4
4
|
import { buildInputBody, checkSubmitErrors } from './input-parser.js';
|
|
@@ -7,7 +7,7 @@ import { log } from './logger.js';
|
|
|
7
7
|
import { shouldSkipColumn } from './columns.js';
|
|
8
8
|
import { loadAppConfig, loadAppJsonBaseline, saveAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference, loadConfig } from './config.js';
|
|
9
9
|
import { loadMetadataSchema, saveMetadataSchema } from './metadata-schema.js';
|
|
10
|
-
import {
|
|
10
|
+
import { isMetadataFile } from './filenames.js';
|
|
11
11
|
import { setFileTimestamps } from './timestamps.js';
|
|
12
12
|
import { checkStoredTicket, clearGlobalTicket } from './ticketing.js';
|
|
13
13
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from './modify-key.js';
|
|
@@ -283,7 +283,8 @@ export async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
|
283
283
|
|
|
284
284
|
for (const [key, value] of Object.entries(meta)) {
|
|
285
285
|
if (shouldSkipColumn(key)) continue;
|
|
286
|
-
|
|
286
|
+
// UID is included only if the developer manually placed it in the metadata file.
|
|
287
|
+
// The CLI never auto-generates UIDs — the server assigns them on insert.
|
|
287
288
|
if (value === null || value === undefined) continue;
|
|
288
289
|
|
|
289
290
|
const strValue = String(value);
|
|
@@ -366,51 +367,35 @@ export async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
|
366
367
|
}
|
|
367
368
|
|
|
368
369
|
if (returnedUID) {
|
|
369
|
-
//
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
log.dim(` Renamed metadata: ${basename(metaPath)} → ${basename(newMetaPath)}`);
|
|
388
|
-
log.success(`UID assigned: ${returnedUID}`);
|
|
389
|
-
|
|
390
|
-
// Restore timestamps for metadata and companion files
|
|
391
|
-
const config = await loadConfig();
|
|
392
|
-
const serverTz = config.ServerTimezone;
|
|
393
|
-
if (serverTz && returnedLastUpdated) {
|
|
394
|
-
try {
|
|
395
|
-
await setFileTimestamps(newMetaPath, returnedLastUpdated, returnedLastUpdated, serverTz);
|
|
396
|
-
for (const col of (meta._companionReferenceColumns || meta._contentColumns || [])) {
|
|
397
|
-
const ref = meta[col];
|
|
398
|
-
if (ref && String(ref).startsWith('@')) {
|
|
399
|
-
const fp = join(metaDir, String(ref).substring(1));
|
|
400
|
-
await upsertDeployEntry(fp, returnedUID, entity, col);
|
|
401
|
-
try { await setFileTimestamps(fp, returnedLastUpdated, returnedLastUpdated, serverTz); } catch {}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
405
|
-
const mediaFp = join(metaDir, String(meta._mediaFile).substring(1));
|
|
406
|
-
await upsertDeployEntry(mediaFp, returnedUID, entity, 'File');
|
|
370
|
+
// UID lives in the JSON content — write it in-place, no filename rename needed
|
|
371
|
+
const metaDir = dirname(metaPath);
|
|
372
|
+
meta.UID = returnedUID;
|
|
373
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
374
|
+
log.success(`UID assigned: ${returnedUID}`);
|
|
375
|
+
|
|
376
|
+
// Restore timestamps for metadata and companion files
|
|
377
|
+
const config = await loadConfig();
|
|
378
|
+
const serverTz = config.ServerTimezone;
|
|
379
|
+
if (serverTz && returnedLastUpdated) {
|
|
380
|
+
try {
|
|
381
|
+
await setFileTimestamps(metaPath, returnedLastUpdated, returnedLastUpdated, serverTz);
|
|
382
|
+
for (const col of (meta._companionReferenceColumns || meta._contentColumns || [])) {
|
|
383
|
+
const ref = meta[col];
|
|
384
|
+
if (ref && String(ref).startsWith('@')) {
|
|
385
|
+
const fp = join(metaDir, String(ref).substring(1));
|
|
386
|
+
await upsertDeployEntry(fp, returnedUID, entity, col);
|
|
387
|
+
try { await setFileTimestamps(fp, returnedLastUpdated, returnedLastUpdated, serverTz); } catch {}
|
|
407
388
|
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
389
|
+
}
|
|
390
|
+
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
391
|
+
const mediaFp = join(metaDir, String(meta._mediaFile).substring(1));
|
|
392
|
+
await upsertDeployEntry(mediaFp, returnedUID, entity, 'File');
|
|
393
|
+
}
|
|
394
|
+
} catch { /* non-critical */ }
|
|
413
395
|
}
|
|
396
|
+
|
|
397
|
+
// Update .app_baseline.json so subsequent pull/diff recognize this record
|
|
398
|
+
await updateBaselineWithNewRecord(meta, returnedUID, returnedLastUpdated);
|
|
414
399
|
log.dim(` Run "dbo pull -e ${entity} ${returnedUID}" to populate all columns`);
|
|
415
400
|
}
|
|
416
401
|
}
|
|
@@ -497,8 +482,8 @@ export async function findUnaddedFiles(dir, ig, referencedFiles) {
|
|
|
497
482
|
const raw = await readFile(join(dir, entry.name), 'utf8');
|
|
498
483
|
if (!raw.trim()) continue;
|
|
499
484
|
const meta = JSON.parse(raw);
|
|
500
|
-
// Only count records that are on the server (have
|
|
501
|
-
if (!meta.
|
|
485
|
+
// Only count records that are on the server (have _CreatedOn or _LastUpdated)
|
|
486
|
+
if (!meta._CreatedOn && !meta._LastUpdated) continue;
|
|
502
487
|
collectCompanionRefs(meta, localRefs);
|
|
503
488
|
} catch { /* skip unreadable */ }
|
|
504
489
|
}
|
|
@@ -566,7 +551,7 @@ async function _scanMetadataRefs(dir, referenced) {
|
|
|
566
551
|
const raw = await readFile(fullPath, 'utf8');
|
|
567
552
|
if (!raw.trim()) continue;
|
|
568
553
|
const meta = JSON.parse(raw);
|
|
569
|
-
if (!meta._CreatedOn && !meta.
|
|
554
|
+
if (!meta._CreatedOn && !meta._LastUpdated) continue; // only count server-confirmed records
|
|
570
555
|
|
|
571
556
|
// Collect all @ references (including from inline output children)
|
|
572
557
|
const allRefs = new Set();
|
package/src/lib/structure.js
CHANGED
|
@@ -16,7 +16,7 @@ export const BINS_DIR = 'lib/bins';
|
|
|
16
16
|
export const SCAFFOLD_DIRS = [
|
|
17
17
|
'lib/bins',
|
|
18
18
|
'lib/app_version',
|
|
19
|
-
'lib/
|
|
19
|
+
'lib/data_source',
|
|
20
20
|
'lib/extension',
|
|
21
21
|
'lib/security',
|
|
22
22
|
'src',
|
|
@@ -32,7 +32,6 @@ export const SCAFFOLD_DIRS = [
|
|
|
32
32
|
*/
|
|
33
33
|
export const ON_DEMAND_ENTITY_DIRS = new Set([
|
|
34
34
|
'automation',
|
|
35
|
-
'data_source',
|
|
36
35
|
'entity_column',
|
|
37
36
|
'entity_column_value',
|
|
38
37
|
'group',
|
|
@@ -48,7 +47,6 @@ export const ON_DEMAND_ENTITY_DIRS = new Set([
|
|
|
48
47
|
*/
|
|
49
48
|
export const DEFAULT_PROJECT_DIRS = [
|
|
50
49
|
...SCAFFOLD_DIRS,
|
|
51
|
-
'lib/data_source',
|
|
52
50
|
'lib/group',
|
|
53
51
|
'lib/redirect',
|
|
54
52
|
'lib/site',
|
|
@@ -87,16 +85,26 @@ export const ENTITY_DIR_NAMES = new Set([
|
|
|
87
85
|
'site',
|
|
88
86
|
]);
|
|
89
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Entity types that are co-located in another entity's directory.
|
|
90
|
+
* Maps entity name → directory name (both live under lib/<dirName>/).
|
|
91
|
+
*/
|
|
92
|
+
const ENTITY_DIR_REMAP = {
|
|
93
|
+
entity: 'data_source',
|
|
94
|
+
};
|
|
95
|
+
|
|
90
96
|
/**
|
|
91
97
|
* Resolve the local directory path for an entity-dir type.
|
|
92
|
-
*
|
|
98
|
+
* Most entities map to "lib/<entityName>"; remapped entities (e.g. "entity"
|
|
99
|
+
* → "lib/data_source") are defined in ENTITY_DIR_REMAP.
|
|
93
100
|
* Use this instead of bare entity name concatenation everywhere.
|
|
94
101
|
*
|
|
95
102
|
* @param {string} entityName - Entity key from ENTITY_DIR_NAMES (e.g. "extension")
|
|
96
103
|
* @returns {string} - Path relative to project root (e.g. "lib/extension")
|
|
97
104
|
*/
|
|
98
105
|
export function resolveEntityDirPath(entityName) {
|
|
99
|
-
|
|
106
|
+
const dirName = ENTITY_DIR_REMAP[entityName] ?? entityName;
|
|
107
|
+
return `${LIB_DIR}/${dirName}`;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
// ─── Core Asset Entity Classification ─────────────────────────────────────
|
|
@@ -421,7 +429,9 @@ export async function loadDescriptorMapping() {
|
|
|
421
429
|
|
|
422
430
|
/**
|
|
423
431
|
* Resolve the sub-directory path for a single extension record.
|
|
424
|
-
* Returns "extension/<MappedName>"
|
|
432
|
+
* Returns "lib/extension/<MappedName>" for mapped descriptors,
|
|
433
|
+
* "lib/extension/<descriptor>" for unmapped descriptors with a value,
|
|
434
|
+
* or "lib/extension" for records with no descriptor.
|
|
425
435
|
*
|
|
426
436
|
* @param {Object} record - Extension record with a .Descriptor field
|
|
427
437
|
* @param {Object} mapping - descriptor key → dir name (from buildDescriptorMapping)
|
|
@@ -429,8 +439,13 @@ export async function loadDescriptorMapping() {
|
|
|
429
439
|
*/
|
|
430
440
|
export function resolveExtensionSubDir(record, mapping) {
|
|
431
441
|
const descriptor = record.Descriptor;
|
|
432
|
-
if (!descriptor
|
|
442
|
+
if (!descriptor) {
|
|
433
443
|
return EXTENSION_DESCRIPTORS_DIR;
|
|
434
444
|
}
|
|
435
|
-
|
|
445
|
+
if (mapping[descriptor]) {
|
|
446
|
+
return `${EXTENSION_DESCRIPTORS_DIR}/${mapping[descriptor]}`;
|
|
447
|
+
}
|
|
448
|
+
// Unmapped descriptor (no descriptor_definition record): use the raw descriptor
|
|
449
|
+
// value as the directory name — matches the directory created by buildDescriptorPrePass.
|
|
450
|
+
return `${EXTENSION_DESCRIPTORS_DIR}/${descriptor}`;
|
|
436
451
|
}
|
package/src/lib/ticketing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
|
-
import { projectDir } from './config.js';
|
|
4
|
+
import { projectDir, saveRepositoryIntegrationID, loadRepositoryIntegrationID } from './config.js';
|
|
5
5
|
|
|
6
6
|
const TICKETING_FILE = 'ticketing.local.json';
|
|
7
7
|
|
|
@@ -283,13 +283,14 @@ export async function checkStoredTicket(options, context = '') {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
/**
|
|
286
|
-
*
|
|
287
|
-
* Checks per-record ticket first, then global ticket.
|
|
286
|
+
* Look up and return the active stored ticket for a submission, if no --ticket flag is set.
|
|
287
|
+
* Checks per-record ticket first, then global ticket. Callers pass the returned value
|
|
288
|
+
* to _OverrideTicketID in extraParams.
|
|
288
289
|
*
|
|
289
|
-
* @param {string[]} dataExprs -
|
|
290
|
-
* @param {string} entity -
|
|
291
|
-
* @param {string|number} rowId -
|
|
292
|
-
* @param {string} uid - Record UID for per-record lookup
|
|
290
|
+
* @param {string[]} dataExprs - Unused; kept for backwards compatibility
|
|
291
|
+
* @param {string} entity - Unused; kept for backwards compatibility
|
|
292
|
+
* @param {string|number} rowId - Unused; kept for backwards compatibility
|
|
293
|
+
* @param {string} uid - Record UID for per-record ticket lookup
|
|
293
294
|
* @param {Object} options - Command options
|
|
294
295
|
* @param {string|null} [sessionOverride] - One-time ticket override from pre-flight prompt
|
|
295
296
|
*/
|
|
@@ -301,10 +302,66 @@ export async function applyStoredTicketToSubmission(dataExprs, entity, rowId, ui
|
|
|
301
302
|
const ticketToUse = sessionOverride || recordTicket || globalTicket;
|
|
302
303
|
|
|
303
304
|
if (ticketToUse) {
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
// Don't add _LastUpdatedTicketID to dataExprs — callers set _OverrideTicketID in extraParams,
|
|
306
|
+
// which is sufficient and avoids server-side RowUID lookup failures on some entities (e.g. extension).
|
|
306
307
|
log.dim(` Applying ticket: ${ticketToUse}`);
|
|
307
308
|
return ticketToUse;
|
|
308
309
|
}
|
|
309
310
|
return null;
|
|
310
311
|
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Fetch the app object from the server and cache RepositoryIntegrationID in .app/config.json.
|
|
315
|
+
*
|
|
316
|
+
* Uses UpdatedAfter=<today> to keep the response small — children records are filtered
|
|
317
|
+
* but the top-level app record (including RepositoryIntegrationID) is always returned.
|
|
318
|
+
*
|
|
319
|
+
* @param {DboClient} client
|
|
320
|
+
* @param {string} appShortName
|
|
321
|
+
* @returns {Promise<{ id: string|null, fetched: boolean }>}
|
|
322
|
+
* id: the RepositoryIntegrationID value (or null if not set)
|
|
323
|
+
* fetched: true if the server responded successfully, false on network/parse failure
|
|
324
|
+
*/
|
|
325
|
+
export async function fetchAndCacheRepositoryIntegration(client, appShortName) {
|
|
326
|
+
if (!appShortName) return { id: null, fetched: false };
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const today = new Date().toISOString().substring(0, 10); // YYYY-MM-DD
|
|
330
|
+
const result = await client.get(
|
|
331
|
+
`/api/app/object/${encodeURIComponent(appShortName)}`,
|
|
332
|
+
{ UpdatedAfter: today }
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (!result.ok && !result.successful) return { id: null, fetched: false };
|
|
336
|
+
|
|
337
|
+
const data = result.payload || result.data;
|
|
338
|
+
if (!data) return { id: null, fetched: false };
|
|
339
|
+
|
|
340
|
+
// Normalize response shape (array, Rows wrapper, or direct object)
|
|
341
|
+
let appRecord;
|
|
342
|
+
if (Array.isArray(data)) {
|
|
343
|
+
appRecord = data.length > 0 ? data[0] : null;
|
|
344
|
+
} else if (data?.Rows?.length > 0) {
|
|
345
|
+
appRecord = data.Rows[0];
|
|
346
|
+
} else if (data?.rows?.length > 0) {
|
|
347
|
+
appRecord = data.rows[0];
|
|
348
|
+
} else if (data && typeof data === 'object' && (data.UID || data.ShortName)) {
|
|
349
|
+
appRecord = data;
|
|
350
|
+
} else {
|
|
351
|
+
return { id: null, fetched: false };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!appRecord) return { id: null, fetched: false };
|
|
355
|
+
|
|
356
|
+
const id = (appRecord.RepositoryIntegrationID != null && appRecord.RepositoryIntegrationID !== '')
|
|
357
|
+
? appRecord.RepositoryIntegrationID
|
|
358
|
+
: null;
|
|
359
|
+
|
|
360
|
+
// Persist so push can fall back to this if the next fetch fails
|
|
361
|
+
await saveRepositoryIntegrationID(id);
|
|
362
|
+
|
|
363
|
+
return { id, fetched: true };
|
|
364
|
+
} catch {
|
|
365
|
+
return { id: null, fetched: false };
|
|
366
|
+
}
|
|
367
|
+
}
|