@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.
@@ -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 { requireSession, getSdkProxy } from '../lib/proxy/context.js';
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
- const session = requireSession();
168
- const proxy = getSdkProxy(session);
169
- const productTag = opts.product ?? extraArgs[0] ?? session.project?.product_tag;
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.execute('product', 'environments.fetch', [productTag, body.slug]);
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.execute('product', 'environments.fetch', [productTag, body.slug ?? updateSlug]);
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
- .requiredOption('-t, --tag <tag>', 'Product tag')
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
- .requiredOption('-t, --tag <tag>', 'Product tag')
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 createSdkProxyClient({
38
+ return getSdkProxyForContext({
23
39
  apiUrl: s.apiUrl,
24
40
  workspaceId: s.project.workspace_id,
25
- userId: s.credentials.user_id,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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",