@ductape/cli 0.3.2 → 0.3.4
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/dist/commands/products.js +25 -6
- package/dist/index.js +17 -8
- package/dist/lib/proxy/context.d.ts +12 -0
- package/dist/lib/proxy/context.js +18 -4
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { bodyRequiredHint, resolveBody } from '../lib/read-body.js';
|
|
|
5
5
|
import { createProduct, deleteProduct, getProduct, listProductApps, listProducts, updateProduct, } from '../lib/platform-api.js';
|
|
6
6
|
import { printJson } from '../lib/output.js';
|
|
7
7
|
import { requireWorkspaceContext } from '../lib/workspace-context.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getSdkProxyForContext, getOptionalProject } from '../lib/proxy/context.js';
|
|
9
9
|
const VERBS = ['list', 'get', 'create', 'update', 'delete'];
|
|
10
10
|
export async function runProducts(verb, opts, extraArgs) {
|
|
11
11
|
const v = verb.toLowerCase();
|
|
@@ -158,15 +158,34 @@ export async function runProductComponents(verb, opts) {
|
|
|
158
158
|
components,
|
|
159
159
|
}, Boolean(opts.json));
|
|
160
160
|
}
|
|
161
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
/**
|
|
163
|
+
* environments.fetch immediately after a create/update can race a replication/read-after-write
|
|
164
|
+
* lag and come back empty even though the write succeeded. Retry a few times before treating it
|
|
165
|
+
* as a real failure.
|
|
166
|
+
*/
|
|
167
|
+
async function fetchEnvironmentWithRetry(proxy, productTag, slug, attempts = 3, delayMs = 400) {
|
|
168
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
169
|
+
const found = await proxy.execute('product', 'environments.fetch', [productTag, slug]);
|
|
170
|
+
if (found)
|
|
171
|
+
return found;
|
|
172
|
+
if (attempt < attempts)
|
|
173
|
+
await sleep(delayMs);
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
161
177
|
export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
162
178
|
const v = verb.toLowerCase();
|
|
163
179
|
const allowed = ['list', 'get', 'fetch', 'create', 'update'];
|
|
164
180
|
if (!allowed.includes(v)) {
|
|
165
181
|
throw new Error(`Unknown environments verb "${verb}". Use: ${allowed.join(', ')}`);
|
|
166
182
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
// No linked project required: the product tag always comes from an explicit argument here.
|
|
184
|
+
// Only fall back to a linked project's product_tag (without requiring one to exist) when the
|
|
185
|
+
// caller supplied neither --product nor a positional tag.
|
|
186
|
+
const ctx = requireWorkspaceContext();
|
|
187
|
+
const proxy = getSdkProxyForContext(ctx);
|
|
188
|
+
const productTag = opts.product ?? extraArgs[0] ?? getOptionalProject()?.product_tag;
|
|
170
189
|
if (!productTag)
|
|
171
190
|
throw new Error('Provide the product tag as the first argument or --product <tag>');
|
|
172
191
|
if (v === 'list') {
|
|
@@ -190,7 +209,7 @@ export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
|
190
209
|
return;
|
|
191
210
|
}
|
|
192
211
|
await proxy.execute('product', 'environments.create', [productTag, body]);
|
|
193
|
-
const verified = await proxy
|
|
212
|
+
const verified = await fetchEnvironmentWithRetry(proxy, productTag, body.slug);
|
|
194
213
|
if (!verified)
|
|
195
214
|
throw new Error(`Environment "${body.slug}" was not found after creation.`);
|
|
196
215
|
printJson({ product: productTag, slug: body.slug, created: true, environment: verified }, Boolean(opts.json));
|
|
@@ -200,7 +219,7 @@ export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
|
200
219
|
if (!updateSlug)
|
|
201
220
|
throw new Error('Environment update requires --slug <slug>.');
|
|
202
221
|
await proxy.execute('product', 'environments.update', [productTag, updateSlug, body]);
|
|
203
|
-
const verified = await proxy
|
|
222
|
+
const verified = await fetchEnvironmentWithRetry(proxy, productTag, body.slug ?? updateSlug);
|
|
204
223
|
if (!verified)
|
|
205
224
|
throw new Error(`Environment "${updateSlug}" was not found after update.`);
|
|
206
225
|
printJson({ product: productTag, slug: body.slug ?? updateSlug, updated: true, environment: verified }, Boolean(opts.json));
|
package/dist/index.js
CHANGED
|
@@ -541,17 +541,21 @@ const productComponents = products
|
|
|
541
541
|
.description('Compact, non-secret product component inventory');
|
|
542
542
|
productComponents
|
|
543
543
|
.command('list')
|
|
544
|
-
|
|
544
|
+
// NOTE: cannot use -t/--tag here — it collides with the parent `products` command's own
|
|
545
|
+
// -t/--tag option (used by `products get`/`products update`). Same Commander ancestor/
|
|
546
|
+
// descendant flag-collision issue documented on `environments create`/`update` above.
|
|
547
|
+
.requiredOption('--product-tag <tag>', 'Product tag')
|
|
545
548
|
.option('--profile <name>')
|
|
546
549
|
.option('--json', 'JSON output')
|
|
547
|
-
.action(wrap((opts) => runProductComponents('list', opts)));
|
|
550
|
+
.action(wrap((opts) => runProductComponents('list', { ...opts, tag: opts.productTag })));
|
|
548
551
|
productComponents
|
|
549
552
|
.command('get')
|
|
550
|
-
|
|
553
|
+
// See NOTE above on `list` — same -t/--tag collision with the parent `products` command.
|
|
554
|
+
.requiredOption('--product-tag <tag>', 'Product tag')
|
|
551
555
|
.requiredOption('--type <type>', 'Component type, e.g. notifications or events')
|
|
552
556
|
.option('--profile <name>')
|
|
553
557
|
.option('--json', 'JSON output')
|
|
554
|
-
.action(wrap((opts) => runProductComponents('get', opts)));
|
|
558
|
+
.action(wrap((opts) => runProductComponents('get', { ...opts, tag: opts.productTag })));
|
|
555
559
|
const productEnvironments = products
|
|
556
560
|
.command('environments')
|
|
557
561
|
.description('Product environments (list, get, idempotent create, update)');
|
|
@@ -564,15 +568,20 @@ productEnvironments
|
|
|
564
568
|
productEnvironments
|
|
565
569
|
.command('create <product_tag>')
|
|
566
570
|
.description('Idempotently create a product environment and verify persistence')
|
|
567
|
-
|
|
571
|
+
// NOTE: cannot use -f/--file here — it collides with the parent `products` command's own
|
|
572
|
+
// -f/--file option (used by `products create`/`products update`). Commander resolves a flag
|
|
573
|
+
// shared by an ancestor and a descendant command against the ancestor, so the value never
|
|
574
|
+
// reaches this subcommand's own opts, and requiredOption fails even when -f is supplied.
|
|
575
|
+
.requiredOption('--env-file <path>', 'Environment JSON: env_name, description, slug, active?')
|
|
568
576
|
.option('--json', 'JSON output')
|
|
569
|
-
.action(wrap((productTag, opts) => runProductEnvironments('create', { product: productTag, file: opts.
|
|
577
|
+
.action(wrap((productTag, opts) => runProductEnvironments('create', { product: productTag, file: opts.envFile, json: opts.json }, [])));
|
|
570
578
|
productEnvironments
|
|
571
579
|
.command('update <product_tag> <slug>')
|
|
572
580
|
.description('Update a product environment and verify persistence')
|
|
573
|
-
|
|
581
|
+
// See NOTE above on `create` — same -f/--file collision with the parent `products` command.
|
|
582
|
+
.requiredOption('--env-file <path>', 'Environment patch JSON')
|
|
574
583
|
.option('--json', 'JSON output')
|
|
575
|
-
.action(wrap((productTag, slug, opts) => runProductEnvironments('update', { product: productTag, slug, file: opts.
|
|
584
|
+
.action(wrap((productTag, slug, opts) => runProductEnvironments('update', { product: productTag, slug, file: opts.envFile, json: opts.json }, [])));
|
|
576
585
|
productEnvironments
|
|
577
586
|
.command('get <product_tag> [slug]')
|
|
578
587
|
.description('Fetch a single product environment by slug')
|
|
@@ -9,6 +9,18 @@ export interface SessionContext {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function requireCredentials(): Credentials;
|
|
11
11
|
export declare function requireSession(): SessionContext;
|
|
12
|
+
/**
|
|
13
|
+
* Builds an SDK proxy from just apiUrl + workspaceId + credentials — no linked project
|
|
14
|
+
* required. Use this (via requireWorkspaceContext()) for any command that already has an
|
|
15
|
+
* explicit product tag from its arguments and doesn't otherwise need project-scoped runtime
|
|
16
|
+
* context (env_slug, database/graph runtime state). Commands that need those still require a
|
|
17
|
+
* full linked project via requireSession()/getSdkProxy().
|
|
18
|
+
*/
|
|
19
|
+
export declare function getSdkProxyForContext(ctx: {
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
credentials: Credentials;
|
|
23
|
+
}): SdkProxyClient;
|
|
12
24
|
export declare function getSdkProxy(session?: SessionContext): SdkProxyClient;
|
|
13
25
|
export declare function getDbProxy(session?: SessionContext): DbProxyClient;
|
|
14
26
|
export declare function getDbContext(session?: SessionContext): import("../context-store.js").DatabaseContext;
|
|
@@ -17,14 +17,28 @@ export function requireSession() {
|
|
|
17
17
|
const apiUrl = getApiUrl(profile);
|
|
18
18
|
return { credentials, project, apiUrl };
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds an SDK proxy from just apiUrl + workspaceId + credentials — no linked project
|
|
22
|
+
* required. Use this (via requireWorkspaceContext()) for any command that already has an
|
|
23
|
+
* explicit product tag from its arguments and doesn't otherwise need project-scoped runtime
|
|
24
|
+
* context (env_slug, database/graph runtime state). Commands that need those still require a
|
|
25
|
+
* full linked project via requireSession()/getSdkProxy().
|
|
26
|
+
*/
|
|
27
|
+
export function getSdkProxyForContext(ctx) {
|
|
28
|
+
return createSdkProxyClient({
|
|
29
|
+
apiUrl: ctx.apiUrl,
|
|
30
|
+
workspaceId: ctx.workspaceId,
|
|
31
|
+
userId: ctx.credentials.user_id,
|
|
32
|
+
publicKey: ctx.credentials.public_key,
|
|
33
|
+
token: ctx.credentials.auth_token,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
20
36
|
export function getSdkProxy(session) {
|
|
21
37
|
const s = session ?? requireSession();
|
|
22
|
-
return
|
|
38
|
+
return getSdkProxyForContext({
|
|
23
39
|
apiUrl: s.apiUrl,
|
|
24
40
|
workspaceId: s.project.workspace_id,
|
|
25
|
-
|
|
26
|
-
publicKey: s.credentials.public_key,
|
|
27
|
-
token: s.credentials.auth_token,
|
|
41
|
+
credentials: s.credentials,
|
|
28
42
|
});
|
|
29
43
|
}
|
|
30
44
|
export function getDbProxy(session) {
|
package/package.json
CHANGED