@ductape/cli 0.3.3 → 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.
@@ -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();
@@ -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
- const session = requireSession();
168
- const proxy = getSdkProxy(session);
169
- const productTag = opts.product ?? extraArgs[0] ?? session.project?.product_tag;
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.execute('product', 'environments.fetch', [productTag, body.slug]);
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.execute('product', 'environments.fetch', [productTag, body.slug ?? updateSlug]);
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
- .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.4",
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",