@doubledigit/cli 0.1.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/LICENSE +21 -0
- package/dist/codegen.d.ts +12 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +107 -0
- package/dist/commands/add.d.ts +26 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +548 -0
- package/dist/commands/browse.d.ts +8 -0
- package/dist/commands/browse.d.ts.map +1 -0
- package/dist/commands/browse.js +116 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +218 -0
- package/dist/commands/db.d.ts +2 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/db.js +64 -0
- package/dist/commands/disable.d.ts +5 -0
- package/dist/commands/disable.d.ts.map +1 -0
- package/dist/commands/disable.js +29 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +88 -0
- package/dist/commands/enable.d.ts +5 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/enable.js +29 -0
- package/dist/commands/info.d.ts +8 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/info.js +84 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +44 -0
- package/dist/commands/marketplace.d.ts +11 -0
- package/dist/commands/marketplace.d.ts.map +1 -0
- package/dist/commands/marketplace.js +205 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +58 -0
- package/dist/commands/outdated.d.ts +8 -0
- package/dist/commands/outdated.d.ts.map +1 -0
- package/dist/commands/outdated.js +107 -0
- package/dist/commands/reconcile.d.ts +12 -0
- package/dist/commands/reconcile.d.ts.map +1 -0
- package/dist/commands/reconcile.js +175 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +37 -0
- package/dist/commands/sync.d.ts +5 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +34 -0
- package/dist/commands/uninstall.d.ts +14 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +190 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +37 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +181 -0
- package/dist/lib/github-auth.d.ts +8 -0
- package/dist/lib/github-auth.d.ts.map +1 -0
- package/dist/lib/github-auth.js +30 -0
- package/dist/lib/lock-file.d.ts +67 -0
- package/dist/lib/lock-file.d.ts.map +1 -0
- package/dist/lib/lock-file.js +117 -0
- package/dist/lib/marketplace-schema.d.ts +607 -0
- package/dist/lib/marketplace-schema.d.ts.map +1 -0
- package/dist/lib/marketplace-schema.js +111 -0
- package/dist/lib/marketplace.d.ts +57 -0
- package/dist/lib/marketplace.d.ts.map +1 -0
- package/dist/lib/marketplace.js +270 -0
- package/dist/lib/onboarding.d.ts +84 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/lib/onboarding.js +1004 -0
- package/dist/lib/rewrite-extension-tsconfig.d.ts +22 -0
- package/dist/lib/rewrite-extension-tsconfig.d.ts.map +1 -0
- package/dist/lib/rewrite-extension-tsconfig.js +80 -0
- package/dist/lib/source-parser.d.ts +35 -0
- package/dist/lib/source-parser.d.ts.map +1 -0
- package/dist/lib/source-parser.js +121 -0
- package/dist/lib/validators.d.ts +73 -0
- package/dist/lib/validators.d.ts.map +1 -0
- package/dist/lib/validators.js +435 -0
- package/dist/paths.d.ts +46 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +85 -0
- package/dist/scanner.d.ts +41 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +100 -0
- package/package.json +49 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-install validation checks for `dd install`.
|
|
3
|
+
*
|
|
4
|
+
* Validates local directory conflicts, remote repository existence,
|
|
5
|
+
* downloaded package structure, and slug prefix collisions before
|
|
6
|
+
* (or just after) downloading from GitHub.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { resolveGitHubToken } from './github-auth.js';
|
|
11
|
+
function ok(warnings = []) {
|
|
12
|
+
return { valid: true, errors: [], warnings };
|
|
13
|
+
}
|
|
14
|
+
function fail(errors, warnings = []) {
|
|
15
|
+
return { valid: false, errors, warnings };
|
|
16
|
+
}
|
|
17
|
+
function merge(...results) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const warnings = [];
|
|
20
|
+
for (const r of results) {
|
|
21
|
+
errors.push(...r.errors);
|
|
22
|
+
warnings.push(...r.warnings);
|
|
23
|
+
}
|
|
24
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Local directory check
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Check if a local directory already exists for this app name across
|
|
31
|
+
* all known installation roots (packages/, extensions/micro-apps/,
|
|
32
|
+
* extensions/payload-plugins/).
|
|
33
|
+
*/
|
|
34
|
+
export function validateLocalDirectory(scanRoots, appName) {
|
|
35
|
+
for (const root of scanRoots) {
|
|
36
|
+
const targetDir = path.join(root, appName);
|
|
37
|
+
if (fs.existsSync(targetDir)) {
|
|
38
|
+
return fail([
|
|
39
|
+
`Directory already exists: ${targetDir}\n` +
|
|
40
|
+
'Use --force to overwrite or choose a different name.',
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return ok();
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Name collision check
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Check if an app with this name already exists in any of the
|
|
51
|
+
* installation roots.
|
|
52
|
+
*/
|
|
53
|
+
export function validateNoNameCollision(scanRoots, appName) {
|
|
54
|
+
for (const root of scanRoots) {
|
|
55
|
+
const targetDir = path.join(root, appName);
|
|
56
|
+
if (!fs.existsSync(targetDir))
|
|
57
|
+
continue;
|
|
58
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
59
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
60
|
+
return fail([
|
|
61
|
+
`Directory "${appName}" exists at ${targetDir} but has no package.json. ` +
|
|
62
|
+
'Remove it manually or choose a different name.',
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
67
|
+
const isDdApp = pkg.ddapp === true;
|
|
68
|
+
const isPayloadPlugin = pkg.ddPackageType === 'payload-plugin';
|
|
69
|
+
return fail([
|
|
70
|
+
`Package "${pkg.name || appName}" already exists at ${targetDir}.` +
|
|
71
|
+
(isDdApp || isPayloadPlugin
|
|
72
|
+
? ' It is a dd-managed extension — use `dd uninstall` first.'
|
|
73
|
+
: ' Choose a different name.'),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return fail([
|
|
78
|
+
`Directory "${appName}" exists at ${targetDir} and its package.json could not be parsed.`,
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return ok();
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Remote source validation
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
/**
|
|
88
|
+
* Validate that the remote GitHub repository exists and the path is accessible.
|
|
89
|
+
*/
|
|
90
|
+
export async function validateRemoteSource(source) {
|
|
91
|
+
const token = resolveGitHubToken();
|
|
92
|
+
const headers = {
|
|
93
|
+
Accept: 'application/vnd.github.v3+json',
|
|
94
|
+
'User-Agent': 'dd-cli',
|
|
95
|
+
};
|
|
96
|
+
if (token) {
|
|
97
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
98
|
+
}
|
|
99
|
+
// 1. Check the repository itself
|
|
100
|
+
const repoUrl = `https://api.github.com/repos/${source.owner}/${source.repo}`;
|
|
101
|
+
let repoRes;
|
|
102
|
+
try {
|
|
103
|
+
repoRes = await fetch(repoUrl, { headers });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return fail([
|
|
107
|
+
`Network error checking repository: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
if (repoRes.status === 404) {
|
|
111
|
+
return fail([
|
|
112
|
+
`Repository not found: ${source.owner}/${source.repo}\n` +
|
|
113
|
+
'Check the owner/repo spelling. If this is a private repo, set GITHUB_TOKEN or authenticate with `gh auth login`.',
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
if (repoRes.status === 401 || repoRes.status === 403) {
|
|
117
|
+
return fail([
|
|
118
|
+
'Authentication required for private repository.\n' +
|
|
119
|
+
'Set GITHUB_TOKEN or authenticate with `gh auth login` using an account that has repo access.',
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
if (!repoRes.ok) {
|
|
123
|
+
return fail([
|
|
124
|
+
`GitHub API error (${repoRes.status}) checking repository: ${source.owner}/${source.repo}`,
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
// 2. If a subdirectory is specified, verify it exists
|
|
128
|
+
if (source.subdir) {
|
|
129
|
+
const ref = source.ref ? `?ref=${encodeURIComponent(source.ref)}` : '';
|
|
130
|
+
const contentsUrl = `https://api.github.com/repos/${source.owner}/${source.repo}` +
|
|
131
|
+
`/contents/${source.subdir}${ref}`;
|
|
132
|
+
let contentsRes;
|
|
133
|
+
try {
|
|
134
|
+
contentsRes = await fetch(contentsUrl, { headers });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return fail([
|
|
138
|
+
`Network error checking path: ${err instanceof Error ? err.message : String(err)}`,
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
if (contentsRes.status === 404) {
|
|
142
|
+
const refHint = source.ref ? ` (ref: ${source.ref})` : '';
|
|
143
|
+
return fail([
|
|
144
|
+
`Path not found in repository: ${source.subdir}${refHint}\n` +
|
|
145
|
+
'Check that the subdirectory path exists and is spelled correctly.',
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
if (!contentsRes.ok) {
|
|
149
|
+
return fail([
|
|
150
|
+
`GitHub API error (${contentsRes.status}) checking path: ${source.subdir}`,
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return ok();
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Package kind detection
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Detect the extension kind from a downloaded package's package.json.
|
|
161
|
+
* Returns 'payload-plugin' when ddPackageType is set, otherwise 'micro-app'.
|
|
162
|
+
*/
|
|
163
|
+
export function detectPackageKind(packageDir) {
|
|
164
|
+
return readManagedPackageMetadata(packageDir).kind ?? 'micro-app';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read package.json metadata without importing the package.
|
|
168
|
+
* Returns whether the directory looks like a dd-managed extension.
|
|
169
|
+
*/
|
|
170
|
+
export function readManagedPackageMetadata(packageDir) {
|
|
171
|
+
const pkgJsonPath = path.join(packageDir, 'package.json');
|
|
172
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
173
|
+
return { isManagedExtension: false };
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
177
|
+
const kind = pkg.ddPackageType === 'payload-plugin'
|
|
178
|
+
? 'payload-plugin'
|
|
179
|
+
: pkg.ddapp === true
|
|
180
|
+
? 'micro-app'
|
|
181
|
+
: undefined;
|
|
182
|
+
return {
|
|
183
|
+
npmName: typeof pkg.name === 'string' ? pkg.name : undefined,
|
|
184
|
+
kind,
|
|
185
|
+
isManagedExtension: kind !== undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return { isManagedExtension: false };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Downloaded package validation
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
/**
|
|
196
|
+
* After download: validate the downloaded package has the correct structure.
|
|
197
|
+
* When kind is 'payload-plugin', checks for ddPackageType instead of ddapp.
|
|
198
|
+
*/
|
|
199
|
+
export function validateDownloadedPackage(packageDir, kind) {
|
|
200
|
+
const errors = [];
|
|
201
|
+
const warnings = [];
|
|
202
|
+
// 1. package.json must exist
|
|
203
|
+
const pkgJsonPath = path.join(packageDir, 'package.json');
|
|
204
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
205
|
+
return fail(['Downloaded package is missing package.json']);
|
|
206
|
+
}
|
|
207
|
+
let pkg;
|
|
208
|
+
try {
|
|
209
|
+
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return fail(['Could not parse package.json in downloaded package']);
|
|
213
|
+
}
|
|
214
|
+
// 2. Kind-specific identity check
|
|
215
|
+
const effectiveKind = kind ?? 'micro-app';
|
|
216
|
+
if (effectiveKind === 'payload-plugin') {
|
|
217
|
+
if (pkg.ddPackageType !== 'payload-plugin') {
|
|
218
|
+
errors.push('package.json is missing "ddPackageType": "payload-plugin" — this may not be a valid payload plugin');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
if (pkg.ddapp !== true) {
|
|
223
|
+
errors.push('package.json is missing "ddapp": true — this may not be a valid dd micro-app');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 3. Must have a name field
|
|
227
|
+
if (!pkg.name || typeof pkg.name !== 'string') {
|
|
228
|
+
errors.push('package.json is missing a "name" field');
|
|
229
|
+
}
|
|
230
|
+
// 4. Check entry point exists
|
|
231
|
+
const entryPoint = resolvePackageEntryPoint(packageDir, pkg);
|
|
232
|
+
if (!entryPoint) {
|
|
233
|
+
warnings.push('No entry point found (checked src/index.ts, src/index.tsx, exports["."], and main)');
|
|
234
|
+
}
|
|
235
|
+
return errors.length > 0 ? fail(errors, warnings) : ok(warnings);
|
|
236
|
+
}
|
|
237
|
+
export function resolvePackageEntryPoint(packageDir, pkg) {
|
|
238
|
+
// Check common entry points
|
|
239
|
+
const commonEntries = ['src/index.ts', 'src/index.tsx'];
|
|
240
|
+
for (const entry of commonEntries) {
|
|
241
|
+
if (fs.existsSync(path.join(packageDir, entry)))
|
|
242
|
+
return entry;
|
|
243
|
+
}
|
|
244
|
+
// Check exports["."] in package.json
|
|
245
|
+
const exports = pkg.exports;
|
|
246
|
+
if (exports && typeof exports === 'object' && '.' in exports) {
|
|
247
|
+
const dotExport = exports['.'];
|
|
248
|
+
const exportPath = typeof dotExport === 'string'
|
|
249
|
+
? dotExport
|
|
250
|
+
: typeof dotExport === 'object' && dotExport !== null
|
|
251
|
+
? dotExport.import ||
|
|
252
|
+
dotExport.default
|
|
253
|
+
: undefined;
|
|
254
|
+
if (typeof exportPath === 'string') {
|
|
255
|
+
const resolved = path.join(packageDir, exportPath);
|
|
256
|
+
if (fs.existsSync(resolved))
|
|
257
|
+
return exportPath;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const main = pkg.main;
|
|
261
|
+
if (typeof main === 'string') {
|
|
262
|
+
const resolved = path.join(packageDir, main);
|
|
263
|
+
if (fs.existsSync(resolved))
|
|
264
|
+
return main;
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const DDAPP_KEY_RE = /key:\s*['"]([^'"]+)['"]/;
|
|
269
|
+
const DDAPP_SLUG_RE = /slugPrefix:\s*['"]([^'"]+)['"]/;
|
|
270
|
+
function addCandidateEntry(candidates, packageDir, entry) {
|
|
271
|
+
if (typeof entry !== 'string' || entry.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
const normalizedEntry = entry.replace(/^\.\//, '');
|
|
274
|
+
if (fs.existsSync(path.join(packageDir, normalizedEntry))) {
|
|
275
|
+
candidates.add(normalizedEntry);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function getMicroAppMetadataCandidates(packageDir, pkg, preferredEntryPoint) {
|
|
279
|
+
const candidates = new Set();
|
|
280
|
+
addCandidateEntry(candidates, packageDir, preferredEntryPoint);
|
|
281
|
+
addCandidateEntry(candidates, packageDir, resolvePackageEntryPoint(packageDir, pkg));
|
|
282
|
+
for (const entry of [
|
|
283
|
+
'src/index.ts',
|
|
284
|
+
'src/index.tsx',
|
|
285
|
+
'src/micro-app/index.ts',
|
|
286
|
+
'src/micro-app/index.tsx',
|
|
287
|
+
]) {
|
|
288
|
+
addCandidateEntry(candidates, packageDir, entry);
|
|
289
|
+
}
|
|
290
|
+
const exports = pkg.exports;
|
|
291
|
+
if (exports && typeof exports === 'object' && '.' in exports) {
|
|
292
|
+
const dotExport = exports['.'];
|
|
293
|
+
if (typeof dotExport === 'string') {
|
|
294
|
+
addCandidateEntry(candidates, packageDir, dotExport);
|
|
295
|
+
}
|
|
296
|
+
else if (typeof dotExport === 'object' && dotExport !== null) {
|
|
297
|
+
addCandidateEntry(candidates, packageDir, dotExport.import);
|
|
298
|
+
addCandidateEntry(candidates, packageDir, dotExport.default);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
addCandidateEntry(candidates, packageDir, pkg.main);
|
|
302
|
+
return [...candidates];
|
|
303
|
+
}
|
|
304
|
+
export function extractMicroAppMetadata(packageDir, pkg, preferredEntryPoint) {
|
|
305
|
+
let key;
|
|
306
|
+
let slugPrefix;
|
|
307
|
+
for (const entry of getMicroAppMetadataCandidates(packageDir, pkg, preferredEntryPoint)) {
|
|
308
|
+
const entryPath = path.join(packageDir, entry);
|
|
309
|
+
try {
|
|
310
|
+
const content = fs.readFileSync(entryPath, 'utf-8');
|
|
311
|
+
if (!key) {
|
|
312
|
+
const keyMatch = content.match(DDAPP_KEY_RE);
|
|
313
|
+
if (keyMatch)
|
|
314
|
+
key = keyMatch[1];
|
|
315
|
+
}
|
|
316
|
+
if (!slugPrefix) {
|
|
317
|
+
const slugMatch = content.match(DDAPP_SLUG_RE);
|
|
318
|
+
if (slugMatch)
|
|
319
|
+
slugPrefix = slugMatch[1];
|
|
320
|
+
}
|
|
321
|
+
if (key && slugPrefix)
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Skip unreadable files
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { key, slugPrefix };
|
|
329
|
+
}
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Slug prefix collision check
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
/**
|
|
334
|
+
* Check slug prefix collisions with existing installed dd apps across
|
|
335
|
+
* all known package roots.
|
|
336
|
+
*
|
|
337
|
+
* Scans index files for `slugPrefix: '...'` patterns since we cannot
|
|
338
|
+
* import the modules at CLI time.
|
|
339
|
+
*/
|
|
340
|
+
export function validateSlugPrefix(scanRoots, slugPrefix, excludeKey) {
|
|
341
|
+
const collisions = [];
|
|
342
|
+
for (const root of scanRoots) {
|
|
343
|
+
if (!fs.existsSync(root))
|
|
344
|
+
continue;
|
|
345
|
+
let dirs;
|
|
346
|
+
try {
|
|
347
|
+
dirs = fs.readdirSync(root, { withFileTypes: true });
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
for (const dir of dirs) {
|
|
353
|
+
if (!dir.isDirectory())
|
|
354
|
+
continue;
|
|
355
|
+
if (excludeKey && dir.name === excludeKey)
|
|
356
|
+
continue;
|
|
357
|
+
const pkgJsonPath = path.join(root, dir.name, 'package.json');
|
|
358
|
+
if (!fs.existsSync(pkgJsonPath))
|
|
359
|
+
continue;
|
|
360
|
+
try {
|
|
361
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
362
|
+
if (pkg.ddapp !== true)
|
|
363
|
+
continue;
|
|
364
|
+
const metadata = extractMicroAppMetadata(path.join(root, dir.name), pkg);
|
|
365
|
+
if (metadata.slugPrefix === slugPrefix) {
|
|
366
|
+
collisions.push(dir.name);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (collisions.length > 0) {
|
|
375
|
+
return fail([
|
|
376
|
+
`Slug prefix "${slugPrefix}" is already used by: ${collisions.join(', ')}.\n` +
|
|
377
|
+
'Each micro-app must have a unique slug prefix to avoid collection collisions.',
|
|
378
|
+
]);
|
|
379
|
+
}
|
|
380
|
+
return ok();
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if another installed workspace package already uses the same npm name.
|
|
384
|
+
* This prevents installing the same payload plugin under multiple folder aliases.
|
|
385
|
+
* Scans all known package roots including extensions/.
|
|
386
|
+
*/
|
|
387
|
+
export function validateNoPackageNameCollision(scanRoots, npmName, excludeKey) {
|
|
388
|
+
for (const root of scanRoots) {
|
|
389
|
+
if (!fs.existsSync(root))
|
|
390
|
+
continue;
|
|
391
|
+
let dirs;
|
|
392
|
+
try {
|
|
393
|
+
dirs = fs.readdirSync(root, { withFileTypes: true });
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
for (const dir of dirs) {
|
|
399
|
+
if (!dir.isDirectory())
|
|
400
|
+
continue;
|
|
401
|
+
if (excludeKey && dir.name === excludeKey)
|
|
402
|
+
continue;
|
|
403
|
+
const pkgJsonPath = path.join(root, dir.name, 'package.json');
|
|
404
|
+
if (!fs.existsSync(pkgJsonPath))
|
|
405
|
+
continue;
|
|
406
|
+
try {
|
|
407
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
408
|
+
if (pkg.name === npmName) {
|
|
409
|
+
return fail([
|
|
410
|
+
`Package name collision: "${npmName}" is already installed at ${path.join(root, dir.name)}.\n` +
|
|
411
|
+
'Installing the same npm package under multiple folder names would corrupt dependency and TS path wiring.',
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return ok();
|
|
421
|
+
}
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Aggregate pre-install validation
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
/**
|
|
426
|
+
* Run all pre-install validations (local + remote) in one call.
|
|
427
|
+
*/
|
|
428
|
+
export async function runPreInstallValidation(scanRoots, appName, source) {
|
|
429
|
+
const local = merge(validateLocalDirectory(scanRoots, appName), validateNoNameCollision(scanRoots, appName));
|
|
430
|
+
// Short-circuit if local checks already failed
|
|
431
|
+
if (!local.valid)
|
|
432
|
+
return local;
|
|
433
|
+
const remote = await validateRemoteSource(source);
|
|
434
|
+
return merge(local, remote);
|
|
435
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve key workspace paths relative to the monorepo root.
|
|
3
|
+
*
|
|
4
|
+
* The CLI can be run from anywhere inside the monorepo.
|
|
5
|
+
* We walk upward from cwd until we find pnpm-workspace.yaml.
|
|
6
|
+
*/
|
|
7
|
+
export interface WorkspacePaths {
|
|
8
|
+
/** Monorepo root (contains pnpm-workspace.yaml) */
|
|
9
|
+
root: string;
|
|
10
|
+
/** packages/ directory (internal framework + legacy extensions) */
|
|
11
|
+
packagesDir: string;
|
|
12
|
+
/** extensions/micro-apps/ directory */
|
|
13
|
+
extensionsMicroAppsDir: string;
|
|
14
|
+
/** extensions/payload-plugins/ directory */
|
|
15
|
+
extensionsPayloadPluginsDir: string;
|
|
16
|
+
/** apps/main-app/ directory */
|
|
17
|
+
mainAppDir: string;
|
|
18
|
+
/** dd-apps.config.json path */
|
|
19
|
+
configPath: string;
|
|
20
|
+
/** dd-apps.lock.json path */
|
|
21
|
+
lockFilePath: string;
|
|
22
|
+
/** Generated micro-apps.ts path */
|
|
23
|
+
microAppsOutputPath: string;
|
|
24
|
+
/** Generated payload-plugins.ts path */
|
|
25
|
+
payloadPluginsOutputPath: string;
|
|
26
|
+
/** main-app package.json */
|
|
27
|
+
mainAppPackageJsonPath: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function resolveWorkspacePaths(cwd?: string): WorkspacePaths;
|
|
30
|
+
/** Absolute install directory for an extension of the given kind. */
|
|
31
|
+
export declare function installDirForKind(paths: WorkspacePaths, kind: 'micro-app' | 'payload-plugin', name: string): string;
|
|
32
|
+
/** Root-relative install path for lock file entries and display. */
|
|
33
|
+
export declare function installRelPath(kind: 'micro-app' | 'payload-plugin', name: string): string;
|
|
34
|
+
/** All directories where an extension can be installed (for directory/name checks). */
|
|
35
|
+
export declare function allInstallRoots(paths: WorkspacePaths): string[];
|
|
36
|
+
/** All directories that may contain discoverable packages (for collision/scan checks). */
|
|
37
|
+
export declare function allScanRoots(paths: WorkspacePaths): string[];
|
|
38
|
+
/** Legacy install location used before the extensions/ migration. */
|
|
39
|
+
export declare function legacyInstallDir(paths: WorkspacePaths, name: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Ordered candidate install paths for resolving current and legacy installs.
|
|
42
|
+
* Preferred kind is probed first, followed by the other extension root and
|
|
43
|
+
* the pre-Phase-4 packages/<name> location.
|
|
44
|
+
*/
|
|
45
|
+
export declare function installProbePaths(paths: WorkspacePaths, name: string, preferredKind?: 'micro-app' | 'payload-plugin'): string[];
|
|
46
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,4CAA4C;IAC5C,2BAA2B,EAAE,MAAM,CAAC;IACpC,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wCAAwC;IACxC,wBAAwB,EAAE,MAAM,CAAC;IACjC,4BAA4B;IAC5B,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAgBD,wBAAgB,qBAAqB,CAAC,GAAG,SAAgB,GAAG,cAAc,CAoBzE;AAED,qEAAqE;AACrE,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,WAAW,GAAG,gBAAgB,EACpC,IAAI,EAAE,MAAM,GACX,MAAM,CAKR;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,GAAG,gBAAgB,EACpC,IAAI,EAAE,MAAM,GACX,MAAM,CAIR;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,EAAE,CAM/D;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,EAAE,CAO5D;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,MAAM,GACX,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,MAAM,EACZ,aAAa,CAAC,EAAE,WAAW,GAAG,gBAAgB,GAC7C,MAAM,EAAE,CAcV"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve key workspace paths relative to the monorepo root.
|
|
3
|
+
*
|
|
4
|
+
* The CLI can be run from anywhere inside the monorepo.
|
|
5
|
+
* We walk upward from cwd until we find pnpm-workspace.yaml.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
function findMonorepoRoot(startDir) {
|
|
10
|
+
let dir = startDir;
|
|
11
|
+
while (dir !== path.dirname(dir)) {
|
|
12
|
+
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
throw new Error('Could not find monorepo root (no pnpm-workspace.yaml found). ' +
|
|
18
|
+
'Make sure you are running this command from within the monorepo.');
|
|
19
|
+
}
|
|
20
|
+
export function resolveWorkspacePaths(cwd = process.cwd()) {
|
|
21
|
+
const root = findMonorepoRoot(cwd);
|
|
22
|
+
const mainAppDir = path.join(root, 'apps', 'main-app');
|
|
23
|
+
if (!fs.existsSync(mainAppDir)) {
|
|
24
|
+
throw new Error(`Main app not found at ${mainAppDir}`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
root,
|
|
28
|
+
packagesDir: path.join(root, 'packages'),
|
|
29
|
+
extensionsMicroAppsDir: path.join(root, 'extensions', 'micro-apps'),
|
|
30
|
+
extensionsPayloadPluginsDir: path.join(root, 'extensions', 'payload-plugins'),
|
|
31
|
+
mainAppDir,
|
|
32
|
+
configPath: path.join(mainAppDir, 'dd-apps.config.json'),
|
|
33
|
+
lockFilePath: path.join(root, 'dd-apps.lock.json'),
|
|
34
|
+
microAppsOutputPath: path.join(mainAppDir, 'src', 'lib', 'micro-apps.ts'),
|
|
35
|
+
payloadPluginsOutputPath: path.join(mainAppDir, 'src', 'lib', 'payload-plugins.ts'),
|
|
36
|
+
mainAppPackageJsonPath: path.join(mainAppDir, 'package.json'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Absolute install directory for an extension of the given kind. */
|
|
40
|
+
export function installDirForKind(paths, kind, name) {
|
|
41
|
+
const base = kind === 'payload-plugin'
|
|
42
|
+
? paths.extensionsPayloadPluginsDir
|
|
43
|
+
: paths.extensionsMicroAppsDir;
|
|
44
|
+
return path.join(base, name);
|
|
45
|
+
}
|
|
46
|
+
/** Root-relative install path for lock file entries and display. */
|
|
47
|
+
export function installRelPath(kind, name) {
|
|
48
|
+
return kind === 'payload-plugin'
|
|
49
|
+
? `extensions/payload-plugins/${name}`
|
|
50
|
+
: `extensions/micro-apps/${name}`;
|
|
51
|
+
}
|
|
52
|
+
/** All directories where an extension can be installed (for directory/name checks). */
|
|
53
|
+
export function allInstallRoots(paths) {
|
|
54
|
+
return [
|
|
55
|
+
paths.packagesDir,
|
|
56
|
+
paths.extensionsMicroAppsDir,
|
|
57
|
+
paths.extensionsPayloadPluginsDir,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
/** All directories that may contain discoverable packages (for collision/scan checks). */
|
|
61
|
+
export function allScanRoots(paths) {
|
|
62
|
+
return [
|
|
63
|
+
paths.packagesDir,
|
|
64
|
+
path.join(paths.packagesDir, 'adapters'),
|
|
65
|
+
paths.extensionsMicroAppsDir,
|
|
66
|
+
paths.extensionsPayloadPluginsDir,
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
/** Legacy install location used before the extensions/ migration. */
|
|
70
|
+
export function legacyInstallDir(paths, name) {
|
|
71
|
+
return path.join(paths.packagesDir, name);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Ordered candidate install paths for resolving current and legacy installs.
|
|
75
|
+
* Preferred kind is probed first, followed by the other extension root and
|
|
76
|
+
* the pre-Phase-4 packages/<name> location.
|
|
77
|
+
*/
|
|
78
|
+
export function installProbePaths(paths, name, preferredKind) {
|
|
79
|
+
const candidates = [];
|
|
80
|
+
if (preferredKind) {
|
|
81
|
+
candidates.push(installDirForKind(paths, preferredKind, name));
|
|
82
|
+
}
|
|
83
|
+
candidates.push(installDirForKind(paths, 'micro-app', name), installDirForKind(paths, 'payload-plugin', name), legacyInstallDir(paths, name));
|
|
84
|
+
return [...new Set(candidates)];
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan workspace packages/ and extensions/ directories for micro-apps
|
|
3
|
+
* (ddapp: true) and standalone payload plugins (ddPackageType: 'payload-plugin').
|
|
4
|
+
*/
|
|
5
|
+
import type { WorkspacePaths } from './paths.js';
|
|
6
|
+
export interface DiscoveredApp {
|
|
7
|
+
/** Folder name (= DDApp key) */
|
|
8
|
+
key: string;
|
|
9
|
+
/** NPM package name, e.g. @doubledigit/meal-planner */
|
|
10
|
+
npmName: string;
|
|
11
|
+
/** JS-safe import name, e.g. mealPlanner */
|
|
12
|
+
importName: string;
|
|
13
|
+
/** Absolute path to the package directory */
|
|
14
|
+
dir: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DiscoveredPayloadPlugin {
|
|
17
|
+
/** Folder name */
|
|
18
|
+
key: string;
|
|
19
|
+
/** NPM package name, e.g. @doubledigit/some-plugin */
|
|
20
|
+
npmName: string;
|
|
21
|
+
/** JS-safe import name, e.g. somePlugin */
|
|
22
|
+
importName: string;
|
|
23
|
+
/** Absolute path to the package directory */
|
|
24
|
+
dir: string;
|
|
25
|
+
}
|
|
26
|
+
/** Unified result from scanning the workspace. */
|
|
27
|
+
export interface ScanResult {
|
|
28
|
+
microApps: DiscoveredApp[];
|
|
29
|
+
payloadPlugins: DiscoveredPayloadPlugin[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scan packages/, packages/adapters/, extensions/micro-apps/, and
|
|
33
|
+
* extensions/payload-plugins/ for micro-apps and payload plugins.
|
|
34
|
+
*
|
|
35
|
+
* packages/ is scanned first so internal/legacy packages take precedence
|
|
36
|
+
* during the transition period.
|
|
37
|
+
*/
|
|
38
|
+
export declare function scanWorkspace(paths: WorkspacePaths): ScanResult;
|
|
39
|
+
/** @deprecated Use scanWorkspace(paths) instead — returns only micro-apps for backward compat. */
|
|
40
|
+
export declare function scanForDDApps(packagesDir: string): DiscoveredApp[];
|
|
41
|
+
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,uBAAuB;IACtC,kBAAkB;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,cAAc,EAAE,uBAAuB,EAAE,CAAC;CAC3C;AA6ED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CAgB/D;AAED,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CAMlE"}
|