oneclient 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/artifact-create.d.ts +15 -0
- package/dist/artifact-create.d.ts.map +1 -0
- package/dist/artifact-create.js +130 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1 -0
- package/dist/credential-routing.d.ts +5 -0
- package/dist/credential-routing.d.ts.map +1 -0
- package/dist/credential-routing.js +32 -0
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +88 -0
- package/dist/deployment-wait.d.ts +16 -0
- package/dist/deployment-wait.d.ts.map +1 -0
- package/dist/deployment-wait.js +31 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +624 -0
- package/dist/platform-auth.d.ts +4 -0
- package/dist/platform-auth.d.ts.map +1 -0
- package/dist/platform-auth.js +61 -0
- package/dist/presenter.d.ts +9 -0
- package/dist/presenter.d.ts.map +1 -0
- package/dist/presenter.js +106 -0
- package/dist/secret-input.d.ts +8 -0
- package/dist/secret-input.d.ts.map +1 -0
- package/dist/secret-input.js +33 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +10 -0
- package/llms.txt +17 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, createHmac } from "node:crypto";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { createClient } from "@oneclient/sdk";
|
|
9
|
+
import { deploymentWaitTimeout, waitForDeployment } from "./deployment-wait.js";
|
|
10
|
+
import { createArtifact } from "./artifact-create.js";
|
|
11
|
+
import { clearDeveloperCredentials, readDeveloperCredentials, writeDeveloperCredentials, } from "./credentials.js";
|
|
12
|
+
import { revokeDeveloperSession, sendDeveloperOtp, verifyDeveloperOtp } from "./platform-auth.js";
|
|
13
|
+
import { deployTokenFromEnvironment, parseServerKeyScopes, serverKeyFromEnvironment, } from "./credential-routing.js";
|
|
14
|
+
import { secretValueFromOptions } from "./secret-input.js";
|
|
15
|
+
import { brand, formatOutput, statusLine } from "./presenter.js";
|
|
16
|
+
import { slugify } from "./slug.js";
|
|
17
|
+
const program = new Command();
|
|
18
|
+
program
|
|
19
|
+
.name("oneclient")
|
|
20
|
+
.description("Deploy and manage OneClient projects")
|
|
21
|
+
.version("0.1.0")
|
|
22
|
+
.option("--api-url <url>", "Control or environment API URL", process.env.ONECLIENT_API_URL)
|
|
23
|
+
.option("--organization <id>", "Organization ID", process.env.ONECLIENT_ORGANIZATION_ID)
|
|
24
|
+
.option("--json", "Print stable JSON for scripts and agents", false)
|
|
25
|
+
.option("--no-color", "Disable ANSI color in human-readable output");
|
|
26
|
+
program.addHelpText("beforeAll", () => process.stdout.isTTY ? `${brand(colorEnabled())}\n\n` : "");
|
|
27
|
+
program
|
|
28
|
+
.command("login")
|
|
29
|
+
.description("Sign in as a platform developer with an email OTP")
|
|
30
|
+
.requiredOption("--email <email>")
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const apiUrl = await resolveApiUrl();
|
|
33
|
+
await runStep(`Sending a private sign-in code to ${options.email.trim()}`, () => sendDeveloperOtp(apiUrl, options.email));
|
|
34
|
+
const otp = process.env.ONECLIENT_OTP ?? (await promptForOtp());
|
|
35
|
+
const sessionToken = await runStep("Verifying your code", () => verifyDeveloperOtp(apiUrl, options.email, otp));
|
|
36
|
+
await writeDeveloperCredentials({ apiUrl, sessionToken });
|
|
37
|
+
print(await sessionClient().then((api) => api.account.me()), "Signed in");
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command("logout")
|
|
41
|
+
.description("Revoke the current developer session and remove local credentials")
|
|
42
|
+
.action(async () => {
|
|
43
|
+
const credentials = await resolvedDeveloperCredentials();
|
|
44
|
+
if (!credentials) {
|
|
45
|
+
print({ signedOut: true, credentialRemoved: false });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let credentialRemoved = false;
|
|
49
|
+
try {
|
|
50
|
+
await revokeDeveloperSession(credentials.apiUrl, credentials.sessionToken);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
credentialRemoved = await clearDeveloperCredentials();
|
|
54
|
+
}
|
|
55
|
+
print({ signedOut: true, credentialRemoved });
|
|
56
|
+
});
|
|
57
|
+
program
|
|
58
|
+
.command("whoami")
|
|
59
|
+
.description("Show the signed-in platform developer")
|
|
60
|
+
.action(async () => print(await sessionClient().then((api) => api.account.me()), "Account"));
|
|
61
|
+
program
|
|
62
|
+
.command("organizations")
|
|
63
|
+
.description("List organizations available to the signed-in developer")
|
|
64
|
+
.action(async () => print(await sessionClient().then((api) => api.organizations.list()), "Organizations"));
|
|
65
|
+
program
|
|
66
|
+
.command("init")
|
|
67
|
+
.description("Create or select an organization, then provision your first project")
|
|
68
|
+
.option("--organization <id>", "Use an existing organization")
|
|
69
|
+
.option("--organization-name <name>", "Create an organization with this name")
|
|
70
|
+
.option("--project-name <name>", "Project name")
|
|
71
|
+
.option("--placement <placement>", "eu, us, or apac", "eu")
|
|
72
|
+
.action(async (options) => {
|
|
73
|
+
if (!new Set(["eu", "us", "apac"]).has(options.placement))
|
|
74
|
+
throw new Error("placement must be eu, us, or apac");
|
|
75
|
+
const api = await sessionClient();
|
|
76
|
+
const organizations = await api.organizations.list();
|
|
77
|
+
let organization = options.organization
|
|
78
|
+
? organizations.find((item) => item.id === options.organization)
|
|
79
|
+
: organizations.length === 1
|
|
80
|
+
? organizations[0]
|
|
81
|
+
: undefined;
|
|
82
|
+
if (!organization && options.organization)
|
|
83
|
+
throw new Error(`Organization ${options.organization} is not available to this account`);
|
|
84
|
+
if (!organization) {
|
|
85
|
+
const name = options.organizationName ?? (await promptForText("Organization name", "My organization"));
|
|
86
|
+
organization = await runStep("Creating your organization", () => api.organizations.create({ name, slug: slugify(name) }));
|
|
87
|
+
}
|
|
88
|
+
const projectName = options.projectName ?? (await promptForText("Project name", "My first project"));
|
|
89
|
+
const project = await runStep("Provisioning development and production", () => sessionClient(organization.id).then((client) => client.projects.create({
|
|
90
|
+
organizationId: organization.id,
|
|
91
|
+
name: projectName,
|
|
92
|
+
slug: slugify(projectName),
|
|
93
|
+
placement: options.placement,
|
|
94
|
+
})));
|
|
95
|
+
print({
|
|
96
|
+
organization: { id: organization.id, name: organization.name },
|
|
97
|
+
project,
|
|
98
|
+
next: [
|
|
99
|
+
"Open https://console.one-client.com/projects",
|
|
100
|
+
"Create an sk_* server key for your development environment",
|
|
101
|
+
"Run `oneclient docs` for the five-minute quickstart",
|
|
102
|
+
],
|
|
103
|
+
}, "Your OneClient workspace is ready");
|
|
104
|
+
});
|
|
105
|
+
program
|
|
106
|
+
.command("organization:create")
|
|
107
|
+
.description("Create an organization")
|
|
108
|
+
.requiredOption("--name <name>")
|
|
109
|
+
.option("--slug <slug>", "Defaults to a slug generated from the name")
|
|
110
|
+
.action(async (options) => print(await sessionClient().then((api) => api.organizations.create({
|
|
111
|
+
name: options.name,
|
|
112
|
+
slug: options.slug ?? slugify(options.name),
|
|
113
|
+
})), "Organization created"));
|
|
114
|
+
program
|
|
115
|
+
.command("projects")
|
|
116
|
+
.description("List projects")
|
|
117
|
+
.action(async () => print(await sessionClient().then((api) => api.projects.list()), "Projects"));
|
|
118
|
+
program
|
|
119
|
+
.command("project:create")
|
|
120
|
+
.description("Create a project and its development and production environments")
|
|
121
|
+
.requiredOption("--organization <id>")
|
|
122
|
+
.requiredOption("--name <name>")
|
|
123
|
+
.option("--slug <slug>", "Defaults to a slug generated from the name")
|
|
124
|
+
.option("--placement <placement>", "eu, us, or apac", "eu")
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
print(await sessionClient(options.organization).then((api) => api.projects.create({
|
|
127
|
+
organizationId: options.organization,
|
|
128
|
+
name: options.name,
|
|
129
|
+
slug: options.slug ?? slugify(options.name),
|
|
130
|
+
placement: options.placement,
|
|
131
|
+
})), "Project created");
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command("project:get")
|
|
135
|
+
.description("Show a project and all of its environments")
|
|
136
|
+
.requiredOption("--project <id>")
|
|
137
|
+
.action(async (options) => print(await sessionClient().then((api) => api.projects.get(options.project))));
|
|
138
|
+
program
|
|
139
|
+
.command("preview:create")
|
|
140
|
+
.description("Create an isolated, prepaid preview environment with a TTL")
|
|
141
|
+
.requiredOption("--project <id>")
|
|
142
|
+
.requiredOption("--ttl-minutes <minutes>", "60 to 10080 minutes")
|
|
143
|
+
.action(async (options) => {
|
|
144
|
+
const ttlMinutes = Number(options.ttlMinutes);
|
|
145
|
+
if (!Number.isInteger(ttlMinutes) || ttlMinutes < 60 || ttlMinutes > 7 * 24 * 60)
|
|
146
|
+
throw new Error("ttl-minutes must be an integer from 60 through 10080");
|
|
147
|
+
print(await sessionClient().then((api) => api.projects.createPreview(options.project, { ttlMinutes })));
|
|
148
|
+
});
|
|
149
|
+
program
|
|
150
|
+
.command("preview:delete")
|
|
151
|
+
.description("Delete an isolated preview environment")
|
|
152
|
+
.requiredOption("--environment <id>")
|
|
153
|
+
.action(async (options) => print(await sessionClient().then((api) => api.projects.deletePreview(options.environment))));
|
|
154
|
+
program
|
|
155
|
+
.command("usage")
|
|
156
|
+
.description("Show the prepaid wallet and retention reserve")
|
|
157
|
+
.action(async () => print(await sessionClient().then((api) => api.usage.summary()), "Usage & balance"));
|
|
158
|
+
program
|
|
159
|
+
.command("status")
|
|
160
|
+
.description("Show account, organization, project, and spend status in one view")
|
|
161
|
+
.action(async () => {
|
|
162
|
+
const base = await sessionClient();
|
|
163
|
+
const [account, organizations] = await Promise.all([
|
|
164
|
+
base.account.me(),
|
|
165
|
+
base.organizations.list(),
|
|
166
|
+
]);
|
|
167
|
+
const requestedOrganization = program.opts().organization;
|
|
168
|
+
const organization = requestedOrganization
|
|
169
|
+
? organizations.find((item) => item.id === requestedOrganization)
|
|
170
|
+
: organizations.length === 1
|
|
171
|
+
? organizations[0]
|
|
172
|
+
: undefined;
|
|
173
|
+
const [projects, usage] = organization
|
|
174
|
+
? await Promise.all([
|
|
175
|
+
sessionClient(organization.id).then((api) => api.projects.list()),
|
|
176
|
+
sessionClient(organization.id).then((api) => api.usage.summary()),
|
|
177
|
+
])
|
|
178
|
+
: [[], null];
|
|
179
|
+
print({
|
|
180
|
+
developer: account.user.email,
|
|
181
|
+
organization: organization
|
|
182
|
+
? { id: organization.id, name: organization.name, slug: organization.slug }
|
|
183
|
+
: null,
|
|
184
|
+
projects: projects.length,
|
|
185
|
+
availableBalanceMicros: usage?.availableBalanceMicros ?? null,
|
|
186
|
+
reservedMicros: usage?.reservedMicros ?? null,
|
|
187
|
+
hardStop: true,
|
|
188
|
+
next: organization
|
|
189
|
+
? "Run `oneclient projects` or `oneclient usage` for details."
|
|
190
|
+
: "Pass --organization <id> to include projects and spend.",
|
|
191
|
+
}, "Workspace status");
|
|
192
|
+
});
|
|
193
|
+
program
|
|
194
|
+
.command("activity")
|
|
195
|
+
.description("Show recent organization activity")
|
|
196
|
+
.option("--limit <count>", "Number of entries", "20")
|
|
197
|
+
.action(async (options) => {
|
|
198
|
+
const limit = Number(options.limit);
|
|
199
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
200
|
+
throw new Error("limit must be an integer from 1 through 100");
|
|
201
|
+
print(await sessionClient().then((api) => api.usage.activity(limit)), "Recent activity");
|
|
202
|
+
});
|
|
203
|
+
program
|
|
204
|
+
.command("deployments")
|
|
205
|
+
.description("List immutable deployments for an environment")
|
|
206
|
+
.requiredOption("--environment <id>")
|
|
207
|
+
.action(async (options) => print(await deployClient().then((api) => api.deployments.list(options.environment)), "Deployments"));
|
|
208
|
+
program
|
|
209
|
+
.command("models")
|
|
210
|
+
.description("List currently available managed AI models")
|
|
211
|
+
.action(async () => print(await serverClient().then((api) => api.ai.models()), "AI model catalog"));
|
|
212
|
+
program
|
|
213
|
+
.command("doctor")
|
|
214
|
+
.description("Check local credentials and live OneClient connectivity")
|
|
215
|
+
.action(async () => {
|
|
216
|
+
const credentials = await resolvedDeveloperCredentials();
|
|
217
|
+
let account = null;
|
|
218
|
+
let controlReachable = false;
|
|
219
|
+
if (credentials) {
|
|
220
|
+
const response = await sessionClient().then((api) => api.account.me());
|
|
221
|
+
account = response.user.email;
|
|
222
|
+
controlReachable = true;
|
|
223
|
+
}
|
|
224
|
+
print({
|
|
225
|
+
node: process.version,
|
|
226
|
+
controlApi: credentials?.apiUrl ?? null,
|
|
227
|
+
signedIn: Boolean(credentials),
|
|
228
|
+
account,
|
|
229
|
+
controlReachable,
|
|
230
|
+
deployToken: deployTokenFromEnvironment()?.startsWith("dp_") ?? false,
|
|
231
|
+
serverKey: serverKeyFromEnvironment()?.startsWith("sk_") ?? false,
|
|
232
|
+
}, "Doctor");
|
|
233
|
+
});
|
|
234
|
+
program
|
|
235
|
+
.command("docs")
|
|
236
|
+
.description("Show the human, API, and AI documentation entry points")
|
|
237
|
+
.action(() => print({
|
|
238
|
+
quickstart: "https://one-client.com/docs/quickstart",
|
|
239
|
+
guides: "https://one-client.com/docs",
|
|
240
|
+
apiReference: "https://console.one-client.com/api-reference",
|
|
241
|
+
aiGuide: "https://one-client.com/docs/ai",
|
|
242
|
+
llms: "https://one-client.com/llms.txt",
|
|
243
|
+
mcp: "https://control.one-client.com/mcp",
|
|
244
|
+
}, "Documentation"));
|
|
245
|
+
program
|
|
246
|
+
.command("mcp")
|
|
247
|
+
.description("Show the remote MCP endpoint and safe credential setup")
|
|
248
|
+
.action(() => print({
|
|
249
|
+
endpoint: "https://control.one-client.com/mcp",
|
|
250
|
+
transport: "Streamable HTTP",
|
|
251
|
+
authorization: "Bearer sk_* or dp_*",
|
|
252
|
+
guidance: "Use an environment-scoped key and store it in your AI client's secret store.",
|
|
253
|
+
skill: "npm install @oneclient/skill",
|
|
254
|
+
}, "AI connection"));
|
|
255
|
+
program
|
|
256
|
+
.command("billing:portal")
|
|
257
|
+
.description("Create a secure Stripe billing-portal session")
|
|
258
|
+
.action(async () => print(await sessionClient().then((api) => api.billing.portal()), "Billing portal"));
|
|
259
|
+
program
|
|
260
|
+
.command("billing:subscribe")
|
|
261
|
+
.description("Create a subscription Checkout session")
|
|
262
|
+
.action(async () => print(await sessionClient().then((api) => api.billing.subscriptionCheckout()), "Subscription checkout"));
|
|
263
|
+
program
|
|
264
|
+
.command("billing:topup")
|
|
265
|
+
.description("Create a prepaid credit Checkout session")
|
|
266
|
+
.requiredOption("--usd <amount>", "USD amount; minimum $25")
|
|
267
|
+
.action(async (options) => {
|
|
268
|
+
const dollars = Number(options.usd);
|
|
269
|
+
if (!Number.isFinite(dollars) || dollars < 25 || Math.round(dollars * 100) !== dollars * 100)
|
|
270
|
+
throw new Error("usd must be at least 25 with no more than two decimal places");
|
|
271
|
+
const amountMicros = String(Math.round(dollars * 1_000_000));
|
|
272
|
+
print(await sessionClient().then((api) => api.billing.topupCheckout(amountMicros)), "Credit checkout");
|
|
273
|
+
});
|
|
274
|
+
program
|
|
275
|
+
.command("domain:list")
|
|
276
|
+
.description("List application, API, and sending domains")
|
|
277
|
+
.action(async () => print(await sessionClient().then((api) => api.domains.list()), "Domains"));
|
|
278
|
+
program
|
|
279
|
+
.command("domain:add")
|
|
280
|
+
.description("Attach a custom hostname")
|
|
281
|
+
.requiredOption("--environment <id>")
|
|
282
|
+
.requiredOption("--hostname <hostname>")
|
|
283
|
+
.option("--purpose <purpose>", "app, api, or sending", "app")
|
|
284
|
+
.action(async (options) => {
|
|
285
|
+
print(await sessionClient().then((api) => api.domains.add({
|
|
286
|
+
environmentId: options.environment,
|
|
287
|
+
hostname: options.hostname,
|
|
288
|
+
purpose: options.purpose,
|
|
289
|
+
})));
|
|
290
|
+
});
|
|
291
|
+
program
|
|
292
|
+
.command("deploy-token:create")
|
|
293
|
+
.description("Create an environment-scoped dp_* token (shown once)")
|
|
294
|
+
.requiredOption("--environment <id>")
|
|
295
|
+
.requiredOption("--name <name>")
|
|
296
|
+
.option("--expires-at <timestamp>", "ISO-8601 expiration time")
|
|
297
|
+
.action(async (options) => {
|
|
298
|
+
const result = await sessionClient().then((api) => api.deployTokens.create({
|
|
299
|
+
environmentId: options.environment,
|
|
300
|
+
name: options.name,
|
|
301
|
+
...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
|
|
302
|
+
}));
|
|
303
|
+
process.stderr.write("Store this deploy token now; OneClient will not show it again.\n");
|
|
304
|
+
print(result);
|
|
305
|
+
});
|
|
306
|
+
program
|
|
307
|
+
.command("keys")
|
|
308
|
+
.description("List publishable and server keys for an environment")
|
|
309
|
+
.requiredOption("--environment <id>")
|
|
310
|
+
.action(async (options) => print(await sessionClient().then((api) => api.credentials.list(options.environment))));
|
|
311
|
+
program
|
|
312
|
+
.command("key:create")
|
|
313
|
+
.description("Create an environment publishable or scoped server key")
|
|
314
|
+
.requiredOption("--environment <id>")
|
|
315
|
+
.requiredOption("--prefix <prefix>", "pk or sk")
|
|
316
|
+
.requiredOption("--name <name>")
|
|
317
|
+
.option("--scopes <scopes>", "Comma-separated scopes required for sk keys")
|
|
318
|
+
.option("--expires-at <timestamp>", "ISO-8601 expiration time")
|
|
319
|
+
.action(async (options) => {
|
|
320
|
+
const prefix = options.prefix;
|
|
321
|
+
if (prefix !== "pk" && prefix !== "sk")
|
|
322
|
+
throw new Error("prefix must be pk or sk");
|
|
323
|
+
const scopes = parseServerKeyScopes(prefix, options.scopes);
|
|
324
|
+
const result = await sessionClient().then((api) => api.credentials.create(options.environment, {
|
|
325
|
+
prefix,
|
|
326
|
+
name: options.name,
|
|
327
|
+
...(scopes ? { scopes } : {}),
|
|
328
|
+
...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
|
|
329
|
+
}));
|
|
330
|
+
if (result.token)
|
|
331
|
+
process.stderr.write("Store this key now; OneClient will not show it again.\n");
|
|
332
|
+
print(result);
|
|
333
|
+
});
|
|
334
|
+
program
|
|
335
|
+
.command("key:revoke")
|
|
336
|
+
.description("Revoke an environment key")
|
|
337
|
+
.requiredOption("--environment <id>")
|
|
338
|
+
.requiredOption("--key <id>")
|
|
339
|
+
.action(async (options) => print(await sessionClient().then((api) => api.credentials.revoke(options.environment, options.key))));
|
|
340
|
+
program
|
|
341
|
+
.command("secrets")
|
|
342
|
+
.description("List secret names and versions for an environment")
|
|
343
|
+
.requiredOption("--environment <id>")
|
|
344
|
+
.action(async (options) => print(await sessionClient().then((api) => api.secrets.list(options.environment))));
|
|
345
|
+
program
|
|
346
|
+
.command("secret:put")
|
|
347
|
+
.description("Create a versioned deployment secret without exposing its value in arguments")
|
|
348
|
+
.requiredOption("--environment <id>")
|
|
349
|
+
.requiredOption("--name <name>")
|
|
350
|
+
.option("--from-env <variable>", "Read the value from a named environment variable")
|
|
351
|
+
.option("--stdin", "Read exact secret bytes from piped standard input")
|
|
352
|
+
.action(async (options) => {
|
|
353
|
+
const value = await secretValueFromOptions(options);
|
|
354
|
+
print(await sessionClient().then((api) => api.secrets.put(options.environment, options.name, value)));
|
|
355
|
+
});
|
|
356
|
+
program
|
|
357
|
+
.command("secret:delete")
|
|
358
|
+
.description("Retire a deployment secret on the next version")
|
|
359
|
+
.requiredOption("--environment <id>")
|
|
360
|
+
.requiredOption("--name <name>")
|
|
361
|
+
.action(async (options) => print(await sessionClient().then((api) => api.secrets.delete(options.environment, options.name))));
|
|
362
|
+
program
|
|
363
|
+
.command("query")
|
|
364
|
+
.description("Run a policy-controlled data query")
|
|
365
|
+
.requiredOption("--table <table>")
|
|
366
|
+
.requiredOption("--select <fields>", "Comma-separated field names")
|
|
367
|
+
.option("--limit <limit>", "Maximum rows", "100")
|
|
368
|
+
.action(async (options) => {
|
|
369
|
+
print(await serverClient().then((api) => api
|
|
370
|
+
.from(options.table)
|
|
371
|
+
.select(...options.select.split(",").map((field) => field.trim()))
|
|
372
|
+
.limit(Number(options.limit))
|
|
373
|
+
.execute()));
|
|
374
|
+
});
|
|
375
|
+
program
|
|
376
|
+
.command("artifact:create")
|
|
377
|
+
.description("Create a validated manifest and tar archive from a project directory")
|
|
378
|
+
.requiredOption("--directory <path>")
|
|
379
|
+
.requiredOption("--manifest <path>")
|
|
380
|
+
.requiredOption("--archive <path>")
|
|
381
|
+
.requiredOption("--framework <framework>", "static, vite, next-opennext, or worker")
|
|
382
|
+
.option("--root-directory <path>")
|
|
383
|
+
.option("--entrypoint <path>")
|
|
384
|
+
.option("--build-command <command>")
|
|
385
|
+
.option("--output-directory <path>")
|
|
386
|
+
.option("--lockfile <manager>", "pnpm, npm, yarn, or bun")
|
|
387
|
+
.action(async (options) => {
|
|
388
|
+
if (!["static", "vite", "next-opennext", "worker"].includes(options.framework))
|
|
389
|
+
throw new Error("framework must be static, vite, next-opennext, or worker");
|
|
390
|
+
if (options.lockfile && !["pnpm", "npm", "yarn", "bun"].includes(options.lockfile))
|
|
391
|
+
throw new Error("lockfile must be pnpm, npm, yarn, or bun");
|
|
392
|
+
const manifest = await createArtifact({
|
|
393
|
+
directory: options.directory,
|
|
394
|
+
archivePath: options.archive,
|
|
395
|
+
manifestPath: options.manifest,
|
|
396
|
+
framework: options.framework,
|
|
397
|
+
...(options.rootDirectory ? { rootDirectory: options.rootDirectory } : {}),
|
|
398
|
+
...(options.entrypoint ? { entrypoint: options.entrypoint } : {}),
|
|
399
|
+
...(options.buildCommand ? { buildCommand: options.buildCommand } : {}),
|
|
400
|
+
...(options.outputDirectory ? { outputDirectory: options.outputDirectory } : {}),
|
|
401
|
+
...(options.lockfile
|
|
402
|
+
? { lockfile: options.lockfile }
|
|
403
|
+
: {}),
|
|
404
|
+
});
|
|
405
|
+
print({
|
|
406
|
+
archive: options.archive,
|
|
407
|
+
manifest: options.manifest,
|
|
408
|
+
files: manifest.files.length,
|
|
409
|
+
totalBytes: manifest.totalBytes,
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
program
|
|
413
|
+
.command("deploy")
|
|
414
|
+
.description("Upload a signed artifact and create an immutable deployment")
|
|
415
|
+
.requiredOption("--manifest <path>")
|
|
416
|
+
.requiredOption("--archive <path>", "Tar archive whose files exactly match the manifest")
|
|
417
|
+
.requiredOption("--project <id>")
|
|
418
|
+
.requiredOption("--environment <id>")
|
|
419
|
+
.option("--production", "Promote after upload", false)
|
|
420
|
+
.option("--wait", "Wait until the deployment is ready without promoting", false)
|
|
421
|
+
.option("--wait-timeout <seconds>", "Maximum build wait", "600")
|
|
422
|
+
.action(async (options) => {
|
|
423
|
+
const deployToken = deployTokenFromEnvironment();
|
|
424
|
+
if (!deployToken?.startsWith("dp_"))
|
|
425
|
+
throw new Error("Deploy requires ONECLIENT_DEPLOY_TOKEN with a dp_* token");
|
|
426
|
+
const parsed = JSON.parse(await readFile(options.manifest, "utf8"));
|
|
427
|
+
const { signature: _, ...unsigned } = parsed;
|
|
428
|
+
const manifest = {
|
|
429
|
+
...unsigned,
|
|
430
|
+
signature: createHmac("sha256", deployToken)
|
|
431
|
+
.update(stableStringify(unsigned))
|
|
432
|
+
.digest("hex"),
|
|
433
|
+
};
|
|
434
|
+
const artifactDigest = await hashFile(options.archive);
|
|
435
|
+
const api = await deployClient();
|
|
436
|
+
const deployment = await api.request("/v1/deployments", {
|
|
437
|
+
method: "POST",
|
|
438
|
+
body: {
|
|
439
|
+
projectId: options.project,
|
|
440
|
+
environmentId: options.environment,
|
|
441
|
+
artifactDigest,
|
|
442
|
+
manifest,
|
|
443
|
+
},
|
|
444
|
+
idempotent: true,
|
|
445
|
+
});
|
|
446
|
+
const archive = await readFile(options.archive);
|
|
447
|
+
const result = await api.deployments.upload(deployment.deploymentId, archive, {
|
|
448
|
+
artifactDigest,
|
|
449
|
+
contentLength: (await stat(options.archive)).size,
|
|
450
|
+
});
|
|
451
|
+
if (options.production || options.wait) {
|
|
452
|
+
const ready = await waitForDeployment(deployment.deploymentId, {
|
|
453
|
+
getStatus: () => api.deployments.get(deployment.deploymentId),
|
|
454
|
+
timeoutMs: deploymentWaitTimeout(options.waitTimeout),
|
|
455
|
+
});
|
|
456
|
+
if (options.production) {
|
|
457
|
+
const promotion = ready.status === "promoted"
|
|
458
|
+
? ready
|
|
459
|
+
: await api.deployments.promote(deployment.deploymentId);
|
|
460
|
+
print({
|
|
461
|
+
...result,
|
|
462
|
+
deployment: ready,
|
|
463
|
+
promotion,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
print({
|
|
468
|
+
...result,
|
|
469
|
+
deployment: ready,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else
|
|
474
|
+
print(result);
|
|
475
|
+
});
|
|
476
|
+
program
|
|
477
|
+
.command("deployment:promote")
|
|
478
|
+
.description("Atomically promote or roll back to an immutable deployment")
|
|
479
|
+
.requiredOption("--deployment <id>")
|
|
480
|
+
.action(async (options) => {
|
|
481
|
+
print(await deployClient().then((api) => api.request(`/v1/deployments/${encodeURIComponent(options.deployment)}/promote`, {
|
|
482
|
+
method: "POST",
|
|
483
|
+
idempotent: true,
|
|
484
|
+
})));
|
|
485
|
+
});
|
|
486
|
+
program
|
|
487
|
+
.command("deployment:logs")
|
|
488
|
+
.description("Print redacted build logs for a deployment")
|
|
489
|
+
.requiredOption("--deployment <id>")
|
|
490
|
+
.action(async (options) => {
|
|
491
|
+
const logs = await deployClient().then((api) => api.deployments.logs(options.deployment));
|
|
492
|
+
process.stdout.write(logs.endsWith("\n") ? logs : `${logs}\n`);
|
|
493
|
+
});
|
|
494
|
+
program.parseAsync().catch((error) => {
|
|
495
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
496
|
+
process.stderr.write(`${statusLine("error", message, colorEnabled())}\n`);
|
|
497
|
+
process.exitCode = 1;
|
|
498
|
+
});
|
|
499
|
+
async function sessionClient(organizationId) {
|
|
500
|
+
const options = program.opts();
|
|
501
|
+
const credentials = await resolvedDeveloperCredentials();
|
|
502
|
+
if (!credentials) {
|
|
503
|
+
throw new Error("Sign in with `oneclient login --email you@example.com`");
|
|
504
|
+
}
|
|
505
|
+
return createClient({
|
|
506
|
+
baseUrl: options.apiUrl ?? credentials.apiUrl,
|
|
507
|
+
session: true,
|
|
508
|
+
headers: { Authorization: `Bearer ${credentials.sessionToken}` },
|
|
509
|
+
...((organizationId ?? options.organization)
|
|
510
|
+
? { organizationId: organizationId ?? options.organization }
|
|
511
|
+
: {}),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async function deployClient() {
|
|
515
|
+
const options = program.opts();
|
|
516
|
+
const deployToken = deployTokenFromEnvironment();
|
|
517
|
+
if (!deployToken?.startsWith("dp_"))
|
|
518
|
+
throw new Error("This command requires ONECLIENT_DEPLOY_TOKEN with a dp_* token");
|
|
519
|
+
return createClient({
|
|
520
|
+
baseUrl: await resolveApiUrl(),
|
|
521
|
+
deployToken,
|
|
522
|
+
...(options.organization ? { organizationId: options.organization } : {}),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async function serverClient() {
|
|
526
|
+
const options = program.opts();
|
|
527
|
+
const serverKey = serverKeyFromEnvironment();
|
|
528
|
+
if (!serverKey?.startsWith("sk_"))
|
|
529
|
+
throw new Error("This command requires ONECLIENT_SERVER_KEY with an sk_* key");
|
|
530
|
+
return createClient({
|
|
531
|
+
baseUrl: await resolveApiUrl(),
|
|
532
|
+
serverKey,
|
|
533
|
+
...(options.organization ? { organizationId: options.organization } : {}),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
async function resolvedDeveloperCredentials() {
|
|
537
|
+
const options = program.opts();
|
|
538
|
+
const stored = await readDeveloperCredentials();
|
|
539
|
+
const sessionToken = process.env.ONECLIENT_SESSION_TOKEN ?? stored?.sessionToken;
|
|
540
|
+
if (!sessionToken)
|
|
541
|
+
return null;
|
|
542
|
+
const apiUrl = options.apiUrl ?? stored?.apiUrl;
|
|
543
|
+
if (!apiUrl)
|
|
544
|
+
throw new Error("Set ONECLIENT_API_URL or pass --api-url");
|
|
545
|
+
return { apiUrl, sessionToken };
|
|
546
|
+
}
|
|
547
|
+
async function resolveApiUrl() {
|
|
548
|
+
const options = program.opts();
|
|
549
|
+
const apiUrl = options.apiUrl ?? (await readDeveloperCredentials())?.apiUrl;
|
|
550
|
+
if (!apiUrl)
|
|
551
|
+
throw new Error("Set ONECLIENT_API_URL or pass --api-url");
|
|
552
|
+
return apiUrl;
|
|
553
|
+
}
|
|
554
|
+
async function promptForOtp() {
|
|
555
|
+
if (!process.stdin.isTTY)
|
|
556
|
+
throw new Error("Set ONECLIENT_OTP for a non-interactive login");
|
|
557
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
558
|
+
try {
|
|
559
|
+
return await prompt.question("Email OTP: ");
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
prompt.close();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function promptForText(question, fallback) {
|
|
566
|
+
if (!process.stdin.isTTY)
|
|
567
|
+
throw new Error(`Pass --${slugify(question)} in non-interactive mode`);
|
|
568
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
569
|
+
try {
|
|
570
|
+
const answer = (await prompt.question(`${question} (${fallback}): `)).trim();
|
|
571
|
+
return answer || fallback;
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
prompt.close();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function runStep(message, action) {
|
|
578
|
+
const human = humanOutput();
|
|
579
|
+
if (human)
|
|
580
|
+
process.stderr.write(`${statusLine("working", message, colorEnabled())}\n`);
|
|
581
|
+
try {
|
|
582
|
+
const result = await action();
|
|
583
|
+
if (human)
|
|
584
|
+
process.stderr.write(`${statusLine("done", message, colorEnabled())}\n`);
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (human)
|
|
589
|
+
process.stderr.write(`${statusLine("error", message, colorEnabled())}\n`);
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function humanOutput() {
|
|
594
|
+
return Boolean(process.stdout.isTTY && !program.opts().json);
|
|
595
|
+
}
|
|
596
|
+
function colorEnabled() {
|
|
597
|
+
return Boolean(process.stdout.isTTY &&
|
|
598
|
+
process.env.NO_COLOR === undefined &&
|
|
599
|
+
program.opts().color !== false);
|
|
600
|
+
}
|
|
601
|
+
function print(value, title) {
|
|
602
|
+
process.stdout.write(formatOutput(value, {
|
|
603
|
+
color: colorEnabled(),
|
|
604
|
+
json: !humanOutput(),
|
|
605
|
+
...(title ? { title } : {}),
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
function stableStringify(value) {
|
|
609
|
+
if (value === null || typeof value !== "object")
|
|
610
|
+
return JSON.stringify(value);
|
|
611
|
+
if (Array.isArray(value))
|
|
612
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
613
|
+
const object = value;
|
|
614
|
+
return `{${Object.keys(object)
|
|
615
|
+
.sort()
|
|
616
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`)
|
|
617
|
+
.join(",")}}`;
|
|
618
|
+
}
|
|
619
|
+
async function hashFile(path) {
|
|
620
|
+
const hash = createHash("sha256");
|
|
621
|
+
for await (const chunk of createReadStream(path))
|
|
622
|
+
hash.update(chunk);
|
|
623
|
+
return hash.digest("hex");
|
|
624
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function sendDeveloperOtp(apiUrl: string, email: string, fetcher?: typeof globalThis.fetch): Promise<void>;
|
|
2
|
+
export declare function verifyDeveloperOtp(apiUrl: string, email: string, otp: string, fetcher?: typeof globalThis.fetch): Promise<string>;
|
|
3
|
+
export declare function revokeDeveloperSession(apiUrl: string, sessionToken: string, fetcher?: typeof globalThis.fetch): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=platform-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-auth.d.ts","sourceRoot":"","sources":["../src/platform-auth.ts"],"names":[],"mappings":"AAOA,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAO,UAAU,CAAC,KAAwB,GAClD,OAAO,CAAC,IAAI,CAAC,CAEf"}
|