@tenderprompt/accounts 0.8.3 → 0.9.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 +6 -355
- package/cli.js +8 -0
- package/package.json +7 -29
- package/dist/api.d.ts +0 -109
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -141
- package/dist/api.js.map +0 -1
- package/dist/artifact.d.ts +0 -8
- package/dist/artifact.d.ts.map +0 -1
- package/dist/artifact.js +0 -205
- package/dist/artifact.js.map +0 -1
- package/dist/auth-store.d.ts +0 -51
- package/dist/auth-store.d.ts.map +0 -1
- package/dist/auth-store.js +0 -242
- package/dist/auth-store.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -4
- package/dist/cli.js.map +0 -1
- package/dist/contracts.d.ts +0 -226
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -5
- package/dist/contracts.js.map +0 -1
- package/dist/errors.d.ts +0 -10
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -19
- package/dist/errors.js.map +0 -1
- package/dist/files.d.ts +0 -22
- package/dist/files.d.ts.map +0 -1
- package/dist/files.js +0 -272
- package/dist/files.js.map +0 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +0 -1
- package/dist/main.d.ts +0 -37
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js +0 -2131
- package/dist/main.js.map +0 -1
- package/dist/oauth.d.ts +0 -67
- package/dist/oauth.d.ts.map +0 -1
- package/dist/oauth.js +0 -187
- package/dist/oauth.js.map +0 -1
- package/dist/processes.d.ts +0 -12
- package/dist/processes.d.ts.map +0 -1
- package/dist/processes.js +0 -48
- package/dist/processes.js.map +0 -1
- package/dist/profile-bindings.d.ts +0 -26
- package/dist/profile-bindings.d.ts.map +0 -1
- package/dist/profile-bindings.js +0 -214
- package/dist/profile-bindings.js.map +0 -1
- package/dist/starter.d.ts +0 -20
- package/dist/starter.d.ts.map +0 -1
- package/dist/starter.js +0 -154
- package/dist/starter.js.map +0 -1
- package/skill/tender-accounts/SKILL.md +0 -372
- package/templates/shopify-customer-account/AGENTS.md +0 -15
- package/templates/shopify-customer-account/README.md +0 -50
- package/templates/shopify-customer-account/apps/gateway/AGENTS.md +0 -10
- package/templates/shopify-customer-account/apps/gateway/README.md +0 -9
- package/templates/shopify-customer-account/apps/gateway/database-schema.json +0 -49
- package/templates/shopify-customer-account/apps/gateway/dot.dev.vars.example +0 -12
- package/templates/shopify-customer-account/apps/gateway/migrations/0001_customer_auth.sql +0 -76
- package/templates/shopify-customer-account/apps/gateway/package.json +0 -25
- package/templates/shopify-customer-account/apps/gateway/scripts/build-worker.mjs +0 -15
- package/templates/shopify-customer-account/apps/gateway/scripts/pack-tender-artifact.mjs +0 -85
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/hostname-policy.ts +0 -66
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/schema.ts +0 -90
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/session-store.ts +0 -864
- package/templates/shopify-customer-account/apps/gateway/src/platform/customer-auth/shopify-route-adapter.ts +0 -1157
- package/templates/shopify-customer-account/apps/gateway/src/platform/runtime-hostname.ts +0 -46
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.test.ts +0 -70
- package/templates/shopify-customer-account/apps/gateway/src/portal-service.ts +0 -71
- package/templates/shopify-customer-account/apps/gateway/src/runtime-config.test.ts +0 -30
- package/templates/shopify-customer-account/apps/gateway/src/runtime-config.ts +0 -56
- package/templates/shopify-customer-account/apps/gateway/src/worker.ts +0 -85
- package/templates/shopify-customer-account/apps/gateway/tender-accounts.json +0 -10
- package/templates/shopify-customer-account/apps/gateway/tender-app.config.json +0 -40
- package/templates/shopify-customer-account/apps/gateway/tsconfig.json +0 -13
- package/templates/shopify-customer-account/apps/gateway/vitest.config.ts +0 -8
- package/templates/shopify-customer-account/apps/gateway/worker-configuration.d.ts +0 -26
- package/templates/shopify-customer-account/apps/gateway/wrangler.jsonc +0 -42
- package/templates/shopify-customer-account/apps/portal/AGENTS.md +0 -10
- package/templates/shopify-customer-account/apps/portal/README.md +0 -7
- package/templates/shopify-customer-account/apps/portal/index.html +0 -13
- package/templates/shopify-customer-account/apps/portal/package.json +0 -33
- package/templates/shopify-customer-account/apps/portal/scripts/build-worker.mjs +0 -11
- package/templates/shopify-customer-account/apps/portal/scripts/pack-tender-artifact.mjs +0 -63
- package/templates/shopify-customer-account/apps/portal/scripts/portal-worker.mjs +0 -84
- package/templates/shopify-customer-account/apps/portal/scripts/portal-worker.test.mjs +0 -14
- package/templates/shopify-customer-account/apps/portal/src/App.tsx +0 -111
- package/templates/shopify-customer-account/apps/portal/src/main.tsx +0 -10
- package/templates/shopify-customer-account/apps/portal/src/styles.css +0 -54
- package/templates/shopify-customer-account/apps/portal/tender-accounts.json +0 -10
- package/templates/shopify-customer-account/apps/portal/tender-app.config.json +0 -19
- package/templates/shopify-customer-account/apps/portal/tsconfig.json +0 -14
- package/templates/shopify-customer-account/apps/portal/vite.config.ts +0 -7
- package/templates/shopify-customer-account/apps/portal/worker-configuration.d.ts +0 -11
- package/templates/shopify-customer-account/apps/portal/wrangler.jsonc +0 -22
- package/templates/shopify-customer-account/dot.dev.vars.example +0 -4
- package/templates/shopify-customer-account/dot.gitignore +0 -9
- package/templates/shopify-customer-account/package.json +0 -22
- package/templates/shopify-customer-account/scripts/artifact-contract.mjs +0 -109
package/dist/main.js
DELETED
|
@@ -1,2131 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { homedir, hostname, tmpdir } from "node:os";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import { AgentApiClient } from "./api.js";
|
|
8
|
-
import { CLI_AUTH_SCHEMA, DEFAULT_AUTH_PROFILE, createDefaultAuthStore, validateAuthProfile, validateNamedAuthProfile, } from "./auth-store.js";
|
|
9
|
-
import { labelLocalArtifact, verifyArtifact } from "./artifact.js";
|
|
10
|
-
import { BUILD_ENVELOPE_FILENAME, CLI_CONFIG_SCHEMA, CLI_LINK_SCHEMA, } from "./contracts.js";
|
|
11
|
-
import { CliApiError, CliError } from "./errors.js";
|
|
12
|
-
import { loadProjectContext, normalizeApiUrl, pathExists, readBoundedFile, readOptionalToken, requiredProjectId, resolveArtifactDirectory, safeArtifactPath, writeProjectLink, } from "./files.js";
|
|
13
|
-
import { OAuthApiClient, createPkcePair, openSystemBrowser, waitForLoopbackAuthorization, } from "./oauth.js";
|
|
14
|
-
import { captureCommand, runProjectCommand } from "./processes.js";
|
|
15
|
-
import { createDefaultProfileBindings, } from "./profile-bindings.js";
|
|
16
|
-
import { initializeStarter } from "./starter.js";
|
|
17
|
-
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
18
|
-
if (typeof packageMetadata.version !== "string" || packageMetadata.version.length === 0) {
|
|
19
|
-
throw new Error("Tender Accounts CLI package version is missing.");
|
|
20
|
-
}
|
|
21
|
-
export const CLI_VERSION = packageMetadata.version;
|
|
22
|
-
const DEFAULT_RUNTIME = {
|
|
23
|
-
cwd: process.cwd(),
|
|
24
|
-
homeDirectory: homedir(),
|
|
25
|
-
environment: process.env,
|
|
26
|
-
stdin: process.stdin,
|
|
27
|
-
stdout: process.stdout,
|
|
28
|
-
stderr: process.stderr,
|
|
29
|
-
fetcher: fetch,
|
|
30
|
-
sleep: async (milliseconds) => await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)),
|
|
31
|
-
runProjectCommand,
|
|
32
|
-
captureCommand,
|
|
33
|
-
authStore: createDefaultAuthStore(process.env),
|
|
34
|
-
profileBindings: createDefaultProfileBindings(process.env),
|
|
35
|
-
openBrowser: openSystemBrowser,
|
|
36
|
-
waitForLoopbackAuthorization,
|
|
37
|
-
openLogStream: (url, protocols) => new WebSocket(url, protocols),
|
|
38
|
-
onInterrupt: (handler) => {
|
|
39
|
-
process.once("SIGINT", handler);
|
|
40
|
-
process.once("SIGTERM", handler);
|
|
41
|
-
return () => {
|
|
42
|
-
process.off("SIGINT", handler);
|
|
43
|
-
process.off("SIGTERM", handler);
|
|
44
|
-
};
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
export async function runCli(argv, overrides = {}) {
|
|
48
|
-
const runtime = { ...DEFAULT_RUNTIME, ...overrides };
|
|
49
|
-
const jsonRequested = argv.includes("--json");
|
|
50
|
-
try {
|
|
51
|
-
const { topic, values } = commandTopic(argv);
|
|
52
|
-
if (topic === "help") {
|
|
53
|
-
runtime.stdout.write(`${helpText(values[0])}\n`);
|
|
54
|
-
return 0;
|
|
55
|
-
}
|
|
56
|
-
if (topic === "version") {
|
|
57
|
-
runtime.stdout.write(`${CLI_VERSION}\n`);
|
|
58
|
-
return 0;
|
|
59
|
-
}
|
|
60
|
-
const options = parseOptions(values);
|
|
61
|
-
assertAllowedOptions(topic, options);
|
|
62
|
-
const cwd = resolve(runtime.cwd, options.flags.get("cwd") ?? ".");
|
|
63
|
-
const result = await execute(topic, options, cwd, runtime);
|
|
64
|
-
if (result.raw !== undefined)
|
|
65
|
-
runtime.stdout.write(result.raw.endsWith("\n") ? result.raw : `${result.raw}\n`);
|
|
66
|
-
else if (result.value !== undefined)
|
|
67
|
-
writeOutput(result.value, options, runtime);
|
|
68
|
-
return result.exitCode ?? 0;
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
const normalized = normalizeError(error);
|
|
72
|
-
if (jsonRequested) {
|
|
73
|
-
runtime.stderr.write(`${JSON.stringify({ error: normalized })}\n`);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
runtime.stderr.write(`Error: ${normalized.message}\n`);
|
|
77
|
-
if (normalized.hint)
|
|
78
|
-
runtime.stderr.write(`Try: ${normalized.hint}\n`);
|
|
79
|
-
if (normalized.requestId)
|
|
80
|
-
runtime.stderr.write(`request_id: ${normalized.requestId}\n`);
|
|
81
|
-
}
|
|
82
|
-
return 1;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
async function execute(topic, options, cwd, runtime) {
|
|
86
|
-
switch (topic) {
|
|
87
|
-
case "init":
|
|
88
|
-
return { value: await initializeStarter({
|
|
89
|
-
cwd,
|
|
90
|
-
directory: options.flags.get("directory"),
|
|
91
|
-
name: requiredStarterName(options),
|
|
92
|
-
slug: options.flags.get("slug"),
|
|
93
|
-
template: options.flags.get("template"),
|
|
94
|
-
dryRun: options.booleans.has("dry-run"),
|
|
95
|
-
}) };
|
|
96
|
-
case "config example":
|
|
97
|
-
return { value: exampleConfig() };
|
|
98
|
-
case "skill print":
|
|
99
|
-
return options.json
|
|
100
|
-
? { value: { schema: "tender.accounts-skill/v1", content: await readSkill() } }
|
|
101
|
-
: { raw: await readSkill() };
|
|
102
|
-
case "skill install":
|
|
103
|
-
return { value: await installSkill(cwd, options, runtime) };
|
|
104
|
-
case "dev":
|
|
105
|
-
return await runDev(cwd, options, runtime);
|
|
106
|
-
case "check":
|
|
107
|
-
return { value: await runBuildStage(cwd, options, runtime, "check") };
|
|
108
|
-
case "build":
|
|
109
|
-
return { value: await buildProject(cwd, options, runtime) };
|
|
110
|
-
case "auth status":
|
|
111
|
-
return { value: await authStatus(cwd, options, runtime) };
|
|
112
|
-
case "auth list":
|
|
113
|
-
return { value: await authList(runtime) };
|
|
114
|
-
case "auth create":
|
|
115
|
-
return { value: await authCreate(cwd, options, runtime) };
|
|
116
|
-
case "auth delete":
|
|
117
|
-
return { value: await authDelete(cwd, options, runtime) };
|
|
118
|
-
case "auth activate":
|
|
119
|
-
return { value: await authActivate(cwd, options, runtime) };
|
|
120
|
-
case "auth deactivate":
|
|
121
|
-
return { value: await authDeactivate(cwd, options, runtime) };
|
|
122
|
-
case "auth login":
|
|
123
|
-
return { value: await authLogin(cwd, options, runtime) };
|
|
124
|
-
case "auth logout":
|
|
125
|
-
return { value: await authLogout(cwd, options, runtime) };
|
|
126
|
-
case "link":
|
|
127
|
-
return { value: await linkProject(cwd, options, runtime) };
|
|
128
|
-
case "doctor":
|
|
129
|
-
return await doctor(cwd, options, runtime);
|
|
130
|
-
case "preview":
|
|
131
|
-
return { value: await preview(cwd, options, runtime) };
|
|
132
|
-
case "source status":
|
|
133
|
-
return { value: await sourceStatus(cwd, options, runtime) };
|
|
134
|
-
case "source branches":
|
|
135
|
-
return { value: await sourceBranches(cwd, options, runtime) };
|
|
136
|
-
case "source log":
|
|
137
|
-
return { value: await sourceLog(cwd, options, runtime) };
|
|
138
|
-
case "source compare":
|
|
139
|
-
return { value: await sourceCompare(cwd, options, runtime) };
|
|
140
|
-
case "source default set":
|
|
141
|
-
return { value: await sourceDefaultSet(cwd, options, runtime) };
|
|
142
|
-
case "source promote":
|
|
143
|
-
return { value: await sourcePromote(cwd, options, runtime) };
|
|
144
|
-
case "source history replace":
|
|
145
|
-
return { value: await sourceHistoryReplace(cwd, options, runtime) };
|
|
146
|
-
case "source operation status":
|
|
147
|
-
return { value: await sourceOperationStatus(cwd, options, runtime) };
|
|
148
|
-
case "source operation retry":
|
|
149
|
-
return { value: await sourceOperationRetry(cwd, options, runtime) };
|
|
150
|
-
case "source change create":
|
|
151
|
-
return { value: await sourceChangeCreate(cwd, options, runtime) };
|
|
152
|
-
case "source change list":
|
|
153
|
-
return { value: await sourceChangeList(cwd, options, runtime) };
|
|
154
|
-
case "source change show":
|
|
155
|
-
return { value: await sourceChangeShow(cwd, options, runtime) };
|
|
156
|
-
case "source change close":
|
|
157
|
-
return { value: await sourceChangeClose(cwd, options, runtime) };
|
|
158
|
-
case "source connect":
|
|
159
|
-
return { value: await sourceConnect(cwd, options, runtime) };
|
|
160
|
-
case "source pull":
|
|
161
|
-
return { value: await sourcePull(cwd, options, runtime) };
|
|
162
|
-
case "source token":
|
|
163
|
-
return { value: await sourceToken(cwd, options, runtime) };
|
|
164
|
-
case "source push":
|
|
165
|
-
return { value: await sourcePush(cwd, options, runtime) };
|
|
166
|
-
case "tail":
|
|
167
|
-
await tail(cwd, options, runtime);
|
|
168
|
-
return {};
|
|
169
|
-
case "delivery status":
|
|
170
|
-
return { value: await deliveryStatus(cwd, options, runtime) };
|
|
171
|
-
case "delivery preview":
|
|
172
|
-
return { value: await deliveryPreview(cwd, options, runtime) };
|
|
173
|
-
case "delivery retry":
|
|
174
|
-
return { value: await deliveryRetry(cwd, options, runtime) };
|
|
175
|
-
default:
|
|
176
|
-
throw new CliError("command_unknown", `Unknown command: ${topic}.`, "tender-accounts --help");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
async function sourceStatus(cwd, options, runtime) {
|
|
180
|
-
const context = await optionalContext(cwd);
|
|
181
|
-
const { client, projectId } = await sourceProjectAccess(cwd, context?.link ?? null, options, runtime, false);
|
|
182
|
-
const result = await client.sourceRepository(projectId);
|
|
183
|
-
return {
|
|
184
|
-
status: result.repository ? "connected" : "not_connected",
|
|
185
|
-
projectId,
|
|
186
|
-
repository: result.repository ? publicManagedSourceRepository(result.repository) : null,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function publicManagedSourceRepository(repository) {
|
|
190
|
-
const { namespace: _providerNamespace, remoteUrl: _providerRemoteUrl, ...publicRepository } = repository;
|
|
191
|
-
return {
|
|
192
|
-
...publicRepository,
|
|
193
|
-
transport: "tender-managed",
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
async function sourceBranches(cwd, options, runtime) {
|
|
197
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
198
|
-
let credential = null;
|
|
199
|
-
try {
|
|
200
|
-
const token = await access.client.sourceToken(access.repository.id, 15 * 60, "read");
|
|
201
|
-
credential = token.token;
|
|
202
|
-
const result = await runtime.captureCommand("git", ["ls-remote", "--heads", access.repository.remoteUrl], cwd, managedGitEnvironment(runtime.environment, credential, managedGitCredentialHeaderKey(access.repository.remoteUrl)));
|
|
203
|
-
const branches = parseManagedSourceRefs(result.stdout).map(({ ref, sha }) => ({
|
|
204
|
-
name: ref.slice("refs/heads/".length),
|
|
205
|
-
sha,
|
|
206
|
-
default: ref === `refs/heads/${access.repository.defaultBranch}`,
|
|
207
|
-
providerDefault: access.repository.providerDefaultBranch !== null
|
|
208
|
-
&& ref === `refs/heads/${access.repository.providerDefaultBranch}`,
|
|
209
|
-
}));
|
|
210
|
-
return {
|
|
211
|
-
status: "ready",
|
|
212
|
-
projectId: access.projectId,
|
|
213
|
-
repositoryId: access.repository.id,
|
|
214
|
-
defaultBranch: access.repository.defaultBranch,
|
|
215
|
-
providerDefaultBranch: access.repository.providerDefaultBranch,
|
|
216
|
-
branches,
|
|
217
|
-
credentialPersisted: false,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
finally {
|
|
221
|
-
credential = null;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
async function sourceLog(cwd, options, runtime) {
|
|
225
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
226
|
-
const branch = options.flags.get("branch") ?? access.repository.defaultBranch;
|
|
227
|
-
assertGitBranchName(branch);
|
|
228
|
-
const limit = numberFlag(options, "limit", 1, 100, 20);
|
|
229
|
-
return withManagedSourceMirror(cwd, access.repository, access.client, runtime, async (mirror) => {
|
|
230
|
-
const refs = await managedSourceMirrorRefs(mirror, runtime);
|
|
231
|
-
if (!refs.has(`refs/heads/${branch}`)) {
|
|
232
|
-
throw new CliError("source_branch_missing", `Managed source branch ${branch} does not exist.`);
|
|
233
|
-
}
|
|
234
|
-
await runtime.captureCommand("git", ["fetch", "--no-tags", "origin", `refs/heads/${branch}:refs/remotes/origin/${branch}`], mirror.path, mirror.environment);
|
|
235
|
-
const result = await runtime.captureCommand("git", ["log", `--max-count=${limit}`, "--format=%H%x09%P%x09%aI%x09%an%x09%s", `refs/remotes/origin/${branch}`], mirror.path, managedGitCheckoutEnvironment(runtime.environment));
|
|
236
|
-
return {
|
|
237
|
-
status: "ready",
|
|
238
|
-
projectId: access.projectId,
|
|
239
|
-
repositoryId: access.repository.id,
|
|
240
|
-
branch,
|
|
241
|
-
commits: result.stdout.split(/\r?\n/u).filter(Boolean).map(parseManagedSourceCommit),
|
|
242
|
-
credentialPersisted: false,
|
|
243
|
-
};
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
async function sourceCompare(cwd, options, runtime) {
|
|
247
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
248
|
-
const base = requiredFlag(options, "base");
|
|
249
|
-
const head = requiredFlag(options, "head");
|
|
250
|
-
assertGitBranchName(base);
|
|
251
|
-
assertGitBranchName(head);
|
|
252
|
-
return withManagedSourceMirror(cwd, access.repository, access.client, runtime, async (mirror) => {
|
|
253
|
-
const refs = await managedSourceMirrorRefs(mirror, runtime);
|
|
254
|
-
const missing = [base, head].filter((branch) => !refs.has(`refs/heads/${branch}`));
|
|
255
|
-
if (missing.length > 0) {
|
|
256
|
-
throw new CliError("source_branch_missing", `Managed source branch ${missing.join(", ")} does not exist.`);
|
|
257
|
-
}
|
|
258
|
-
await runtime.captureCommand("git", [
|
|
259
|
-
"fetch", "--no-tags", "origin",
|
|
260
|
-
`refs/heads/${base}:refs/remotes/origin/${base}`,
|
|
261
|
-
`refs/heads/${head}:refs/remotes/origin/${head}`,
|
|
262
|
-
], mirror.path, mirror.environment);
|
|
263
|
-
const checkoutEnv = managedGitCheckoutEnvironment(runtime.environment);
|
|
264
|
-
const baseSha = (await runtime.captureCommand("git", ["rev-parse", `refs/remotes/origin/${base}`], mirror.path, checkoutEnv)).stdout;
|
|
265
|
-
const headSha = (await runtime.captureCommand("git", ["rev-parse", `refs/remotes/origin/${head}`], mirror.path, checkoutEnv)).stdout;
|
|
266
|
-
let mergeBase = null;
|
|
267
|
-
try {
|
|
268
|
-
mergeBase = (await runtime.captureCommand("git", ["merge-base", baseSha, headSha], mirror.path, checkoutEnv)).stdout || null;
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
mergeBase = null;
|
|
272
|
-
}
|
|
273
|
-
const counts = (await runtime.captureCommand("git", ["rev-list", "--left-right", "--count", `${baseSha}...${headSha}`], mirror.path, checkoutEnv)).stdout.split(/\s+/u).map(Number);
|
|
274
|
-
const changedFiles = (await runtime.captureCommand("git", ["diff", "--name-status", "--no-renames", baseSha, headSha], mirror.path, checkoutEnv)).stdout.split(/\r?\n/u).filter(Boolean);
|
|
275
|
-
const files = changedFiles.slice(0, 500).map((line) => {
|
|
276
|
-
const [status, ...path] = line.split("\t");
|
|
277
|
-
return { status, path: path.join("\t") };
|
|
278
|
-
});
|
|
279
|
-
const relationship = baseSha === headSha
|
|
280
|
-
? "identical"
|
|
281
|
-
: mergeBase === baseSha
|
|
282
|
-
? "fast_forward"
|
|
283
|
-
: mergeBase === headSha
|
|
284
|
-
? "behind"
|
|
285
|
-
: mergeBase === null
|
|
286
|
-
? "unrelated"
|
|
287
|
-
: "diverged";
|
|
288
|
-
return {
|
|
289
|
-
status: "ready",
|
|
290
|
-
projectId: access.projectId,
|
|
291
|
-
repositoryId: access.repository.id,
|
|
292
|
-
base: { branch: base, sha: baseSha, commitsOnlyHere: counts[0] ?? 0 },
|
|
293
|
-
head: { branch: head, sha: headSha, commitsOnlyHere: counts[1] ?? 0 },
|
|
294
|
-
mergeBase,
|
|
295
|
-
relationship,
|
|
296
|
-
files,
|
|
297
|
-
filesTruncated: changedFiles.length > files.length,
|
|
298
|
-
credentialPersisted: false,
|
|
299
|
-
};
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
async function sourceDefaultSet(cwd, options, runtime) {
|
|
303
|
-
const targetBranch = requiredFlag(options, "branch");
|
|
304
|
-
const expectedDefaultBranch = requiredFlag(options, "expected-current");
|
|
305
|
-
assertGitBranchName(targetBranch);
|
|
306
|
-
assertGitBranchName(expectedDefaultBranch);
|
|
307
|
-
return requestAndWaitForSourceOperation(cwd, options, runtime, {
|
|
308
|
-
kind: "set_default",
|
|
309
|
-
targetBranch,
|
|
310
|
-
expectedDefaultBranch,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
async function sourcePromote(cwd, options, runtime) {
|
|
314
|
-
const headBranch = requiredFlag(options, "head");
|
|
315
|
-
const expectedCurrentSha = requiredSourceSha(options, "expected-current");
|
|
316
|
-
assertGitBranchName(headBranch);
|
|
317
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
318
|
-
const targetBranch = options.flags.get("base") ?? access.repository.defaultBranch;
|
|
319
|
-
assertGitBranchName(targetBranch);
|
|
320
|
-
return requestAndWaitForSourceOperation(cwd, options, runtime, {
|
|
321
|
-
kind: "promote",
|
|
322
|
-
targetBranch,
|
|
323
|
-
headBranch,
|
|
324
|
-
expectedCurrentSha,
|
|
325
|
-
}, access);
|
|
326
|
-
}
|
|
327
|
-
async function sourceHistoryReplace(cwd, options, runtime) {
|
|
328
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
329
|
-
const confirmation = requiredFlag(options, "confirm");
|
|
330
|
-
if (confirmation !== access.repository.id) {
|
|
331
|
-
throw new CliError("source_history_confirmation_required", "History replacement requires --confirm with the exact managed repository ID.", `tender-accounts source history replace --head ${requiredFlag(options, "head")} --expected-current <sha> --confirm ${access.repository.id} --dry-run --json`);
|
|
332
|
-
}
|
|
333
|
-
const headBranch = requiredFlag(options, "head");
|
|
334
|
-
const targetBranch = options.flags.get("target") ?? access.repository.defaultBranch;
|
|
335
|
-
const expectedCurrentSha = requiredSourceSha(options, "expected-current");
|
|
336
|
-
assertGitBranchName(headBranch);
|
|
337
|
-
assertGitBranchName(targetBranch);
|
|
338
|
-
return requestAndWaitForSourceOperation(cwd, options, runtime, {
|
|
339
|
-
kind: "history_replace",
|
|
340
|
-
targetBranch,
|
|
341
|
-
headBranch,
|
|
342
|
-
expectedCurrentSha,
|
|
343
|
-
}, access);
|
|
344
|
-
}
|
|
345
|
-
async function sourceOperationStatus(cwd, options, runtime) {
|
|
346
|
-
const context = await optionalContext(cwd);
|
|
347
|
-
const { client } = await authenticatedClient(cwd, context?.link ?? null, options, runtime);
|
|
348
|
-
return (await client.sourceOperation(requiredSourceOperationId(requiredFlag(options, "operation")))).operation;
|
|
349
|
-
}
|
|
350
|
-
async function sourceOperationRetry(cwd, options, runtime) {
|
|
351
|
-
const context = await optionalContext(cwd);
|
|
352
|
-
const { client } = await authenticatedClient(cwd, context?.link ?? null, options, runtime);
|
|
353
|
-
const requested = await client.retrySourceOperation(requiredSourceOperationId(requiredFlag(options, "operation")));
|
|
354
|
-
if (options.booleans.has("no-wait"))
|
|
355
|
-
return requested.operation;
|
|
356
|
-
return await waitForSourceOperation(client, requested.operation, options, runtime);
|
|
357
|
-
}
|
|
358
|
-
async function requestAndWaitForSourceOperation(cwd, options, runtime, input, resolvedAccess) {
|
|
359
|
-
const access = resolvedAccess ?? await sourceReadAccess(cwd, options, runtime);
|
|
360
|
-
const idempotencyKey = options.flags.get("idempotency-key")
|
|
361
|
-
?? `cli-${input.kind}-${crypto.randomUUID()}`;
|
|
362
|
-
const requested = await access.client.createSourceOperation(access.repository.id, {
|
|
363
|
-
...input,
|
|
364
|
-
idempotencyKey,
|
|
365
|
-
dryRun: options.booleans.has("dry-run"),
|
|
366
|
-
});
|
|
367
|
-
if (options.booleans.has("no-wait"))
|
|
368
|
-
return requested.operation;
|
|
369
|
-
return await waitForSourceOperation(access.client, requested.operation, options, runtime);
|
|
370
|
-
}
|
|
371
|
-
async function waitForSourceOperation(client, initial, options, runtime) {
|
|
372
|
-
const timeoutSeconds = numberFlag(options, "timeout-seconds", 10, 15 * 60, 10 * 60);
|
|
373
|
-
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
374
|
-
let operation = initial;
|
|
375
|
-
while (operation.status === "requested" || operation.status === "running") {
|
|
376
|
-
if (Date.now() >= deadline) {
|
|
377
|
-
throw new CliError("source_operation_timeout", `Source operation ${operation.id} is still ${operation.status}.`, `tender-accounts source operation status --operation ${operation.id} --json`);
|
|
378
|
-
}
|
|
379
|
-
await runtime.sleep(2_000);
|
|
380
|
-
operation = (await client.sourceOperation(operation.id)).operation;
|
|
381
|
-
}
|
|
382
|
-
if (operation.status === "failed") {
|
|
383
|
-
throw new CliError(operation.error?.code ?? "source_operation_failed", operation.error?.message ?? "The managed source operation failed.", `tender-accounts source operation retry --operation ${operation.id} --json`);
|
|
384
|
-
}
|
|
385
|
-
return operation;
|
|
386
|
-
}
|
|
387
|
-
async function sourceReadAccess(cwd, options, runtime) {
|
|
388
|
-
const context = await optionalContext(cwd);
|
|
389
|
-
const access = await sourceProjectAccess(cwd, context?.link ?? null, options, runtime, false);
|
|
390
|
-
const source = await access.client.sourceRepository(access.projectId);
|
|
391
|
-
if (!source.repository) {
|
|
392
|
-
throw new CliError("source_repository_missing", "This project does not have a Tender-managed Git repository.");
|
|
393
|
-
}
|
|
394
|
-
if (source.repository.status !== "active") {
|
|
395
|
-
throw new CliError("source_repository_paused", "This Tender-managed repository is paused.");
|
|
396
|
-
}
|
|
397
|
-
return { ...access, repository: source.repository };
|
|
398
|
-
}
|
|
399
|
-
async function withManagedSourceMirror(cwd, repository, client, runtime, callback) {
|
|
400
|
-
const mirrorPath = await mkdtemp(join(tmpdir(), "tender-accounts-source-read-"));
|
|
401
|
-
let credential = null;
|
|
402
|
-
try {
|
|
403
|
-
await runtime.captureCommand("git", ["init", "--bare", mirrorPath], cwd, managedGitCheckoutEnvironment(runtime.environment));
|
|
404
|
-
await runtime.captureCommand("git", ["remote", "add", "origin", repository.remoteUrl], mirrorPath, managedGitCheckoutEnvironment(runtime.environment));
|
|
405
|
-
const token = await client.sourceToken(repository.id, 15 * 60, "read");
|
|
406
|
-
credential = token.token;
|
|
407
|
-
return await callback({
|
|
408
|
-
path: mirrorPath,
|
|
409
|
-
environment: managedGitEnvironment(runtime.environment, credential, managedGitCredentialHeaderKey(repository.remoteUrl)),
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
finally {
|
|
413
|
-
credential = null;
|
|
414
|
-
await rm(mirrorPath, { recursive: true, force: true });
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function parseManagedSourceRefs(stdout) {
|
|
418
|
-
const refs = [];
|
|
419
|
-
for (const line of stdout.split(/\r?\n/u)) {
|
|
420
|
-
const match = /^([0-9a-f]{40})\s+(refs\/heads\/[A-Za-z0-9._/-]{1,480})$/u.exec(line);
|
|
421
|
-
if (match?.[1] && match[2])
|
|
422
|
-
refs.push({ sha: match[1], ref: match[2] });
|
|
423
|
-
}
|
|
424
|
-
return refs.sort((left, right) => left.ref.localeCompare(right.ref));
|
|
425
|
-
}
|
|
426
|
-
async function managedSourceMirrorRefs(mirror, runtime) {
|
|
427
|
-
const result = await runtime.captureCommand("git", ["ls-remote", "--heads", "origin"], mirror.path, mirror.environment);
|
|
428
|
-
return new Map(parseManagedSourceRefs(result.stdout).map(({ ref, sha }) => [ref, sha]));
|
|
429
|
-
}
|
|
430
|
-
function parseManagedSourceCommit(line) {
|
|
431
|
-
const [sha, parents, authoredAt, author, ...subject] = line.split("\t");
|
|
432
|
-
return {
|
|
433
|
-
sha,
|
|
434
|
-
parents: parents ? parents.split(" ").filter(Boolean) : [],
|
|
435
|
-
authoredAt,
|
|
436
|
-
author,
|
|
437
|
-
subject: subject.join("\t"),
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
function requiredSourceSha(options, name) {
|
|
441
|
-
const value = requiredFlag(options, name);
|
|
442
|
-
if (!/^[0-9a-f]{40}$/u.test(value)) {
|
|
443
|
-
throw new CliError("source_revision_invalid", `--${name} must be an exact lowercase 40-character Git SHA.`);
|
|
444
|
-
}
|
|
445
|
-
return value;
|
|
446
|
-
}
|
|
447
|
-
function requiredSourceOperationId(value) {
|
|
448
|
-
if (!/^sop_[A-Za-z0-9_-]{8,64}$/u.test(value)) {
|
|
449
|
-
throw new CliError("source_operation_id_invalid", "--operation must be the sop_ ID returned by a source mutation.");
|
|
450
|
-
}
|
|
451
|
-
return value;
|
|
452
|
-
}
|
|
453
|
-
async function sourcePull(cwd, options, runtime) {
|
|
454
|
-
const context = await optionalContext(cwd);
|
|
455
|
-
const access = await sourceProjectAccess(cwd, context?.link ?? null, options, runtime, true);
|
|
456
|
-
const source = await access.client.sourceRepository(access.projectId);
|
|
457
|
-
if (!source.repository) {
|
|
458
|
-
throw new CliError("source_repository_missing", "This project does not have an active Tender-managed Git repository to pull.", "Open the merchant-hosted connected repository, or ask a merchant administrator to connect Tender-managed Git.");
|
|
459
|
-
}
|
|
460
|
-
const repository = source.repository;
|
|
461
|
-
if (repository.status !== "active") {
|
|
462
|
-
throw new CliError("source_repository_paused", "This Tender-managed repository is paused and cannot be pulled.", "Use tender-accounts source status --project <project-id> --json to inspect the authoritative source.");
|
|
463
|
-
}
|
|
464
|
-
const credentialHeaderKey = managedGitCredentialHeaderKey(repository.remoteUrl);
|
|
465
|
-
const remote = options.flags.get("remote") ?? "tender";
|
|
466
|
-
assertGitRemoteName(remote);
|
|
467
|
-
const existingGitRoot = await optionalGitRoot(cwd, runtime);
|
|
468
|
-
let checkoutRoot;
|
|
469
|
-
let action;
|
|
470
|
-
let selectedRemote = remote;
|
|
471
|
-
let selectedBranch = repository.defaultBranch;
|
|
472
|
-
let beforeRevision = null;
|
|
473
|
-
if (!existingGitRoot) {
|
|
474
|
-
const entries = await readdir(cwd);
|
|
475
|
-
if (entries.length > 0) {
|
|
476
|
-
throw new CliError("source_pull_target_not_empty", "Tender can clone the connected repository only into an empty directory.", "Run this command from an empty workspace, or open the existing matching Git checkout and retry there.");
|
|
477
|
-
}
|
|
478
|
-
checkoutRoot = cwd;
|
|
479
|
-
action = "clone";
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
checkoutRoot = existingGitRoot;
|
|
483
|
-
action = "pull";
|
|
484
|
-
selectedRemote = await matchingSourceRemote(checkoutRoot, remote, repository.remoteUrl, runtime);
|
|
485
|
-
selectedBranch = (await runtime.captureCommand("git", ["branch", "--show-current"], checkoutRoot)).stdout;
|
|
486
|
-
if (!selectedBranch) {
|
|
487
|
-
throw new CliError("source_pull_detached_head", "Tender will not pull while the checkout has a detached HEAD.", `Switch to ${repository.defaultBranch}, then retry tender-accounts source pull.`);
|
|
488
|
-
}
|
|
489
|
-
const state = await sourceState(checkoutRoot, runtime);
|
|
490
|
-
if (state.dirty) {
|
|
491
|
-
throw new CliError("source_pull_dirty_worktree", "Tender will not pull over uncommitted or untracked files.", "Commit, stash, or remove the local changes, then retry tender-accounts source pull.");
|
|
492
|
-
}
|
|
493
|
-
beforeRevision = state.revision;
|
|
494
|
-
}
|
|
495
|
-
assertGitBranchName(selectedBranch);
|
|
496
|
-
let appRoot = managedSourceAppRoot(checkoutRoot, repository.workingDirectory);
|
|
497
|
-
if (action === "pull") {
|
|
498
|
-
appRoot = await verifiedManagedSourceAppRoot(checkoutRoot, repository.workingDirectory);
|
|
499
|
-
await compatibleManagedSourceContext(appRoot, access.projectId, access.apiUrl);
|
|
500
|
-
}
|
|
501
|
-
if (options.booleans.has("dry-run")) {
|
|
502
|
-
return {
|
|
503
|
-
status: "planned",
|
|
504
|
-
action,
|
|
505
|
-
projectId: access.projectId,
|
|
506
|
-
repositoryId: repository.id,
|
|
507
|
-
remote: selectedRemote,
|
|
508
|
-
transport: "tender-managed",
|
|
509
|
-
branch: selectedBranch,
|
|
510
|
-
checkoutRoot,
|
|
511
|
-
appRoot,
|
|
512
|
-
credentialIssued: false,
|
|
513
|
-
wroteFiles: false,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
let credential = null;
|
|
517
|
-
try {
|
|
518
|
-
const token = await access.client.sourceToken(repository.id, 15 * 60, "read");
|
|
519
|
-
if (token.remoteUrl !== repository.remoteUrl) {
|
|
520
|
-
throw new CliError("source_token_target_mismatch", "Tender refused a repository credential whose target did not match the connected source.");
|
|
521
|
-
}
|
|
522
|
-
credential = token.token;
|
|
523
|
-
const environment = managedGitEnvironment(runtime.environment, credential, credentialHeaderKey);
|
|
524
|
-
if (action === "clone") {
|
|
525
|
-
await runtime.captureCommand("git", [
|
|
526
|
-
"clone",
|
|
527
|
-
"--no-checkout",
|
|
528
|
-
"--no-recurse-submodules",
|
|
529
|
-
"--origin", selectedRemote,
|
|
530
|
-
"--branch", repository.defaultBranch,
|
|
531
|
-
repository.remoteUrl,
|
|
532
|
-
".",
|
|
533
|
-
], checkoutRoot, environment);
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
await runtime.captureCommand("git", ["fetch", "--no-recurse-submodules", selectedRemote, selectedBranch], checkoutRoot, environment);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
finally {
|
|
540
|
-
credential = null;
|
|
541
|
-
}
|
|
542
|
-
const checkoutEnvironment = managedGitCheckoutEnvironment(runtime.environment);
|
|
543
|
-
if (action === "clone") {
|
|
544
|
-
await runtime.captureCommand("git", ["reset", "--hard", "HEAD"], checkoutRoot, checkoutEnvironment);
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
await runtime.captureCommand("git", ["merge", "--ff-only", "FETCH_HEAD"], checkoutRoot, checkoutEnvironment);
|
|
548
|
-
}
|
|
549
|
-
appRoot = await verifiedManagedSourceAppRoot(checkoutRoot, repository.workingDirectory);
|
|
550
|
-
const pulledContext = await compatibleManagedSourceContext(appRoot, access.projectId, access.apiUrl);
|
|
551
|
-
const linkCreated = !pulledContext.link;
|
|
552
|
-
const linkPath = linkCreated
|
|
553
|
-
? await writeProjectLink(appRoot, {
|
|
554
|
-
schema: CLI_LINK_SCHEMA,
|
|
555
|
-
projectId: access.projectId,
|
|
556
|
-
apiUrl: access.apiUrl,
|
|
557
|
-
})
|
|
558
|
-
: pulledContext.linkPath;
|
|
559
|
-
const afterRevision = (await sourceState(checkoutRoot, runtime)).revision;
|
|
560
|
-
return {
|
|
561
|
-
status: action === "clone" ? "cloned" : beforeRevision === afterRevision ? "up_to_date" : "updated",
|
|
562
|
-
action,
|
|
563
|
-
projectId: access.projectId,
|
|
564
|
-
repositoryId: repository.id,
|
|
565
|
-
remote: selectedRemote,
|
|
566
|
-
branch: selectedBranch,
|
|
567
|
-
checkoutRoot,
|
|
568
|
-
appRoot,
|
|
569
|
-
sourceRevision: afterRevision,
|
|
570
|
-
linkPath,
|
|
571
|
-
linkCreated,
|
|
572
|
-
credentialPersisted: false,
|
|
573
|
-
next: appRoot === checkoutRoot
|
|
574
|
-
? "tender-accounts doctor --json"
|
|
575
|
-
: `tender-accounts doctor --cwd ${repository.workingDirectory} --json`,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
async function compatibleManagedSourceContext(appRoot, projectId, apiUrl) {
|
|
579
|
-
const context = await loadProjectContext(appRoot).catch((error) => {
|
|
580
|
-
if (error instanceof CliError && error.code === "file_missing") {
|
|
581
|
-
throw new CliError("source_checkout_config_missing", `The connected repository does not contain tender-accounts.json at ${appRoot}.`, "Ask a merchant administrator to repair the managed source working directory before continuing.");
|
|
582
|
-
}
|
|
583
|
-
throw error;
|
|
584
|
-
});
|
|
585
|
-
if (context.link && (context.link.projectId !== projectId
|
|
586
|
-
|| context.link.apiUrl !== apiUrl)) {
|
|
587
|
-
throw new CliError("source_checkout_link_conflict", "The pulled app is already linked to a different Tender project or API origin.", "Do not replace the link automatically; verify the checkout and project with a merchant administrator.");
|
|
588
|
-
}
|
|
589
|
-
return context;
|
|
590
|
-
}
|
|
591
|
-
async function sourceProjectAccess(cwd, link, options, runtime, requireDeveloper) {
|
|
592
|
-
const resolved = await authenticatedClient(cwd, link, options, runtime);
|
|
593
|
-
const principal = await resolved.client.whoami();
|
|
594
|
-
if (resolved.profile)
|
|
595
|
-
await savePrincipalSummary(runtime.authStore, resolved.profile, principal);
|
|
596
|
-
const explicitProject = options.flags.get("project");
|
|
597
|
-
const requestedProject = explicitProject ? requiredProjectId(explicitProject) : link?.projectId;
|
|
598
|
-
const project = requestedProject
|
|
599
|
-
? principal.projects.find((candidate) => candidate.projectId === requestedProject)
|
|
600
|
-
: principal.projects.length === 1 ? principal.projects[0] : undefined;
|
|
601
|
-
if (!project) {
|
|
602
|
-
throw new CliError(requestedProject ? "project_not_permitted" : "project_required", requestedProject
|
|
603
|
-
? "The current credential is not permitted to use that project."
|
|
604
|
-
: "The credential grants more than one project; select one explicitly.", `tender-accounts source status --project ${principal.projects[0]?.projectId ?? "prj_..."} --json`);
|
|
605
|
-
}
|
|
606
|
-
if (explicitProject && link && project.projectId !== link.projectId) {
|
|
607
|
-
throw new CliError("project_link_mismatch", "The requested project does not match this checkout's Tender link.", "Run the command from an empty workspace, or use the project already recorded in .tender/link.json.");
|
|
608
|
-
}
|
|
609
|
-
if (requireDeveloper && project.role !== "developer") {
|
|
610
|
-
throw new CliError("project_role_insufficient", "The selected project grant is read-only and cannot issue a repository credential.", "Ask a merchant administrator for developer access to this exact project.");
|
|
611
|
-
}
|
|
612
|
-
return { client: resolved.client, apiUrl: resolved.apiUrl, projectId: project.projectId };
|
|
613
|
-
}
|
|
614
|
-
async function optionalGitRoot(cwd, runtime) {
|
|
615
|
-
try {
|
|
616
|
-
return resolve((await runtime.captureCommand("git", ["rev-parse", "--show-toplevel"], cwd)).stdout);
|
|
617
|
-
}
|
|
618
|
-
catch {
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
async function matchingSourceRemote(gitRoot, requestedRemote, remoteUrl, runtime) {
|
|
623
|
-
const remotes = (await runtime.captureCommand("git", ["remote"], gitRoot)).stdout.split(/\r?\n/u).filter(Boolean);
|
|
624
|
-
const matches = [];
|
|
625
|
-
for (const remote of remotes) {
|
|
626
|
-
const current = (await runtime.captureCommand("git", ["remote", "get-url", remote], gitRoot)).stdout;
|
|
627
|
-
if (current === remoteUrl)
|
|
628
|
-
matches.push(remote);
|
|
629
|
-
if (remote === requestedRemote && current !== remoteUrl) {
|
|
630
|
-
throw new CliError("source_remote_conflict", `Git remote ${requestedRemote} points somewhere other than this project's connected source.`, "Open the matching checkout or choose its existing remote with --remote after verifying the URL.");
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if (matches.includes(requestedRemote))
|
|
634
|
-
return requestedRemote;
|
|
635
|
-
if (matches.length === 1)
|
|
636
|
-
return matches[0];
|
|
637
|
-
if (matches.length > 1) {
|
|
638
|
-
throw new CliError("source_remote_ambiguous", "More than one Git remote points to the connected Tender source.", "Select the intended existing remote explicitly with --remote.");
|
|
639
|
-
}
|
|
640
|
-
throw new CliError("source_checkout_mismatch", "This Git checkout does not match the project's connected Tender source.", "Run tender-accounts source pull from an empty workspace to clone the exact repository.");
|
|
641
|
-
}
|
|
642
|
-
function managedSourceAppRoot(checkoutRoot, workingDirectory) {
|
|
643
|
-
if (workingDirectory === ".")
|
|
644
|
-
return checkoutRoot;
|
|
645
|
-
if (!workingDirectory || workingDirectory.startsWith("/") || workingDirectory.includes("\\") || workingDirectory.includes("%")) {
|
|
646
|
-
throw new CliError("source_working_directory_invalid", "The managed source working directory is unsafe.");
|
|
647
|
-
}
|
|
648
|
-
const segments = workingDirectory.split("/");
|
|
649
|
-
if (segments.some((segment) => !segment || segment === "." || segment === ".." || !/^[A-Za-z0-9._-]+$/u.test(segment))) {
|
|
650
|
-
throw new CliError("source_working_directory_invalid", "The managed source working directory is unsafe.");
|
|
651
|
-
}
|
|
652
|
-
const appRoot = resolve(checkoutRoot, workingDirectory);
|
|
653
|
-
const pathFromCheckout = relative(checkoutRoot, appRoot);
|
|
654
|
-
if (pathFromCheckout === ".." || pathFromCheckout.startsWith(`..${sep}`)) {
|
|
655
|
-
throw new CliError("source_working_directory_invalid", "The managed source working directory escapes the checkout.");
|
|
656
|
-
}
|
|
657
|
-
return appRoot;
|
|
658
|
-
}
|
|
659
|
-
async function verifiedManagedSourceAppRoot(checkoutRoot, workingDirectory) {
|
|
660
|
-
const lexicalAppRoot = managedSourceAppRoot(checkoutRoot, workingDirectory);
|
|
661
|
-
const realCheckoutRoot = await realpath(checkoutRoot);
|
|
662
|
-
let cursor = realCheckoutRoot;
|
|
663
|
-
if (workingDirectory !== ".") {
|
|
664
|
-
for (const segment of workingDirectory.split("/")) {
|
|
665
|
-
cursor = resolve(cursor, segment);
|
|
666
|
-
let entry;
|
|
667
|
-
try {
|
|
668
|
-
entry = await lstat(cursor);
|
|
669
|
-
}
|
|
670
|
-
catch (error) {
|
|
671
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
672
|
-
throw new CliError("source_working_directory_missing", `The managed source working directory does not exist: ${workingDirectory}.`, "Ask a merchant administrator to repair the connected source working directory.");
|
|
673
|
-
}
|
|
674
|
-
throw error;
|
|
675
|
-
}
|
|
676
|
-
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
677
|
-
throw new CliError("source_working_directory_unsafe", `The managed source working directory contains a symlink or non-directory component: ${workingDirectory}.`, "Replace it with regular directories inside the connected repository before retrying.");
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
const realAppRoot = await realpath(lexicalAppRoot);
|
|
682
|
-
const pathFromCheckout = relative(realCheckoutRoot, realAppRoot);
|
|
683
|
-
if (pathFromCheckout === ".." || pathFromCheckout.startsWith(`..${sep}`)) {
|
|
684
|
-
throw new CliError("source_working_directory_unsafe", "The managed source working directory escapes the checkout.");
|
|
685
|
-
}
|
|
686
|
-
return lexicalAppRoot;
|
|
687
|
-
}
|
|
688
|
-
function managedGitCredentialHeaderKey(remoteUrl) {
|
|
689
|
-
let url;
|
|
690
|
-
try {
|
|
691
|
-
url = new URL(remoteUrl);
|
|
692
|
-
}
|
|
693
|
-
catch {
|
|
694
|
-
throw new CliError("source_remote_invalid", "The connected managed Git URL is invalid.");
|
|
695
|
-
}
|
|
696
|
-
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
|
|
697
|
-
throw new CliError("source_remote_invalid", "The connected managed Git URL must be credential-free HTTPS without query parameters or fragments.");
|
|
698
|
-
}
|
|
699
|
-
return `http.${url.toString()}.extraHeader`;
|
|
700
|
-
}
|
|
701
|
-
function managedGitEnvironment(environment, credential, credentialHeaderKey) {
|
|
702
|
-
return {
|
|
703
|
-
...environment,
|
|
704
|
-
GIT_CONFIG_COUNT: "1",
|
|
705
|
-
GIT_CONFIG_KEY_0: credentialHeaderKey,
|
|
706
|
-
GIT_CONFIG_VALUE_0: `Authorization: Bearer ${credential}`,
|
|
707
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
708
|
-
GIT_LFS_SKIP_SMUDGE: "1",
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
function managedGitCheckoutEnvironment(environment) {
|
|
712
|
-
return {
|
|
713
|
-
...environment,
|
|
714
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
715
|
-
GIT_LFS_SKIP_SMUDGE: "1",
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
async function sourceConnect(cwd, options, runtime) {
|
|
719
|
-
const context = await loadProjectContext(cwd);
|
|
720
|
-
if (!context.link)
|
|
721
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.", "tender-accounts link --json");
|
|
722
|
-
const projectId = requiredProjectId(context.link.projectId);
|
|
723
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
724
|
-
const gitRoot = await requireGitRoot(context.root, runtime);
|
|
725
|
-
const derivedWorkingDirectory = relative(gitRoot, context.root).split(sep).join("/") || ".";
|
|
726
|
-
if (derivedWorkingDirectory.startsWith("../")) {
|
|
727
|
-
throw new CliError("source_working_directory_invalid", "The Tender app must be inside the Git repository being connected.");
|
|
728
|
-
}
|
|
729
|
-
const publication = options.flags.get("publication") ?? "preview-only";
|
|
730
|
-
if (publication !== "preview-only" && publication !== "default-branch") {
|
|
731
|
-
throw new CliError("source_publication_invalid", "--publication must be preview-only or default-branch.");
|
|
732
|
-
}
|
|
733
|
-
const workingDirectory = options.flags.get("working-directory") ?? derivedWorkingDirectory;
|
|
734
|
-
const artifactLatestPath = options.flags.get("artifact-path")
|
|
735
|
-
?? `${context.config.artifactDirectory}/${BUILD_ENVELOPE_FILENAME}`;
|
|
736
|
-
const created = await client.connectSourceRepository(projectId, {
|
|
737
|
-
repoName: options.flags.get("repo-name") ?? `${safeRepositoryName(basename(gitRoot))}-source`,
|
|
738
|
-
defaultBranch: options.flags.get("default-branch") ?? "main",
|
|
739
|
-
installCommand: options.flags.get("install-command") ?? "npm ci",
|
|
740
|
-
validateCommand: context.config.commands.check,
|
|
741
|
-
buildCommand: context.config.commands.package,
|
|
742
|
-
workingDirectory,
|
|
743
|
-
artifactLatestPath,
|
|
744
|
-
autoPublishDefaultBranch: publication === "default-branch",
|
|
745
|
-
});
|
|
746
|
-
const remote = options.flags.get("remote") ?? "tender";
|
|
747
|
-
await ensureGitRemote(gitRoot, remote, created.repository.remoteUrl, runtime);
|
|
748
|
-
return {
|
|
749
|
-
status: "connected",
|
|
750
|
-
projectId,
|
|
751
|
-
remote,
|
|
752
|
-
repository: publicManagedSourceRepository(created.repository),
|
|
753
|
-
next: `tender-accounts source push --remote ${remote} --json`,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
async function sourceToken(cwd, options, runtime) {
|
|
757
|
-
const context = await loadProjectContext(cwd);
|
|
758
|
-
if (!context.link)
|
|
759
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.", "tender-accounts link --json");
|
|
760
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
761
|
-
const source = await client.sourceRepository(requiredProjectId(context.link.projectId));
|
|
762
|
-
if (!source.repository)
|
|
763
|
-
throw new CliError("source_repository_missing", "Connect a Tender-managed repository before requesting source access.", "tender-accounts source connect --json");
|
|
764
|
-
return {
|
|
765
|
-
status: "issued",
|
|
766
|
-
repositoryId: source.repository.id,
|
|
767
|
-
...(await client.sourceToken(source.repository.id, numberFlag(options, "ttl-seconds", 15 * 60, 7 * 24 * 60 * 60, 60 * 60), "read")),
|
|
768
|
-
warning: "This read-only repository credential is shown once. Keep it out of source and shell history.",
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
async function sourcePush(cwd, options, runtime) {
|
|
772
|
-
const context = await loadProjectContext(cwd);
|
|
773
|
-
if (!context.link)
|
|
774
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.", "tender-accounts link --json");
|
|
775
|
-
const projectId = requiredProjectId(context.link.projectId);
|
|
776
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
777
|
-
const source = await client.sourceRepository(projectId);
|
|
778
|
-
if (!source.repository)
|
|
779
|
-
throw new CliError("source_repository_missing", "Connect a Tender-managed repository before pushing.", "tender-accounts source connect --json");
|
|
780
|
-
const gitRoot = await requireGitRoot(context.root, runtime);
|
|
781
|
-
const remote = options.flags.get("remote") ?? "tender";
|
|
782
|
-
await ensureGitRemote(gitRoot, remote, source.repository.remoteUrl, runtime);
|
|
783
|
-
const currentBranch = (await runtime.captureCommand("git", ["branch", "--show-current"], gitRoot)).stdout;
|
|
784
|
-
const branch = options.flags.get("branch") ?? (currentBranch || source.repository.defaultBranch);
|
|
785
|
-
assertGitBranchName(branch);
|
|
786
|
-
const state = await sourceState(gitRoot, runtime);
|
|
787
|
-
if (source.repository.changeRequestProtection === "enabling") {
|
|
788
|
-
throw new CliError("source_review_activation_pending", "Reviewed changes are still activating. Retry after Tender confirms previous write access was revoked.");
|
|
789
|
-
}
|
|
790
|
-
if (source.repository.changeRequestProtection === "enabled") {
|
|
791
|
-
if (!currentBranch) {
|
|
792
|
-
throw new CliError("source_push_detached_head", "Tender will not submit a detached HEAD. Create or switch to a feature branch first.");
|
|
793
|
-
}
|
|
794
|
-
if (branch === source.repository.defaultBranch) {
|
|
795
|
-
throw new CliError("source_default_branch_protected", `${source.repository.defaultBranch} is protected. Push a feature branch, then create a reviewed change request.`, "tender-accounts source push --branch my-change --json");
|
|
796
|
-
}
|
|
797
|
-
return await submitProtectedSourcePush(gitRoot, projectId, source.repository, branch, state, options, client, runtime);
|
|
798
|
-
}
|
|
799
|
-
let credential = null;
|
|
800
|
-
try {
|
|
801
|
-
const token = await client.sourceToken(source.repository.id, 15 * 60);
|
|
802
|
-
credential = token.token;
|
|
803
|
-
await runtime.captureCommand("git", ["push", remote, `HEAD:refs/heads/${branch}`], gitRoot, managedGitEnvironment(runtime.environment, credential, managedGitCredentialHeaderKey(source.repository.remoteUrl)));
|
|
804
|
-
}
|
|
805
|
-
finally {
|
|
806
|
-
credential = null;
|
|
807
|
-
}
|
|
808
|
-
return {
|
|
809
|
-
status: "pushed",
|
|
810
|
-
projectId,
|
|
811
|
-
repositoryId: source.repository.id,
|
|
812
|
-
remote,
|
|
813
|
-
branch,
|
|
814
|
-
sourceRevision: state.revision,
|
|
815
|
-
dirtyWorkingTreeExcluded: state.dirty,
|
|
816
|
-
next: "tender-accounts source status --json",
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
async function submitProtectedSourcePush(gitRoot, projectId, repository, branch, state, options, client, runtime) {
|
|
820
|
-
let credential = null;
|
|
821
|
-
let expectedCurrentSha = null;
|
|
822
|
-
try {
|
|
823
|
-
const token = await client.sourceToken(repository.id, 15 * 60, "read");
|
|
824
|
-
credential = token.token;
|
|
825
|
-
const remote = await runtime.captureCommand("git", ["ls-remote", "--heads", repository.remoteUrl, `refs/heads/${branch}`], gitRoot, managedGitEnvironment(runtime.environment, credential, managedGitCredentialHeaderKey(repository.remoteUrl)));
|
|
826
|
-
const refs = parseManagedSourceRefs(remote.stdout);
|
|
827
|
-
expectedCurrentSha = refs[0]?.sha ?? null;
|
|
828
|
-
}
|
|
829
|
-
finally {
|
|
830
|
-
credential = null;
|
|
831
|
-
}
|
|
832
|
-
const temporary = await mkdtemp(join(tmpdir(), "tender-source-push-"));
|
|
833
|
-
const bundlePath = join(temporary, "source.bundle");
|
|
834
|
-
try {
|
|
835
|
-
await runtime.captureCommand("git", ["bundle", "create", bundlePath, "HEAD"], gitRoot, managedGitCheckoutEnvironment(runtime.environment));
|
|
836
|
-
const bytes = new Uint8Array(await readFile(bundlePath));
|
|
837
|
-
if (bytes.byteLength > 50 * 1024 * 1024) {
|
|
838
|
-
throw new CliError("source_bundle_too_large", "The committed source bundle exceeds Tender's 50 MiB limit.");
|
|
839
|
-
}
|
|
840
|
-
const bundleSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
841
|
-
const retryWindow = Math.floor(Date.now() / (24 * 60 * 60 * 1000));
|
|
842
|
-
const pushIntentDigest = createHash("sha256")
|
|
843
|
-
.update(`${repository.id}\0${branch}\0${state.revision}\0${retryWindow}`)
|
|
844
|
-
.digest("hex");
|
|
845
|
-
const requested = await client.createSourcePush(repository.id, {
|
|
846
|
-
idempotencyKey: `source-push-${pushIntentDigest}`,
|
|
847
|
-
branch,
|
|
848
|
-
expectedCurrentSha,
|
|
849
|
-
sourceRevision: state.revision,
|
|
850
|
-
bundleSha256,
|
|
851
|
-
bundleBytes: bytes.byteLength,
|
|
852
|
-
});
|
|
853
|
-
let push = requested.push;
|
|
854
|
-
if (push.status === "uploading")
|
|
855
|
-
push = (await client.uploadSourceBundle(push.id, bytes)).push;
|
|
856
|
-
if (!options.booleans.has("no-wait")) {
|
|
857
|
-
const deadline = Date.now() + numberFlag(options, "timeout-seconds", 30, 900, 300) * 1000;
|
|
858
|
-
while (push.status === "requested" || push.status === "running" || push.status === "uploading") {
|
|
859
|
-
if (Date.now() >= deadline) {
|
|
860
|
-
throw new CliError("source_push_timeout", `Source push ${push.id} is still ${push.status}.`, `tender-accounts source push --branch ${branch} --no-wait --json`);
|
|
861
|
-
}
|
|
862
|
-
await runtime.sleep(1500);
|
|
863
|
-
push = (await client.sourcePush(push.id)).push;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (push.status === "failed") {
|
|
867
|
-
throw new CliError(push.error?.code ?? "source_push_failed", push.error?.message ?? "Tender could not push the exact feature branch.");
|
|
868
|
-
}
|
|
869
|
-
if (push.status === "expired") {
|
|
870
|
-
throw new CliError(push.error?.code ?? "source_push_expired", push.error?.message ?? "The exact source push expired before it completed.");
|
|
871
|
-
}
|
|
872
|
-
return {
|
|
873
|
-
status: push.status === "succeeded" ? "pushed" : push.status,
|
|
874
|
-
projectId,
|
|
875
|
-
repositoryId: repository.id,
|
|
876
|
-
pushId: push.id,
|
|
877
|
-
branch,
|
|
878
|
-
sourceRevision: state.revision,
|
|
879
|
-
dirtyWorkingTreeExcluded: state.dirty,
|
|
880
|
-
credentialPersisted: false,
|
|
881
|
-
defaultBranchProtected: true,
|
|
882
|
-
next: `tender-accounts source change create --head ${branch} --title \"Describe this change\" --json`,
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
finally {
|
|
886
|
-
await rm(temporary, { recursive: true, force: true });
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
async function sourceChangeCreate(cwd, options, runtime) {
|
|
890
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
891
|
-
if (access.repository.changeRequestProtection !== "enabled") {
|
|
892
|
-
throw new CliError("source_review_not_enabled", "A merchant administrator must enable reviewed changes for this managed repository first.");
|
|
893
|
-
}
|
|
894
|
-
const baseBranch = options.flags.get("base") ?? access.repository.defaultBranch;
|
|
895
|
-
const headBranch = requiredFlag(options, "head");
|
|
896
|
-
const title = requiredFlag(options, "title");
|
|
897
|
-
assertGitBranchName(baseBranch);
|
|
898
|
-
assertGitBranchName(headBranch);
|
|
899
|
-
if (baseBranch === headBranch)
|
|
900
|
-
throw new CliError("source_change_invalid", "The head branch must differ from the protected base branch.");
|
|
901
|
-
return withManagedSourceMirror(cwd, access.repository, access.client, runtime, async (mirror) => {
|
|
902
|
-
const refs = await managedSourceMirrorRefs(mirror, runtime);
|
|
903
|
-
const baseSha = refs.get(`refs/heads/${baseBranch}`);
|
|
904
|
-
const headSha = refs.get(`refs/heads/${headBranch}`);
|
|
905
|
-
if (!baseSha || !headSha)
|
|
906
|
-
throw new CliError("source_branch_missing", "Both the protected base and proposed head branches must exist.");
|
|
907
|
-
const created = await access.client.createSourceChangeRequest(access.repository.id, {
|
|
908
|
-
title,
|
|
909
|
-
description: options.flags.get("description") ?? "",
|
|
910
|
-
baseBranch,
|
|
911
|
-
headBranch,
|
|
912
|
-
baseSha,
|
|
913
|
-
headSha,
|
|
914
|
-
});
|
|
915
|
-
return {
|
|
916
|
-
status: created.changeRequest.status,
|
|
917
|
-
created: created.created,
|
|
918
|
-
projectId: access.projectId,
|
|
919
|
-
repositoryId: access.repository.id,
|
|
920
|
-
changeRequest: created.changeRequest,
|
|
921
|
-
next: created.changeRequest.status === "ready_for_review"
|
|
922
|
-
? "A merchant administrator can open the exact preview and approve this change in Tender."
|
|
923
|
-
: "Wait for Tender to validate and seal the exact preview, then inspect this change again.",
|
|
924
|
-
};
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
async function sourceChangeList(cwd, options, runtime) {
|
|
928
|
-
const access = await sourceReadAccess(cwd, options, runtime);
|
|
929
|
-
const state = (options.flags.get("state") ?? "open");
|
|
930
|
-
if (!new Set(["open", "closed", "all"]).has(state))
|
|
931
|
-
throw new CliError("argument_invalid", "--state must be open, closed, or all.");
|
|
932
|
-
const result = await access.client.sourceChangeRequests(access.repository.id, state);
|
|
933
|
-
return { status: "ready", projectId: access.projectId, repositoryId: access.repository.id, changeRequests: result.changeRequests };
|
|
934
|
-
}
|
|
935
|
-
async function sourceChangeShow(cwd, options, runtime) {
|
|
936
|
-
const context = await loadProjectContext(cwd);
|
|
937
|
-
if (!context.link)
|
|
938
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.");
|
|
939
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
940
|
-
const result = await client.sourceChangeRequest(requiredSourceChangeRequestId(requiredFlag(options, "change")));
|
|
941
|
-
requireLinkedSourceChange(context.link.projectId, result.changeRequest.projectId);
|
|
942
|
-
return result;
|
|
943
|
-
}
|
|
944
|
-
async function sourceChangeClose(cwd, options, runtime) {
|
|
945
|
-
const context = await loadProjectContext(cwd);
|
|
946
|
-
if (!context.link)
|
|
947
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.");
|
|
948
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
949
|
-
const changeRequestId = requiredSourceChangeRequestId(requiredFlag(options, "change"));
|
|
950
|
-
const current = await client.sourceChangeRequest(changeRequestId);
|
|
951
|
-
requireLinkedSourceChange(context.link.projectId, current.changeRequest.projectId);
|
|
952
|
-
return client.closeSourceChangeRequest(changeRequestId);
|
|
953
|
-
}
|
|
954
|
-
function requireLinkedSourceChange(expectedProjectId, actualProjectId) {
|
|
955
|
-
if (actualProjectId !== expectedProjectId) {
|
|
956
|
-
throw new CliError("project_link_mismatch", "That source change belongs to another Tender project. Run the command from its linked checkout.");
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
function requiredSourceChangeRequestId(value) {
|
|
960
|
-
if (!/^scr_[A-Za-z0-9_-]{8,64}$/u.test(value)) {
|
|
961
|
-
throw new CliError("source_change_id_invalid", "--change must be the scr_ ID returned by source change create.");
|
|
962
|
-
}
|
|
963
|
-
return value;
|
|
964
|
-
}
|
|
965
|
-
async function requireGitRoot(root, runtime) {
|
|
966
|
-
try {
|
|
967
|
-
return resolve((await runtime.captureCommand("git", ["rev-parse", "--show-toplevel"], root)).stdout);
|
|
968
|
-
}
|
|
969
|
-
catch {
|
|
970
|
-
throw new CliError("source_repository_required", "This app must be inside a Git repository before managed source can be connected.");
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
async function ensureGitRemote(gitRoot, remote, remoteUrl, runtime) {
|
|
974
|
-
assertGitRemoteName(remote);
|
|
975
|
-
const remotes = (await runtime.captureCommand("git", ["remote"], gitRoot)).stdout.split(/\r?\n/u).filter(Boolean);
|
|
976
|
-
if (!remotes.includes(remote)) {
|
|
977
|
-
await runtime.captureCommand("git", ["remote", "add", remote, remoteUrl], gitRoot);
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
const current = (await runtime.captureCommand("git", ["remote", "get-url", remote], gitRoot)).stdout;
|
|
981
|
-
if (current !== remoteUrl) {
|
|
982
|
-
throw new CliError("source_remote_conflict", `Git remote ${remote} already points somewhere else. Tender will not replace it automatically.`, `Choose another name with --remote, or update ${remote} yourself after verifying the URL.`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
function assertGitRemoteName(remote) {
|
|
986
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(remote)) {
|
|
987
|
-
throw new CliError("source_remote_invalid", "The Git remote name is invalid.");
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
function assertGitBranchName(branch) {
|
|
991
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(branch)) {
|
|
992
|
-
throw new CliError("source_branch_invalid", "The managed source branch name is invalid.");
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
function safeRepositoryName(value) {
|
|
996
|
-
const normalized = value.replaceAll(/[^A-Za-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
997
|
-
return normalized.length >= 2 ? normalized.slice(0, 100) : "tender-app";
|
|
998
|
-
}
|
|
999
|
-
async function tail(cwd, options, runtime) {
|
|
1000
|
-
const context = await loadProjectContext(cwd);
|
|
1001
|
-
if (!context.link) {
|
|
1002
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.", "tender-accounts link --json");
|
|
1003
|
-
}
|
|
1004
|
-
const projectId = requiredProjectId(context.link.projectId);
|
|
1005
|
-
const resolved = await authenticatedClient(cwd, context.link, options, runtime);
|
|
1006
|
-
const production = options.booleans.has("production");
|
|
1007
|
-
const deliveryId = options.flags.get("delivery");
|
|
1008
|
-
if (production === Boolean(deliveryId)) {
|
|
1009
|
-
throw new CliError("runtime_log_target_required", "Choose exactly one runtime log target: --delivery dly_... or --production.", "tender-accounts tail --delivery dly_... --json");
|
|
1010
|
-
}
|
|
1011
|
-
if (deliveryId && !/^dly_[A-Za-z0-9_-]{8,64}$/u.test(deliveryId)) {
|
|
1012
|
-
throw new CliError("developer_delivery_id_required", "--delivery must be a dly_ developer-delivery ID.");
|
|
1013
|
-
}
|
|
1014
|
-
const filters = {
|
|
1015
|
-
statuses: csvFlag(options, "status"),
|
|
1016
|
-
methods: csvFlag(options, "method"),
|
|
1017
|
-
search: options.flags.get("search") ?? null,
|
|
1018
|
-
samplingRate: decimalFlag(options, "sampling-rate", 0, 1, 1),
|
|
1019
|
-
};
|
|
1020
|
-
const session = await resolved.client.createRuntimeLogSession(projectId, {
|
|
1021
|
-
environment: production ? "production" : "preview",
|
|
1022
|
-
...(deliveryId ? { deliveryId } : {}),
|
|
1023
|
-
filters,
|
|
1024
|
-
});
|
|
1025
|
-
const streamUrl = new URL(session.streamUrl);
|
|
1026
|
-
streamUrl.protocol = streamUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
1027
|
-
runtime.stderr.write(`Streaming ${session.environment} logs for ${projectId} until ${session.expiresAt}. Press Ctrl+C to stop.\n`);
|
|
1028
|
-
const socket = runtime.openLogStream(streamUrl.toString(), [session.protocol, `tender-auth.${session.streamToken}`]);
|
|
1029
|
-
let interrupted = false;
|
|
1030
|
-
const removeInterrupt = runtime.onInterrupt(() => {
|
|
1031
|
-
interrupted = true;
|
|
1032
|
-
socket.close(1000, "CLI interrupted");
|
|
1033
|
-
});
|
|
1034
|
-
try {
|
|
1035
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
1036
|
-
socket.addEventListener("message", (event) => {
|
|
1037
|
-
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
1038
|
-
if (options.json)
|
|
1039
|
-
runtime.stdout.write(`${raw}\n`);
|
|
1040
|
-
else
|
|
1041
|
-
runtime.stdout.write(`${formatRuntimeLogEvent(raw)}\n`);
|
|
1042
|
-
});
|
|
1043
|
-
socket.addEventListener("close", (event) => {
|
|
1044
|
-
if (!interrupted && event.code !== 1000) {
|
|
1045
|
-
rejectPromise(new CliError("runtime_log_stream_closed", `Runtime log stream closed (${event.code}${event.reason ? `: ${event.reason}` : ""}).`));
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
resolvePromise();
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
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.")));
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
finally {
|
|
1055
|
-
removeInterrupt();
|
|
1056
|
-
await resolved.client.closeRuntimeLogSession(session.id).catch(() => undefined);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
function formatRuntimeLogEvent(raw) {
|
|
1060
|
-
try {
|
|
1061
|
-
const value = JSON.parse(raw);
|
|
1062
|
-
if (!isRecord(value))
|
|
1063
|
-
return terminalSafe(raw);
|
|
1064
|
-
const event = isRecord(value.event) ? value.event : {};
|
|
1065
|
-
const timestamp = typeof value.timestamp === "number" ? new Date(value.timestamp).toISOString() : "unknown-time";
|
|
1066
|
-
const request = [event.method, event.url, event.status]
|
|
1067
|
-
.filter((part) => part !== undefined)
|
|
1068
|
-
.map((part) => terminalSafe(String(part)))
|
|
1069
|
-
.join(" ");
|
|
1070
|
-
const correlation = isRecord(value.correlation) ? value.correlation : {};
|
|
1071
|
-
const correlationSuffix = [
|
|
1072
|
-
["ray", correlation.rayId],
|
|
1073
|
-
["trace", correlation.traceId],
|
|
1074
|
-
["span", correlation.spanId],
|
|
1075
|
-
].flatMap(([label, identifier]) => (typeof identifier === "string" ? [` ${label}=${terminalSafe(identifier)}`] : [])).join("");
|
|
1076
|
-
const lines = [
|
|
1077
|
-
`${timestamp} ${terminalSafe(String(value.outcome ?? "unknown"))}${request ? ` ${request}` : ""}${correlationSuffix}`,
|
|
1078
|
-
];
|
|
1079
|
-
if (Array.isArray(value.logs)) {
|
|
1080
|
-
for (const log of value.logs) {
|
|
1081
|
-
if (isRecord(log))
|
|
1082
|
-
lines.push(` ${String(log.level ?? "log")}: ${JSON.stringify(log.message ?? [])}`);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
if (Array.isArray(value.exceptions)) {
|
|
1086
|
-
for (const exception of value.exceptions) {
|
|
1087
|
-
if (isRecord(exception)) {
|
|
1088
|
-
lines.push(` exception: ${terminalSafe(String(exception.name ?? "Error"))}: ${terminalSafe(String(exception.message ?? ""))}`);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
return lines.join("\n");
|
|
1093
|
-
}
|
|
1094
|
-
catch {
|
|
1095
|
-
return terminalSafe(raw);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
function terminalSafe(value) {
|
|
1099
|
-
return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, (character) => (`\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`));
|
|
1100
|
-
}
|
|
1101
|
-
async function authStatus(cwd, options, runtime) {
|
|
1102
|
-
const context = await optionalContext(cwd);
|
|
1103
|
-
const resolved = await authenticatedClient(cwd, context?.link ?? null, options, runtime);
|
|
1104
|
-
const principal = await resolved.client.whoami();
|
|
1105
|
-
if (resolved.profile)
|
|
1106
|
-
await savePrincipalSummary(runtime.authStore, resolved.profile, principal);
|
|
1107
|
-
return {
|
|
1108
|
-
...principal,
|
|
1109
|
-
authentication: {
|
|
1110
|
-
source: resolved.source,
|
|
1111
|
-
apiUrl: resolved.apiUrl,
|
|
1112
|
-
...(resolved.accessTokenExpiresAt ? { accessTokenExpiresAt: resolved.accessTokenExpiresAt } : {}),
|
|
1113
|
-
...(resolved.source === "login" ? { profile: resolved.profile, storage: runtime.authStore.description() } : {}),
|
|
1114
|
-
},
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
async function authList(runtime) {
|
|
1118
|
-
const profiles = await runtime.authStore.list();
|
|
1119
|
-
const bindings = await runtime.profileBindings.list();
|
|
1120
|
-
return {
|
|
1121
|
-
schema: "tender.accounts-auth-profiles/v1",
|
|
1122
|
-
storage: runtime.authStore.description(),
|
|
1123
|
-
bindingStorage: runtime.profileBindings.description(),
|
|
1124
|
-
profiles: profiles.map(({ profile, auth }) => ({
|
|
1125
|
-
profile,
|
|
1126
|
-
default: profile === DEFAULT_AUTH_PROFILE,
|
|
1127
|
-
apiUrl: auth.apiUrl,
|
|
1128
|
-
status: auth.pendingDevice ? "authorization_pending" : "authenticated",
|
|
1129
|
-
...(auth.pendingDevice ? { expiresAt: auth.pendingDevice.expiresAt } : {}),
|
|
1130
|
-
...(!auth.pendingDevice && auth.refreshTokenExpiresAt ? { expiresAt: auth.refreshTokenExpiresAt } : {}),
|
|
1131
|
-
boundDirectories: bindings
|
|
1132
|
-
.filter((binding) => binding.profile === profile)
|
|
1133
|
-
.map((binding) => binding.directory),
|
|
1134
|
-
containsCredential: false,
|
|
1135
|
-
})),
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
async function authLogin(cwd, options, runtime) {
|
|
1139
|
-
return await authenticateProfile(cwd, options, runtime, DEFAULT_AUTH_PROFILE, options.booleans.has("force"));
|
|
1140
|
-
}
|
|
1141
|
-
async function authCreate(cwd, options, runtime) {
|
|
1142
|
-
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
1143
|
-
const profile = validateNamedAuthProfile(requiredPositional(options, 0, "profile name"));
|
|
1144
|
-
return await authenticateProfile(cwd, options, runtime, profile, true);
|
|
1145
|
-
}
|
|
1146
|
-
async function authenticateProfile(cwd, options, runtime, profile, replaceExisting) {
|
|
1147
|
-
const context = await optionalContext(cwd);
|
|
1148
|
-
const apiUrl = selectedApiUrl(options, context?.link ?? null, runtime.environment);
|
|
1149
|
-
const projectHint = options.flags.get("project") ?? context?.link?.projectId;
|
|
1150
|
-
const clientName = options.flags.get("client-name") ?? `Tender CLI on ${hostname()}`;
|
|
1151
|
-
const existing = await runtime.authStore.read(profile);
|
|
1152
|
-
if (existing && !replaceExisting) {
|
|
1153
|
-
const replacement = profile === DEFAULT_AUTH_PROFILE
|
|
1154
|
-
? "tender-accounts auth login --force"
|
|
1155
|
-
: `tender-accounts auth create ${profile}`;
|
|
1156
|
-
throw new CliError("already_authenticated", existing.pendingDevice
|
|
1157
|
-
? `Tender CLI profile ${profile} already has a pending device authorization.`
|
|
1158
|
-
: `Tender CLI profile ${profile} is already authenticated.`, existing.pendingDevice
|
|
1159
|
-
? `Run tender-accounts auth status --profile ${profile} --json after approving it, or ${replacement} to replace it.`
|
|
1160
|
-
: `Run tender-accounts auth status --profile ${profile} --json, or ${replacement} to replace it.`);
|
|
1161
|
-
}
|
|
1162
|
-
if (existing?.refreshToken) {
|
|
1163
|
-
await new OAuthApiClient(existing.apiUrl, runtime.fetcher).revoke(existing.refreshToken);
|
|
1164
|
-
}
|
|
1165
|
-
if (existing)
|
|
1166
|
-
await clearStoredProfile(runtime.authStore, profile, existing);
|
|
1167
|
-
const oauth = new OAuthApiClient(apiUrl, runtime.fetcher);
|
|
1168
|
-
if (options.booleans.has("device")) {
|
|
1169
|
-
const device = await oauth.deviceAuthorization({ clientName, ...(projectHint ? { projectHint } : {}) });
|
|
1170
|
-
const expiresAt = new Date(Date.now() + device.expires_in * 1000).toISOString();
|
|
1171
|
-
const pending = {
|
|
1172
|
-
schema: CLI_AUTH_SCHEMA,
|
|
1173
|
-
apiUrl,
|
|
1174
|
-
pendingDevice: {
|
|
1175
|
-
deviceCode: device.device_code,
|
|
1176
|
-
userCode: device.user_code,
|
|
1177
|
-
verificationUri: device.verification_uri,
|
|
1178
|
-
verificationUriComplete: device.verification_uri_complete,
|
|
1179
|
-
expiresAt,
|
|
1180
|
-
intervalSeconds: device.interval,
|
|
1181
|
-
},
|
|
1182
|
-
};
|
|
1183
|
-
if (!await runtime.authStore.compareAndSwap(profile, null, pending))
|
|
1184
|
-
throw authProfileChanged(profile);
|
|
1185
|
-
if (!options.booleans.has("no-open"))
|
|
1186
|
-
await runtime.openBrowser(device.verification_uri_complete);
|
|
1187
|
-
if (!options.booleans.has("wait")) {
|
|
1188
|
-
return {
|
|
1189
|
-
status: "authorization_pending",
|
|
1190
|
-
flow: "device",
|
|
1191
|
-
userCode: device.user_code,
|
|
1192
|
-
verificationUrl: device.verification_uri,
|
|
1193
|
-
verificationUrlComplete: device.verification_uri_complete,
|
|
1194
|
-
profile,
|
|
1195
|
-
expiresAt,
|
|
1196
|
-
next: `Approve the request, then run tender-accounts auth status --profile ${profile} --json.`,
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
const token = await waitForDeviceToken(oauth, pending, profile, runtime);
|
|
1200
|
-
await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, pending);
|
|
1201
|
-
return await authenticatedLoginSummary(apiUrl, token, profile, runtime);
|
|
1202
|
-
}
|
|
1203
|
-
const pkce = createPkcePair();
|
|
1204
|
-
const callback = await runtime.waitForLoopbackAuthorization({
|
|
1205
|
-
timeoutMilliseconds: numberFlag(options, "timeout-seconds", 60, 900, 600) * 1000,
|
|
1206
|
-
noOpen: options.booleans.has("no-open"),
|
|
1207
|
-
openBrowser: runtime.openBrowser,
|
|
1208
|
-
notify: (authorizationUrl) => runtime.stderr.write(`Open this URL to authorize Tender CLI:\n${authorizationUrl}\n`),
|
|
1209
|
-
start: async (redirectUri) => {
|
|
1210
|
-
const request = await oauth.createAuthorizationRequest({
|
|
1211
|
-
clientName,
|
|
1212
|
-
redirectUri,
|
|
1213
|
-
codeChallenge: pkce.challenge,
|
|
1214
|
-
state: pkce.state,
|
|
1215
|
-
...(projectHint ? { projectHint } : {}),
|
|
1216
|
-
});
|
|
1217
|
-
return { authorizationUrl: request.authorization_url, expectedState: pkce.state };
|
|
1218
|
-
},
|
|
1219
|
-
});
|
|
1220
|
-
const token = await oauth.exchangeAuthorizationCode({
|
|
1221
|
-
code: callback.code,
|
|
1222
|
-
codeVerifier: pkce.verifier,
|
|
1223
|
-
redirectUri: callback.redirectUri,
|
|
1224
|
-
});
|
|
1225
|
-
await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, null);
|
|
1226
|
-
return await authenticatedLoginSummary(apiUrl, token, profile, runtime);
|
|
1227
|
-
}
|
|
1228
|
-
async function authActivate(cwd, options, runtime) {
|
|
1229
|
-
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
1230
|
-
const profile = validateNamedAuthProfile(requiredPositional(options, 0, "profile name"));
|
|
1231
|
-
if (!await runtime.authStore.read(profile)) {
|
|
1232
|
-
throw new CliError("auth_profile_not_found", `Tender Accounts auth profile ${profile} does not exist.`, `tender-accounts auth create ${profile}`);
|
|
1233
|
-
}
|
|
1234
|
-
const directory = resolve(cwd, options.positionals[1] ?? ".");
|
|
1235
|
-
await runtime.profileBindings.activate(profile, directory);
|
|
1236
|
-
return {
|
|
1237
|
-
status: "activated",
|
|
1238
|
-
profile,
|
|
1239
|
-
directory,
|
|
1240
|
-
inheritedByDescendants: true,
|
|
1241
|
-
containsCredential: false,
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
async function authDeactivate(cwd, options, runtime) {
|
|
1245
|
-
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
1246
|
-
const directory = resolve(cwd, options.positionals[0] ?? ".");
|
|
1247
|
-
const removed = await runtime.profileBindings.deactivate(directory);
|
|
1248
|
-
const fallback = await runtime.profileBindings.resolve(directory);
|
|
1249
|
-
return {
|
|
1250
|
-
status: "deactivated",
|
|
1251
|
-
profile: removed.profile,
|
|
1252
|
-
directory: removed.directory,
|
|
1253
|
-
activeProfile: fallback?.profile ?? (await runtime.authStore.read(DEFAULT_AUTH_PROFILE) ? DEFAULT_AUTH_PROFILE : null),
|
|
1254
|
-
containsCredential: false,
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
async function authDelete(_cwd, options, runtime) {
|
|
1258
|
-
assertNoEnvironmentCredentialForProfileManagement(runtime.environment);
|
|
1259
|
-
const profile = validateNamedAuthProfile(requiredPositional(options, 0, "profile name"));
|
|
1260
|
-
const stored = await runtime.authStore.read(profile);
|
|
1261
|
-
if (!stored)
|
|
1262
|
-
throw new CliError("auth_profile_not_found", `Tender Accounts auth profile ${profile} does not exist.`);
|
|
1263
|
-
if (!options.booleans.has("local") && stored.refreshToken) {
|
|
1264
|
-
await new OAuthApiClient(stored.apiUrl, runtime.fetcher).revoke(stored.refreshToken);
|
|
1265
|
-
}
|
|
1266
|
-
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
1267
|
-
const removedBindings = await runtime.profileBindings.removeProfile(profile);
|
|
1268
|
-
return {
|
|
1269
|
-
status: "deleted",
|
|
1270
|
-
profile,
|
|
1271
|
-
remoteSessionRevoked: !options.booleans.has("local") && Boolean(stored.refreshToken),
|
|
1272
|
-
removedBindings,
|
|
1273
|
-
storage: runtime.authStore.description(),
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
function assertNoEnvironmentCredentialForProfileManagement(environment) {
|
|
1277
|
-
if (!environment.TENDER_ACCOUNTS_TOKEN)
|
|
1278
|
-
return;
|
|
1279
|
-
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.");
|
|
1280
|
-
}
|
|
1281
|
-
async function authLogout(_cwd, options, runtime) {
|
|
1282
|
-
if (options.booleans.has("all") && options.booleans.has("local")) {
|
|
1283
|
-
await runtime.authStore.clearAll();
|
|
1284
|
-
await runtime.profileBindings.clearAll();
|
|
1285
|
-
return {
|
|
1286
|
-
status: "logged_out",
|
|
1287
|
-
profiles: "all",
|
|
1288
|
-
remoteSessionRevoked: false,
|
|
1289
|
-
remoteSessionsRevoked: 0,
|
|
1290
|
-
storage: runtime.authStore.description(),
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
const profiles = await runtime.authStore.list();
|
|
1294
|
-
if (profiles.length === 0)
|
|
1295
|
-
return { status: "not_authenticated", remoteSessionRevoked: false };
|
|
1296
|
-
if (!options.booleans.has("all") && !profiles.some(({ profile }) => profile === DEFAULT_AUTH_PROFILE)) {
|
|
1297
|
-
return {
|
|
1298
|
-
status: "not_authenticated",
|
|
1299
|
-
profile: DEFAULT_AUTH_PROFILE,
|
|
1300
|
-
namedProfilesRetained: profiles.length,
|
|
1301
|
-
remoteSessionRevoked: false,
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
const selectedProfiles = options.booleans.has("all")
|
|
1305
|
-
? profiles.map(({ profile }) => profile)
|
|
1306
|
-
: [DEFAULT_AUTH_PROFILE];
|
|
1307
|
-
let revoked = 0;
|
|
1308
|
-
for (const profile of selectedProfiles) {
|
|
1309
|
-
const stored = await runtime.authStore.read(profile);
|
|
1310
|
-
if (!stored)
|
|
1311
|
-
continue;
|
|
1312
|
-
if (!options.booleans.has("local") && stored.refreshToken) {
|
|
1313
|
-
await new OAuthApiClient(stored.apiUrl, runtime.fetcher).revoke(stored.refreshToken);
|
|
1314
|
-
revoked += 1;
|
|
1315
|
-
}
|
|
1316
|
-
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
1317
|
-
}
|
|
1318
|
-
if (options.booleans.has("all"))
|
|
1319
|
-
await runtime.profileBindings.clearAll();
|
|
1320
|
-
return {
|
|
1321
|
-
status: "logged_out",
|
|
1322
|
-
profiles: selectedProfiles,
|
|
1323
|
-
remoteSessionRevoked: revoked > 0,
|
|
1324
|
-
remoteSessionsRevoked: revoked,
|
|
1325
|
-
storage: runtime.authStore.description(),
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
async function linkProject(cwd, options, runtime) {
|
|
1329
|
-
const context = await loadProjectContext(cwd);
|
|
1330
|
-
const apiUrl = selectedApiUrl(options, context.link, runtime.environment);
|
|
1331
|
-
const resolved = await authenticatedClient(context.root, context.link, options, runtime);
|
|
1332
|
-
const { client } = resolved;
|
|
1333
|
-
const principal = await client.whoami();
|
|
1334
|
-
if (resolved.profile)
|
|
1335
|
-
await savePrincipalSummary(runtime.authStore, resolved.profile, principal);
|
|
1336
|
-
const requested = options.flags.get("project");
|
|
1337
|
-
const project = requested
|
|
1338
|
-
? principal.projects.find((candidate) => candidate.projectId === requiredProjectId(requested))
|
|
1339
|
-
: principal.projects.length === 1 ? principal.projects[0] : undefined;
|
|
1340
|
-
if (!project) {
|
|
1341
|
-
throw new CliError(requested ? "project_not_permitted" : "project_required", requested
|
|
1342
|
-
? "The current credential is not permitted to use that project."
|
|
1343
|
-
: "The credential grants more than one project; select one explicitly.", `tender-accounts link --project ${principal.projects[0]?.projectId ?? "prj_..."} --json`);
|
|
1344
|
-
}
|
|
1345
|
-
if (project.role !== "developer") {
|
|
1346
|
-
throw new CliError("project_role_insufficient", "The selected project grant is read-only and cannot create previews.", "Ask a merchant administrator for a short-lived developer credential scoped to this project.");
|
|
1347
|
-
}
|
|
1348
|
-
const path = await writeProjectLink(context.root, {
|
|
1349
|
-
schema: CLI_LINK_SCHEMA,
|
|
1350
|
-
projectId: project.projectId,
|
|
1351
|
-
apiUrl,
|
|
1352
|
-
});
|
|
1353
|
-
return {
|
|
1354
|
-
status: "linked",
|
|
1355
|
-
tenantId: principal.tenantId,
|
|
1356
|
-
projectId: project.projectId,
|
|
1357
|
-
role: project.role,
|
|
1358
|
-
apiUrl,
|
|
1359
|
-
...(resolved.profile ? { activeProfile: resolved.profile } : {}),
|
|
1360
|
-
linkPath: path,
|
|
1361
|
-
containsCredential: false,
|
|
1362
|
-
reminder: "Keep .tender/ and .dev.vars out of Git.",
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
async function doctor(cwd, options, runtime) {
|
|
1366
|
-
const checks = [];
|
|
1367
|
-
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
1368
|
-
checks.push({
|
|
1369
|
-
name: "node",
|
|
1370
|
-
status: nodeMajor >= 22 ? "pass" : "fail",
|
|
1371
|
-
message: nodeMajor >= 22 ? `Node ${process.versions.node}` : "Node 22 or newer is required.",
|
|
1372
|
-
});
|
|
1373
|
-
let context = null;
|
|
1374
|
-
try {
|
|
1375
|
-
context = await loadProjectContext(cwd);
|
|
1376
|
-
checks.push({ name: "config", status: "pass", message: context.configPath });
|
|
1377
|
-
}
|
|
1378
|
-
catch (error) {
|
|
1379
|
-
checks.push({ name: "config", status: "fail", message: normalizeError(error).message });
|
|
1380
|
-
}
|
|
1381
|
-
if (context) {
|
|
1382
|
-
checks.push(context.link
|
|
1383
|
-
? { name: "link", status: "pass", message: `${context.link.projectId} at ${context.link.apiUrl}` }
|
|
1384
|
-
: { name: "link", status: "fail", message: "Run tender-accounts link after receiving a project-scoped credential." });
|
|
1385
|
-
try {
|
|
1386
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
1387
|
-
const principal = await client.whoami();
|
|
1388
|
-
const project = context.link
|
|
1389
|
-
? principal.projects.find((candidate) => candidate.projectId === context.link?.projectId)
|
|
1390
|
-
: undefined;
|
|
1391
|
-
checks.push({ name: "authentication", status: "pass", message: `${principal.ownerEmail} · ${principal.label}` });
|
|
1392
|
-
checks.push(project
|
|
1393
|
-
? { name: "project-access", status: project.role === "developer" ? "pass" : "fail", message: `${project.projectId} · ${project.role}` }
|
|
1394
|
-
: { name: "project-access", status: "fail", message: "The linked project is outside this credential's scope." });
|
|
1395
|
-
}
|
|
1396
|
-
catch (error) {
|
|
1397
|
-
checks.push({ name: "authentication", status: "fail", message: normalizeError(error).message });
|
|
1398
|
-
}
|
|
1399
|
-
const artifactDirectory = resolveArtifactDirectory(context);
|
|
1400
|
-
checks.push(await pathExists(resolve(artifactDirectory, BUILD_ENVELOPE_FILENAME))
|
|
1401
|
-
? { name: "artifact", status: "pass", message: artifactDirectory }
|
|
1402
|
-
: { name: "artifact", status: "warn", message: "No packaged artifact yet; tender-accounts build will create it." });
|
|
1403
|
-
}
|
|
1404
|
-
const failed = checks.some((check) => check.status === "fail");
|
|
1405
|
-
return {
|
|
1406
|
-
value: { status: failed ? "failed" : "ready", checks },
|
|
1407
|
-
exitCode: failed ? 1 : 0,
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
async function runDev(cwd, options, runtime) {
|
|
1411
|
-
const context = await loadProjectContext(cwd);
|
|
1412
|
-
if (!context.config.commands.dev) {
|
|
1413
|
-
throw new CliError("dev_command_missing", "This app does not declare commands.dev in tender-accounts.json.");
|
|
1414
|
-
}
|
|
1415
|
-
await runtime.runProjectCommand({
|
|
1416
|
-
command: context.config.commands.dev,
|
|
1417
|
-
cwd: context.root,
|
|
1418
|
-
environment: runtime.environment,
|
|
1419
|
-
machineOutput: options.json,
|
|
1420
|
-
});
|
|
1421
|
-
return { value: { status: "stopped", command: context.config.commands.dev } };
|
|
1422
|
-
}
|
|
1423
|
-
async function runBuildStage(cwd, options, runtime, stage, extraEnvironment = {}) {
|
|
1424
|
-
const context = await loadProjectContext(cwd);
|
|
1425
|
-
const command = context.config.commands[stage];
|
|
1426
|
-
runtime.stderr.write(`tender-accounts: ${stage}\n`);
|
|
1427
|
-
await runtime.runProjectCommand({
|
|
1428
|
-
command,
|
|
1429
|
-
cwd: context.root,
|
|
1430
|
-
environment: { ...runtime.environment, ...extraEnvironment },
|
|
1431
|
-
machineOutput: options.json,
|
|
1432
|
-
});
|
|
1433
|
-
return { status: "succeeded", stage, command };
|
|
1434
|
-
}
|
|
1435
|
-
async function buildProject(cwd, options, runtime) {
|
|
1436
|
-
const context = await loadProjectContext(cwd);
|
|
1437
|
-
const source = await sourceState(context.root, runtime);
|
|
1438
|
-
await runBuildStage(cwd, options, runtime, "check", { TENDER_SOURCE_REVISION: source.revision });
|
|
1439
|
-
await runBuildStage(cwd, options, runtime, "package", { TENDER_SOURCE_REVISION: source.revision });
|
|
1440
|
-
let artifact = await verifyArtifact(resolveArtifactDirectory(context));
|
|
1441
|
-
artifact = await labelLocalArtifact(artifact, { gitRevision: source.revision, dirty: source.dirty });
|
|
1442
|
-
return buildSummary(artifact, source.dirty);
|
|
1443
|
-
}
|
|
1444
|
-
async function preview(cwd, options, runtime) {
|
|
1445
|
-
const context = await loadProjectContext(cwd);
|
|
1446
|
-
const build = await buildProject(cwd, options, runtime);
|
|
1447
|
-
const artifact = await verifyArtifact(resolveArtifactDirectory(context));
|
|
1448
|
-
if (options.booleans.has("dry-run")) {
|
|
1449
|
-
return { ...build, status: "validated", uploaded: false, productionChanged: false };
|
|
1450
|
-
}
|
|
1451
|
-
if (!context.link) {
|
|
1452
|
-
throw new CliError("project_not_linked", "This app is not linked to a Tender Accounts project.", "tender-accounts link --json");
|
|
1453
|
-
}
|
|
1454
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
1455
|
-
const suffix = artifact.envelope.bundleDigest.slice(0, 24);
|
|
1456
|
-
const submission = await client.createSubmission(context.link.projectId, {
|
|
1457
|
-
idempotencyKey: options.flags.get("submission-key") ?? `submit-${suffix}`,
|
|
1458
|
-
envelope: artifact.envelope,
|
|
1459
|
-
});
|
|
1460
|
-
for (const file of submission.files) {
|
|
1461
|
-
if (file.uploaded)
|
|
1462
|
-
continue;
|
|
1463
|
-
const bytes = await readBoundedFile(safeArtifactPath(artifact.directory, file.path), file.bytes);
|
|
1464
|
-
await client.uploadFile(submission.id, file.index, bytes);
|
|
1465
|
-
}
|
|
1466
|
-
const finalized = await client.finalizeSubmission(submission.id);
|
|
1467
|
-
if (finalized.status !== "ready" || !finalized.releaseId) {
|
|
1468
|
-
throw new CliError("artifact_finalize_failed", "The artifact submission did not finalize to a ready release.");
|
|
1469
|
-
}
|
|
1470
|
-
let delivery = await client.createDelivery(context.link.projectId, {
|
|
1471
|
-
submissionId: finalized.id,
|
|
1472
|
-
idempotencyKey: options.flags.get("delivery-key") ?? `preview-${suffix}`,
|
|
1473
|
-
});
|
|
1474
|
-
if (options.booleans.has("no-wait")) {
|
|
1475
|
-
return deliverySummary(context.link.projectId, finalized.id, finalized.releaseId, delivery);
|
|
1476
|
-
}
|
|
1477
|
-
delivery = await waitForDelivery(client, delivery, options, runtime);
|
|
1478
|
-
const session = await client.previewSession(delivery.id, options.flags.get("return-path") ?? context.config.returnPath);
|
|
1479
|
-
return {
|
|
1480
|
-
...deliverySummary(context.link.projectId, finalized.id, finalized.releaseId, delivery),
|
|
1481
|
-
status: "succeeded",
|
|
1482
|
-
sourceRevision: artifact.envelope.sourceRevision,
|
|
1483
|
-
bundleDigest: artifact.envelope.bundleDigest,
|
|
1484
|
-
previewUrl: session.url,
|
|
1485
|
-
previewGrantId: session.grantId,
|
|
1486
|
-
previewExpiresAt: session.expiresAt,
|
|
1487
|
-
productionChanged: false,
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
async function deliveryStatus(cwd, options, runtime) {
|
|
1491
|
-
const context = await loadProjectContext(cwd);
|
|
1492
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
1493
|
-
return await client.delivery(requiredDeliveryId(options));
|
|
1494
|
-
}
|
|
1495
|
-
async function deliveryPreview(cwd, options, runtime) {
|
|
1496
|
-
const context = await loadProjectContext(cwd);
|
|
1497
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
1498
|
-
const deliveryId = requiredDeliveryId(options);
|
|
1499
|
-
const session = await client.previewSession(deliveryId, options.flags.get("return-path") ?? context.config.returnPath);
|
|
1500
|
-
return { status: "ready", deliveryId, previewUrl: session.url, expiresAt: session.expiresAt };
|
|
1501
|
-
}
|
|
1502
|
-
async function deliveryRetry(cwd, options, runtime) {
|
|
1503
|
-
const context = await loadProjectContext(cwd);
|
|
1504
|
-
const { client } = await authenticatedClient(cwd, context.link, options, runtime);
|
|
1505
|
-
return await client.retryDelivery(requiredDeliveryId(options));
|
|
1506
|
-
}
|
|
1507
|
-
async function authenticatedClient(cwd, link, options, runtime) {
|
|
1508
|
-
const apiUrl = selectedApiUrl(options, link, runtime.environment);
|
|
1509
|
-
const stdin = options.booleans.has("token-stdin") ? runtime.stdin : undefined;
|
|
1510
|
-
const explicit = await readOptionalToken(cwd, runtime.environment, stdin);
|
|
1511
|
-
if (explicit) {
|
|
1512
|
-
const source = stdin
|
|
1513
|
-
? "stdin"
|
|
1514
|
-
: runtime.environment.TENDER_ACCOUNTS_TOKEN
|
|
1515
|
-
? "environment"
|
|
1516
|
-
: "dev-vars";
|
|
1517
|
-
return { client: new AgentApiClient(apiUrl, explicit, runtime.fetcher), apiUrl, source };
|
|
1518
|
-
}
|
|
1519
|
-
const profile = await selectAuthProfile(cwd, options, runtime);
|
|
1520
|
-
const stored = await resolveStoredLogin(apiUrl, profile, runtime);
|
|
1521
|
-
return {
|
|
1522
|
-
client: new AgentApiClient(apiUrl, stored.accessToken, runtime.fetcher),
|
|
1523
|
-
apiUrl,
|
|
1524
|
-
source: "login",
|
|
1525
|
-
profile,
|
|
1526
|
-
accessTokenExpiresAt: stored.accessTokenExpiresAt,
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
async function selectAuthProfile(cwd, options, runtime) {
|
|
1530
|
-
const explicit = options.flags.get("profile") ?? runtime.environment.TENDER_ACCOUNTS_PROFILE;
|
|
1531
|
-
if (explicit)
|
|
1532
|
-
return validateAuthProfile(explicit);
|
|
1533
|
-
const binding = await runtime.profileBindings.resolve(cwd);
|
|
1534
|
-
if (binding)
|
|
1535
|
-
return binding.profile;
|
|
1536
|
-
const profiles = await runtime.authStore.list();
|
|
1537
|
-
if (profiles.some(({ profile }) => profile === DEFAULT_AUTH_PROFILE))
|
|
1538
|
-
return DEFAULT_AUTH_PROFILE;
|
|
1539
|
-
if (profiles.length === 0) {
|
|
1540
|
-
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");
|
|
1541
|
-
}
|
|
1542
|
-
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}.`);
|
|
1543
|
-
}
|
|
1544
|
-
function selectedApiUrl(options, link, environment) {
|
|
1545
|
-
return normalizeApiUrl(options.flags.get("api-url")
|
|
1546
|
-
?? environment.TENDER_ACCOUNTS_API_URL
|
|
1547
|
-
?? link?.apiUrl
|
|
1548
|
-
?? "https://agents.tenderprompt.dev");
|
|
1549
|
-
}
|
|
1550
|
-
async function resolveStoredLogin(apiUrl, profile, runtime) {
|
|
1551
|
-
let stored = await runtime.authStore.read(profile);
|
|
1552
|
-
if (!stored) {
|
|
1553
|
-
throw new CliError("authentication_required", `Tender Accounts auth profile ${profile} is not logged in.`, authCommandForProfile(profile));
|
|
1554
|
-
}
|
|
1555
|
-
if (stored.apiUrl !== apiUrl) {
|
|
1556
|
-
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" : ""}`);
|
|
1557
|
-
}
|
|
1558
|
-
const oauth = new OAuthApiClient(apiUrl, runtime.fetcher);
|
|
1559
|
-
if (stored.pendingDevice) {
|
|
1560
|
-
const pendingDevice = stored.pendingDevice;
|
|
1561
|
-
if (Date.parse(pendingDevice.expiresAt) <= Date.now()) {
|
|
1562
|
-
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
1563
|
-
throw new CliError("device_authorization_expired", "The pending device authorization expired.", `${authCommandForProfile(profile)} --device --json`);
|
|
1564
|
-
}
|
|
1565
|
-
try {
|
|
1566
|
-
const token = await oauth.exchangeDeviceCode(pendingDevice.deviceCode);
|
|
1567
|
-
stored = await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, stored);
|
|
1568
|
-
}
|
|
1569
|
-
catch (error) {
|
|
1570
|
-
if (error instanceof CliApiError && error.code === "authorization_pending") {
|
|
1571
|
-
throw new CliError("authorization_pending", `Approve Tender CLI with code ${pendingDevice.userCode}.`, `Open ${pendingDevice.verificationUriComplete}, then run tender-accounts auth status --profile ${profile} --json.`);
|
|
1572
|
-
}
|
|
1573
|
-
throw error;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
if (!stored.accessToken || !stored.accessTokenExpiresAt || !stored.refreshToken || !stored.refreshTokenExpiresAt) {
|
|
1577
|
-
throw new CliError("authentication_required", "The stored Tender Accounts login is incomplete.", `${authCommandForProfile(profile)}${profile === DEFAULT_AUTH_PROFILE ? " --force" : ""}`);
|
|
1578
|
-
}
|
|
1579
|
-
if (Date.parse(stored.refreshTokenExpiresAt) <= Date.now()) {
|
|
1580
|
-
await clearStoredProfile(runtime.authStore, profile, stored);
|
|
1581
|
-
throw new CliError("authentication_expired", "The Tender Accounts login expired.", authCommandForProfile(profile));
|
|
1582
|
-
}
|
|
1583
|
-
if (Date.parse(stored.accessTokenExpiresAt) <= Date.now() + 30_000) {
|
|
1584
|
-
try {
|
|
1585
|
-
const token = await oauth.refresh(stored.refreshToken);
|
|
1586
|
-
stored = await saveTokenResponse(runtime.authStore, profile, apiUrl, token, oauth, stored, true);
|
|
1587
|
-
}
|
|
1588
|
-
catch (error) {
|
|
1589
|
-
if (!(error instanceof CliApiError && error.code === "invalid_grant"))
|
|
1590
|
-
throw error;
|
|
1591
|
-
const winner = await concurrentRefreshWinner(runtime.authStore, profile, stored);
|
|
1592
|
-
if (!winner)
|
|
1593
|
-
throw error;
|
|
1594
|
-
stored = winner;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
if (!stored.accessToken || !stored.accessTokenExpiresAt) {
|
|
1598
|
-
throw new CliError("authentication_required", "Tender Accounts could not restore the stored login.", `${authCommandForProfile(profile)}${profile === DEFAULT_AUTH_PROFILE ? " --force" : ""}`);
|
|
1599
|
-
}
|
|
1600
|
-
return { accessToken: stored.accessToken, accessTokenExpiresAt: stored.accessTokenExpiresAt };
|
|
1601
|
-
}
|
|
1602
|
-
function authCommandForProfile(profile) {
|
|
1603
|
-
return profile === DEFAULT_AUTH_PROFILE
|
|
1604
|
-
? "tender-accounts auth login"
|
|
1605
|
-
: `tender-accounts auth create ${profile}`;
|
|
1606
|
-
}
|
|
1607
|
-
async function saveTokenResponse(store, profile, apiUrl, token, oauth, previous, acceptConcurrentRefreshWinner = false) {
|
|
1608
|
-
const now = Date.now();
|
|
1609
|
-
const stored = {
|
|
1610
|
-
schema: CLI_AUTH_SCHEMA,
|
|
1611
|
-
apiUrl,
|
|
1612
|
-
accessToken: token.access_token,
|
|
1613
|
-
accessTokenExpiresAt: new Date(now + token.expires_in * 1000).toISOString(),
|
|
1614
|
-
refreshToken: token.refresh_token,
|
|
1615
|
-
refreshTokenExpiresAt: new Date(now + token.refresh_expires_in * 1000).toISOString(),
|
|
1616
|
-
...(previous?.principal ? { principal: previous.principal } : {}),
|
|
1617
|
-
};
|
|
1618
|
-
if (!await store.compareAndSwap(profile, previous, stored)) {
|
|
1619
|
-
try {
|
|
1620
|
-
await oauth.revoke(token.refresh_token);
|
|
1621
|
-
}
|
|
1622
|
-
catch {
|
|
1623
|
-
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.`);
|
|
1624
|
-
}
|
|
1625
|
-
if (acceptConcurrentRefreshWinner && previous) {
|
|
1626
|
-
const winner = await concurrentRefreshWinner(store, profile, previous);
|
|
1627
|
-
if (winner)
|
|
1628
|
-
return winner;
|
|
1629
|
-
}
|
|
1630
|
-
throw authProfileChanged(profile);
|
|
1631
|
-
}
|
|
1632
|
-
return stored;
|
|
1633
|
-
}
|
|
1634
|
-
async function concurrentRefreshWinner(store, profile, previous) {
|
|
1635
|
-
const current = await store.read(profile);
|
|
1636
|
-
if (!current
|
|
1637
|
-
|| current.apiUrl !== previous.apiUrl
|
|
1638
|
-
|| current.pendingDevice
|
|
1639
|
-
|| current.refreshToken === previous.refreshToken
|
|
1640
|
-
|| !sameStoredPrincipal(current, previous)
|
|
1641
|
-
|| !current.accessToken
|
|
1642
|
-
|| !current.accessTokenExpiresAt
|
|
1643
|
-
|| !current.refreshToken
|
|
1644
|
-
|| !current.refreshTokenExpiresAt
|
|
1645
|
-
|| Date.parse(current.accessTokenExpiresAt) <= Date.now() + 30_000
|
|
1646
|
-
|| Date.parse(current.refreshTokenExpiresAt) <= Date.now()) {
|
|
1647
|
-
return null;
|
|
1648
|
-
}
|
|
1649
|
-
return current;
|
|
1650
|
-
}
|
|
1651
|
-
function sameStoredPrincipal(left, right) {
|
|
1652
|
-
if (!left.principal || !right.principal)
|
|
1653
|
-
return false;
|
|
1654
|
-
return left.principal.tenantId === right.principal.tenantId
|
|
1655
|
-
&& left.principal.ownerEmail === right.principal.ownerEmail
|
|
1656
|
-
&& [...left.principal.projects].sort().join("\n") === [...right.principal.projects].sort().join("\n");
|
|
1657
|
-
}
|
|
1658
|
-
async function clearStoredProfile(store, profile, expected) {
|
|
1659
|
-
if (!await store.compareAndSwap(profile, expected, null))
|
|
1660
|
-
throw authProfileChanged(profile);
|
|
1661
|
-
}
|
|
1662
|
-
function authProfileChanged(profile) {
|
|
1663
|
-
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.`);
|
|
1664
|
-
}
|
|
1665
|
-
async function waitForDeviceToken(oauth, pending, profile, runtime) {
|
|
1666
|
-
const device = pending.pendingDevice;
|
|
1667
|
-
if (!device)
|
|
1668
|
-
throw new CliError("device_authorization_missing", "The device authorization was not saved.");
|
|
1669
|
-
while (Date.parse(device.expiresAt) > Date.now()) {
|
|
1670
|
-
try {
|
|
1671
|
-
return await oauth.exchangeDeviceCode(device.deviceCode);
|
|
1672
|
-
}
|
|
1673
|
-
catch (error) {
|
|
1674
|
-
if (!(error instanceof CliApiError && error.code === "authorization_pending"))
|
|
1675
|
-
throw error;
|
|
1676
|
-
await runtime.sleep(device.intervalSeconds * 1000);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
await clearStoredProfile(runtime.authStore, profile, pending);
|
|
1680
|
-
throw new CliError("device_authorization_expired", "The device authorization expired.", `${authCommandForProfile(profile)} --device --json`);
|
|
1681
|
-
}
|
|
1682
|
-
async function authenticatedLoginSummary(apiUrl, token, profile, runtime) {
|
|
1683
|
-
const principal = await new AgentApiClient(apiUrl, token.access_token, runtime.fetcher).whoami();
|
|
1684
|
-
await savePrincipalSummary(runtime.authStore, profile, principal);
|
|
1685
|
-
return {
|
|
1686
|
-
status: "authenticated",
|
|
1687
|
-
flow: "oauth",
|
|
1688
|
-
apiUrl,
|
|
1689
|
-
profile,
|
|
1690
|
-
ownerEmail: principal.ownerEmail,
|
|
1691
|
-
tenantId: principal.tenantId,
|
|
1692
|
-
projects: principal.projects,
|
|
1693
|
-
accessTokenExpiresAt: new Date(Date.now() + token.expires_in * 1000).toISOString(),
|
|
1694
|
-
storage: runtime.authStore.description(),
|
|
1695
|
-
containsCredential: false,
|
|
1696
|
-
};
|
|
1697
|
-
}
|
|
1698
|
-
async function savePrincipalSummary(store, profile, principal) {
|
|
1699
|
-
const stored = await store.read(profile);
|
|
1700
|
-
if (!stored)
|
|
1701
|
-
return;
|
|
1702
|
-
const updated = {
|
|
1703
|
-
...stored,
|
|
1704
|
-
principal: {
|
|
1705
|
-
tenantId: principal.tenantId,
|
|
1706
|
-
ownerEmail: principal.ownerEmail,
|
|
1707
|
-
projects: principal.projects.map(({ projectId }) => projectId),
|
|
1708
|
-
},
|
|
1709
|
-
};
|
|
1710
|
-
if (!await store.compareAndSwap(profile, stored, updated))
|
|
1711
|
-
throw authProfileChanged(profile);
|
|
1712
|
-
}
|
|
1713
|
-
async function sourceState(root, runtime) {
|
|
1714
|
-
let revision;
|
|
1715
|
-
try {
|
|
1716
|
-
revision = (await runtime.captureCommand("git", ["rev-parse", "HEAD"], root)).stdout;
|
|
1717
|
-
}
|
|
1718
|
-
catch {
|
|
1719
|
-
throw new CliError("source_revision_unavailable", "The app must be inside a Git repository with an initial commit before it can be packaged.", "git add . && git commit -m 'Initialize account app'");
|
|
1720
|
-
}
|
|
1721
|
-
if (!/^[a-f0-9]{40}$/u.test(revision)) {
|
|
1722
|
-
throw new CliError("source_revision_unavailable", "Git did not return a 40-character source revision.");
|
|
1723
|
-
}
|
|
1724
|
-
const status = await runtime.captureCommand("git", ["status", "--porcelain", "--untracked-files=normal"], root);
|
|
1725
|
-
return { revision, dirty: status.stdout.length > 0 };
|
|
1726
|
-
}
|
|
1727
|
-
async function waitForDelivery(client, initial, options, runtime) {
|
|
1728
|
-
const timeoutSeconds = numberFlag(options, "timeout-seconds", 60, 1800, 900);
|
|
1729
|
-
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
1730
|
-
let current = initial;
|
|
1731
|
-
while (current.workflow.status === "requested" || current.workflow.status === "running") {
|
|
1732
|
-
if (Date.now() >= deadline) {
|
|
1733
|
-
throw new CliError("preview_timeout", `Preview deployment is still ${current.workflow.status}.`, `tender-accounts delivery status --delivery ${current.id} --json`);
|
|
1734
|
-
}
|
|
1735
|
-
await runtime.sleep(2000);
|
|
1736
|
-
current = await client.delivery(current.id);
|
|
1737
|
-
}
|
|
1738
|
-
if (current.workflow.status !== "succeeded") {
|
|
1739
|
-
throw new CliError(current.workflow.error?.code ?? "preview_failed", current.workflow.error?.message ?? "The preview workflow failed.", `tender-accounts delivery retry --delivery ${current.id} --json`);
|
|
1740
|
-
}
|
|
1741
|
-
return current;
|
|
1742
|
-
}
|
|
1743
|
-
function deliverySummary(projectId, submissionId, releaseId, delivery) {
|
|
1744
|
-
return {
|
|
1745
|
-
status: delivery.workflow.status,
|
|
1746
|
-
projectId,
|
|
1747
|
-
submissionId,
|
|
1748
|
-
releaseId,
|
|
1749
|
-
deliveryId: delivery.id,
|
|
1750
|
-
workflowId: delivery.workflow.id,
|
|
1751
|
-
compositionId: delivery.workflow.result?.compositionId ?? null,
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
function buildSummary(artifact, dirty) {
|
|
1755
|
-
return {
|
|
1756
|
-
status: "built",
|
|
1757
|
-
app: artifact.envelope.manifest.app.slug,
|
|
1758
|
-
sourceRevision: artifact.envelope.sourceRevision,
|
|
1759
|
-
dirty,
|
|
1760
|
-
bundleDigest: artifact.envelope.bundleDigest,
|
|
1761
|
-
artifactDirectory: artifact.directory,
|
|
1762
|
-
fileCount: artifact.fileCount,
|
|
1763
|
-
totalBytes: artifact.totalBytes,
|
|
1764
|
-
declaredResources: artifact.envelope.manifest.resources.length,
|
|
1765
|
-
declaredServices: artifact.envelope.manifest.services.length,
|
|
1766
|
-
declaredSecrets: artifact.envelope.manifest.secrets ?? [],
|
|
1767
|
-
};
|
|
1768
|
-
}
|
|
1769
|
-
async function optionalContext(cwd) {
|
|
1770
|
-
try {
|
|
1771
|
-
return await loadProjectContext(cwd);
|
|
1772
|
-
}
|
|
1773
|
-
catch (error) {
|
|
1774
|
-
if (error instanceof CliError && error.code === "file_missing")
|
|
1775
|
-
return null;
|
|
1776
|
-
throw error;
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
async function installSkill(cwd, options, runtime) {
|
|
1780
|
-
const agent = options.flags.get("agent") ?? "codex";
|
|
1781
|
-
const requestedDirectory = options.flags.get("directory");
|
|
1782
|
-
const base = requestedDirectory
|
|
1783
|
-
? resolve(cwd, requestedDirectory)
|
|
1784
|
-
: agent === "codex"
|
|
1785
|
-
? resolve(runtime.homeDirectory, ".agents/skills")
|
|
1786
|
-
: agent === "claude"
|
|
1787
|
-
? resolve(runtime.homeDirectory, ".claude/skills")
|
|
1788
|
-
: null;
|
|
1789
|
-
if (!base) {
|
|
1790
|
-
throw new CliError("skill_target_invalid", "--agent must be codex or claude unless --directory is provided.");
|
|
1791
|
-
}
|
|
1792
|
-
const target = resolve(base, "tender-accounts/SKILL.md");
|
|
1793
|
-
if (await pathExists(target) && !options.booleans.has("force")) {
|
|
1794
|
-
throw new CliError("skill_exists", `A Tender Accounts skill already exists at ${target}.`, `tender-accounts skill install --agent ${agent} --force`);
|
|
1795
|
-
}
|
|
1796
|
-
await mkdir(dirname(target), { recursive: true });
|
|
1797
|
-
await copyFile(skillPath(), target);
|
|
1798
|
-
return { status: "installed", agent, path: target };
|
|
1799
|
-
}
|
|
1800
|
-
async function readSkill() {
|
|
1801
|
-
return await readFile(skillPath(), "utf8");
|
|
1802
|
-
}
|
|
1803
|
-
function skillPath() {
|
|
1804
|
-
return fileURLToPath(new URL("../skill/tender-accounts/SKILL.md", import.meta.url));
|
|
1805
|
-
}
|
|
1806
|
-
function exampleConfig() {
|
|
1807
|
-
return {
|
|
1808
|
-
schema: CLI_CONFIG_SCHEMA,
|
|
1809
|
-
commands: {
|
|
1810
|
-
dev: "npm run dev",
|
|
1811
|
-
check: "npm run check",
|
|
1812
|
-
package: "npm run pack:tender",
|
|
1813
|
-
},
|
|
1814
|
-
artifactDirectory: ".tender/artifacts/current",
|
|
1815
|
-
returnPath: "/account",
|
|
1816
|
-
};
|
|
1817
|
-
}
|
|
1818
|
-
function commandTopic(argv) {
|
|
1819
|
-
if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
|
|
1820
|
-
return { topic: "help", values: argv.slice(1) };
|
|
1821
|
-
}
|
|
1822
|
-
if (argv[0] === "--version" || argv[0] === "version")
|
|
1823
|
-
return { topic: "version", values: argv.slice(1) };
|
|
1824
|
-
const parent = argv[0];
|
|
1825
|
-
if (parent === "auth" || parent === "delivery" || parent === "source" || parent === "skill" || parent === "config") {
|
|
1826
|
-
const child = argv[1];
|
|
1827
|
-
if (!child || child.startsWith("--"))
|
|
1828
|
-
return { topic: "help", values: [parent] };
|
|
1829
|
-
if (parent === "source" && (child === "default" || child === "history" || child === "operation" || child === "change")) {
|
|
1830
|
-
const grandchild = argv[2];
|
|
1831
|
-
if (!grandchild || grandchild.startsWith("--"))
|
|
1832
|
-
return { topic: "help", values: [`${parent} ${child}`] };
|
|
1833
|
-
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1834
|
-
return { topic: "help", values: [`${parent} ${child} ${grandchild}`] };
|
|
1835
|
-
}
|
|
1836
|
-
return { topic: `${parent} ${child} ${grandchild}`, values: argv.slice(3) };
|
|
1837
|
-
}
|
|
1838
|
-
if (argv.includes("--help") || argv.includes("-h"))
|
|
1839
|
-
return { topic: "help", values: [`${parent} ${child}`] };
|
|
1840
|
-
return { topic: `${parent} ${child}`, values: argv.slice(2) };
|
|
1841
|
-
}
|
|
1842
|
-
if (argv.includes("--help") || argv.includes("-h"))
|
|
1843
|
-
return { topic: "help", values: [parent] };
|
|
1844
|
-
const legacy = {
|
|
1845
|
-
"auth-check": "auth status",
|
|
1846
|
-
status: "delivery status",
|
|
1847
|
-
"preview-session": "delivery preview",
|
|
1848
|
-
};
|
|
1849
|
-
return { topic: legacy[parent] ?? parent ?? "help", values: argv.slice(1) };
|
|
1850
|
-
}
|
|
1851
|
-
function parseOptions(values) {
|
|
1852
|
-
const flags = new Map();
|
|
1853
|
-
const booleans = new Set();
|
|
1854
|
-
const positionals = [];
|
|
1855
|
-
const booleanNames = new Set([
|
|
1856
|
-
"json", "token-stdin", "dry-run", "no-wait", "force",
|
|
1857
|
-
"device", "wait", "no-open", "local", "all", "production",
|
|
1858
|
-
]);
|
|
1859
|
-
for (let index = 0; index < values.length; index += 1) {
|
|
1860
|
-
const raw = values[index];
|
|
1861
|
-
if (!raw?.startsWith("--")) {
|
|
1862
|
-
if (raw)
|
|
1863
|
-
positionals.push(raw);
|
|
1864
|
-
continue;
|
|
1865
|
-
}
|
|
1866
|
-
const name = raw.slice(2);
|
|
1867
|
-
if (name === "token") {
|
|
1868
|
-
throw new CliError("credential_argument_forbidden", "Never pass credentials as command-line arguments.");
|
|
1869
|
-
}
|
|
1870
|
-
if (booleanNames.has(name)) {
|
|
1871
|
-
if (booleans.has(name))
|
|
1872
|
-
throw new CliError("argument_duplicate", `Duplicate --${name} option.`);
|
|
1873
|
-
booleans.add(name);
|
|
1874
|
-
continue;
|
|
1875
|
-
}
|
|
1876
|
-
const value = values[index + 1];
|
|
1877
|
-
if (!value || value.startsWith("--") || flags.has(name)) {
|
|
1878
|
-
throw new CliError("argument_value_missing", `Option --${name} requires exactly one value.`);
|
|
1879
|
-
}
|
|
1880
|
-
flags.set(name, value);
|
|
1881
|
-
index += 1;
|
|
1882
|
-
}
|
|
1883
|
-
return { flags, booleans, positionals, json: booleans.has("json") };
|
|
1884
|
-
}
|
|
1885
|
-
function assertAllowedOptions(topic, options) {
|
|
1886
|
-
const common = ["cwd", "json"];
|
|
1887
|
-
const authentication = ["api-url", "token-stdin", "profile"];
|
|
1888
|
-
const allowed = {
|
|
1889
|
-
init: [...common, "name", "slug", "directory", "template", "dry-run"],
|
|
1890
|
-
"config example": common,
|
|
1891
|
-
"skill print": common,
|
|
1892
|
-
"skill install": [...common, "agent", "directory", "force"],
|
|
1893
|
-
dev: common,
|
|
1894
|
-
check: common,
|
|
1895
|
-
build: common,
|
|
1896
|
-
"auth status": [...common, ...authentication],
|
|
1897
|
-
"auth list": common,
|
|
1898
|
-
"auth create": [...common, "api-url", "project", "client-name", "timeout-seconds", "device", "wait", "no-open"],
|
|
1899
|
-
"auth delete": [...common, "local"],
|
|
1900
|
-
"auth activate": common,
|
|
1901
|
-
"auth deactivate": common,
|
|
1902
|
-
"auth login": [...common, "api-url", "project", "client-name", "timeout-seconds", "device", "wait", "no-open", "force"],
|
|
1903
|
-
"auth logout": [...common, "local", "all"],
|
|
1904
|
-
link: [...common, ...authentication, "project"],
|
|
1905
|
-
doctor: [...common, ...authentication],
|
|
1906
|
-
preview: [
|
|
1907
|
-
...common,
|
|
1908
|
-
...authentication,
|
|
1909
|
-
"dry-run",
|
|
1910
|
-
"no-wait",
|
|
1911
|
-
"submission-key",
|
|
1912
|
-
"delivery-key",
|
|
1913
|
-
"timeout-seconds",
|
|
1914
|
-
"return-path",
|
|
1915
|
-
],
|
|
1916
|
-
"source status": [...common, ...authentication, "project"],
|
|
1917
|
-
"source branches": [...common, ...authentication, "project"],
|
|
1918
|
-
"source log": [...common, ...authentication, "project", "branch", "limit"],
|
|
1919
|
-
"source compare": [...common, ...authentication, "project", "base", "head"],
|
|
1920
|
-
"source default set": [
|
|
1921
|
-
...common, ...authentication, "project", "branch", "expected-current",
|
|
1922
|
-
"idempotency-key", "dry-run", "no-wait", "timeout-seconds",
|
|
1923
|
-
],
|
|
1924
|
-
"source promote": [
|
|
1925
|
-
...common, ...authentication, "project", "head", "base", "expected-current",
|
|
1926
|
-
"idempotency-key", "dry-run", "no-wait", "timeout-seconds",
|
|
1927
|
-
],
|
|
1928
|
-
"source history replace": [
|
|
1929
|
-
...common, ...authentication, "project", "head", "target", "expected-current", "confirm",
|
|
1930
|
-
"idempotency-key", "dry-run", "no-wait", "timeout-seconds",
|
|
1931
|
-
],
|
|
1932
|
-
"source operation status": [...common, ...authentication, "operation"],
|
|
1933
|
-
"source operation retry": [...common, ...authentication, "operation", "no-wait", "timeout-seconds"],
|
|
1934
|
-
"source change create": [...common, ...authentication, "project", "title", "description", "base", "head"],
|
|
1935
|
-
"source change list": [...common, ...authentication, "project", "state"],
|
|
1936
|
-
"source change show": [...common, ...authentication, "change"],
|
|
1937
|
-
"source change close": [...common, ...authentication, "change"],
|
|
1938
|
-
"source connect": [
|
|
1939
|
-
...common,
|
|
1940
|
-
...authentication,
|
|
1941
|
-
"repo-name",
|
|
1942
|
-
"default-branch",
|
|
1943
|
-
"publication",
|
|
1944
|
-
"remote",
|
|
1945
|
-
"install-command",
|
|
1946
|
-
"working-directory",
|
|
1947
|
-
"artifact-path",
|
|
1948
|
-
],
|
|
1949
|
-
"source pull": [...common, ...authentication, "project", "remote", "dry-run"],
|
|
1950
|
-
"source token": [...common, ...authentication, "ttl-seconds"],
|
|
1951
|
-
"source push": [...common, ...authentication, "branch", "remote", "no-wait", "timeout-seconds"],
|
|
1952
|
-
tail: [
|
|
1953
|
-
...common,
|
|
1954
|
-
...authentication,
|
|
1955
|
-
"delivery",
|
|
1956
|
-
"production",
|
|
1957
|
-
"status",
|
|
1958
|
-
"method",
|
|
1959
|
-
"search",
|
|
1960
|
-
"sampling-rate",
|
|
1961
|
-
],
|
|
1962
|
-
"delivery status": [...common, ...authentication, "delivery"],
|
|
1963
|
-
"delivery preview": [...common, ...authentication, "delivery", "return-path"],
|
|
1964
|
-
"delivery retry": [...common, ...authentication, "delivery"],
|
|
1965
|
-
};
|
|
1966
|
-
const accepted = new Set(allowed[topic] ?? common);
|
|
1967
|
-
const provided = [...options.flags.keys(), ...options.booleans];
|
|
1968
|
-
const unexpected = provided.find((name) => !accepted.has(name));
|
|
1969
|
-
if (unexpected) {
|
|
1970
|
-
throw new CliError("argument_unknown", `Option --${unexpected} is not valid for ${topic}.`, `tender-accounts ${topic} --help`);
|
|
1971
|
-
}
|
|
1972
|
-
const positionalLimits = {
|
|
1973
|
-
"auth create": { minimum: 1, maximum: 1 },
|
|
1974
|
-
"auth delete": { minimum: 1, maximum: 1 },
|
|
1975
|
-
"auth activate": { minimum: 1, maximum: 2 },
|
|
1976
|
-
"auth deactivate": { minimum: 0, maximum: 1 },
|
|
1977
|
-
};
|
|
1978
|
-
const limit = positionalLimits[topic] ?? { minimum: 0, maximum: 0 };
|
|
1979
|
-
if (options.positionals.length < limit.minimum) {
|
|
1980
|
-
throw new CliError("argument_required", `Missing ${topic.startsWith("auth ") ? "profile name" : "argument"}.`, `tender-accounts ${topic} --help`);
|
|
1981
|
-
}
|
|
1982
|
-
if (options.positionals.length > limit.maximum) {
|
|
1983
|
-
throw new CliError("argument_unexpected", `Unexpected argument: ${options.positionals[limit.maximum] ?? ""}.`, `tender-accounts ${topic} --help`);
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
function requiredPositional(options, index, label) {
|
|
1987
|
-
const value = options.positionals[index];
|
|
1988
|
-
if (!value)
|
|
1989
|
-
throw new CliError("argument_required", `Missing ${label}.`);
|
|
1990
|
-
return value;
|
|
1991
|
-
}
|
|
1992
|
-
function requiredFlag(options, name) {
|
|
1993
|
-
const value = options.flags.get(name);
|
|
1994
|
-
if (!value) {
|
|
1995
|
-
throw new CliError("argument_required", `Missing --${name}.`, `tender-accounts delivery status --${name} <value> --json`);
|
|
1996
|
-
}
|
|
1997
|
-
return value;
|
|
1998
|
-
}
|
|
1999
|
-
function requiredDeliveryId(options) {
|
|
2000
|
-
const value = requiredFlag(options, "delivery");
|
|
2001
|
-
if (value.startsWith("dwf_")) {
|
|
2002
|
-
throw new CliError("developer_delivery_id_required", "--delivery expects a dly_ developer-delivery ID, not a dwf_ production workflow ID.", "Merchant administrators resume a waiting production workflow from the app's Activity page.");
|
|
2003
|
-
}
|
|
2004
|
-
if (!/^dly_[A-Za-z0-9_-]{8,64}$/u.test(value)) {
|
|
2005
|
-
throw new CliError("developer_delivery_id_required", "--delivery must be the dly_ developer-delivery ID returned by tender-accounts preview.");
|
|
2006
|
-
}
|
|
2007
|
-
return value;
|
|
2008
|
-
}
|
|
2009
|
-
function requiredStarterName(options) {
|
|
2010
|
-
const value = options.flags.get("name");
|
|
2011
|
-
if (!value) {
|
|
2012
|
-
throw new CliError("argument_required", "Missing --name.", "tender-accounts init --name \"My customer account\" --directory ./my-account");
|
|
2013
|
-
}
|
|
2014
|
-
return value;
|
|
2015
|
-
}
|
|
2016
|
-
function numberFlag(options, name, minimum, maximum, fallback) {
|
|
2017
|
-
const raw = options.flags.get(name);
|
|
2018
|
-
if (!raw)
|
|
2019
|
-
return fallback;
|
|
2020
|
-
const value = Number(raw);
|
|
2021
|
-
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
2022
|
-
throw new CliError("argument_invalid", `--${name} must be an integer between ${minimum} and ${maximum}.`);
|
|
2023
|
-
}
|
|
2024
|
-
return value;
|
|
2025
|
-
}
|
|
2026
|
-
function decimalFlag(options, name, minimumExclusive, maximum, fallback) {
|
|
2027
|
-
const raw = options.flags.get(name);
|
|
2028
|
-
if (!raw)
|
|
2029
|
-
return fallback;
|
|
2030
|
-
const value = Number(raw);
|
|
2031
|
-
if (!Number.isFinite(value) || value <= minimumExclusive || value > maximum) {
|
|
2032
|
-
throw new CliError("argument_invalid", `--${name} must be greater than ${minimumExclusive} and at most ${maximum}.`);
|
|
2033
|
-
}
|
|
2034
|
-
return value;
|
|
2035
|
-
}
|
|
2036
|
-
function csvFlag(options, name) {
|
|
2037
|
-
const raw = options.flags.get(name);
|
|
2038
|
-
if (!raw)
|
|
2039
|
-
return [];
|
|
2040
|
-
const values = [...new Set(raw.split(",").map((value) => value.trim()).filter(Boolean))];
|
|
2041
|
-
if (values.length > 12 || values.some((value) => value.length > 32)) {
|
|
2042
|
-
throw new CliError("argument_invalid", `--${name} has too many or overly long values.`);
|
|
2043
|
-
}
|
|
2044
|
-
return values;
|
|
2045
|
-
}
|
|
2046
|
-
function writeOutput(value, options, runtime) {
|
|
2047
|
-
if (options.json) {
|
|
2048
|
-
runtime.stdout.write(`${JSON.stringify(value)}\n`);
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
if (isRecord(value) && typeof value.previewUrl === "string") {
|
|
2052
|
-
runtime.stdout.write(`preview ready\nurl: ${value.previewUrl}\ndelivery_id: ${String(value.deliveryId ?? "")}\n`);
|
|
2053
|
-
return;
|
|
2054
|
-
}
|
|
2055
|
-
if (isRecord(value) && Array.isArray(value.checks)) {
|
|
2056
|
-
for (const check of value.checks) {
|
|
2057
|
-
if (isRecord(check))
|
|
2058
|
-
runtime.stdout.write(`${String(check.status).padEnd(4)} ${String(check.name)}: ${String(check.message)}\n`);
|
|
2059
|
-
}
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
runtime.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
2063
|
-
}
|
|
2064
|
-
function helpText(topic) {
|
|
2065
|
-
const topics = {
|
|
2066
|
-
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",
|
|
2067
|
-
auth: "Usage: tender-accounts auth <login|create|activate|deactivate|status|list|delete|logout> [options]",
|
|
2068
|
-
"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.",
|
|
2069
|
-
"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",
|
|
2070
|
-
"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.",
|
|
2071
|
-
"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.",
|
|
2072
|
-
"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.",
|
|
2073
|
-
"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.",
|
|
2074
|
-
"auth list": "Usage: tender-accounts auth list [--json]\n\nList locally stored profile names, expiration state, and bound directories without credentials or tenant metadata.",
|
|
2075
|
-
"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.",
|
|
2076
|
-
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.",
|
|
2077
|
-
doctor: "Usage: tender-accounts doctor [--json]\n\nValidate configuration, authentication, project scope, and local artifact readiness.",
|
|
2078
|
-
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.",
|
|
2079
|
-
check: "Usage: tender-accounts check [--json]\n\nRun the app's declared validation command.",
|
|
2080
|
-
build: "Usage: tender-accounts build [--json]\n\nRun validation and packaging, then verify the complete portable artifact.",
|
|
2081
|
-
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.",
|
|
2082
|
-
source: "Usage: tender-accounts source <status|branches|log|compare|connect|pull|push|change|default|promote|history|operation> [options]\n\nUse standard Git through Tender for a gateway or service project. Source changes never publish production directly.",
|
|
2083
|
-
"source status": "Usage: tender-accounts source status [--project PROJECT_ID] [--json]\n\nRead the managed repository, immutable build contract, and recent source builds. --project works before a checkout is linked.\n\nExamples:\n tender-accounts source status --project prj_... --json\n tender-accounts source status --json",
|
|
2084
|
-
"source branches": "Usage: tender-accounts source branches [--project PROJECT_ID] [--json]\n\nList exact branch refs and identify Tender's authoritative default separately from the provider's create-time default. Uses a short-lived read credential without persisting it.",
|
|
2085
|
-
"source log": "Usage: tender-accounts source log [--project PROJECT_ID] [--branch BRANCH] [--limit 20] [--json]\n\nRead bounded commit history from one exact managed branch without requiring a checkout.",
|
|
2086
|
-
"source compare": "Usage: tender-accounts source compare [--project PROJECT_ID] --base BRANCH --head BRANCH [--json]\n\nCompare exact managed refs and report identical, fast-forward, behind, diverged, or unrelated history plus a bounded file list.",
|
|
2087
|
-
"source default": "Usage: tender-accounts source default set --branch BRANCH --expected-current BRANCH [--dry-run] [--json]",
|
|
2088
|
-
"source default set": "Usage: tender-accounts source default set [--project PROJECT_ID] --branch BRANCH --expected-current BRANCH [--idempotency-key KEY] [--dry-run] [--no-wait] [--json]\n\nSet an existing branch as Tender's authoritative default with compare-and-swap. This metadata-only operation cannot change production.",
|
|
2089
|
-
"source promote": "Usage: tender-accounts source promote [--project PROJECT_ID] --head BRANCH [--base BRANCH] --expected-current SHA [--idempotency-key KEY] [--dry-run] [--no-wait] [--json]\n\nPromote a successfully previewed head into the current default branch using an exact expected-SHA lease. Normal preview/publication policy applies to the resulting push; protected gateways remain preview-only.",
|
|
2090
|
-
"source history": "Usage: tender-accounts source history replace --head BRANCH --expected-current SHA --confirm REPOSITORY_ID [--dry-run] [--json]",
|
|
2091
|
-
"source history replace": "Usage: tender-accounts source history replace [--project PROJECT_ID] --head BRANCH [--target BRANCH] --expected-current SHA --confirm REPOSITORY_ID [--idempotency-key KEY] [--dry-run] [--no-wait] [--json]\n\nExceptional source migration only: archive the exact previous target, then replace it with an expected-SHA compare-and-swap. Automatic production publication is always suppressed.",
|
|
2092
|
-
"source operation": "Usage: tender-accounts source operation <status|retry> --operation sop_... [--json]",
|
|
2093
|
-
"source operation status": "Usage: tender-accounts source operation status --operation sop_... [--json]\n\nRead durable status and safe evidence for one exact source operation.",
|
|
2094
|
-
"source operation retry": "Usage: tender-accounts source operation retry --operation sop_... [--no-wait] [--timeout-seconds N] [--json]\n\nRetry or resume the same durable source operation without changing its reviewed intent. This never publishes production.",
|
|
2095
|
-
"source change": "Usage: tender-accounts source change <create|list|show|close> [options]\n\nCreate and inspect exact preview-backed changes. Approval and landing stay merchant-admin actions in Tender.",
|
|
2096
|
-
"source change create": "Usage: tender-accounts source change create --head BRANCH --title TITLE [--description TEXT] [--base BRANCH] [--json]\n\nOpen a review for the exact protected-base and feature-branch SHAs. Tender binds checks and approval to this immutable snapshot.",
|
|
2097
|
-
"source change list": "Usage: tender-accounts source change list [--state open|closed|all] [--json]\n\nList reviewed source changes for this exact linked project.",
|
|
2098
|
-
"source change show": "Usage: tender-accounts source change show --change scr_... [--json]\n\nRead exact snapshot, preview, decision, and landing state.",
|
|
2099
|
-
"source change close": "Usage: tender-accounts source change close --change scr_... [--json]\n\nClose an unlanded change without changing source or production.",
|
|
2100
|
-
"source connect": "Usage: tender-accounts source connect [--repo-name NAME] [--default-branch NAME] [--publication preview-only|default-branch] [--remote NAME] [--install-command COMMAND] [--working-directory PATH] [--artifact-path PATH] [--json]\n\nCreate or reconnect this linked project to Tender-managed Git. Build and artifact commands come from tender-accounts.json. Protected gateways accept preview-only publication.",
|
|
2101
|
-
"source pull": "Usage: tender-accounts source pull [--project PROJECT_ID] [--remote NAME] [--dry-run] [--json]\n\nClone the exact connected Tender-managed repository into an empty workspace, or fast-forward an existing clean matching checkout. The app is linked automatically and the short-lived Git credential is never persisted.\n\nExamples:\n tender-accounts source pull --project prj_... --dry-run --json\n tender-accounts source pull --project prj_... --json\n tender-accounts source pull --json",
|
|
2102
|
-
"source token": "Usage: tender-accounts source token [--ttl-seconds N] [--json]\n\nIssue a short-lived read-only repository credential. It is shown once and is never stored by the CLI.",
|
|
2103
|
-
"source push": "Usage: tender-accounts source push [--branch NAME] [--remote NAME] [--no-wait] [--json]\n\nPush the exact committed revision. Reviewed repositories upload a bounded Git bundle and never expose a write credential; the default branch remains protected. Dirty working-tree changes are excluded.",
|
|
2104
|
-
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.",
|
|
2105
|
-
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.",
|
|
2106
|
-
"delivery status": "Usage: tender-accounts delivery status --delivery dly_... [--json]\n\nRead one developer preview delivery. Do not pass a dwf_ production workflow ID.",
|
|
2107
|
-
"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.",
|
|
2108
|
-
"delivery retry": "Usage: tender-accounts delivery retry --delivery dly_... [--json]\n\nRetry one requested or failed developer preview idempotently. A dwf_ production workflow is resumed by a merchant administrator from Activity.",
|
|
2109
|
-
skill: "Usage: tender-accounts skill <print|install> [options]",
|
|
2110
|
-
"skill print": "Usage: tender-accounts skill print [--json]",
|
|
2111
|
-
"skill install": "Usage: tender-accounts skill install [--agent codex|claude] [--directory PATH] [--force] [--json]\n\nInstall globally for the current user by default: ~/.agents/skills for Codex or ~/.claude/skills for Claude. --directory is an explicit custom or repository-local override.",
|
|
2112
|
-
config: "Usage: tender-accounts config example [--json]",
|
|
2113
|
-
"config example": "Usage: tender-accounts config example [--json]\n\nPrint the committed tender-accounts.json contract.",
|
|
2114
|
-
};
|
|
2115
|
-
if (topic && topics[topic])
|
|
2116
|
-
return topics[topic];
|
|
2117
|
-
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 source status Inspect Tender-managed Git and recent builds\n source branches List exact managed branches and default policy\n source compare Compare two managed branches\n source connect Connect this linked project to managed Git\n source pull Clone or fast-forward the exact connected repository\n source token Issue a short-lived repository credential\n source push Push the exact commit without persisting a token\n source promote Fast-forward the default after successful preview checks\n source history Run guarded, archived source-history migration\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.`;
|
|
2118
|
-
}
|
|
2119
|
-
function normalizeError(error) {
|
|
2120
|
-
if (error instanceof CliApiError) {
|
|
2121
|
-
return { code: error.code, message: error.message, ...(error.hint ? { hint: error.hint } : {}), ...(error.requestId ? { requestId: error.requestId } : {}) };
|
|
2122
|
-
}
|
|
2123
|
-
if (error instanceof CliError) {
|
|
2124
|
-
return { code: error.code, message: error.message, ...(error.hint ? { hint: error.hint } : {}) };
|
|
2125
|
-
}
|
|
2126
|
-
return { code: "cli_failed", message: error instanceof Error ? error.message : "Tender Accounts CLI failed." };
|
|
2127
|
-
}
|
|
2128
|
-
function isRecord(value) {
|
|
2129
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2130
|
-
}
|
|
2131
|
-
//# sourceMappingURL=main.js.map
|