@tenderprompt/accounts 0.3.4 → 0.5.0
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/README.md +44 -9
- package/dist/api.d.ts +12 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +9 -0
- package/dist/api.js.map +1 -1
- package/dist/auth-store.d.ts +26 -7
- package/dist/auth-store.d.ts.map +1 -1
- package/dist/auth-store.js +165 -25
- package/dist/auth-store.js.map +1 -1
- package/dist/contracts.d.ts +13 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +3 -0
- package/dist/files.js.map +1 -1
- package/dist/main.d.ts +17 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +419 -60
- package/dist/main.js.map +1 -1
- package/dist/profile-bindings.d.ts +26 -0
- package/dist/profile-bindings.d.ts.map +1 -0
- package/dist/profile-bindings.js +214 -0
- package/dist/profile-bindings.js.map +1 -0
- package/dist/starter.d.ts.map +1 -1
- package/dist/starter.js +2 -1
- package/dist/starter.js.map +1 -1
- package/package.json +3 -1
- package/skill/tender-accounts/SKILL.md +108 -9
- package/templates/shopify-customer-account/AGENTS.md +1 -0
- package/templates/shopify-customer-account/README.md +5 -2
- package/templates/shopify-customer-account/apps/gateway/AGENTS.md +1 -0
- package/templates/shopify-customer-account/apps/gateway/dot.dev.vars.example +1 -1
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.test.ts +59 -7
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.ts +23 -3
- package/templates/shopify-customer-account/apps/portal/AGENTS.md +1 -0
- package/templates/shopify-customer-account/apps/portal/README.md +2 -0
- package/templates/shopify-customer-account/dot.dev.vars.example +1 -1
package/dist/main.js
CHANGED
|
@@ -3,15 +3,16 @@ import { copyFile, mkdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
5
|
import { AgentApiClient } from "./api.js";
|
|
6
|
-
import { CLI_AUTH_SCHEMA, createDefaultAuthStore, } from "./auth-store.js";
|
|
6
|
+
import { CLI_AUTH_SCHEMA, DEFAULT_AUTH_PROFILE, createDefaultAuthStore, validateAuthProfile, validateNamedAuthProfile, } from "./auth-store.js";
|
|
7
7
|
import { labelLocalArtifact, verifyArtifact } from "./artifact.js";
|
|
8
8
|
import { BUILD_ENVELOPE_FILENAME, CLI_CONFIG_SCHEMA, CLI_LINK_SCHEMA, } from "./contracts.js";
|
|
9
9
|
import { CliApiError, CliError } from "./errors.js";
|
|
10
10
|
import { loadProjectContext, normalizeApiUrl, pathExists, readBoundedFile, readOptionalToken, requiredProjectId, resolveArtifactDirectory, safeArtifactPath, writeProjectLink, } from "./files.js";
|
|
11
11
|
import { OAuthApiClient, createPkcePair, openSystemBrowser, waitForLoopbackAuthorization, } from "./oauth.js";
|
|
12
12
|
import { captureCommand, runProjectCommand } from "./processes.js";
|
|
13
|
+
import { createDefaultProfileBindings, } from "./profile-bindings.js";
|
|
13
14
|
import { initializeStarter } from "./starter.js";
|
|
14
|
-
export const CLI_VERSION = "0.
|
|
15
|
+
export const CLI_VERSION = "0.4.0";
|
|
15
16
|
const DEFAULT_RUNTIME = {
|
|
16
17
|
cwd: process.cwd(),
|
|
17
18
|
environment: process.env,
|
|
@@ -23,8 +24,18 @@ const DEFAULT_RUNTIME = {
|
|
|
23
24
|
runProjectCommand,
|
|
24
25
|
captureCommand,
|
|
25
26
|
authStore: createDefaultAuthStore(process.env),
|
|
27
|
+
profileBindings: createDefaultProfileBindings(process.env),
|
|
26
28
|
openBrowser: openSystemBrowser,
|
|
27
29
|
waitForLoopbackAuthorization,
|
|
30
|
+
openLogStream: (url, protocols) => new WebSocket(url, protocols),
|
|
31
|
+
onInterrupt: (handler) => {
|
|
32
|
+
process.once("SIGINT", handler);
|
|
33
|
+
process.once("SIGTERM", handler);
|
|
34
|
+
return () => {
|
|
35
|
+
process.off("SIGINT", handler);
|
|
36
|
+
process.off("SIGTERM", handler);
|
|
37
|
+
};
|
|
38
|
+
},
|
|
28
39
|
};
|
|
29
40
|
export async function runCli(argv, overrides = {}) {
|
|
30
41
|
const runtime = { ...DEFAULT_RUNTIME, ...overrides };
|
|
@@ -91,16 +102,29 @@ async function execute(topic, options, cwd, runtime) {
|
|
|
91
102
|
return { value: await buildProject(cwd, options, runtime) };
|
|
92
103
|
case "auth status":
|
|
93
104
|
return { value: await authStatus(cwd, options, runtime) };
|
|
105
|
+
case "auth list":
|
|
106
|
+
return { value: await authList(runtime) };
|
|
107
|
+
case "auth create":
|
|
108
|
+
return { value: await authCreate(cwd, options, runtime) };
|
|
109
|
+
case "auth delete":
|
|
110
|
+
return { value: await authDelete(cwd, options, runtime) };
|
|
111
|
+
case "auth activate":
|
|
112
|
+
return { value: await authActivate(cwd, options, runtime) };
|
|
113
|
+
case "auth deactivate":
|
|
114
|
+
return { value: await authDeactivate(cwd, options, runtime) };
|
|
94
115
|
case "auth login":
|
|
95
116
|
return { value: await authLogin(cwd, options, runtime) };
|
|
96
117
|
case "auth logout":
|
|
97
|
-
return { value: await authLogout(options, runtime) };
|
|
118
|
+
return { value: await authLogout(cwd, options, runtime) };
|
|
98
119
|
case "link":
|
|
99
120
|
return { value: await linkProject(cwd, options, runtime) };
|
|
100
121
|
case "doctor":
|
|
101
122
|
return await doctor(cwd, options, runtime);
|
|
102
123
|
case "preview":
|
|
103
124
|
return { value: await preview(cwd, options, runtime) };
|
|
125
|
+
case "tail":
|
|
126
|
+
await tail(cwd, options, runtime);
|
|
127
|
+
return {};
|
|
104
128
|
case "delivery status":
|
|
105
129
|
return { value: await deliveryStatus(cwd, options, runtime) };
|
|
106
130
|
case "delivery preview":
|
|
@@ -111,38 +135,166 @@ async function execute(topic, options, cwd, runtime) {
|
|
|
111
135
|
throw new CliError("command_unknown", `Unknown command: ${topic}.`, "tender-accounts --help");
|
|
112
136
|
}
|
|
113
137
|
}
|
|
138
|
+
async function tail(cwd, options, runtime) {
|
|
139
|
+
const context = await loadProjectContext(cwd);
|
|
140
|
+
if (!context.link) {
|
|
141
|
+
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.", "tender-accounts link --json");
|
|
142
|
+
}
|
|
143
|
+
const projectId = requiredProjectId(context.link.projectId);
|
|
144
|
+
const resolved = await authenticatedClient(cwd, context.link, options, runtime);
|
|
145
|
+
const production = options.booleans.has("production");
|
|
146
|
+
const deliveryId = options.flags.get("delivery");
|
|
147
|
+
if (production === Boolean(deliveryId)) {
|
|
148
|
+
throw new CliError("runtime_log_target_required", "Choose exactly one runtime log target: --delivery dly_... or --production.", "tender-accounts tail --delivery dly_... --json");
|
|
149
|
+
}
|
|
150
|
+
if (deliveryId && !/^dly_[A-Za-z0-9_-]{8,64}$/u.test(deliveryId)) {
|
|
151
|
+
throw new CliError("developer_delivery_id_required", "--delivery must be a dly_ developer-delivery ID.");
|
|
152
|
+
}
|
|
153
|
+
const filters = {
|
|
154
|
+
statuses: csvFlag(options, "status"),
|
|
155
|
+
methods: csvFlag(options, "method"),
|
|
156
|
+
search: options.flags.get("search") ?? null,
|
|
157
|
+
samplingRate: decimalFlag(options, "sampling-rate", 0, 1, 1),
|
|
158
|
+
};
|
|
159
|
+
const session = await resolved.client.createRuntimeLogSession(projectId, {
|
|
160
|
+
environment: production ? "production" : "preview",
|
|
161
|
+
...(deliveryId ? { deliveryId } : {}),
|
|
162
|
+
filters,
|
|
163
|
+
});
|
|
164
|
+
const streamUrl = new URL(session.streamUrl);
|
|
165
|
+
streamUrl.protocol = streamUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
166
|
+
runtime.stderr.write(`Streaming ${session.environment} logs for ${projectId} until ${session.expiresAt}. Press Ctrl+C to stop.\n`);
|
|
167
|
+
const socket = runtime.openLogStream(streamUrl.toString(), [session.protocol, `tender-auth.${session.streamToken}`]);
|
|
168
|
+
let interrupted = false;
|
|
169
|
+
const removeInterrupt = runtime.onInterrupt(() => {
|
|
170
|
+
interrupted = true;
|
|
171
|
+
socket.close(1000, "CLI interrupted");
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
175
|
+
socket.addEventListener("message", (event) => {
|
|
176
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
177
|
+
if (options.json)
|
|
178
|
+
runtime.stdout.write(`${raw}\n`);
|
|
179
|
+
else
|
|
180
|
+
runtime.stdout.write(`${formatRuntimeLogEvent(raw)}\n`);
|
|
181
|
+
});
|
|
182
|
+
socket.addEventListener("close", (event) => {
|
|
183
|
+
if (!interrupted && event.code !== 1000) {
|
|
184
|
+
rejectPromise(new CliError("runtime_log_stream_closed", `Runtime log stream closed (${event.code}${event.reason ? `: ${event.reason}` : ""}).`));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
resolvePromise();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
socket.addEventListener("error", () => rejectPromise(new CliError("runtime_log_stream_failed", "Tender could not connect to the runtime log stream.", "Run tender-accounts doctor --json, then start a new tail session.")));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
removeInterrupt();
|
|
195
|
+
await resolved.client.closeRuntimeLogSession(session.id).catch(() => undefined);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function formatRuntimeLogEvent(raw) {
|
|
199
|
+
try {
|
|
200
|
+
const value = JSON.parse(raw);
|
|
201
|
+
if (!isRecord(value))
|
|
202
|
+
return terminalSafe(raw);
|
|
203
|
+
const event = isRecord(value.event) ? value.event : {};
|
|
204
|
+
const timestamp = typeof value.timestamp === "number" ? new Date(value.timestamp).toISOString() : "unknown-time";
|
|
205
|
+
const request = [event.method, event.url, event.status]
|
|
206
|
+
.filter((part) => part !== undefined)
|
|
207
|
+
.map((part) => terminalSafe(String(part)))
|
|
208
|
+
.join(" ");
|
|
209
|
+
const lines = [`${timestamp} ${terminalSafe(String(value.outcome ?? "unknown"))}${request ? ` ${request}` : ""}`];
|
|
210
|
+
if (Array.isArray(value.logs)) {
|
|
211
|
+
for (const log of value.logs) {
|
|
212
|
+
if (isRecord(log))
|
|
213
|
+
lines.push(` ${String(log.level ?? "log")}: ${JSON.stringify(log.message ?? [])}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(value.exceptions)) {
|
|
217
|
+
for (const exception of value.exceptions) {
|
|
218
|
+
if (isRecord(exception)) {
|
|
219
|
+
lines.push(` exception: ${terminalSafe(String(exception.name ?? "Error"))}: ${terminalSafe(String(exception.message ?? ""))}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return terminalSafe(raw);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function terminalSafe(value) {
|
|
230
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, (character) => (`\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`));
|
|
231
|
+
}
|
|
114
232
|
async function authStatus(cwd, options, runtime) {
|
|
115
233
|
const context = await optionalContext(cwd);
|
|
116
234
|
const resolved = await authenticatedClient(cwd, context?.link ?? null, options, runtime);
|
|
117
235
|
const principal = await resolved.client.whoami();
|
|
236
|
+
if (resolved.profile)
|
|
237
|
+
await savePrincipalSummary(runtime.authStore, resolved.profile, principal);
|
|
118
238
|
return {
|
|
119
239
|
...principal,
|
|
120
240
|
authentication: {
|
|
121
241
|
source: resolved.source,
|
|
122
242
|
apiUrl: resolved.apiUrl,
|
|
123
243
|
...(resolved.accessTokenExpiresAt ? { accessTokenExpiresAt: resolved.accessTokenExpiresAt } : {}),
|
|
124
|
-
...(resolved.source === "login" ? { storage: runtime.authStore.description() } : {}),
|
|
244
|
+
...(resolved.source === "login" ? { profile: resolved.profile, storage: runtime.authStore.description() } : {}),
|
|
125
245
|
},
|
|
126
246
|
};
|
|
127
247
|
}
|
|
248
|
+
async function authList(runtime) {
|
|
249
|
+
const profiles = await runtime.authStore.list();
|
|
250
|
+
const bindings = await runtime.profileBindings.list();
|
|
251
|
+
return {
|
|
252
|
+
schema: "tender.accounts-auth-profiles/v1",
|
|
253
|
+
storage: runtime.authStore.description(),
|
|
254
|
+
bindingStorage: runtime.profileBindings.description(),
|
|
255
|
+
profiles: profiles.map(({ profile, auth }) => ({
|
|
256
|
+
profile,
|
|
257
|
+
default: profile === DEFAULT_AUTH_PROFILE,
|
|
258
|
+
apiUrl: auth.apiUrl,
|
|
259
|
+
status: auth.pendingDevice ? "authorization_pending" : "authenticated",
|
|
260
|
+
...(auth.pendingDevice ? { expiresAt: auth.pendingDevice.expiresAt } : {}),
|
|
261
|
+
...(!auth.pendingDevice && auth.refreshTokenExpiresAt ? { expiresAt: auth.refreshTokenExpiresAt } : {}),
|
|
262
|
+
boundDirectories: bindings
|
|
263
|
+
.filter((binding) => binding.profile === profile)
|
|
264
|
+
.map((binding) => binding.directory),
|
|
265
|
+
containsCredential: false,
|
|
266
|
+
})),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
128
269
|
async function authLogin(cwd, options, runtime) {
|
|
270
|
+
return await authenticateProfile(cwd, options, runtime, DEFAULT_AUTH_PROFILE, options.booleans.has("force"));
|
|
271
|
+
}
|
|
272
|
+
async function authCreate(cwd, options, runtime) {
|
|
273
|
+
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
274
|
+
const profile = validateNamedAuthProfile(requiredPositional(options, 0, "profile name"));
|
|
275
|
+
return await authenticateProfile(cwd, options, runtime, profile, true);
|
|
276
|
+
}
|
|
277
|
+
async function authenticateProfile(cwd, options, runtime, profile, replaceExisting) {
|
|
129
278
|
const context = await optionalContext(cwd);
|
|
130
279
|
const apiUrl = selectedApiUrl(options, context?.link ?? null, runtime.environment);
|
|
131
280
|
const projectHint = options.flags.get("project") ?? context?.link?.projectId;
|
|
132
281
|
const clientName = options.flags.get("client-name") ?? `Tender CLI on ${hostname()}`;
|
|
133
|
-
const existing = await runtime.authStore.read();
|
|
134
|
-
if (existing && !
|
|
282
|
+
const existing = await runtime.authStore.read(profile);
|
|
283
|
+
if (existing && !replaceExisting) {
|
|
284
|
+
const replacement = profile === DEFAULT_AUTH_PROFILE
|
|
285
|
+
? "tender-accounts auth login --force"
|
|
286
|
+
: `tender-accounts auth create ${profile}`;
|
|
135
287
|
throw new CliError("already_authenticated", existing.pendingDevice
|
|
136
|
-
?
|
|
137
|
-
:
|
|
138
|
-
?
|
|
139
|
-
:
|
|
288
|
+
? `Tender CLI profile ${profile} already has a pending device authorization.`
|
|
289
|
+
: `Tender CLI profile ${profile} is already authenticated.`, existing.pendingDevice
|
|
290
|
+
? `Run tender-accounts auth status --profile ${profile} --json after approving it, or ${replacement} to replace it.`
|
|
291
|
+
: `Run tender-accounts auth status --profile ${profile} --json, or ${replacement} to replace it.`);
|
|
140
292
|
}
|
|
141
293
|
if (existing?.refreshToken) {
|
|
142
294
|
await new OAuthApiClient(existing.apiUrl, runtime.fetcher).revoke(existing.refreshToken);
|
|
143
295
|
}
|
|
144
296
|
if (existing)
|
|
145
|
-
await runtime.authStore
|
|
297
|
+
await clearStoredProfile(runtime.authStore, profile, existing);
|
|
146
298
|
const oauth = new OAuthApiClient(apiUrl, runtime.fetcher);
|
|
147
299
|
if (options.booleans.has("device")) {
|
|
148
300
|
const device = await oauth.deviceAuthorization({ clientName, ...(projectHint ? { projectHint } : {}) });
|
|
@@ -159,7 +311,8 @@ async function authLogin(cwd, options, runtime) {
|
|
|
159
311
|
intervalSeconds: device.interval,
|
|
160
312
|
},
|
|
161
313
|
};
|
|
162
|
-
await runtime.authStore.
|
|
314
|
+
if (!await runtime.authStore.compareAndSwap(profile, null, pending))
|
|
315
|
+
throw authProfileChanged(profile);
|
|
163
316
|
if (!options.booleans.has("no-open"))
|
|
164
317
|
await runtime.openBrowser(device.verification_uri_complete);
|
|
165
318
|
if (!options.booleans.has("wait")) {
|
|
@@ -169,13 +322,14 @@ async function authLogin(cwd, options, runtime) {
|
|
|
169
322
|
userCode: device.user_code,
|
|
170
323
|
verificationUrl: device.verification_uri,
|
|
171
324
|
verificationUrlComplete: device.verification_uri_complete,
|
|
325
|
+
profile,
|
|
172
326
|
expiresAt,
|
|
173
|
-
next:
|
|
327
|
+
next: `Approve the request, then run tender-accounts auth status --profile ${profile} --json.`,
|
|
174
328
|
};
|
|
175
329
|
}
|
|
176
|
-
const token = await waitForDeviceToken(oauth, pending, runtime);
|
|
177
|
-
await saveTokenResponse(runtime.authStore, apiUrl, token);
|
|
178
|
-
return await authenticatedLoginSummary(apiUrl, token, runtime);
|
|
330
|
+
const token = await waitForDeviceToken(oauth, pending, profile, runtime);
|
|
331
|
+
await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, pending);
|
|
332
|
+
return await authenticatedLoginSummary(apiUrl, token, profile, runtime);
|
|
179
333
|
}
|
|
180
334
|
const pkce = createPkcePair();
|
|
181
335
|
const callback = await runtime.waitForLoopbackAuthorization({
|
|
@@ -199,32 +353,117 @@ async function authLogin(cwd, options, runtime) {
|
|
|
199
353
|
codeVerifier: pkce.verifier,
|
|
200
354
|
redirectUri: callback.redirectUri,
|
|
201
355
|
});
|
|
202
|
-
await saveTokenResponse(runtime.authStore, apiUrl, token);
|
|
203
|
-
return await authenticatedLoginSummary(apiUrl, token, runtime);
|
|
356
|
+
await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, null);
|
|
357
|
+
return await authenticatedLoginSummary(apiUrl, token, profile, runtime);
|
|
204
358
|
}
|
|
205
|
-
async function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
359
|
+
async function authActivate(cwd, options, runtime) {
|
|
360
|
+
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
361
|
+
const profile = validateNamedAuthProfile(requiredPositional(options, 0, "profile name"));
|
|
362
|
+
if (!await runtime.authStore.read(profile)) {
|
|
363
|
+
throw new CliError("auth_profile_not_found", `Tender Accounts auth profile ${profile} does not exist.`, `tender-accounts auth create ${profile}`);
|
|
209
364
|
}
|
|
210
|
-
const
|
|
365
|
+
const directory = resolve(cwd, options.positionals[1] ?? ".");
|
|
366
|
+
await runtime.profileBindings.activate(profile, directory);
|
|
367
|
+
return {
|
|
368
|
+
status: "activated",
|
|
369
|
+
profile,
|
|
370
|
+
directory,
|
|
371
|
+
inheritedByDescendants: true,
|
|
372
|
+
containsCredential: false,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function authDeactivate(cwd, options, runtime) {
|
|
376
|
+
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
377
|
+
const directory = resolve(cwd, options.positionals[0] ?? ".");
|
|
378
|
+
const removed = await runtime.profileBindings.deactivate(directory);
|
|
379
|
+
const fallback = await runtime.profileBindings.resolve(directory);
|
|
380
|
+
return {
|
|
381
|
+
status: "deactivated",
|
|
382
|
+
profile: removed.profile,
|
|
383
|
+
directory: removed.directory,
|
|
384
|
+
activeProfile: fallback?.profile ?? (await runtime.authStore.read(DEFAULT_AUTH_PROFILE) ? DEFAULT_AUTH_PROFILE : null),
|
|
385
|
+
containsCredential: false,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function authDelete(_cwd, options, runtime) {
|
|
389
|
+
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
390
|
+
const profile = validateNamedAuthProfile(requiredPositional(options, 0, "profile name"));
|
|
391
|
+
const stored = await runtime.authStore.read(profile);
|
|
211
392
|
if (!stored)
|
|
212
|
-
|
|
213
|
-
if (stored.refreshToken) {
|
|
393
|
+
throw new CliError("auth_profile_not_found", `Tender Accounts auth profile ${profile} does not exist.`);
|
|
394
|
+
if (!options.booleans.has("local") && stored.refreshToken) {
|
|
214
395
|
await new OAuthApiClient(stored.apiUrl, runtime.fetcher).revoke(stored.refreshToken);
|
|
215
396
|
}
|
|
216
|
-
await runtime.authStore
|
|
397
|
+
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
398
|
+
const removedBindings = await runtime.profileBindings.removeProfile(profile);
|
|
399
|
+
return {
|
|
400
|
+
status: "deleted",
|
|
401
|
+
profile,
|
|
402
|
+
remoteSessionRevoked: !options.booleans.has("local") && Boolean(stored.refreshToken),
|
|
403
|
+
removedBindings,
|
|
404
|
+
storage: runtime.authStore.description(),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function assertNoEnvironmentCredentialForProfileManagement(environment) {
|
|
408
|
+
if (!environment.TENDER_ACCOUNTS_TOKEN)
|
|
409
|
+
return;
|
|
410
|
+
throw new CliError("auth_profile_environment_override", "TENDER_ACCOUNTS_TOKEN overrides local auth profiles, so profile management is disabled for this command.", "Unset TENDER_ACCOUNTS_TOKEN, then retry the auth profile command.");
|
|
411
|
+
}
|
|
412
|
+
async function authLogout(_cwd, options, runtime) {
|
|
413
|
+
if (options.booleans.has("all") && options.booleans.has("local")) {
|
|
414
|
+
await runtime.authStore.clearAll();
|
|
415
|
+
await runtime.profileBindings.clearAll();
|
|
416
|
+
return {
|
|
417
|
+
status: "logged_out",
|
|
418
|
+
profiles: "all",
|
|
419
|
+
remoteSessionRevoked: false,
|
|
420
|
+
remoteSessionsRevoked: 0,
|
|
421
|
+
storage: runtime.authStore.description(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const profiles = await runtime.authStore.list();
|
|
425
|
+
if (profiles.length === 0)
|
|
426
|
+
return { status: "not_authenticated", remoteSessionRevoked: false };
|
|
427
|
+
if (!options.booleans.has("all") && !profiles.some(({ profile }) => profile === DEFAULT_AUTH_PROFILE)) {
|
|
428
|
+
return {
|
|
429
|
+
status: "not_authenticated",
|
|
430
|
+
profile: DEFAULT_AUTH_PROFILE,
|
|
431
|
+
namedProfilesRetained: profiles.length,
|
|
432
|
+
remoteSessionRevoked: false,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const selectedProfiles = options.booleans.has("all")
|
|
436
|
+
? profiles.map(({ profile }) => profile)
|
|
437
|
+
: [DEFAULT_AUTH_PROFILE];
|
|
438
|
+
let revoked = 0;
|
|
439
|
+
for (const profile of selectedProfiles) {
|
|
440
|
+
const stored = await runtime.authStore.read(profile);
|
|
441
|
+
if (!stored)
|
|
442
|
+
continue;
|
|
443
|
+
if (!options.booleans.has("local") && stored.refreshToken) {
|
|
444
|
+
await new OAuthApiClient(stored.apiUrl, runtime.fetcher).revoke(stored.refreshToken);
|
|
445
|
+
revoked += 1;
|
|
446
|
+
}
|
|
447
|
+
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
448
|
+
}
|
|
449
|
+
if (options.booleans.has("all"))
|
|
450
|
+
await runtime.profileBindings.clearAll();
|
|
217
451
|
return {
|
|
218
452
|
status: "logged_out",
|
|
219
|
-
|
|
453
|
+
profiles: selectedProfiles,
|
|
454
|
+
remoteSessionRevoked: revoked > 0,
|
|
455
|
+
remoteSessionsRevoked: revoked,
|
|
220
456
|
storage: runtime.authStore.description(),
|
|
221
457
|
};
|
|
222
458
|
}
|
|
223
459
|
async function linkProject(cwd, options, runtime) {
|
|
224
460
|
const context = await loadProjectContext(cwd);
|
|
225
461
|
const apiUrl = selectedApiUrl(options, context.link, runtime.environment);
|
|
226
|
-
const
|
|
462
|
+
const resolved = await authenticatedClient(context.root, context.link, options, runtime);
|
|
463
|
+
const { client } = resolved;
|
|
227
464
|
const principal = await client.whoami();
|
|
465
|
+
if (resolved.profile)
|
|
466
|
+
await savePrincipalSummary(runtime.authStore, resolved.profile, principal);
|
|
228
467
|
const requested = options.flags.get("project");
|
|
229
468
|
const project = requested
|
|
230
469
|
? principal.projects.find((candidate) => candidate.projectId === requiredProjectId(requested))
|
|
@@ -248,6 +487,7 @@ async function linkProject(cwd, options, runtime) {
|
|
|
248
487
|
projectId: project.projectId,
|
|
249
488
|
role: project.role,
|
|
250
489
|
apiUrl,
|
|
490
|
+
...(resolved.profile ? { activeProfile: resolved.profile } : {}),
|
|
251
491
|
linkPath: path,
|
|
252
492
|
containsCredential: false,
|
|
253
493
|
reminder: "Keep .tender/ and .dev.vars out of Git.",
|
|
@@ -407,63 +647,85 @@ async function authenticatedClient(cwd, link, options, runtime) {
|
|
|
407
647
|
: "dev-vars";
|
|
408
648
|
return { client: new AgentApiClient(apiUrl, explicit, runtime.fetcher), apiUrl, source };
|
|
409
649
|
}
|
|
410
|
-
const
|
|
650
|
+
const profile = await selectAuthProfile(cwd, options, runtime);
|
|
651
|
+
const stored = await resolveStoredLogin(apiUrl, profile, runtime);
|
|
411
652
|
return {
|
|
412
653
|
client: new AgentApiClient(apiUrl, stored.accessToken, runtime.fetcher),
|
|
413
654
|
apiUrl,
|
|
414
655
|
source: "login",
|
|
656
|
+
profile,
|
|
415
657
|
accessTokenExpiresAt: stored.accessTokenExpiresAt,
|
|
416
658
|
};
|
|
417
659
|
}
|
|
660
|
+
async function selectAuthProfile(cwd, options, runtime) {
|
|
661
|
+
const explicit = options.flags.get("profile") ?? runtime.environment.TENDER_ACCOUNTS_PROFILE;
|
|
662
|
+
if (explicit)
|
|
663
|
+
return validateAuthProfile(explicit);
|
|
664
|
+
const binding = await runtime.profileBindings.resolve(cwd);
|
|
665
|
+
if (binding)
|
|
666
|
+
return binding.profile;
|
|
667
|
+
const profiles = await runtime.authStore.list();
|
|
668
|
+
if (profiles.some(({ profile }) => profile === DEFAULT_AUTH_PROFILE))
|
|
669
|
+
return DEFAULT_AUTH_PROFILE;
|
|
670
|
+
if (profiles.length === 0) {
|
|
671
|
+
throw new CliError("authentication_required", "No Tender Accounts credential or CLI login was found.", "tender-accounts auth login, or tender-accounts auth create merchant-name");
|
|
672
|
+
}
|
|
673
|
+
throw new CliError("auth_profile_required", "Named Tender Accounts logins are available, but this directory does not select one and no default login exists.", `Run tender-accounts auth list --json, then tender-accounts auth activate ${profiles[0].profile}.`);
|
|
674
|
+
}
|
|
418
675
|
function selectedApiUrl(options, link, environment) {
|
|
419
676
|
return normalizeApiUrl(options.flags.get("api-url")
|
|
420
677
|
?? environment.TENDER_ACCOUNTS_API_URL
|
|
421
678
|
?? link?.apiUrl
|
|
422
679
|
?? "https://agents.tenderprompt.dev");
|
|
423
680
|
}
|
|
424
|
-
async function resolveStoredLogin(apiUrl, runtime) {
|
|
425
|
-
let stored = await runtime.authStore.read();
|
|
681
|
+
async function resolveStoredLogin(apiUrl, profile, runtime) {
|
|
682
|
+
let stored = await runtime.authStore.read(profile);
|
|
426
683
|
if (!stored) {
|
|
427
|
-
throw new CliError("authentication_required",
|
|
684
|
+
throw new CliError("authentication_required", `Tender Accounts auth profile ${profile} is not logged in.`, authCommandForProfile(profile));
|
|
428
685
|
}
|
|
429
686
|
if (stored.apiUrl !== apiUrl) {
|
|
430
|
-
throw new CliError("auth_origin_mismatch", `The stored login belongs to ${stored.apiUrl}, not ${apiUrl}.`,
|
|
687
|
+
throw new CliError("auth_origin_mismatch", `The stored login belongs to ${stored.apiUrl}, not ${apiUrl}.`, `${authCommandForProfile(profile)} --api-url ${apiUrl}${profile === DEFAULT_AUTH_PROFILE ? " --force" : ""}`);
|
|
431
688
|
}
|
|
432
689
|
const oauth = new OAuthApiClient(apiUrl, runtime.fetcher);
|
|
433
690
|
if (stored.pendingDevice) {
|
|
434
691
|
const pendingDevice = stored.pendingDevice;
|
|
435
692
|
if (Date.parse(pendingDevice.expiresAt) <= Date.now()) {
|
|
436
|
-
await runtime.authStore
|
|
437
|
-
throw new CliError("device_authorization_expired", "The pending device authorization expired.",
|
|
693
|
+
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
694
|
+
throw new CliError("device_authorization_expired", "The pending device authorization expired.", `${authCommandForProfile(profile)} --device --json`);
|
|
438
695
|
}
|
|
439
696
|
try {
|
|
440
697
|
const token = await oauth.exchangeDeviceCode(pendingDevice.deviceCode);
|
|
441
|
-
stored = await saveTokenResponse(runtime.authStore, apiUrl, token);
|
|
698
|
+
stored = await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, stored);
|
|
442
699
|
}
|
|
443
700
|
catch (error) {
|
|
444
701
|
if (error instanceof CliApiError && error.code === "authorization_pending") {
|
|
445
|
-
throw new CliError("authorization_pending", `Approve Tender CLI with code ${pendingDevice.userCode}.`, `Open ${pendingDevice.verificationUriComplete}, then run tender-accounts auth status --json.`);
|
|
702
|
+
throw new CliError("authorization_pending", `Approve Tender CLI with code ${pendingDevice.userCode}.`, `Open ${pendingDevice.verificationUriComplete}, then run tender-accounts auth status --profile ${profile} --json.`);
|
|
446
703
|
}
|
|
447
704
|
throw error;
|
|
448
705
|
}
|
|
449
706
|
}
|
|
450
707
|
if (!stored.accessToken || !stored.accessTokenExpiresAt || !stored.refreshToken || !stored.refreshTokenExpiresAt) {
|
|
451
|
-
throw new CliError("authentication_required", "The stored Tender Accounts login is incomplete.",
|
|
708
|
+
throw new CliError("authentication_required", "The stored Tender Accounts login is incomplete.", `${authCommandForProfile(profile)}${profile === DEFAULT_AUTH_PROFILE ? " --force" : ""}`);
|
|
452
709
|
}
|
|
453
710
|
if (Date.parse(stored.refreshTokenExpiresAt) <= Date.now()) {
|
|
454
|
-
await runtime.authStore
|
|
455
|
-
throw new CliError("authentication_expired", "The Tender Accounts login expired.",
|
|
711
|
+
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
712
|
+
throw new CliError("authentication_expired", "The Tender Accounts login expired.", authCommandForProfile(profile));
|
|
456
713
|
}
|
|
457
714
|
if (Date.parse(stored.accessTokenExpiresAt) <= Date.now() + 30_000) {
|
|
458
715
|
const token = await oauth.refresh(stored.refreshToken);
|
|
459
|
-
stored = await saveTokenResponse(runtime.authStore, apiUrl, token);
|
|
716
|
+
stored = await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, stored);
|
|
460
717
|
}
|
|
461
718
|
if (!stored.accessToken || !stored.accessTokenExpiresAt) {
|
|
462
|
-
throw new CliError("authentication_required", "Tender Accounts could not restore the stored login.",
|
|
719
|
+
throw new CliError("authentication_required", "Tender Accounts could not restore the stored login.", `${authCommandForProfile(profile)}${profile === DEFAULT_AUTH_PROFILE ? " --force" : ""}`);
|
|
463
720
|
}
|
|
464
721
|
return { accessToken: stored.accessToken, accessTokenExpiresAt: stored.accessTokenExpiresAt };
|
|
465
722
|
}
|
|
466
|
-
|
|
723
|
+
function authCommandForProfile(profile) {
|
|
724
|
+
return profile === DEFAULT_AUTH_PROFILE
|
|
725
|
+
? "tender-accounts auth login"
|
|
726
|
+
: `tender-accounts auth create ${profile}`;
|
|
727
|
+
}
|
|
728
|
+
async function saveTokenResponse(store, profile, apiUrl, token, oauth, previous) {
|
|
467
729
|
const now = Date.now();
|
|
468
730
|
const stored = {
|
|
469
731
|
schema: CLI_AUTH_SCHEMA,
|
|
@@ -472,11 +734,27 @@ async function saveTokenResponse(store, apiUrl, token) {
|
|
|
472
734
|
accessTokenExpiresAt: new Date(now + token.expires_in * 1000).toISOString(),
|
|
473
735
|
refreshToken: token.refresh_token,
|
|
474
736
|
refreshTokenExpiresAt: new Date(now + token.refresh_expires_in * 1000).toISOString(),
|
|
737
|
+
...(previous?.principal ? { principal: previous.principal } : {}),
|
|
475
738
|
};
|
|
476
|
-
await store.
|
|
739
|
+
if (!await store.compareAndSwap(profile, previous, stored)) {
|
|
740
|
+
try {
|
|
741
|
+
await oauth.revoke(token.refresh_token);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
throw new CliError("auth_profile_race_revoke_failed", `Tender Accounts profile ${profile} changed while authentication was rotating, and the newly issued session could not be revoked safely.`, `Run tender-accounts auth status --profile ${profile} --json, then tender-accounts auth delete ${profile} if the session is not expected.`);
|
|
745
|
+
}
|
|
746
|
+
throw authProfileChanged(profile);
|
|
747
|
+
}
|
|
477
748
|
return stored;
|
|
478
749
|
}
|
|
479
|
-
async function
|
|
750
|
+
async function clearStoredProfile(store, profile, expected) {
|
|
751
|
+
if (!await store.compareAndSwap(profile, expected, null))
|
|
752
|
+
throw authProfileChanged(profile);
|
|
753
|
+
}
|
|
754
|
+
function authProfileChanged(profile) {
|
|
755
|
+
return new CliError("auth_profile_changed", `Tender Accounts profile ${profile} changed while this command was running. No newer local login was overwritten.`, `Run tender-accounts auth status --profile ${profile} --json, then retry the command.`);
|
|
756
|
+
}
|
|
757
|
+
async function waitForDeviceToken(oauth, pending, profile, runtime) {
|
|
480
758
|
const device = pending.pendingDevice;
|
|
481
759
|
if (!device)
|
|
482
760
|
throw new CliError("device_authorization_missing", "The device authorization was not saved.");
|
|
@@ -490,15 +768,17 @@ async function waitForDeviceToken(oauth, pending, runtime) {
|
|
|
490
768
|
await runtime.sleep(device.intervalSeconds * 1000);
|
|
491
769
|
}
|
|
492
770
|
}
|
|
493
|
-
await runtime.authStore
|
|
494
|
-
throw new CliError("device_authorization_expired", "The device authorization expired.",
|
|
771
|
+
await clearStoredProfile(runtime.authStore, profile, pending);
|
|
772
|
+
throw new CliError("device_authorization_expired", "The device authorization expired.", `${authCommandForProfile(profile)} --device --json`);
|
|
495
773
|
}
|
|
496
|
-
async function authenticatedLoginSummary(apiUrl, token, runtime) {
|
|
774
|
+
async function authenticatedLoginSummary(apiUrl, token, profile, runtime) {
|
|
497
775
|
const principal = await new AgentApiClient(apiUrl, token.access_token, runtime.fetcher).whoami();
|
|
776
|
+
await savePrincipalSummary(runtime.authStore, profile, principal);
|
|
498
777
|
return {
|
|
499
778
|
status: "authenticated",
|
|
500
779
|
flow: "oauth",
|
|
501
780
|
apiUrl,
|
|
781
|
+
profile,
|
|
502
782
|
ownerEmail: principal.ownerEmail,
|
|
503
783
|
tenantId: principal.tenantId,
|
|
504
784
|
projects: principal.projects,
|
|
@@ -507,6 +787,21 @@ async function authenticatedLoginSummary(apiUrl, token, runtime) {
|
|
|
507
787
|
containsCredential: false,
|
|
508
788
|
};
|
|
509
789
|
}
|
|
790
|
+
async function savePrincipalSummary(store, profile, principal) {
|
|
791
|
+
const stored = await store.read(profile);
|
|
792
|
+
if (!stored)
|
|
793
|
+
return;
|
|
794
|
+
const updated = {
|
|
795
|
+
...stored,
|
|
796
|
+
principal: {
|
|
797
|
+
tenantId: principal.tenantId,
|
|
798
|
+
ownerEmail: principal.ownerEmail,
|
|
799
|
+
projects: principal.projects.map(({ projectId }) => projectId),
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
if (!await store.compareAndSwap(profile, stored, updated))
|
|
803
|
+
throw authProfileChanged(profile);
|
|
804
|
+
}
|
|
510
805
|
async function sourceState(root, runtime) {
|
|
511
806
|
let revision;
|
|
512
807
|
try {
|
|
@@ -639,14 +934,18 @@ function commandTopic(argv) {
|
|
|
639
934
|
function parseOptions(values) {
|
|
640
935
|
const flags = new Map();
|
|
641
936
|
const booleans = new Set();
|
|
937
|
+
const positionals = [];
|
|
642
938
|
const booleanNames = new Set([
|
|
643
939
|
"json", "token-stdin", "dry-run", "no-wait", "force",
|
|
644
|
-
"device", "wait", "no-open", "local",
|
|
940
|
+
"device", "wait", "no-open", "local", "all", "production",
|
|
645
941
|
]);
|
|
646
942
|
for (let index = 0; index < values.length; index += 1) {
|
|
647
943
|
const raw = values[index];
|
|
648
|
-
if (!raw?.startsWith("--"))
|
|
649
|
-
|
|
944
|
+
if (!raw?.startsWith("--")) {
|
|
945
|
+
if (raw)
|
|
946
|
+
positionals.push(raw);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
650
949
|
const name = raw.slice(2);
|
|
651
950
|
if (name === "token") {
|
|
652
951
|
throw new CliError("credential_argument_forbidden", "Never pass credentials as command-line arguments.");
|
|
@@ -664,11 +963,11 @@ function parseOptions(values) {
|
|
|
664
963
|
flags.set(name, value);
|
|
665
964
|
index += 1;
|
|
666
965
|
}
|
|
667
|
-
return { flags, booleans, json: booleans.has("json") };
|
|
966
|
+
return { flags, booleans, positionals, json: booleans.has("json") };
|
|
668
967
|
}
|
|
669
968
|
function assertAllowedOptions(topic, options) {
|
|
670
969
|
const common = ["cwd", "json"];
|
|
671
|
-
const authentication = ["api-url", "token-stdin"];
|
|
970
|
+
const authentication = ["api-url", "token-stdin", "profile"];
|
|
672
971
|
const allowed = {
|
|
673
972
|
init: [...common, "name", "slug", "directory", "template", "dry-run"],
|
|
674
973
|
"config example": common,
|
|
@@ -678,8 +977,13 @@ function assertAllowedOptions(topic, options) {
|
|
|
678
977
|
check: common,
|
|
679
978
|
build: common,
|
|
680
979
|
"auth status": [...common, ...authentication],
|
|
980
|
+
"auth list": common,
|
|
981
|
+
"auth create": [...common, "api-url", "project", "client-name", "timeout-seconds", "device", "wait", "no-open"],
|
|
982
|
+
"auth delete": [...common, "local"],
|
|
983
|
+
"auth activate": common,
|
|
984
|
+
"auth deactivate": common,
|
|
681
985
|
"auth login": [...common, "api-url", "project", "client-name", "timeout-seconds", "device", "wait", "no-open", "force"],
|
|
682
|
-
"auth logout": [...common, "local"],
|
|
986
|
+
"auth logout": [...common, "local", "all"],
|
|
683
987
|
link: [...common, ...authentication, "project"],
|
|
684
988
|
doctor: [...common, ...authentication],
|
|
685
989
|
preview: [
|
|
@@ -692,6 +996,16 @@ function assertAllowedOptions(topic, options) {
|
|
|
692
996
|
"timeout-seconds",
|
|
693
997
|
"return-path",
|
|
694
998
|
],
|
|
999
|
+
tail: [
|
|
1000
|
+
...common,
|
|
1001
|
+
...authentication,
|
|
1002
|
+
"delivery",
|
|
1003
|
+
"production",
|
|
1004
|
+
"status",
|
|
1005
|
+
"method",
|
|
1006
|
+
"search",
|
|
1007
|
+
"sampling-rate",
|
|
1008
|
+
],
|
|
695
1009
|
"delivery status": [...common, ...authentication, "delivery"],
|
|
696
1010
|
"delivery preview": [...common, ...authentication, "delivery", "return-path"],
|
|
697
1011
|
"delivery retry": [...common, ...authentication, "delivery"],
|
|
@@ -702,6 +1016,25 @@ function assertAllowedOptions(topic, options) {
|
|
|
702
1016
|
if (unexpected) {
|
|
703
1017
|
throw new CliError("argument_unknown", `Option --${unexpected} is not valid for ${topic}.`, `tender-accounts ${topic} --help`);
|
|
704
1018
|
}
|
|
1019
|
+
const positionalLimits = {
|
|
1020
|
+
"auth create": { minimum: 1, maximum: 1 },
|
|
1021
|
+
"auth delete": { minimum: 1, maximum: 1 },
|
|
1022
|
+
"auth activate": { minimum: 1, maximum: 2 },
|
|
1023
|
+
"auth deactivate": { minimum: 0, maximum: 1 },
|
|
1024
|
+
};
|
|
1025
|
+
const limit = positionalLimits[topic] ?? { minimum: 0, maximum: 0 };
|
|
1026
|
+
if (options.positionals.length < limit.minimum) {
|
|
1027
|
+
throw new CliError("argument_required", `Missing ${topic.startsWith("auth ") ? "profile name" : "argument"}.`, `tender-accounts ${topic} --help`);
|
|
1028
|
+
}
|
|
1029
|
+
if (options.positionals.length > limit.maximum) {
|
|
1030
|
+
throw new CliError("argument_unexpected", `Unexpected argument: ${options.positionals[limit.maximum] ?? ""}.`, `tender-accounts ${topic} --help`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function requiredPositional(options, index, label) {
|
|
1034
|
+
const value = options.positionals[index];
|
|
1035
|
+
if (!value)
|
|
1036
|
+
throw new CliError("argument_required", `Missing ${label}.`);
|
|
1037
|
+
return value;
|
|
705
1038
|
}
|
|
706
1039
|
function requiredFlag(options, name) {
|
|
707
1040
|
const value = options.flags.get(name);
|
|
@@ -737,6 +1070,26 @@ function numberFlag(options, name, minimum, maximum, fallback) {
|
|
|
737
1070
|
}
|
|
738
1071
|
return value;
|
|
739
1072
|
}
|
|
1073
|
+
function decimalFlag(options, name, minimumExclusive, maximum, fallback) {
|
|
1074
|
+
const raw = options.flags.get(name);
|
|
1075
|
+
if (!raw)
|
|
1076
|
+
return fallback;
|
|
1077
|
+
const value = Number(raw);
|
|
1078
|
+
if (!Number.isFinite(value) || value <= minimumExclusive || value > maximum) {
|
|
1079
|
+
throw new CliError("argument_invalid", `--${name} must be greater than ${minimumExclusive} and at most ${maximum}.`);
|
|
1080
|
+
}
|
|
1081
|
+
return value;
|
|
1082
|
+
}
|
|
1083
|
+
function csvFlag(options, name) {
|
|
1084
|
+
const raw = options.flags.get(name);
|
|
1085
|
+
if (!raw)
|
|
1086
|
+
return [];
|
|
1087
|
+
const values = [...new Set(raw.split(",").map((value) => value.trim()).filter(Boolean))];
|
|
1088
|
+
if (values.length > 12 || values.some((value) => value.length > 32)) {
|
|
1089
|
+
throw new CliError("argument_invalid", `--${name} has too many or overly long values.`);
|
|
1090
|
+
}
|
|
1091
|
+
return values;
|
|
1092
|
+
}
|
|
740
1093
|
function writeOutput(value, options, runtime) {
|
|
741
1094
|
if (options.json) {
|
|
742
1095
|
runtime.stdout.write(`${JSON.stringify(value)}\n`);
|
|
@@ -758,16 +1111,22 @@ function writeOutput(value, options, runtime) {
|
|
|
758
1111
|
function helpText(topic) {
|
|
759
1112
|
const topics = {
|
|
760
1113
|
init: "Usage: tender-accounts init --name NAME [--slug SLUG] [--directory PATH] [--template shopify-customer-account] [--dry-run] [--json]\n\nCreate a new merchant-owned Shopify gateway and React portal in an empty directory.\n\nExamples:\n tender-accounts init --name \"Acme account\" --directory ./acme-account\n tender-accounts init --name \"Acme account\" --directory ./acme-account --dry-run --json",
|
|
761
|
-
auth: "Usage: tender-accounts auth <login|status|logout> [options]",
|
|
762
|
-
"auth login": "Usage: tender-accounts auth login [--device] [--project PROJECT_ID] [--no-open] [--wait] [--force] [--json]\n\
|
|
763
|
-
"auth
|
|
764
|
-
"auth
|
|
765
|
-
|
|
1114
|
+
auth: "Usage: tender-accounts auth <login|create|activate|deactivate|status|list|delete|logout> [options]",
|
|
1115
|
+
"auth login": "Usage: tender-accounts auth login [--device] [--project PROJECT_ID] [--no-open] [--wait] [--force] [--json]\n\nCreate or refresh the default login. Browser-capable terminals use Authorization Code with PKCE. --device returns an agent-safe approval URL and code; add --wait to keep polling. Use auth create for named merchant profiles.",
|
|
1116
|
+
"auth create": "Usage: tender-accounts auth create NAME [--device] [--project PROJECT_ID] [--no-open] [--wait] [--json]\n\nCreate or re-authorize one named login without changing the active profile for any directory.\n\nExample:\n tender-accounts auth create acme --device --project prj_... --json",
|
|
1117
|
+
"auth activate": "Usage: tender-accounts auth activate NAME [DIRECTORY] [--json]\n\nActivate a named login for a directory and all descendants. A closer descendant binding wins. The binding is machine-local and is not written to the repository.",
|
|
1118
|
+
"auth deactivate": "Usage: tender-accounts auth deactivate [DIRECTORY] [--json]\n\nRemove the binding declared exactly at the directory. An inherited binding must be removed at the ancestor that declares it.",
|
|
1119
|
+
"auth delete": "Usage: tender-accounts auth delete NAME [--local] [--json]\n\nRevoke and delete one named login plus its directory bindings. --local keeps the remote session intact.",
|
|
1120
|
+
"auth status": "Usage: tender-accounts auth status [--profile NAME] [--api-url URL] [--token-stdin] [--json]\n\nValidate the selected login or explicit project-scoped credential and list its permitted projects. Selection order is explicit profile, nearest private directory binding, then default. Repository files cannot select a local profile.",
|
|
1121
|
+
"auth list": "Usage: tender-accounts auth list [--json]\n\nList locally stored profile names, expiration state, and bound directories without credentials or tenant metadata.",
|
|
1122
|
+
"auth logout": "Usage: tender-accounts auth logout [--all] [--local] [--json]\n\nRevoke the default login. --local removes only the local copy; --all must be explicit to remove every login and directory binding.",
|
|
1123
|
+
link: "Usage: tender-accounts link [--project PROJECT_ID] [--profile NAME] [--api-url URL] [--json]\n\nLink this checkout to exactly one project. The project ID remains a target guard; profile activation stays machine-local.",
|
|
766
1124
|
doctor: "Usage: tender-accounts doctor [--json]\n\nValidate configuration, authentication, project scope, and local artifact readiness.",
|
|
767
1125
|
dev: "Usage: tender-accounts dev\n\nRun the app's declared local development command. This starts local development and does not create a Tender deployment.",
|
|
768
1126
|
check: "Usage: tender-accounts check [--json]\n\nRun the app's declared validation command.",
|
|
769
1127
|
build: "Usage: tender-accounts build [--json]\n\nRun validation and packaging, then verify the complete portable artifact.",
|
|
770
1128
|
preview: "Usage: tender-accounts preview [--dry-run] [--no-wait] [--timeout-seconds N] [--return-path /account] [--json]\n\nBuild and submit an exact preview. --dry-run validates locally without authentication or upload.",
|
|
1129
|
+
tail: "Usage: tender-accounts tail (--delivery dly_... | --production) [--status ok,error,canceled] [--method GET,POST] [--search TEXT] [--sampling-rate 0.25] [--json]\n\nStream sanitized logs for this exact linked project. Preview tails require a succeeded dly_ delivery. Production tails require merchant-administrator authorization. Sessions expire after 15 minutes and do not persist events.",
|
|
771
1130
|
delivery: "Usage: tender-accounts delivery <status|preview|retry> --delivery dly_... [--json]\n\nThese commands accept the developer-delivery ID returned by preview. A dwf_ ID is a production workflow managed by merchant administrators in Activity.",
|
|
772
1131
|
"delivery status": "Usage: tender-accounts delivery status --delivery dly_... [--json]\n\nRead one developer preview delivery. Do not pass a dwf_ production workflow ID.",
|
|
773
1132
|
"delivery preview": "Usage: tender-accounts delivery preview --delivery dly_... [--return-path /account] [--json]\n\nMint a fresh 15-minute same-domain preview session. Do not pass a dwf_ production workflow ID.",
|
|
@@ -780,7 +1139,7 @@ function helpText(topic) {
|
|
|
780
1139
|
};
|
|
781
1140
|
if (topic && topics[topic])
|
|
782
1141
|
return topics[topic];
|
|
783
|
-
return `Tender Accounts merchant developer CLI ${CLI_VERSION}\n\nCommands:\n init Create a new Shopify account starter\n auth login
|
|
1142
|
+
return `Tender Accounts merchant developer CLI ${CLI_VERSION}\n\nCommands:\n init Create a new Shopify account starter\n auth login Create or refresh the default login\n auth create Create or re-authorize a named login\n auth activate Bind a named login to a directory tree\n auth deactivate Remove an exact directory binding\n auth status Validate one login or scoped credential\n auth list List local login profiles and bindings\n auth delete Revoke one named login\n auth logout Revoke the default or every login\n link Link this checkout to one permitted project\n doctor Check developer readiness\n dev Run the project-owned local dev command\n check Run project validation\n build Package and verify a portable artifact\n preview Build and deploy an exact preview\n tail Stream logs for one exact linked project\n delivery status Read preview workflow state\n delivery preview Mint a fresh preview session\n delivery retry Retry a failed preview workflow\n skill install Install the Tender Accounts agent skill\n config example Print the project configuration contract\n\nThere is intentionally no production publish command.\nRun tender-accounts <command> --help for examples.`;
|
|
784
1143
|
}
|
|
785
1144
|
function normalizeError(error) {
|
|
786
1145
|
if (error instanceof CliApiError) {
|