@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen.d.ts +12 -0
  3. package/dist/codegen.d.ts.map +1 -0
  4. package/dist/codegen.js +107 -0
  5. package/dist/commands/add.d.ts +26 -0
  6. package/dist/commands/add.d.ts.map +1 -0
  7. package/dist/commands/add.js +548 -0
  8. package/dist/commands/browse.d.ts +8 -0
  9. package/dist/commands/browse.d.ts.map +1 -0
  10. package/dist/commands/browse.js +116 -0
  11. package/dist/commands/create.d.ts +12 -0
  12. package/dist/commands/create.d.ts.map +1 -0
  13. package/dist/commands/create.js +218 -0
  14. package/dist/commands/db.d.ts +2 -0
  15. package/dist/commands/db.d.ts.map +1 -0
  16. package/dist/commands/db.js +64 -0
  17. package/dist/commands/disable.d.ts +5 -0
  18. package/dist/commands/disable.d.ts.map +1 -0
  19. package/dist/commands/disable.js +29 -0
  20. package/dist/commands/doctor.d.ts +2 -0
  21. package/dist/commands/doctor.d.ts.map +1 -0
  22. package/dist/commands/doctor.js +88 -0
  23. package/dist/commands/enable.d.ts +5 -0
  24. package/dist/commands/enable.d.ts.map +1 -0
  25. package/dist/commands/enable.js +29 -0
  26. package/dist/commands/info.d.ts +8 -0
  27. package/dist/commands/info.d.ts.map +1 -0
  28. package/dist/commands/info.js +84 -0
  29. package/dist/commands/list.d.ts +5 -0
  30. package/dist/commands/list.d.ts.map +1 -0
  31. package/dist/commands/list.js +44 -0
  32. package/dist/commands/marketplace.d.ts +11 -0
  33. package/dist/commands/marketplace.d.ts.map +1 -0
  34. package/dist/commands/marketplace.js +205 -0
  35. package/dist/commands/onboard.d.ts +2 -0
  36. package/dist/commands/onboard.d.ts.map +1 -0
  37. package/dist/commands/onboard.js +58 -0
  38. package/dist/commands/outdated.d.ts +8 -0
  39. package/dist/commands/outdated.d.ts.map +1 -0
  40. package/dist/commands/outdated.js +107 -0
  41. package/dist/commands/reconcile.d.ts +12 -0
  42. package/dist/commands/reconcile.d.ts.map +1 -0
  43. package/dist/commands/reconcile.js +175 -0
  44. package/dist/commands/run.d.ts +2 -0
  45. package/dist/commands/run.d.ts.map +1 -0
  46. package/dist/commands/run.js +37 -0
  47. package/dist/commands/sync.d.ts +5 -0
  48. package/dist/commands/sync.d.ts.map +1 -0
  49. package/dist/commands/sync.js +34 -0
  50. package/dist/commands/uninstall.d.ts +14 -0
  51. package/dist/commands/uninstall.d.ts.map +1 -0
  52. package/dist/commands/uninstall.js +190 -0
  53. package/dist/config.d.ts +19 -0
  54. package/dist/config.d.ts.map +1 -0
  55. package/dist/config.js +37 -0
  56. package/dist/index.d.ts +13 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +181 -0
  59. package/dist/lib/github-auth.d.ts +8 -0
  60. package/dist/lib/github-auth.d.ts.map +1 -0
  61. package/dist/lib/github-auth.js +30 -0
  62. package/dist/lib/lock-file.d.ts +67 -0
  63. package/dist/lib/lock-file.d.ts.map +1 -0
  64. package/dist/lib/lock-file.js +117 -0
  65. package/dist/lib/marketplace-schema.d.ts +607 -0
  66. package/dist/lib/marketplace-schema.d.ts.map +1 -0
  67. package/dist/lib/marketplace-schema.js +111 -0
  68. package/dist/lib/marketplace.d.ts +57 -0
  69. package/dist/lib/marketplace.d.ts.map +1 -0
  70. package/dist/lib/marketplace.js +270 -0
  71. package/dist/lib/onboarding.d.ts +84 -0
  72. package/dist/lib/onboarding.d.ts.map +1 -0
  73. package/dist/lib/onboarding.js +1004 -0
  74. package/dist/lib/rewrite-extension-tsconfig.d.ts +22 -0
  75. package/dist/lib/rewrite-extension-tsconfig.d.ts.map +1 -0
  76. package/dist/lib/rewrite-extension-tsconfig.js +80 -0
  77. package/dist/lib/source-parser.d.ts +35 -0
  78. package/dist/lib/source-parser.d.ts.map +1 -0
  79. package/dist/lib/source-parser.js +121 -0
  80. package/dist/lib/validators.d.ts +73 -0
  81. package/dist/lib/validators.d.ts.map +1 -0
  82. package/dist/lib/validators.js +435 -0
  83. package/dist/paths.d.ts +46 -0
  84. package/dist/paths.d.ts.map +1 -0
  85. package/dist/paths.js +85 -0
  86. package/dist/scanner.d.ts +41 -0
  87. package/dist/scanner.d.ts.map +1 -0
  88. package/dist/scanner.js +100 -0
  89. package/package.json +49 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * dd add <source> — Install a micro-app or payload plugin from GitHub or npm.
3
+ *
4
+ * Sources:
5
+ * gh:owner/repo/extensions/micro-apps/app GitHub micro-app subdirectory
6
+ * gh:owner/repo/extensions/micro-apps/app#v1.0.0 Specific tag/ref
7
+ * gh:owner/repo Full repo as micro-app
8
+ * github:owner/repo/path Alternative prefix
9
+ * https://github.com/owner/repo/... Full URL
10
+ * npm:@scope/package npm registry
11
+ * npm:@scope/package@1.2.3 Specific npm version
12
+ *
13
+ * Flow:
14
+ * 1. Parse source → determine type (github vs npm)
15
+ * 2. Pre-install validation
16
+ * 3. Download (giget for GitHub, pnpm for npm)
17
+ * 4. Post-download validation
18
+ * 5. Install to the appropriate extension root
19
+ * 6. Wire up (deps, TS paths, config, lock file)
20
+ * 7. Run sync + pnpm install
21
+ */
22
+ import fs from 'node:fs';
23
+ import os from 'node:os';
24
+ import path from 'node:path';
25
+ import { execSync } from 'node:child_process';
26
+ import { downloadTemplate } from 'giget';
27
+ import { resolveWorkspacePaths, allInstallRoots, allScanRoots, installDirForKind, installRelPath, installProbePaths, } from '../paths.js';
28
+ import { readConfig, writeConfig } from '../config.js';
29
+ import { sync } from './sync.js';
30
+ import { parseSource } from '../lib/source-parser.js';
31
+ import { addLockEntry, computeContentHash, } from '../lib/lock-file.js';
32
+ import { validateLocalDirectory, validateNoNameCollision, validateNoPackageNameCollision, validateRemoteSource, validateDownloadedPackage, resolvePackageEntryPoint, extractMicroAppMetadata, validateSlugPrefix, detectPackageKind, readManagedPackageMetadata, } from '../lib/validators.js';
33
+ import { resolveExtension, resolveExtensionFromMarketplace, extensionSourceToGhString, readKnownMarketplaces, } from '../lib/marketplace.js';
34
+ import { rewriteExtensionTsconfig } from '../lib/rewrite-extension-tsconfig.js';
35
+ import { resolveGitHubToken } from '../lib/github-auth.js';
36
+ // ---------------------------------------------------------------------------
37
+ // Helpers
38
+ // ---------------------------------------------------------------------------
39
+ /** Extract app name from the last path segment of a source. */
40
+ function extractAppName(source) {
41
+ if (source.subdir) {
42
+ const segments = source.subdir.split('/').filter(Boolean);
43
+ return segments[segments.length - 1];
44
+ }
45
+ return source.repo;
46
+ }
47
+ /** Check if a string looks like an npm source. */
48
+ function isNpmSource(raw) {
49
+ return raw.startsWith('npm:');
50
+ }
51
+ /** Parse npm source → { packageSpec, name }. Validates against shell injection. */
52
+ function parseNpmSource(raw) {
53
+ const spec = raw.slice(4); // strip 'npm:'
54
+ // Strict validation: only allow npm-safe characters (scope, name, version range)
55
+ if (!/^(@[\w.\-]+\/)?[\w.\-]+([@^~>=<\s\d.\-*|]+)?$/.test(spec)) {
56
+ throw new Error(`Invalid npm package specifier: "${spec}"\n` +
57
+ 'Expected format: @scope/name, @scope/name@version, or name@version');
58
+ }
59
+ // Extract short name from scoped package: @scope/foo@version → foo
60
+ const atIdx = spec.lastIndexOf('@');
61
+ const packageName = atIdx > 0 ? spec.slice(0, atIdx) : spec;
62
+ const shortName = packageName.includes('/')
63
+ ? packageName.split('/').pop()
64
+ : packageName;
65
+ return { packageSpec: spec, name: shortName };
66
+ }
67
+ /** Print validation results and exit on errors. */
68
+ function handleValidation(result, label) {
69
+ if (result.warnings.length > 0) {
70
+ for (const w of result.warnings) {
71
+ console.log(` ⚠ ${w}`);
72
+ }
73
+ }
74
+ if (!result.valid) {
75
+ for (const e of result.errors) {
76
+ console.error(` ✘ ${e}`);
77
+ }
78
+ console.error(`\nāŒ ${label} failed.`);
79
+ process.exit(1);
80
+ }
81
+ }
82
+ function findExistingInstalls(paths, name, preferredKind) {
83
+ return installProbePaths(paths, name, preferredKind)
84
+ .filter((dir) => fs.existsSync(dir))
85
+ .map((dir) => ({
86
+ dir,
87
+ ...readManagedPackageMetadata(dir),
88
+ }));
89
+ }
90
+ function snapshotFiles(filePaths) {
91
+ return filePaths.map((filePath) => ({
92
+ path: filePath,
93
+ existed: fs.existsSync(filePath),
94
+ content: fs.existsSync(filePath)
95
+ ? fs.readFileSync(filePath, 'utf-8')
96
+ : undefined,
97
+ }));
98
+ }
99
+ function restoreSnapshots(snapshots) {
100
+ for (const snapshot of snapshots) {
101
+ if (snapshot.existed) {
102
+ fs.mkdirSync(path.dirname(snapshot.path), { recursive: true });
103
+ fs.writeFileSync(snapshot.path, snapshot.content ?? '', 'utf-8');
104
+ }
105
+ else if (fs.existsSync(snapshot.path)) {
106
+ fs.rmSync(snapshot.path, { force: true });
107
+ }
108
+ }
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // npm source handler
112
+ // ---------------------------------------------------------------------------
113
+ async function addFromNpm(_packageSpec, _appName, _options) {
114
+ // npm sources install to node_modules, but the dd sync/codegen system
115
+ // only discovers extensions from workspace roots. Until
116
+ // node_modules-backed discovery is implemented, reject npm sources.
117
+ console.error('\nāŒ npm source is not yet supported.\n\n' +
118
+ ' npm packages install to node_modules/, but extension discovery\n' +
119
+ ' scans the workspace install roots. The app would install but never activate.\n\n' +
120
+ ' Use a GitHub source instead:\n' +
121
+ ' dd add gh:owner/repo/extensions/micro-apps/my-app\n\n' +
122
+ ' npm source support is planned for a future release.\n');
123
+ process.exit(1);
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // GitHub source handler (main flow)
127
+ // ---------------------------------------------------------------------------
128
+ async function addFromGitHub(raw, options, marketplaceCtx) {
129
+ const paths = resolveWorkspacePaths();
130
+ const source = parseSource(raw);
131
+ const appName = options?.name ?? extractAppName(source);
132
+ const preInstallLabel = marketplaceCtx?.kind === 'payload-plugin' ? 'payload plugin' : 'micro-app';
133
+ console.log(`\nšŸ“¦ Installing ${preInstallLabel} from ${source.displayString}\n`);
134
+ // -----------------------------------------------------------------------
135
+ // Pre-install validation
136
+ // -----------------------------------------------------------------------
137
+ console.log('šŸ” Pre-install validation...');
138
+ const installRoots = allInstallRoots(paths);
139
+ if (!options?.force) {
140
+ const localResult = validateLocalDirectory(installRoots, appName);
141
+ handleValidation(localResult, 'Local directory check');
142
+ console.log(' āœ” Local directory is available');
143
+ const collisionResult = validateNoNameCollision(installRoots, appName);
144
+ handleValidation(collisionResult, 'Name collision check');
145
+ console.log(' āœ” No name collision detected');
146
+ }
147
+ else {
148
+ console.log(' ⚠ Skipping local checks (--force)');
149
+ }
150
+ const remoteResult = await validateRemoteSource(source);
151
+ handleValidation(remoteResult, 'Remote source check');
152
+ console.log(' āœ” Remote repository verified');
153
+ console.log('');
154
+ // -----------------------------------------------------------------------
155
+ // Download to temp directory
156
+ // -----------------------------------------------------------------------
157
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-add-'));
158
+ try {
159
+ console.log('ā¬‡ļø Downloading from GitHub...');
160
+ const githubToken = resolveGitHubToken();
161
+ await downloadTemplate(source.templateString, {
162
+ dir: tempDir,
163
+ force: true,
164
+ ...(githubToken ? { auth: githubToken } : {}),
165
+ });
166
+ console.log(' āœ” Downloaded to temp directory');
167
+ console.log('');
168
+ // ---------------------------------------------------------------------
169
+ // Post-download validation
170
+ // ---------------------------------------------------------------------
171
+ console.log('šŸ” Post-download validation...');
172
+ // Detect package kind from downloaded source
173
+ const detectedKind = detectPackageKind(tempDir);
174
+ // Cross-check against marketplace metadata if present
175
+ if (marketplaceCtx?.kind && marketplaceCtx.kind !== detectedKind) {
176
+ console.error(`\nāŒ Kind mismatch: marketplace declares "${marketplaceCtx.kind}" ` +
177
+ `but downloaded package is "${detectedKind}".\n`);
178
+ process.exit(1);
179
+ }
180
+ const effectiveKind = detectedKind;
181
+ const isPayloadPlugin = effectiveKind === 'payload-plugin';
182
+ const downloadResult = validateDownloadedPackage(tempDir, effectiveKind);
183
+ handleValidation(downloadResult, 'Package validation');
184
+ console.log(isPayloadPlugin
185
+ ? ' āœ” Valid payload-plugin package structure'
186
+ : ' āœ” Valid DDApp package structure');
187
+ // Read package.json from downloaded source
188
+ const downloadedPkgPath = path.join(tempDir, 'package.json');
189
+ const downloadedPkg = JSON.parse(fs.readFileSync(downloadedPkgPath, 'utf-8'));
190
+ const detectedEntryPoint = resolvePackageEntryPoint(tempDir, downloadedPkg);
191
+ // Validate package.json contract (kind-specific)
192
+ const pkgErrors = [];
193
+ if (isPayloadPlugin) {
194
+ if (downloadedPkg.ddPackageType !== 'payload-plugin')
195
+ pkgErrors.push('"ddPackageType" must be "payload-plugin"');
196
+ }
197
+ else {
198
+ if (downloadedPkg.ddapp !== true)
199
+ pkgErrors.push('"ddapp" must be true');
200
+ }
201
+ if (!downloadedPkg.name || typeof downloadedPkg.name !== 'string')
202
+ pkgErrors.push('"name" is required');
203
+ if (!downloadedPkg.exports && !downloadedPkg.main)
204
+ pkgErrors.push('"exports" or "main" field is required');
205
+ if (pkgErrors.length > 0) {
206
+ for (const e of pkgErrors)
207
+ console.error(` ✘ ${e}`);
208
+ console.error(isPayloadPlugin
209
+ ? '\nāŒ Payload plugin package.json validation failed.'
210
+ : '\nāŒ DDApp package.json validation failed.');
211
+ process.exit(1);
212
+ }
213
+ console.log(isPayloadPlugin
214
+ ? ' āœ” Payload plugin package.json validated'
215
+ : ' āœ” DDApp package.json validated');
216
+ if (!detectedEntryPoint) {
217
+ console.error('\nāŒ Could not determine the package entry point for TS path mappings.\n' +
218
+ ' Define src/index.ts, src/index.tsx, exports["."], or main in package.json.\n');
219
+ process.exit(1);
220
+ }
221
+ // Micro-app only: validate DDApp key + slug prefix
222
+ if (!isPayloadPlugin) {
223
+ const { key: ddAppKey, slugPrefix: detectedSlugPrefix } = extractMicroAppMetadata(tempDir, downloadedPkg, detectedEntryPoint);
224
+ // Validate --name: the DDApp's internal key must match the install name.
225
+ // The key is the directory name which becomes the config key — mismatch
226
+ // causes runtime enable/disable to break silently.
227
+ if (options?.name && ddAppKey && ddAppKey !== options.name) {
228
+ console.error(`\nāŒ --name "${options.name}" conflicts with the micro-app's internal key "${ddAppKey}".\n` +
229
+ ` The DDApp key is baked into the source code and cannot be overridden.\n` +
230
+ ` Either install without --name, or use --name ${ddAppKey}\n`);
231
+ process.exit(1);
232
+ }
233
+ if (detectedSlugPrefix) {
234
+ const slugResult = validateSlugPrefix(allScanRoots(paths), detectedSlugPrefix, appName);
235
+ handleValidation(slugResult, 'Slug prefix check');
236
+ }
237
+ console.log(' āœ” No slug prefix collision');
238
+ }
239
+ console.log('');
240
+ // Determine npm name from downloaded package
241
+ const npmName = downloadedPkg.name || `@doubledigit/${appName}`;
242
+ const packageNameCollisionResult = validateNoPackageNameCollision(allScanRoots(paths), npmName, appName);
243
+ handleValidation(packageNameCollisionResult, 'Package name collision check');
244
+ console.log(' āœ” No package name collision detected');
245
+ const hasSrcDir = fs.existsSync(path.join(tempDir, 'src'));
246
+ // ---------------------------------------------------------------------
247
+ // Install: move to extensions/<kind>/<name>/ with rollback safety
248
+ // ---------------------------------------------------------------------
249
+ const targetDir = installDirForKind(paths, effectiveKind, appName);
250
+ const targetRelPath = installRelPath(effectiveKind, appName);
251
+ const baseTsConfigPath = path.join(paths.root, 'tsconfig.base.json');
252
+ const mainTsConfigPath = path.join(paths.mainAppDir, 'tsconfig.json');
253
+ console.log(`šŸ“ Installing to ${targetRelPath}...`);
254
+ const existingInstalls = findExistingInstalls(paths, appName, effectiveKind);
255
+ const unmanagedConflicts = existingInstalls.filter((install) => !install.isManagedExtension);
256
+ if (unmanagedConflicts.length > 0) {
257
+ const conflictList = unmanagedConflicts
258
+ .map((install) => path.relative(paths.root, install.dir))
259
+ .join(', ');
260
+ console.error('\nāŒ Existing non-extension package conflicts with this install.\n' +
261
+ ` Conflicting path(s): ${conflictList}\n` +
262
+ ' `dd add --force` only replaces dd-managed extensions; it will not overwrite internal packages.\n');
263
+ process.exit(1);
264
+ }
265
+ if (!options?.force && existingInstalls.length > 0) {
266
+ const conflictList = existingInstalls
267
+ .map((install) => path.relative(paths.root, install.dir))
268
+ .join(', ');
269
+ console.error('\nāŒ Existing extension already installed.\n' +
270
+ ` Conflicting path(s): ${conflictList}\n` +
271
+ ' Re-run with --force to replace the existing install.\n');
272
+ process.exit(1);
273
+ }
274
+ const previousNpmNames = new Set();
275
+ const previousKinds = new Set();
276
+ for (const existingInstall of existingInstalls) {
277
+ if (existingInstall.npmName) {
278
+ previousNpmNames.add(existingInstall.npmName);
279
+ }
280
+ if (existingInstall.kind) {
281
+ previousKinds.add(existingInstall.kind);
282
+ }
283
+ }
284
+ const backups = [];
285
+ if (options?.force) {
286
+ const backupSuffix = Date.now();
287
+ const backupRoot = path.join(paths.root, '.doubledigit', 'tmp-backups');
288
+ fs.mkdirSync(backupRoot, { recursive: true });
289
+ for (const [index, existingInstall] of existingInstalls.entries()) {
290
+ const backupDir = path.join(backupRoot, `${path.basename(existingInstall.dir)}.bak-${backupSuffix}-${index}`);
291
+ fs.renameSync(existingInstall.dir, backupDir);
292
+ backups.push({ originalDir: existingInstall.dir, backupDir });
293
+ console.log(` ⚠ Replacing existing install at ${path.relative(paths.root, existingInstall.dir)}`);
294
+ }
295
+ }
296
+ const rollbackSnapshots = snapshotFiles([
297
+ paths.mainAppPackageJsonPath,
298
+ baseTsConfigPath,
299
+ mainTsConfigPath,
300
+ paths.configPath,
301
+ paths.lockFilePath,
302
+ paths.microAppsOutputPath,
303
+ paths.payloadPluginsOutputPath,
304
+ ]);
305
+ try {
306
+ // Ensure parent directory exists (extensions/<kind>/ may not yet exist)
307
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
308
+ // Copy from temp to target
309
+ fs.cpSync(tempDir, targetDir, { recursive: true });
310
+ // Rewrite tsconfig relative paths for the new installation depth
311
+ rewriteExtensionTsconfig(targetDir, paths.root);
312
+ console.log(` āœ” Installed to ${targetRelPath}`);
313
+ // -------------------------------------------------------------------
314
+ // Wire up (mirrors create.ts pattern)
315
+ // -------------------------------------------------------------------
316
+ // Add dependency to main-app/package.json
317
+ const mainPkg = JSON.parse(fs.readFileSync(paths.mainAppPackageJsonPath, 'utf-8'));
318
+ if (!mainPkg.dependencies)
319
+ mainPkg.dependencies = {};
320
+ for (const previousNpmName of previousNpmNames) {
321
+ if (previousNpmName !== npmName) {
322
+ delete mainPkg.dependencies[previousNpmName];
323
+ }
324
+ }
325
+ mainPkg.dependencies[npmName] = 'workspace:*';
326
+ mainPkg.dependencies = Object.fromEntries(Object.entries(mainPkg.dependencies).sort(([a], [b]) => a.localeCompare(b)));
327
+ fs.writeFileSync(paths.mainAppPackageJsonPath, JSON.stringify(mainPkg, null, 2) + '\n', 'utf-8');
328
+ console.log(' āœ” Added dependency to main-app/package.json');
329
+ // Add TS path mappings to tsconfig.base.json
330
+ if (fs.existsSync(baseTsConfigPath)) {
331
+ const tsconfig = JSON.parse(fs.readFileSync(baseTsConfigPath, 'utf-8'));
332
+ if (!tsconfig.compilerOptions)
333
+ tsconfig.compilerOptions = {};
334
+ if (!tsconfig.compilerOptions.paths)
335
+ tsconfig.compilerOptions.paths = {};
336
+ for (const previousNpmName of previousNpmNames) {
337
+ if (previousNpmName === npmName)
338
+ continue;
339
+ delete tsconfig.compilerOptions.paths[previousNpmName];
340
+ delete tsconfig.compilerOptions.paths[`${previousNpmName}/*`];
341
+ }
342
+ tsconfig.compilerOptions.paths[npmName] = [
343
+ `${targetRelPath}/${detectedEntryPoint}`,
344
+ ];
345
+ if (hasSrcDir) {
346
+ tsconfig.compilerOptions.paths[`${npmName}/*`] = [
347
+ `${targetRelPath}/src/*`,
348
+ ];
349
+ }
350
+ else {
351
+ delete tsconfig.compilerOptions.paths[`${npmName}/*`];
352
+ }
353
+ fs.writeFileSync(baseTsConfigPath, JSON.stringify(tsconfig, null, 2) + '\n', 'utf-8');
354
+ }
355
+ // Add TS path mappings to apps/main-app/tsconfig.json
356
+ if (fs.existsSync(mainTsConfigPath)) {
357
+ const tsconfig = JSON.parse(fs.readFileSync(mainTsConfigPath, 'utf-8'));
358
+ if (!tsconfig.compilerOptions)
359
+ tsconfig.compilerOptions = {};
360
+ if (!tsconfig.compilerOptions.paths)
361
+ tsconfig.compilerOptions.paths = {};
362
+ for (const previousNpmName of previousNpmNames) {
363
+ if (previousNpmName === npmName)
364
+ continue;
365
+ delete tsconfig.compilerOptions.paths[previousNpmName];
366
+ delete tsconfig.compilerOptions.paths[`${previousNpmName}/*`];
367
+ }
368
+ tsconfig.compilerOptions.paths[npmName] = [
369
+ `../../${targetRelPath}/${detectedEntryPoint}`,
370
+ ];
371
+ if (hasSrcDir) {
372
+ tsconfig.compilerOptions.paths[`${npmName}/*`] = [
373
+ `../../${targetRelPath}/src/*`,
374
+ ];
375
+ }
376
+ else {
377
+ delete tsconfig.compilerOptions.paths[`${npmName}/*`];
378
+ }
379
+ fs.writeFileSync(mainTsConfigPath, JSON.stringify(tsconfig, null, 2) + '\n', 'utf-8');
380
+ }
381
+ console.log(' āœ” Added TS path mappings');
382
+ // Add entry to dd-apps.config.json (micro-app only)
383
+ const config = readConfig(paths.configPath);
384
+ if (!isPayloadPlugin) {
385
+ if (!config.apps)
386
+ config.apps = {};
387
+ config.apps[appName] = true;
388
+ writeConfig(paths.configPath, config);
389
+ console.log(' āœ” Updated dd-apps.config.json');
390
+ }
391
+ else if (previousKinds.has('micro-app') && config.apps?.[appName] !== undefined) {
392
+ delete config.apps[appName];
393
+ writeConfig(paths.configPath, config);
394
+ console.log(' āœ” Removed stale dd-apps.config.json entry');
395
+ }
396
+ // Update lock file (persist npmName for uninstall)
397
+ const contentHash = await computeContentHash(targetDir);
398
+ const now = new Date().toISOString();
399
+ const lockEntry = {
400
+ source: raw,
401
+ resolvedSource: source.templateString,
402
+ contentHash,
403
+ ref: marketplaceCtx?.ref ?? source.ref,
404
+ npmName,
405
+ installedAt: now,
406
+ updatedAt: now,
407
+ kind: effectiveKind,
408
+ ...(marketplaceCtx?.marketplace && { marketplace: marketplaceCtx.marketplace }),
409
+ ...(marketplaceCtx?.sha && { sha: marketplaceCtx.sha }),
410
+ ...(marketplaceCtx?.version && { marketplaceVersion: marketplaceCtx.version }),
411
+ installPath: targetRelPath,
412
+ installStrategy: 'workspace-vendor',
413
+ };
414
+ addLockEntry(paths.lockFilePath, appName, lockEntry);
415
+ console.log(' āœ” Updated dd-apps.lock.json');
416
+ console.log('');
417
+ // Run sync
418
+ await sync();
419
+ }
420
+ catch (installErr) {
421
+ console.error(' ✘ Install failed — rolling back workspace changes');
422
+ try {
423
+ restoreSnapshots(rollbackSnapshots);
424
+ if (fs.existsSync(targetDir)) {
425
+ fs.rmSync(targetDir, { recursive: true, force: true });
426
+ }
427
+ for (const backup of backups) {
428
+ if (!fs.existsSync(backup.backupDir))
429
+ continue;
430
+ fs.mkdirSync(path.dirname(backup.originalDir), { recursive: true });
431
+ fs.renameSync(backup.backupDir, backup.originalDir);
432
+ }
433
+ }
434
+ catch {
435
+ const backupDirs = backups.map((backup) => backup.backupDir).join(', ');
436
+ console.error(` ✘ Rollback failed. Backup(s) at: ${backupDirs}`);
437
+ }
438
+ throw installErr;
439
+ }
440
+ for (const backup of backups) {
441
+ if (fs.existsSync(backup.backupDir)) {
442
+ fs.rmSync(backup.backupDir, { recursive: true, force: true });
443
+ }
444
+ }
445
+ // Run pnpm install
446
+ console.log('\nšŸ“¦ Running pnpm install...');
447
+ try {
448
+ execSync('pnpm install', { cwd: paths.root, stdio: 'inherit' });
449
+ }
450
+ catch {
451
+ console.warn('⚠ pnpm install failed. Run it manually.');
452
+ }
453
+ console.log(`
454
+ āœ… ${isPayloadPlugin ? 'Payload plugin' : 'Micro-app'} "${appName}" installed successfully from GitHub!
455
+
456
+ Next steps:
457
+ 1. Review the installed code: ${targetRelPath}/
458
+ 2. Run: pnpm db:migrate:create
459
+ 3. Run: pnpm db:migrate
460
+ 4. Run: pnpm dev
461
+ `);
462
+ }
463
+ finally {
464
+ // Clean up temp directory
465
+ try {
466
+ fs.rmSync(tempDir, { recursive: true, force: true });
467
+ }
468
+ catch {
469
+ // Ignore cleanup failures
470
+ }
471
+ }
472
+ }
473
+ // ---------------------------------------------------------------------------
474
+ // Public API
475
+ // ---------------------------------------------------------------------------
476
+ export async function add(source, options) {
477
+ // 1. npm: prefix → npm install
478
+ if (isNpmSource(source)) {
479
+ const { packageSpec, name } = parseNpmSource(source);
480
+ const appName = options?.name ?? name;
481
+ await addFromNpm(packageSpec, appName, options);
482
+ return;
483
+ }
484
+ // 2. Explicit GitHub prefixes → GitHub install directly
485
+ if (source.startsWith('gh:') ||
486
+ source.startsWith('github:') ||
487
+ source.startsWith('https://')) {
488
+ await addFromGitHub(source, options);
489
+ return;
490
+ }
491
+ // 3. name@marketplace → resolve from a specific marketplace
492
+ // 4. bare name (no prefix, no @) → resolve from any known marketplace
493
+ const { root } = resolveWorkspacePaths();
494
+ const atIdx = source.indexOf('@');
495
+ if (atIdx > 0) {
496
+ // name@marketplace syntax
497
+ const extName = source.slice(0, atIdx);
498
+ const marketplaceName = source.slice(atIdx + 1);
499
+ const known = readKnownMarketplaces(root);
500
+ if (!(marketplaceName in known.marketplaces)) {
501
+ console.error(`Error: marketplace "${marketplaceName}" is not registered.\n` +
502
+ 'Run `dd marketplace add` to register it, then `dd marketplace update` to fetch its catalog.');
503
+ process.exit(1);
504
+ }
505
+ const ext = resolveExtensionFromMarketplace(root, extName, marketplaceName);
506
+ if (!ext) {
507
+ console.error(`Error: extension "${extName}" not found in marketplace "${marketplaceName}".\n` +
508
+ `Run \`dd marketplace update ${marketplaceName}\` to refresh the catalog.`);
509
+ process.exit(1);
510
+ }
511
+ if (options?.name && options.name !== ext.name) {
512
+ console.error(`Error: --name "${options.name}" conflicts with marketplace extension name "${ext.name}".\n` +
513
+ 'Marketplace installs must keep the catalog name so info/outdated/reconcile continue to match.');
514
+ process.exit(1);
515
+ }
516
+ const ghSource = extensionSourceToGhString(ext);
517
+ console.log(`Resolved ${source} → ${ghSource} (from "${marketplaceName}" marketplace)`);
518
+ await addFromGitHub(ghSource, { ...options, name: ext.name }, {
519
+ kind: ext.kind,
520
+ marketplace: marketplaceName,
521
+ sha: ext.source.sha,
522
+ ref: ext.source.ref,
523
+ version: ext.version,
524
+ });
525
+ return;
526
+ }
527
+ // Bare name — try resolving across all known marketplaces
528
+ const ext = resolveExtension(root, source);
529
+ if (ext) {
530
+ if (options?.name && options.name !== ext.name) {
531
+ console.error(`Error: --name "${options.name}" conflicts with marketplace extension name "${ext.name}".\n` +
532
+ 'Marketplace installs must keep the catalog name so info/outdated/reconcile continue to match.');
533
+ process.exit(1);
534
+ }
535
+ const ghSource = extensionSourceToGhString(ext);
536
+ console.log(`Resolved ${source} → ${ghSource} (from "${ext.marketplace}" marketplace)`);
537
+ await addFromGitHub(ghSource, { ...options, name: ext.name }, {
538
+ kind: ext.kind,
539
+ marketplace: ext.marketplace,
540
+ sha: ext.source.sha,
541
+ ref: ext.source.ref,
542
+ version: ext.version,
543
+ });
544
+ return;
545
+ }
546
+ // Fallback: treat as GitHub source (preserves existing behavior for owner/repo strings)
547
+ await addFromGitHub(source, options);
548
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * dd browse [marketplace] — Browse extensions in registered marketplaces.
3
+ *
4
+ * Lists available extensions with kind, version, tags, and install command.
5
+ * Optionally filters by marketplace name or search query.
6
+ */
7
+ export declare function browse(args: string[]): Promise<void>;
8
+ //# sourceMappingURL=browse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyCH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA0G1D"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * dd browse [marketplace] — Browse extensions in registered marketplaces.
3
+ *
4
+ * Lists available extensions with kind, version, tags, and install command.
5
+ * Optionally filters by marketplace name or search query.
6
+ */
7
+ import { readKnownMarketplaces, getAllCachedExtensions, readCachedMarketplace, } from '../lib/marketplace.js';
8
+ import { resolveWorkspacePaths } from '../paths.js';
9
+ // ---------------------------------------------------------------------------
10
+ // Formatting helpers
11
+ // ---------------------------------------------------------------------------
12
+ function kindBadge(kind) {
13
+ const badges = {
14
+ 'micro-app': 'šŸ“¦',
15
+ 'payload-plugin': 'šŸ”Œ',
16
+ };
17
+ return badges[kind] ?? 'šŸ“„';
18
+ }
19
+ function formatExtension(ext) {
20
+ const badge = kindBadge(ext.kind);
21
+ const verified = ext.verified ? ' āœ“' : '';
22
+ const version = ext.version ? ` v${ext.version}` : '';
23
+ const tags = ext.tags.length > 0 ? ` [${ext.tags.join(', ')}]` : '';
24
+ const desc = ext.description ? `\n ${ext.description}` : '';
25
+ return (` ${badge} ${ext.name}${version}${verified} (${ext.kind})${tags}${desc}\n` +
26
+ ` Install: dd add ${ext.name}@${ext.marketplace}`);
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Browse command
30
+ // ---------------------------------------------------------------------------
31
+ export async function browse(args) {
32
+ const paths = resolveWorkspacePaths();
33
+ const known = readKnownMarketplaces(paths.root);
34
+ if (Object.keys(known.marketplaces).length === 0) {
35
+ console.log('\nNo marketplaces registered.\n\n' +
36
+ 'Add one first:\n' +
37
+ ' dd marketplace add owner/repo\n');
38
+ return;
39
+ }
40
+ // Parse args: [marketplace] [--kind <kind>] [--search <query>]
41
+ let marketplaceName;
42
+ let kindFilter;
43
+ let searchQuery;
44
+ for (let i = 0; i < args.length; i++) {
45
+ if (args[i] === '--kind') {
46
+ i++;
47
+ kindFilter = args[i];
48
+ }
49
+ else if (args[i] === '--search' || args[i] === '-s') {
50
+ i++;
51
+ searchQuery = args[i]?.toLowerCase();
52
+ }
53
+ else if (!args[i].startsWith('--')) {
54
+ marketplaceName = args[i];
55
+ }
56
+ }
57
+ let extensions;
58
+ if (marketplaceName) {
59
+ // Specific marketplace
60
+ if (!(marketplaceName in known.marketplaces)) {
61
+ console.error(`\nāŒ Marketplace "${marketplaceName}" is not registered.\n` +
62
+ ' Run `dd marketplace list` to see registered marketplaces.\n');
63
+ process.exit(1);
64
+ }
65
+ const cached = readCachedMarketplace(paths.root, marketplaceName);
66
+ if (!cached) {
67
+ console.log(`\nMarketplace "${marketplaceName}" has no cached catalog.\n` +
68
+ `Run: dd marketplace update ${marketplaceName}\n`);
69
+ return;
70
+ }
71
+ extensions = cached.manifest.extensions.map((e) => ({
72
+ ...e,
73
+ marketplace: marketplaceName,
74
+ }));
75
+ }
76
+ else {
77
+ // All marketplaces
78
+ extensions = getAllCachedExtensions(paths.root);
79
+ }
80
+ // Apply filters
81
+ if (kindFilter) {
82
+ extensions = extensions.filter((e) => e.kind === kindFilter);
83
+ }
84
+ if (searchQuery) {
85
+ extensions = extensions.filter((e) => e.name.toLowerCase().includes(searchQuery) ||
86
+ e.description?.toLowerCase().includes(searchQuery) ||
87
+ e.tags.some((t) => t.toLowerCase().includes(searchQuery)));
88
+ }
89
+ if (extensions.length === 0) {
90
+ const filterHint = searchQuery
91
+ ? ` matching "${searchQuery}"`
92
+ : kindFilter
93
+ ? ` of kind "${kindFilter}"`
94
+ : '';
95
+ console.log(`\nNo extensions found${filterHint}.\n` +
96
+ 'Try `dd marketplace update` to refresh catalogs.\n');
97
+ return;
98
+ }
99
+ // Group by marketplace
100
+ const grouped = new Map();
101
+ for (const ext of extensions) {
102
+ const group = grouped.get(ext.marketplace) ?? [];
103
+ group.push(ext);
104
+ grouped.set(ext.marketplace, group);
105
+ }
106
+ console.log(`\nšŸŖ Available extensions (${extensions.length}):\n`);
107
+ for (const [mpName, exts] of grouped) {
108
+ if (grouped.size > 1) {
109
+ console.log(` ── ${mpName} ──\n`);
110
+ }
111
+ for (const ext of exts) {
112
+ console.log(formatExtension(ext));
113
+ console.log('');
114
+ }
115
+ }
116
+ }