@dboio/cli 0.19.4 → 0.20.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 +69 -16
- package/bin/dbo.js +5 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +6 -1
- package/plugins/claude/dbo/commands/dbo.md +39 -12
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +69 -16
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +162 -0
- 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 +69 -14
- package/src/commands/clone.js +451 -87
- 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 +203 -54
- package/src/commands/status.js +15 -7
- package/src/lib/config.js +137 -10
- package/src/lib/filenames.js +54 -66
- package/src/lib/ignore.js +3 -0
- package/src/lib/insert.js +29 -45
- package/src/lib/structure.js +23 -8
- package/src/lib/ticketing.js +9 -8
- 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/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';
|
|
@@ -366,51 +366,35 @@ export async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
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');
|
|
369
|
+
// UID lives in the JSON content — write it in-place, no filename rename needed
|
|
370
|
+
const metaDir = dirname(metaPath);
|
|
371
|
+
meta.UID = returnedUID;
|
|
372
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
373
|
+
log.success(`UID assigned: ${returnedUID}`);
|
|
374
|
+
|
|
375
|
+
// Restore timestamps for metadata and companion files
|
|
376
|
+
const config = await loadConfig();
|
|
377
|
+
const serverTz = config.ServerTimezone;
|
|
378
|
+
if (serverTz && returnedLastUpdated) {
|
|
379
|
+
try {
|
|
380
|
+
await setFileTimestamps(metaPath, returnedLastUpdated, returnedLastUpdated, serverTz);
|
|
381
|
+
for (const col of (meta._companionReferenceColumns || meta._contentColumns || [])) {
|
|
382
|
+
const ref = meta[col];
|
|
383
|
+
if (ref && String(ref).startsWith('@')) {
|
|
384
|
+
const fp = join(metaDir, String(ref).substring(1));
|
|
385
|
+
await upsertDeployEntry(fp, returnedUID, entity, col);
|
|
386
|
+
try { await setFileTimestamps(fp, returnedLastUpdated, returnedLastUpdated, serverTz); } catch {}
|
|
407
387
|
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
388
|
+
}
|
|
389
|
+
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
390
|
+
const mediaFp = join(metaDir, String(meta._mediaFile).substring(1));
|
|
391
|
+
await upsertDeployEntry(mediaFp, returnedUID, entity, 'File');
|
|
392
|
+
}
|
|
393
|
+
} catch { /* non-critical */ }
|
|
413
394
|
}
|
|
395
|
+
|
|
396
|
+
// Update .app_baseline.json so subsequent pull/diff recognize this record
|
|
397
|
+
await updateBaselineWithNewRecord(meta, returnedUID, returnedLastUpdated);
|
|
414
398
|
log.dim(` Run "dbo pull -e ${entity} ${returnedUID}" to populate all columns`);
|
|
415
399
|
}
|
|
416
400
|
}
|
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
|
@@ -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,8 +302,8 @@ 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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, rename, access } from 'fs/promises';
|
|
2
2
|
import { join, basename, dirname } from 'path';
|
|
3
3
|
import { log } from '../lib/logger.js';
|
|
4
|
-
import { detectLegacyTildeMetadata
|
|
4
|
+
import { detectLegacyTildeMetadata } from '../lib/filenames.js';
|
|
5
5
|
|
|
6
6
|
export const description = 'Rename metadata files from name~uid.metadata.json to name.metadata~uid.json';
|
|
7
7
|
|
|
@@ -32,7 +32,9 @@ export default async function run(_options) {
|
|
|
32
32
|
if (!parsed) continue;
|
|
33
33
|
|
|
34
34
|
const { naturalBase, uid } = parsed;
|
|
35
|
-
|
|
35
|
+
// Migration 008 target is the intermediate name.metadata~uid.json format;
|
|
36
|
+
// migration 013 will later remove the uid suffix entirely.
|
|
37
|
+
const newFilename = `${naturalBase}.metadata~${uid}.json`;
|
|
36
38
|
const newPath = join(dir, newFilename);
|
|
37
39
|
|
|
38
40
|
// Skip if target already exists (avoid overwrite)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { readdir, readFile, rename, unlink, access } from 'fs/promises';
|
|
2
2
|
import { join, basename, dirname, extname } from 'path';
|
|
3
3
|
import { log } from '../lib/logger.js';
|
|
4
|
-
import { parseMetaFilename
|
|
4
|
+
import { parseMetaFilename } from '../lib/filenames.js';
|
|
5
|
+
|
|
6
|
+
// Build the intermediate name.metadata~uid.json format this migration targets.
|
|
7
|
+
// (Migration 013 will later strip the uid from filenames entirely.)
|
|
8
|
+
function buildLegacySuffixFilename(naturalBase, uid) {
|
|
9
|
+
return `${naturalBase}.metadata~${uid}.json`;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
export const description = 'Fix media collision suffix: rename (media) → _media and fix mismatched metadata filenames';
|
|
7
13
|
|
|
@@ -87,7 +93,7 @@ export default async function run(_options) {
|
|
|
87
93
|
// Rename metadata file itself if it contains (media)
|
|
88
94
|
if (parsed.naturalBase.includes('(media)')) {
|
|
89
95
|
const newBase = parsed.naturalBase.replace('(media)', '_media');
|
|
90
|
-
const newMetaFilename =
|
|
96
|
+
const newMetaFilename = buildLegacySuffixFilename(newBase, parsed.uid);
|
|
91
97
|
const newMetaPath = join(dirname(metaPath), newMetaFilename);
|
|
92
98
|
try { await access(newMetaPath); } catch {
|
|
93
99
|
await rename(metaPath, newMetaPath);
|
|
@@ -112,7 +118,7 @@ export default async function run(_options) {
|
|
|
112
118
|
// Already correct
|
|
113
119
|
if (currentParsed.naturalBase === refFilename) continue;
|
|
114
120
|
|
|
115
|
-
const correctFilename =
|
|
121
|
+
const correctFilename = buildLegacySuffixFilename(refFilename, currentParsed.uid);
|
|
116
122
|
const correctPath = join(dirname(metaPath), correctFilename);
|
|
117
123
|
|
|
118
124
|
// If correct metadata already exists, this one is an orphan
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFile, writeFile, rename, readdir, access } from 'fs/promises';
|
|
2
|
+
import { join, basename, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export const description = 'Rename metadata files from name.metadata~uid.json to name.metadata.json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Migration 013 — Remove UID from metadata filenames.
|
|
8
|
+
*
|
|
9
|
+
* Old format: colors.metadata~abc123.json
|
|
10
|
+
* New format: colors.metadata.json
|
|
11
|
+
*
|
|
12
|
+
* The UID is already stored inside the JSON as the "UID" field — no information is lost.
|
|
13
|
+
*
|
|
14
|
+
* Collision resolution uses entity priority:
|
|
15
|
+
* content > output > everything else
|
|
16
|
+
*
|
|
17
|
+
* Within the same entity type, the first record (alphabetically by old filename) wins
|
|
18
|
+
* the unsuffixed slot; subsequent ones get -1, -2, etc.
|
|
19
|
+
*
|
|
20
|
+
* Does NOT rename companion files (they use natural names already).
|
|
21
|
+
*/
|
|
22
|
+
export default async function run(_options) {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
let totalRenamed = 0;
|
|
25
|
+
|
|
26
|
+
const legacyFiles = await findLegacySuffixMetadataFiles(cwd);
|
|
27
|
+
if (legacyFiles.length === 0) return;
|
|
28
|
+
|
|
29
|
+
// Group by directory so we can resolve collisions per-dir
|
|
30
|
+
const byDir = new Map();
|
|
31
|
+
for (const filePath of legacyFiles) {
|
|
32
|
+
const dir = dirname(filePath);
|
|
33
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
34
|
+
byDir.get(dir).push(filePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [dir, files] of byDir) {
|
|
38
|
+
// Read entity type from each file to apply priority ordering
|
|
39
|
+
const withMeta = [];
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
let entity = 'other';
|
|
42
|
+
let uid = null;
|
|
43
|
+
try {
|
|
44
|
+
const content = JSON.parse(await readFile(filePath, 'utf8'));
|
|
45
|
+
entity = content._entity || 'other';
|
|
46
|
+
uid = content.UID || null;
|
|
47
|
+
} catch { /* use defaults */ }
|
|
48
|
+
|
|
49
|
+
// Parse naturalBase from legacy filename: name.metadata~uid.json
|
|
50
|
+
const filename = basename(filePath);
|
|
51
|
+
const m = filename.match(/^(.+)\.metadata~([a-z0-9_]+)\.json$/i);
|
|
52
|
+
const naturalBase = m ? m[1] : filename.replace(/\.metadata~[^.]+\.json$/i, '');
|
|
53
|
+
|
|
54
|
+
withMeta.push({ filePath, naturalBase, entity, uid });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort by entity priority: content first, output second, then others (alphabetically within tier)
|
|
58
|
+
const PRIORITY = { content: 0, output: 1 };
|
|
59
|
+
withMeta.sort((a, b) => {
|
|
60
|
+
const pa = PRIORITY[a.entity] ?? 2;
|
|
61
|
+
const pb = PRIORITY[b.entity] ?? 2;
|
|
62
|
+
if (pa !== pb) return pa - pb;
|
|
63
|
+
return a.naturalBase.localeCompare(b.naturalBase);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Assign new filenames with collision resolution
|
|
67
|
+
const usedBases = new Map(); // naturalBase (lowercase) → count of times used
|
|
68
|
+
|
|
69
|
+
for (const { filePath, naturalBase, entity } of withMeta) {
|
|
70
|
+
const baseKey = naturalBase.toLowerCase();
|
|
71
|
+
const count = usedBases.get(baseKey) || 0;
|
|
72
|
+
usedBases.set(baseKey, count + 1);
|
|
73
|
+
|
|
74
|
+
const newFilename = count === 0
|
|
75
|
+
? `${naturalBase}.metadata.json`
|
|
76
|
+
: `${naturalBase}-${count}.metadata.json`;
|
|
77
|
+
|
|
78
|
+
const newPath = join(dir, newFilename);
|
|
79
|
+
|
|
80
|
+
// Skip if target already exists (safe guard against re-running migration)
|
|
81
|
+
try { await access(newPath); continue; } catch { /* doesn't exist — safe to rename */ }
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await rename(filePath, newPath);
|
|
85
|
+
if (newFilename !== basename(filePath)) {
|
|
86
|
+
console.log(` [${entity}] ${basename(filePath)} → ${newFilename}`);
|
|
87
|
+
totalRenamed++;
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(` (skip) Could not rename ${basename(filePath)}: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (totalRenamed > 0) {
|
|
96
|
+
console.log(` Renamed ${totalRenamed} metadata file(s) — UID now stored in JSON only`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
101
|
+
|
|
102
|
+
async function findLegacySuffixMetadataFiles(dir) {
|
|
103
|
+
const results = [];
|
|
104
|
+
try {
|
|
105
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (SKIP.has(entry.name)) continue;
|
|
108
|
+
const full = join(dir, entry.name);
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
results.push(...await findLegacySuffixMetadataFiles(full));
|
|
111
|
+
} else if (/\.metadata~[a-z0-9_]+\.json$/i.test(entry.name)) {
|
|
112
|
+
results.push(full);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* skip unreadable dirs */ }
|
|
116
|
+
return results;
|
|
117
|
+
}
|