@ductape/cli 0.3.3 → 0.3.5
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 +42 -6
- package/dist/index.js +8 -4
- 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();
|
|
@@ -85,6 +85,13 @@ const COMPONENT_KEYS = {
|
|
|
85
85
|
sessions: ['sessions'],
|
|
86
86
|
features: ['features'],
|
|
87
87
|
jobs: ['jobs'],
|
|
88
|
+
healthchecks: ['healthchecks'],
|
|
89
|
+
health: ['healthchecks'],
|
|
90
|
+
quotas: ['quota', 'quotas'],
|
|
91
|
+
fallbacks: ['fallback', 'fallbacks'],
|
|
92
|
+
functions: ['functions'],
|
|
93
|
+
agents: ['agents'],
|
|
94
|
+
models: ['models'],
|
|
88
95
|
};
|
|
89
96
|
function unwrapProduct(value) {
|
|
90
97
|
if (!value || typeof value !== 'object')
|
|
@@ -158,15 +165,34 @@ export async function runProductComponents(verb, opts) {
|
|
|
158
165
|
components,
|
|
159
166
|
}, Boolean(opts.json));
|
|
160
167
|
}
|
|
168
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
169
|
+
/**
|
|
170
|
+
* environments.fetch immediately after a create/update can race a replication/read-after-write
|
|
171
|
+
* lag and come back empty even though the write succeeded. Retry a few times before treating it
|
|
172
|
+
* as a real failure.
|
|
173
|
+
*/
|
|
174
|
+
async function fetchEnvironmentWithRetry(proxy, productTag, slug, attempts = 3, delayMs = 400) {
|
|
175
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
176
|
+
const found = await proxy.execute('product', 'environments.fetch', [productTag, slug, true]);
|
|
177
|
+
if (found)
|
|
178
|
+
return found;
|
|
179
|
+
if (attempt < attempts)
|
|
180
|
+
await sleep(delayMs);
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
161
184
|
export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
162
185
|
const v = verb.toLowerCase();
|
|
163
186
|
const allowed = ['list', 'get', 'fetch', 'create', 'update'];
|
|
164
187
|
if (!allowed.includes(v)) {
|
|
165
188
|
throw new Error(`Unknown environments verb "${verb}". Use: ${allowed.join(', ')}`);
|
|
166
189
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
// No linked project required: the product tag always comes from an explicit argument here.
|
|
191
|
+
// Only fall back to a linked project's product_tag (without requiring one to exist) when the
|
|
192
|
+
// caller supplied neither --product nor a positional tag.
|
|
193
|
+
const ctx = requireWorkspaceContext();
|
|
194
|
+
const proxy = getSdkProxyForContext(ctx);
|
|
195
|
+
const productTag = opts.product ?? extraArgs[0] ?? getOptionalProject()?.product_tag;
|
|
170
196
|
if (!productTag)
|
|
171
197
|
throw new Error('Provide the product tag as the first argument or --product <tag>');
|
|
172
198
|
if (v === 'list') {
|
|
@@ -190,7 +216,7 @@ export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
|
190
216
|
return;
|
|
191
217
|
}
|
|
192
218
|
await proxy.execute('product', 'environments.create', [productTag, body]);
|
|
193
|
-
const verified = await proxy
|
|
219
|
+
const verified = await fetchEnvironmentWithRetry(proxy, productTag, body.slug);
|
|
194
220
|
if (!verified)
|
|
195
221
|
throw new Error(`Environment "${body.slug}" was not found after creation.`);
|
|
196
222
|
printJson({ product: productTag, slug: body.slug, created: true, environment: verified }, Boolean(opts.json));
|
|
@@ -200,9 +226,19 @@ export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
|
200
226
|
if (!updateSlug)
|
|
201
227
|
throw new Error('Environment update requires --slug <slug>.');
|
|
202
228
|
await proxy.execute('product', 'environments.update', [productTag, updateSlug, body]);
|
|
203
|
-
const verified = await proxy
|
|
229
|
+
const verified = await fetchEnvironmentWithRetry(proxy, productTag, body.slug ?? updateSlug);
|
|
204
230
|
if (!verified)
|
|
205
231
|
throw new Error(`Environment "${updateSlug}" was not found after update.`);
|
|
232
|
+
if (verified === null || typeof verified !== 'object') {
|
|
233
|
+
throw new Error(`Environment "${updateSlug}" returned an invalid response after update.`);
|
|
234
|
+
}
|
|
235
|
+
const mismatches = Object.entries(body)
|
|
236
|
+
.filter(([key, expected]) => !Object.is(verified[key], expected))
|
|
237
|
+
.map(([key]) => key);
|
|
238
|
+
if (mismatches.length > 0) {
|
|
239
|
+
throw new Error(`Environment "${updateSlug}" update could not be verified; ` +
|
|
240
|
+
`the persisted value did not match field(s): ${mismatches.join(', ')}.`);
|
|
241
|
+
}
|
|
206
242
|
printJson({ product: productTag, slug: body.slug ?? updateSlug, updated: true, environment: verified }, Boolean(opts.json));
|
|
207
243
|
return;
|
|
208
244
|
}
|
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)');
|
|
@@ -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