@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,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
|
+
}
|