@ductape/cli 0.3.25 → 0.3.27

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.
@@ -12,6 +12,7 @@ export declare function runProductApps(verb: string, opts: {
12
12
  product?: string;
13
13
  app?: string;
14
14
  envMap?: string[];
15
+ connectionFile?: string;
15
16
  json?: boolean;
16
17
  full?: boolean;
17
18
  }, extraArgs: string[]): Promise<void>;
@@ -2,8 +2,9 @@ import { schemaKeyForEntity } from '../lib/forms/index.js';
2
2
  import { isInteractive, promptTag } from '../lib/forms/prompt.js';
3
3
  import { toInteractiveOpts } from '../lib/interactive-opts.js';
4
4
  import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
5
- import { connectMarketplaceApp, createProduct, deleteProduct, getProduct, getProductAppAction, listProductAppActions, listProductApps, listProducts, updateProduct, } from '../lib/platform-api.js';
5
+ import { connectMarketplaceApp, configureProductApp, createProduct, deleteProduct, getProduct, getProductAppAction, listProductAppActions, listProductApps, listProducts, updateProduct, } from '../lib/platform-api.js';
6
6
  import { printJson } from '../lib/output.js';
7
+ import { readJsonBody } from '../lib/read-body.js';
7
8
  import { requireWorkspaceContext } from '../lib/workspace-context.js';
8
9
  import { getSdkProxyForContext, getOptionalProject } from '../lib/proxy/context.js';
9
10
  const VERBS = ['list', 'get', 'create', 'update', 'delete'];
@@ -83,8 +84,23 @@ export async function runProductApps(verb, opts, extraArgs) {
83
84
  });
84
85
  result = await connectMarketplaceApp(ctx, productId, opts.app, envs);
85
86
  }
87
+ else if (verb.toLowerCase() === 'configure') {
88
+ if (!opts.app)
89
+ throw new Error('Provide --app <connected_app_tag_or_access_tag>');
90
+ if (!opts.connectionFile)
91
+ throw new Error('Provide --connection-file <connection.json>');
92
+ const body = readJsonBody(opts.connectionFile);
93
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
94
+ throw new Error('Connection file must contain a JSON object');
95
+ }
96
+ const envs = body.envs;
97
+ if (!Array.isArray(envs) || envs.length === 0) {
98
+ throw new Error('Connection file requires a non-empty envs array');
99
+ }
100
+ result = await configureProductApp(ctx, productId, opts.app, envs);
101
+ }
86
102
  else {
87
- throw new Error('Supported verbs: list | connect');
103
+ throw new Error('Supported verbs: list | connect | configure');
88
104
  }
89
105
  printJson(result, Boolean(opts.json));
90
106
  }
package/dist/index.js CHANGED
@@ -568,6 +568,15 @@ productApps
568
568
  .option('--profile <name>')
569
569
  .option('--json', 'JSON output')
570
570
  .action(wrap((opts) => runProductApps('connect', opts, [])));
571
+ productApps
572
+ .command('configure')
573
+ .description('Configure or repair a connected App without exposing existing credentials')
574
+ .requiredOption('--product <id-or-tag>', 'Product _id or tag')
575
+ .requiredOption('--app <tag>', 'Connected app tag or product access tag')
576
+ .requiredOption('--connection-file <path>', 'Connection JSON: { envs: [{ product_env_slug, app_env_slug, variables?, auth? }] }')
577
+ .option('--profile <name>')
578
+ .option('--json', 'JSON output')
579
+ .action(wrap((opts) => runProductApps('configure', opts, [])));
571
580
  const productAppActions = productApps.command('actions').description('Actions exposed by an app linked to a product');
572
581
  productAppActions
573
582
  .command('list')
@@ -15,6 +15,8 @@ export declare function connectMarketplaceApp(ctx: WorkspaceContext, product: st
15
15
  app_env_slug: string;
16
16
  product_env_slug: string;
17
17
  }>): Promise<unknown>;
18
+ /** Configure an existing product App link while preserving omitted secret-bearing fields. */
19
+ export declare function configureProductApp(ctx: WorkspaceContext, productRef: string, appRef: string, updates: Array<Record<string, unknown>>): Promise<unknown>;
18
20
  export declare function listApps(ctx: WorkspaceContext, status?: string): Promise<unknown[]>;
19
21
  export declare function getApp(ctx: WorkspaceContext, opts: {
20
22
  id?: string;
@@ -277,6 +277,57 @@ export async function connectMarketplaceApp(ctx, product, appTag, envs) {
277
277
  environments: envs,
278
278
  };
279
279
  }
280
+ /** Configure an existing product App link while preserving omitted secret-bearing fields. */
281
+ export async function configureProductApp(ctx, productRef, appRef, updates) {
282
+ const product = unwrapOne(await getProduct(ctx, /^[a-f\d]{24}$/i.test(productRef) ? { id: productRef } : { tag: productRef }));
283
+ const productTag = String(product.tag ?? (!/^[a-f\d]{24}$/i.test(productRef) ? productRef : ''));
284
+ if (!productTag)
285
+ throw new Error(`Could not resolve product tag for "${productRef}"`);
286
+ const links = Array.isArray(product.apps) ? product.apps : [];
287
+ const normalized = appRef.toLowerCase();
288
+ const link = links.find((item) => String(item.access_tag ?? '').toLowerCase() === normalized
289
+ || String(item.app_tag ?? '').toLowerCase() === normalized
290
+ || String(item.tag ?? '').toLowerCase() === normalized);
291
+ if (!link)
292
+ throw new Error(`App "${appRef}" is not connected to product "${productTag}"`);
293
+ const accessTag = String(link.access_tag ?? '');
294
+ if (!accessTag)
295
+ throw new Error(`Connected App "${appRef}" has no access tag`);
296
+ const existing = Array.isArray(link.envs) ? link.envs : [];
297
+ const byProductEnv = new Map(existing.map((env) => [String(env.product_env_slug ?? ''), { ...env }]));
298
+ for (const update of updates) {
299
+ const productEnv = String(update.product_env_slug ?? '');
300
+ const appEnv = String(update.app_env_slug ?? '');
301
+ if (!productEnv || !appEnv) {
302
+ throw new Error('Each connection env requires product_env_slug and app_env_slug');
303
+ }
304
+ const previous = byProductEnv.get(productEnv) ?? {};
305
+ byProductEnv.set(productEnv, {
306
+ ...previous,
307
+ ...update,
308
+ // Omission means preserve; explicit auth/variables replace that field.
309
+ ...(!Object.prototype.hasOwnProperty.call(update, 'auth') && previous.auth !== undefined
310
+ ? { auth: previous.auth } : {}),
311
+ ...(!Object.prototype.hasOwnProperty.call(update, 'variables') && previous.variables !== undefined
312
+ ? { variables: previous.variables } : {}),
313
+ });
314
+ }
315
+ const envs = [...byProductEnv.values()];
316
+ await proxy(ctx).execute('product', 'apps.update', [productTag, accessTag, { envs }]);
317
+ return {
318
+ configured: true,
319
+ product: productTag,
320
+ app_tag: appRef,
321
+ access_tag: accessTag,
322
+ environments: envs.map((env) => ({
323
+ product_env_slug: env.product_env_slug,
324
+ app_env_slug: env.app_env_slug,
325
+ variables_configured: Array.isArray(env.variables) && env.variables.length > 0,
326
+ auth_configured: Boolean(env.auth),
327
+ shared_credentials_configured: Boolean(env.credentials && typeof env.credentials === 'object' && Object.keys(env.credentials).length > 0),
328
+ })),
329
+ };
330
+ }
280
331
  // —— Apps (apps REST) ——
281
332
  export async function listApps(ctx, status = 'all') {
282
333
  const result = await client(ctx).getPath(`/apps/v1/workspace/${ctx.workspaceId}/${status}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.3.25",
3
+ "version": "0.3.27",
4
4
  "description": "Ductape CLI — local platform, login, link projects, and manage resources via the proxy (Workbench-compatible)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",