@quickengine/cli 0.2.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/LICENSE +29 -0
- package/README.md +61 -0
- package/dist/index.js +1849 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1849 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
4
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
import { QuickApiError as QuickApiError2 } from "@quickengine/quick";
|
|
24
|
+
import { Command } from "commander";
|
|
25
|
+
|
|
26
|
+
// src/config.ts
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { dirname, join } from "path";
|
|
30
|
+
import { createQuick } from "@quickengine/quick";
|
|
31
|
+
var CONFIG_DIR = join(homedir(), ".quick");
|
|
32
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
33
|
+
function credentialFromKey(key) {
|
|
34
|
+
if (key.startsWith("qpk_")) return { type: "publishable", key };
|
|
35
|
+
if (key.startsWith("qsk_")) return { type: "secret", token: key };
|
|
36
|
+
if (key.startsWith("qsc_")) return { type: "scoped", token: key };
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Unrecognized key format. Expected a key starting with qpk_, qsk_, or qsc_."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function readConfigFile() {
|
|
42
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
45
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
46
|
+
return parsed;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveConfig(env = process.env) {
|
|
52
|
+
var _a, _b, _c;
|
|
53
|
+
const file = readConfigFile();
|
|
54
|
+
return {
|
|
55
|
+
baseUrl: (_a = env.QUICK_BASE_URL) != null ? _a : file.baseUrl,
|
|
56
|
+
workspaceId: (_b = env.QUICK_WORKSPACE) != null ? _b : file.workspaceId,
|
|
57
|
+
key: (_c = env.QUICK_KEY) != null ? _c : file.key
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function prune(patch) {
|
|
61
|
+
return Object.fromEntries(
|
|
62
|
+
Object.entries(patch).filter(
|
|
63
|
+
([, value]) => value !== void 0 && value !== ""
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function writeConfigFile(patch) {
|
|
68
|
+
const next = __spreadValues(__spreadValues({}, readConfigFile()), prune(patch));
|
|
69
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
70
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}
|
|
71
|
+
`, {
|
|
72
|
+
mode: 384
|
|
73
|
+
});
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
var MissingConfigError = class extends Error {
|
|
77
|
+
constructor(missing) {
|
|
78
|
+
super(
|
|
79
|
+
`Missing configuration: ${missing.join(
|
|
80
|
+
", "
|
|
81
|
+
)}. Run \`quick config set\`, or set QUICK_BASE_URL / QUICK_WORKSPACE / QUICK_KEY.`
|
|
82
|
+
);
|
|
83
|
+
this.missing = missing;
|
|
84
|
+
this.name = "MissingConfigError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function buildClient(env) {
|
|
88
|
+
const config = resolveConfig(env);
|
|
89
|
+
const missing = [];
|
|
90
|
+
if (!config.baseUrl) missing.push("baseUrl");
|
|
91
|
+
if (!config.workspaceId) missing.push("workspaceId");
|
|
92
|
+
if (!config.key) missing.push("key");
|
|
93
|
+
if (missing.length > 0) throw new MissingConfigError(missing);
|
|
94
|
+
const resolved = config;
|
|
95
|
+
const client = createQuick({
|
|
96
|
+
baseUrl: resolved.baseUrl,
|
|
97
|
+
workspaceId: resolved.workspaceId,
|
|
98
|
+
credential: credentialFromKey(resolved.key)
|
|
99
|
+
});
|
|
100
|
+
return { client, config: resolved };
|
|
101
|
+
}
|
|
102
|
+
function maskKey(key) {
|
|
103
|
+
const [prefix] = key.split("_");
|
|
104
|
+
return `${prefix}_${"\u2022".repeat(8)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/output.ts
|
|
108
|
+
function line(text4 = "") {
|
|
109
|
+
process.stdout.write(`${text4}
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
function errorLine(text4) {
|
|
113
|
+
process.stderr.write(`${text4}
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
function printJson(value) {
|
|
117
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/commands/activity.ts
|
|
122
|
+
var stamp = (iso) => iso.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
123
|
+
function registerActivityCommands(program2) {
|
|
124
|
+
const activity = program2.command("activity").description("Read the workspace's event history");
|
|
125
|
+
activity.command("list").description("The most recent events, newest first").option("--limit <n>", "How many to show").option("--json", "Output JSON").action(async (options) => {
|
|
126
|
+
const { data } = await buildClient().client.activity.list({
|
|
127
|
+
limit: options.limit ? Number(options.limit) : void 0
|
|
128
|
+
});
|
|
129
|
+
if (options.json) return printJson(data);
|
|
130
|
+
if (!data.events.length) return line("No activity yet.");
|
|
131
|
+
for (const event of data.events) {
|
|
132
|
+
line(` ${stamp(event.occurredAt)} ${event.name} ${event.recordId}`);
|
|
133
|
+
}
|
|
134
|
+
line("");
|
|
135
|
+
line(`cursor: ${data.cursor}`);
|
|
136
|
+
});
|
|
137
|
+
activity.command("since <cursor>").description("Everything after a cursor, oldest first").option("--limit <n>", "How many to show").option("--json", "Output JSON").action(
|
|
138
|
+
async (cursor, options) => {
|
|
139
|
+
const { data } = await buildClient().client.activity.since(
|
|
140
|
+
Number(cursor),
|
|
141
|
+
{ limit: options.limit ? Number(options.limit) : void 0 }
|
|
142
|
+
);
|
|
143
|
+
if (options.json) return printJson(data);
|
|
144
|
+
if (!data.events.length) return line(`Nothing new since ${cursor}.`);
|
|
145
|
+
for (const event of data.events) {
|
|
146
|
+
line(
|
|
147
|
+
` ${stamp(event.occurredAt)} ${event.name} ${event.recordId}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
line("");
|
|
151
|
+
line(`cursor: ${data.cursor}`);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/commands/bookings.ts
|
|
157
|
+
import { randomUUID } from "crypto";
|
|
158
|
+
function registerBookingCommands(program2) {
|
|
159
|
+
const bookings = program2.command("bookings").description("Manage the workspace's bookings");
|
|
160
|
+
bookings.command("list").description("List bookings").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--schedule <key>", "Only this schedule (room, person, resource)").option(
|
|
161
|
+
"--status <status>",
|
|
162
|
+
"Filter by requested, confirmed, checked_in, completed, cancelled, or no_show"
|
|
163
|
+
).option("--from <iso>", "Only bookings starting at or after this time").option("--to <iso>", "Only bookings starting at or before this time").action(
|
|
164
|
+
async (options) => {
|
|
165
|
+
const { data } = await buildClient().client.bookings.list({
|
|
166
|
+
limit: Number(options.limit),
|
|
167
|
+
scheduleKey: options.schedule,
|
|
168
|
+
status: options.status,
|
|
169
|
+
from: options.from,
|
|
170
|
+
to: options.to
|
|
171
|
+
});
|
|
172
|
+
if (options.json) return printJson(data);
|
|
173
|
+
if (!data.items.length) return line("No bookings.");
|
|
174
|
+
for (const item of data.items)
|
|
175
|
+
line(
|
|
176
|
+
`${item.id} [${item.status}] ${item.startsAt} ${item.scheduleKey} ${item.title}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
bookings.command("get <id>").description("Show one booking").option("--json", "Output JSON").action(async (id, options) => {
|
|
181
|
+
const { data } = await buildClient().client.bookings.get(id);
|
|
182
|
+
if (options.json) return printJson(data);
|
|
183
|
+
line(`${data.title} (${data.id})`);
|
|
184
|
+
line(` status: ${data.status}`);
|
|
185
|
+
line(` schedule: ${data.scheduleKey}`);
|
|
186
|
+
line(` when: ${data.startsAt} to ${data.endsAt} (${data.timeZone})`);
|
|
187
|
+
if (data.clientName) line(` client: ${data.clientName}`);
|
|
188
|
+
if (data.cancellationReason)
|
|
189
|
+
line(` cancelled: ${data.cancellationReason}`);
|
|
190
|
+
});
|
|
191
|
+
bookings.command("create").description("Book a slot").requiredOption("--client <id>", "Client id").requiredOption("--title <text>", "What the booking is for").requiredOption("--starts <iso>", "Start time, ISO 8601").requiredOption("--ends <iso>", "End time, ISO 8601").option("--time-zone <zone>", "IANA time zone", "UTC").option("--schedule <key>", "Schedule this booking competes for", "default").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
192
|
+
async (options) => {
|
|
193
|
+
var _a;
|
|
194
|
+
const { data } = await buildClient().client.bookings.create(
|
|
195
|
+
{
|
|
196
|
+
clientId: options.client,
|
|
197
|
+
title: options.title,
|
|
198
|
+
startsAt: options.starts,
|
|
199
|
+
endsAt: options.ends,
|
|
200
|
+
timeZone: options.timeZone,
|
|
201
|
+
scheduleKey: options.schedule
|
|
202
|
+
},
|
|
203
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID()
|
|
204
|
+
);
|
|
205
|
+
if (options.json) return printJson(data);
|
|
206
|
+
line(`Booked ${data.title} (${data.id}) at ${data.startsAt}`);
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
bookings.command("status <id> <status>").description(
|
|
210
|
+
"Move a booking between requested, confirmed, checked_in, completed, cancelled, and no_show"
|
|
211
|
+
).option("--reason <text>", "Cancellation reason (when cancelling)").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
212
|
+
async (id, status2, options) => {
|
|
213
|
+
var _a, _b;
|
|
214
|
+
const { data } = await buildClient().client.bookings.setStatus(
|
|
215
|
+
id,
|
|
216
|
+
status2,
|
|
217
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID(),
|
|
218
|
+
{ cancellationReason: (_b = options.reason) != null ? _b : null }
|
|
219
|
+
);
|
|
220
|
+
if (options.json) return printJson(data);
|
|
221
|
+
line(`${data.title} is now ${data.status}`);
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/commands/catalog.ts
|
|
227
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
228
|
+
function formatPrice(item) {
|
|
229
|
+
if (item.priceCents == null) return item.pricingModel;
|
|
230
|
+
return `${(item.priceCents / 100).toFixed(2)} ${item.currency}`;
|
|
231
|
+
}
|
|
232
|
+
function registerCatalogCommands(program2) {
|
|
233
|
+
const catalog = program2.command("catalog").description("Manage the workspace's catalog of products and services");
|
|
234
|
+
catalog.command("list").description("List catalog items").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--status <status>", "Filter by draft, active, or archived").action(
|
|
235
|
+
async (options) => {
|
|
236
|
+
const { data } = await buildClient().client.catalog.list({
|
|
237
|
+
limit: Number(options.limit),
|
|
238
|
+
status: options.status
|
|
239
|
+
});
|
|
240
|
+
if (options.json) return printJson(data);
|
|
241
|
+
if (!data.items.length) return line("No catalog items.");
|
|
242
|
+
for (const item of data.items)
|
|
243
|
+
line(
|
|
244
|
+
`${item.id} ${item.name} [${item.type}/${item.status}] ${formatPrice(item)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
catalog.command("get <id>").description("Show one catalog item with its variants").option("--json", "Output JSON").action(async (id, options) => {
|
|
249
|
+
const { client } = buildClient();
|
|
250
|
+
const { data } = await client.catalog.get(id);
|
|
251
|
+
const variants = await client.catalog.listVariants(id);
|
|
252
|
+
if (options.json) return printJson(__spreadProps(__spreadValues({}, data), { variants: variants.data }));
|
|
253
|
+
line(`${data.name} (${data.id})`);
|
|
254
|
+
line(` type: ${data.type}`);
|
|
255
|
+
line(` status: ${data.status}`);
|
|
256
|
+
line(` price: ${formatPrice(data)}`);
|
|
257
|
+
if (data.description) line(` ${data.description}`);
|
|
258
|
+
if (variants.data.length > 0) {
|
|
259
|
+
line(" variants:");
|
|
260
|
+
for (const variant of variants.data) {
|
|
261
|
+
const opts = variant.options.map((option) => `${option.name}=${option.value}`).join(", ");
|
|
262
|
+
line(
|
|
263
|
+
` ${variant.id} ${opts}${variant.sku ? ` (${variant.sku})` : ""}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
catalog.command("create").description("Create a catalog item").requiredOption("--name <name>", "Item name").requiredOption(
|
|
269
|
+
"--type <type>",
|
|
270
|
+
"physical, digital, service, package, or rental"
|
|
271
|
+
).option(
|
|
272
|
+
"--pricing-model <model>",
|
|
273
|
+
"fixed, starting_at, hourly, custom_quote, or free"
|
|
274
|
+
).option("--price-cents <cents>", "Price in integer cents").option("--sku <sku>", "Stock keeping unit").option("--currency <currency>", "ISO currency code").option("--description <text>", "Description").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
275
|
+
async (options) => {
|
|
276
|
+
var _a;
|
|
277
|
+
const { data } = await buildClient().client.catalog.create(
|
|
278
|
+
{
|
|
279
|
+
name: options.name,
|
|
280
|
+
type: options.type,
|
|
281
|
+
pricingModel: options.pricingModel,
|
|
282
|
+
priceCents: options.priceCents != null ? Number(options.priceCents) : void 0,
|
|
283
|
+
sku: options.sku,
|
|
284
|
+
currency: options.currency,
|
|
285
|
+
description: options.description
|
|
286
|
+
},
|
|
287
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID2()
|
|
288
|
+
);
|
|
289
|
+
if (options.json) return printJson(data);
|
|
290
|
+
line(`Created ${data.name} (${data.id})`);
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/commands/clients.ts
|
|
296
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
297
|
+
function registerClientCommands(program2) {
|
|
298
|
+
const clients = program2.command("clients").description("Manage workspace client records");
|
|
299
|
+
clients.command("list").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").action(async (options) => {
|
|
300
|
+
const { data } = await buildClient().client.clients.list({
|
|
301
|
+
limit: Number(options.limit)
|
|
302
|
+
});
|
|
303
|
+
if (options.json) return printJson(data);
|
|
304
|
+
if (!data.items.length) return line("No clients.");
|
|
305
|
+
for (const item of data.items)
|
|
306
|
+
line(`${item.id} ${item.name}${item.email ? ` ${item.email}` : ""}`);
|
|
307
|
+
});
|
|
308
|
+
clients.command("get <id>").option("--json", "Output JSON").action(async (id, options) => {
|
|
309
|
+
const { data } = await buildClient().client.clients.get(id);
|
|
310
|
+
if (options.json) return printJson(data);
|
|
311
|
+
line(`${data.name} (${data.id})`);
|
|
312
|
+
if (data.email) line(` email: ${data.email}`);
|
|
313
|
+
if (data.phone) line(` phone: ${data.phone}`);
|
|
314
|
+
if (data.company) line(` company: ${data.company}`);
|
|
315
|
+
});
|
|
316
|
+
clients.command("create").requiredOption("--name <name>", "Client name").option("--email <email>", "Email").option("--phone <phone>", "Phone").option("--company <company>", "Company").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
317
|
+
async (options) => {
|
|
318
|
+
var _a;
|
|
319
|
+
const { data } = await buildClient().client.clients.create(
|
|
320
|
+
{
|
|
321
|
+
name: options.name,
|
|
322
|
+
email: options.email,
|
|
323
|
+
phone: options.phone,
|
|
324
|
+
company: options.company
|
|
325
|
+
},
|
|
326
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID3()
|
|
327
|
+
);
|
|
328
|
+
if (options.json) return printJson(data);
|
|
329
|
+
line(`Created ${data.name} (${data.id})`);
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/commands/config.ts
|
|
335
|
+
function registerConfigCommands(program2) {
|
|
336
|
+
const config = program2.command("config").description("Manage the CLI's stored connection settings");
|
|
337
|
+
config.command("set").description("Save base URL, workspace, and/or API key").option(
|
|
338
|
+
"--base-url <url>",
|
|
339
|
+
"Product API origin, e.g. https://api.quickdash.xyz"
|
|
340
|
+
).option("--workspace <id>", "Workspace id to scope requests to").option(
|
|
341
|
+
"--key <key>",
|
|
342
|
+
"An API key (qpk_/qsk_/qsc_) from Account \u2192 workspace \u2192 API keys"
|
|
343
|
+
).action(
|
|
344
|
+
(options) => {
|
|
345
|
+
var _a, _b;
|
|
346
|
+
const saved = writeConfigFile({
|
|
347
|
+
baseUrl: options.baseUrl,
|
|
348
|
+
workspaceId: options.workspace,
|
|
349
|
+
key: options.key
|
|
350
|
+
});
|
|
351
|
+
line(`Saved to ${CONFIG_PATH}`);
|
|
352
|
+
line(` base URL: ${(_a = saved.baseUrl) != null ? _a : "(unset)"}`);
|
|
353
|
+
line(` workspace: ${(_b = saved.workspaceId) != null ? _b : "(unset)"}`);
|
|
354
|
+
line(` key: ${saved.key ? maskKey(saved.key) : "(unset)"}`);
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
config.command("show").description("Show the resolved settings (key masked)").option("--json", "Output JSON").action((options) => {
|
|
358
|
+
var _a, _b, _c, _d, _e;
|
|
359
|
+
const resolved = resolveConfig();
|
|
360
|
+
const view = {
|
|
361
|
+
baseUrl: (_a = resolved.baseUrl) != null ? _a : null,
|
|
362
|
+
workspaceId: (_b = resolved.workspaceId) != null ? _b : null,
|
|
363
|
+
key: resolved.key ? maskKey(resolved.key) : null
|
|
364
|
+
};
|
|
365
|
+
if (options.json) {
|
|
366
|
+
printJson(view);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
line(`base URL: ${(_c = view.baseUrl) != null ? _c : "(unset)"}`);
|
|
370
|
+
line(`workspace: ${(_d = view.workspaceId) != null ? _d : "(unset)"}`);
|
|
371
|
+
line(`key: ${(_e = view.key) != null ? _e : "(unset)"}`);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/commands/contracts.ts
|
|
376
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
377
|
+
function registerContractCommands(program2) {
|
|
378
|
+
const contracts = program2.command("contracts").description("Manage the workspace's contracts and e-signatures");
|
|
379
|
+
contracts.command("list").description("List contracts").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--client <id>", "Only this client").option(
|
|
380
|
+
"--status <status>",
|
|
381
|
+
"draft, sent, partially_signed, completed, declined, expired, voided, or superseded"
|
|
382
|
+
).action(
|
|
383
|
+
async (options) => {
|
|
384
|
+
const { data } = await buildClient().client.contracts.list({
|
|
385
|
+
limit: Number(options.limit),
|
|
386
|
+
clientId: options.client,
|
|
387
|
+
status: options.status
|
|
388
|
+
});
|
|
389
|
+
if (options.json) return printJson(data);
|
|
390
|
+
if (!data.items.length) return line("No contracts.");
|
|
391
|
+
for (const item of data.items)
|
|
392
|
+
line(`${item.id} ${item.number} [${item.status}] ${item.title}`);
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
contracts.command("get <id>").description("Show one contract with its signers").option("--json", "Output JSON").action(async (id, options) => {
|
|
396
|
+
var _a;
|
|
397
|
+
const { data } = await buildClient().client.contracts.get(id);
|
|
398
|
+
if (options.json) return printJson(data);
|
|
399
|
+
line(`${data.number} ${data.title} (${data.id})`);
|
|
400
|
+
line(` status: ${data.status}`);
|
|
401
|
+
line(` client: ${data.clientName}`);
|
|
402
|
+
for (const signer of (_a = data.signers) != null ? _a : [])
|
|
403
|
+
line(
|
|
404
|
+
` ${signer.position}. ${signer.name} <${signer.email}> [${signer.status}]`
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
contracts.command("create").description("Create a draft contract").requiredOption("--title <text>", "Contract title").option("--client <id>", "Client id").option("--version <id>", "File version id of the document to sign").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
408
|
+
async (options) => {
|
|
409
|
+
var _a, _b, _c;
|
|
410
|
+
const { data } = await buildClient().client.contracts.create(
|
|
411
|
+
{
|
|
412
|
+
title: options.title,
|
|
413
|
+
clientId: (_a = options.client) != null ? _a : null,
|
|
414
|
+
fileVersionId: (_b = options.version) != null ? _b : null
|
|
415
|
+
},
|
|
416
|
+
(_c = options.idempotencyKey) != null ? _c : randomUUID4()
|
|
417
|
+
);
|
|
418
|
+
if (options.json) return printJson(data);
|
|
419
|
+
line(`Created ${data.number} (${data.id})`);
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
contracts.command("send <id>").description("Send a contract for signature").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
423
|
+
async (id, options) => {
|
|
424
|
+
var _a;
|
|
425
|
+
const { data } = await buildClient().client.contracts.send(
|
|
426
|
+
id,
|
|
427
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID4()
|
|
428
|
+
);
|
|
429
|
+
if (options.json) return printJson(data);
|
|
430
|
+
line(`Sent ${data.number} to ${data.invitations.length} signer(s):`);
|
|
431
|
+
for (const invitation of data.invitations)
|
|
432
|
+
line(` ${invitation.name} <${invitation.email}>`);
|
|
433
|
+
line("Signing links are emailed to each signer, not shown here.");
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
contracts.command("void <id>").description("Void a contract").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
437
|
+
async (id, options) => {
|
|
438
|
+
var _a;
|
|
439
|
+
const { data } = await buildClient().client.contracts.void(
|
|
440
|
+
id,
|
|
441
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID4()
|
|
442
|
+
);
|
|
443
|
+
if (options.json) return printJson(data);
|
|
444
|
+
line(`${data.number} is now ${data.status}`);
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
contracts.command("revise <id>").description("Supersede a contract with a new revision").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
448
|
+
async (id, options) => {
|
|
449
|
+
var _a;
|
|
450
|
+
const { data } = await buildClient().client.contracts.revise(
|
|
451
|
+
id,
|
|
452
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID4()
|
|
453
|
+
);
|
|
454
|
+
if (options.json) return printJson(data);
|
|
455
|
+
line(`Created revision ${data.number} (${data.id})`);
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/commands/create.ts
|
|
461
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
462
|
+
import { join as join2, resolve } from "path";
|
|
463
|
+
import {
|
|
464
|
+
cancel,
|
|
465
|
+
confirm,
|
|
466
|
+
intro,
|
|
467
|
+
isCancel,
|
|
468
|
+
log,
|
|
469
|
+
note,
|
|
470
|
+
outro,
|
|
471
|
+
text
|
|
472
|
+
} from "@clack/prompts";
|
|
473
|
+
|
|
474
|
+
// src/defaults.ts
|
|
475
|
+
import { createRequire } from "module";
|
|
476
|
+
var import_meta = {};
|
|
477
|
+
var DEFAULT_API_URL = "https://api.quickdash.xyz";
|
|
478
|
+
var require2 = createRequire(import_meta.url);
|
|
479
|
+
var QUICK_SDK_VERSION = require2("@quickengine/quick/package.json").version;
|
|
480
|
+
|
|
481
|
+
// src/scaffold.ts
|
|
482
|
+
var packageJson = (input) => `${JSON.stringify(
|
|
483
|
+
{
|
|
484
|
+
name: input.name,
|
|
485
|
+
private: true,
|
|
486
|
+
type: "module",
|
|
487
|
+
scripts: {
|
|
488
|
+
start: "node --env-file=.env index.js"
|
|
489
|
+
},
|
|
490
|
+
dependencies: {
|
|
491
|
+
"@quickengine/quick": `^${input.sdkVersion}`
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
null,
|
|
495
|
+
2
|
|
496
|
+
)}
|
|
497
|
+
`;
|
|
498
|
+
var entrypoint = () => `import { createQuickServer } from "@quickengine/quick";
|
|
499
|
+
|
|
500
|
+
// A secret key must never reach a browser. This runs on a server, so it is safe
|
|
501
|
+
// here \u2014 and \`createQuickServer\` is the entry point that expects one.
|
|
502
|
+
const quick = createQuickServer({
|
|
503
|
+
baseUrl: process.env.QUICKENGINE_API_URL,
|
|
504
|
+
workspaceId: process.env.QUICKENGINE_WORKSPACE_ID,
|
|
505
|
+
credential: { type: "secret", token: process.env.QUICKENGINE_KEY },
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Read something real, to prove the connection end to end.
|
|
509
|
+
const { data } = await quick.clients.list();
|
|
510
|
+
console.log(\`Connected. This workspace has \${data.items.length} client record(s).\`);
|
|
511
|
+
|
|
512
|
+
// Create one. Every write is idempotent: repeating a request with the same key
|
|
513
|
+
// returns the original result instead of creating a duplicate \u2014 which is what
|
|
514
|
+
// makes retrying safe.
|
|
515
|
+
const created = await quick.clients.create(
|
|
516
|
+
{ name: "Ada Lovelace", email: "ada@example.com" },
|
|
517
|
+
\`seed-\${new Date().toISOString().slice(0, 10)}\`,
|
|
518
|
+
);
|
|
519
|
+
console.log("Created client:", created.data.id);
|
|
520
|
+
`;
|
|
521
|
+
var envFile = (input) => {
|
|
522
|
+
var _a;
|
|
523
|
+
return `QUICKENGINE_API_URL=${input.baseUrl}
|
|
524
|
+
QUICKENGINE_WORKSPACE_ID=${input.workspaceId}
|
|
525
|
+
QUICKENGINE_KEY=${(_a = input.key) != null ? _a : ""}
|
|
526
|
+
`;
|
|
527
|
+
};
|
|
528
|
+
var envExample = (input) => `QUICKENGINE_API_URL=${input.baseUrl}
|
|
529
|
+
QUICKENGINE_WORKSPACE_ID=${input.workspaceId}
|
|
530
|
+
# Create a secret key in Account \u2192 API keys. Never commit this value.
|
|
531
|
+
QUICKENGINE_KEY=
|
|
532
|
+
`;
|
|
533
|
+
var gitignore = () => `node_modules/
|
|
534
|
+
.env
|
|
535
|
+
`;
|
|
536
|
+
var readme = (input) => `# ${input.name}
|
|
537
|
+
|
|
538
|
+
A minimal QuickEngine app: one file, one dependency, one working call.
|
|
539
|
+
|
|
540
|
+
## Run it
|
|
541
|
+
|
|
542
|
+
\`\`\`sh
|
|
543
|
+
npm install
|
|
544
|
+
npm start
|
|
545
|
+
\`\`\`
|
|
546
|
+
|
|
547
|
+
\`.env\` needs a secret key (\`qsk_\u2026\`) from Account \u2192 API keys.${input.key ? " One has already been written for you." : ""}
|
|
548
|
+
|
|
549
|
+
## What \`index.js\` shows
|
|
550
|
+
|
|
551
|
+
- **Reading** \u2014 \`quick.clients.list()\` proves the credential and workspace resolve.
|
|
552
|
+
- **Writing idempotently** \u2014 the create passes an \`idempotencyKey\`, so running
|
|
553
|
+
\`npm start\` twice does not produce two Adas. Retrying is safe by construction.
|
|
554
|
+
|
|
555
|
+
## Where to go next
|
|
556
|
+
|
|
557
|
+
- \`quick\` \u2014 the CLI, for inspecting the same workspace from a terminal.
|
|
558
|
+
- \`quick.invoices\`, \`quick.quotes\`, \`quick.orders\`, \u2026 \u2014 same shape as \`clients\`.
|
|
559
|
+
- \`quick.activity.since(cursor)\` \u2014 everything that happened while you were away.
|
|
560
|
+
- Webhooks \u2014 have QuickEngine call *you* when something changes.
|
|
561
|
+
`;
|
|
562
|
+
function scaffoldFiles(input) {
|
|
563
|
+
const files = [
|
|
564
|
+
{ path: "package.json", contents: packageJson(input) },
|
|
565
|
+
{ path: "index.js", contents: entrypoint() },
|
|
566
|
+
{ path: ".env.example", contents: envExample(input) },
|
|
567
|
+
{ path: ".gitignore", contents: gitignore() },
|
|
568
|
+
{ path: "README.md", contents: readme(input) }
|
|
569
|
+
];
|
|
570
|
+
files.push({ path: ".env", contents: envFile(input), mode: 384 });
|
|
571
|
+
return files;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/commands/create.ts
|
|
575
|
+
function ensure(value) {
|
|
576
|
+
if (isCancel(value)) {
|
|
577
|
+
cancel("Cancelled \u2014 nothing was written.");
|
|
578
|
+
process.exit(0);
|
|
579
|
+
}
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
var NAME = /^[a-z0-9][a-z0-9._-]*$/;
|
|
583
|
+
function registerCreateCommands(program2) {
|
|
584
|
+
const create = program2.command("create").description("Generate a new project wired to a workspace");
|
|
585
|
+
create.command("app [name]").description("A minimal app with the SDK installed and one working call").option("--base-url <url>", "API URL to target").option("--workspace <id>", "Workspace id to target").option("--yes", "Accept defaults without prompting").action(
|
|
586
|
+
async (name, options) => {
|
|
587
|
+
var _a, _b, _c, _d;
|
|
588
|
+
const existing = resolveConfig();
|
|
589
|
+
intro("Create a QuickEngine app");
|
|
590
|
+
const appName = name != null ? name : options.yes ? "quickengine-app" : ensure(
|
|
591
|
+
await text({
|
|
592
|
+
message: "Project name",
|
|
593
|
+
placeholder: "my-backend",
|
|
594
|
+
defaultValue: "quickengine-app",
|
|
595
|
+
initialValue: "quickengine-app",
|
|
596
|
+
validate: (value) => NAME.test((value != null ? value : "").trim()) ? void 0 : "Lowercase letters, digits, dots, dashes, underscores."
|
|
597
|
+
})
|
|
598
|
+
);
|
|
599
|
+
if (!NAME.test(appName)) {
|
|
600
|
+
cancel(`"${appName}" is not a usable project name.`);
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const directory = resolve(process.cwd(), appName);
|
|
605
|
+
if (existsSync2(directory) && readdirSync(directory).length > 0) {
|
|
606
|
+
cancel(`${directory} already exists and is not empty.`);
|
|
607
|
+
process.exitCode = 1;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const baseUrl = (_b = (_a = options.baseUrl) != null ? _a : existing.baseUrl) != null ? _b : DEFAULT_API_URL;
|
|
611
|
+
const workspaceId = (_d = (_c = options.workspace) != null ? _c : existing.workspaceId) != null ? _d : "";
|
|
612
|
+
if (!workspaceId) {
|
|
613
|
+
log.warn(
|
|
614
|
+
"No workspace configured \u2014 .env will need one. Run `quick init` first to skip this."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
let key;
|
|
618
|
+
if (existing.key && !options.yes) {
|
|
619
|
+
const reuse = ensure(
|
|
620
|
+
await confirm({
|
|
621
|
+
message: "Write your current API key into the project's .env?",
|
|
622
|
+
initialValue: true
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
if (reuse) key = existing.key;
|
|
626
|
+
}
|
|
627
|
+
const files = scaffoldFiles({
|
|
628
|
+
name: appName,
|
|
629
|
+
baseUrl,
|
|
630
|
+
workspaceId,
|
|
631
|
+
key,
|
|
632
|
+
sdkVersion: QUICK_SDK_VERSION
|
|
633
|
+
});
|
|
634
|
+
mkdirSync2(directory, { recursive: true });
|
|
635
|
+
for (const file of files) {
|
|
636
|
+
writeFileSync2(join2(directory, file.path), file.contents, {
|
|
637
|
+
mode: file.mode
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
note(
|
|
641
|
+
[
|
|
642
|
+
`cd ${appName}`,
|
|
643
|
+
"npm install",
|
|
644
|
+
key ? "npm start" : "# add QUICKENGINE_KEY to .env, then:\nnpm start"
|
|
645
|
+
].join("\n"),
|
|
646
|
+
"Next"
|
|
647
|
+
);
|
|
648
|
+
log.success(`Created ${files.length} files in ${appName}/`);
|
|
649
|
+
outro(
|
|
650
|
+
key ? "Ready to run." : "Add a secret key from Account \u2192 API keys to .env, then `npm start`."
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/verify.ts
|
|
657
|
+
import { QuickApiError } from "@quickengine/quick";
|
|
658
|
+
async function verifyConnection(config, connect = buildClient) {
|
|
659
|
+
try {
|
|
660
|
+
if (config == null ? void 0 : config.key) credentialFromKey(config.key);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
return {
|
|
663
|
+
ok: false,
|
|
664
|
+
reason: "key",
|
|
665
|
+
detail: error instanceof Error ? error.message : "unrecognized key format"
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const { client } = connect(
|
|
670
|
+
config ? {
|
|
671
|
+
QUICK_BASE_URL: config.baseUrl,
|
|
672
|
+
QUICK_WORKSPACE: config.workspaceId,
|
|
673
|
+
QUICK_KEY: config.key
|
|
674
|
+
} : void 0
|
|
675
|
+
);
|
|
676
|
+
const { data } = await client.clients.list();
|
|
677
|
+
return { ok: true, detail: `read ${data.items.length} client record(s)` };
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (!(error instanceof QuickApiError)) {
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
reason: "network",
|
|
683
|
+
detail: error instanceof Error ? error.message : "unknown error"
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
switch (error.code) {
|
|
687
|
+
// The key was accepted; it simply may not read clients. Connection proven.
|
|
688
|
+
case "CAPABILITY_DENIED":
|
|
689
|
+
case "MODULE_DISABLED":
|
|
690
|
+
return {
|
|
691
|
+
ok: true,
|
|
692
|
+
detail: `connected (this key cannot read clients: ${error.code})`
|
|
693
|
+
};
|
|
694
|
+
case "AUTHENTICATION_REQUIRED":
|
|
695
|
+
case "INVALID_API_KEY":
|
|
696
|
+
case "CREDENTIAL_CHANNEL_MISMATCH":
|
|
697
|
+
return {
|
|
698
|
+
ok: false,
|
|
699
|
+
reason: "key",
|
|
700
|
+
detail: `${error.code} \u2014 check the API key`
|
|
701
|
+
};
|
|
702
|
+
case "WORKSPACE_NOT_FOUND":
|
|
703
|
+
case "WORKSPACE_MISMATCH":
|
|
704
|
+
case "WORKSPACE_REQUIRED":
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
reason: "workspace",
|
|
708
|
+
detail: `${error.code} \u2014 check the workspace id`
|
|
709
|
+
};
|
|
710
|
+
default:
|
|
711
|
+
return {
|
|
712
|
+
ok: false,
|
|
713
|
+
reason: "network",
|
|
714
|
+
detail: `${error.code} (HTTP ${error.status})`
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/commands/doctor.ts
|
|
721
|
+
function check(label, passed, detail) {
|
|
722
|
+
line(`${passed ? "\u2713" : "\u2717"} ${label}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
723
|
+
return passed;
|
|
724
|
+
}
|
|
725
|
+
function registerDoctorCommand(program2) {
|
|
726
|
+
program2.command("doctor").description("Check configuration and connectivity").action(async () => {
|
|
727
|
+
const config = resolveConfig();
|
|
728
|
+
let ok = true;
|
|
729
|
+
ok = check("base URL set", Boolean(config.baseUrl)) && ok;
|
|
730
|
+
ok = check("workspace set", Boolean(config.workspaceId)) && ok;
|
|
731
|
+
ok = check("key set", Boolean(config.key)) && ok;
|
|
732
|
+
if (config.key) {
|
|
733
|
+
try {
|
|
734
|
+
const credential = credentialFromKey(config.key);
|
|
735
|
+
check(`key format (${credential.type})`, true);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
ok = false;
|
|
738
|
+
check(
|
|
739
|
+
"key format",
|
|
740
|
+
false,
|
|
741
|
+
error instanceof Error ? error.message : void 0
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (config.baseUrl && config.workspaceId && config.key) {
|
|
746
|
+
const result = await verifyConnection();
|
|
747
|
+
ok = check("API reachable", result.ok, result.detail) && ok;
|
|
748
|
+
}
|
|
749
|
+
line("");
|
|
750
|
+
line(ok ? "All checks passed." : "Some checks failed \u2014 see above.");
|
|
751
|
+
if (!ok) process.exitCode = 1;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/commands/files.ts
|
|
756
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
757
|
+
var size = (bytes) => bytes < 1024 ? `${bytes}B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
758
|
+
function registerFileCommands(program2) {
|
|
759
|
+
const files = program2.command("files").description("Manage the workspace's folders and documents");
|
|
760
|
+
files.command("folders").description("List folders").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--parent <id>", "Only folders inside this one").option("--root", "Only top-level folders").action(
|
|
761
|
+
async (options) => {
|
|
762
|
+
const { data } = await buildClient().client.files.listFolders({
|
|
763
|
+
limit: Number(options.limit),
|
|
764
|
+
parentId: options.parent,
|
|
765
|
+
rootOnly: options.root
|
|
766
|
+
});
|
|
767
|
+
if (options.json) return printJson(data);
|
|
768
|
+
if (!data.items.length) return line("No folders.");
|
|
769
|
+
for (const folder of data.items)
|
|
770
|
+
line(`${folder.id} ${folder.parentId ? "\u2514 " : ""}${folder.name}`);
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
files.command("new-folder <name>").description("Create a folder").option("--parent <id>", "Parent folder id").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
774
|
+
async (name, options) => {
|
|
775
|
+
var _a, _b;
|
|
776
|
+
const { data } = await buildClient().client.files.createFolder(
|
|
777
|
+
{ name, parentId: (_a = options.parent) != null ? _a : null },
|
|
778
|
+
(_b = options.idempotencyKey) != null ? _b : randomUUID5()
|
|
779
|
+
);
|
|
780
|
+
if (options.json) return printJson(data);
|
|
781
|
+
line(`Created folder ${data.name} (${data.id})`);
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
files.command("list").description("List documents").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--folder <id>", "Only documents in this folder").option("--status <status>", "active, archived, trashed, or deleting").action(
|
|
785
|
+
async (options) => {
|
|
786
|
+
var _a;
|
|
787
|
+
const { data } = await buildClient().client.files.list({
|
|
788
|
+
limit: Number(options.limit),
|
|
789
|
+
folderId: options.folder,
|
|
790
|
+
status: options.status
|
|
791
|
+
});
|
|
792
|
+
if (options.json) return printJson(data);
|
|
793
|
+
if (!data.items.length) return line("No documents.");
|
|
794
|
+
for (const doc of data.items)
|
|
795
|
+
line(
|
|
796
|
+
`${doc.id} [${doc.status}] v${(_a = doc.currentVersionNumber) != null ? _a : "-"} ${doc.title}`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
files.command("get <id>").description("Show one document with its version history").option("--json", "Output JSON").action(async (id, options) => {
|
|
801
|
+
var _a, _b;
|
|
802
|
+
const { data } = await buildClient().client.files.get(id);
|
|
803
|
+
if (options.json) return printJson(data);
|
|
804
|
+
line(`${data.title} (${data.id})`);
|
|
805
|
+
line(` status: ${data.status}`);
|
|
806
|
+
line(` current: v${(_a = data.currentVersionNumber) != null ? _a : "none"}`);
|
|
807
|
+
for (const version of (_b = data.versions) != null ? _b : [])
|
|
808
|
+
line(
|
|
809
|
+
` v${version.versionNumber} [${version.status}] ${version.originalName} ${size(version.sizeBytes)}`
|
|
810
|
+
);
|
|
811
|
+
});
|
|
812
|
+
files.command("status <id> <status>").description(
|
|
813
|
+
"Move a document between active, archived, trashed, and deleting (trash before deleting)"
|
|
814
|
+
).option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
815
|
+
async (id, status2, options) => {
|
|
816
|
+
var _a;
|
|
817
|
+
const { data } = await buildClient().client.files.setStatus(
|
|
818
|
+
id,
|
|
819
|
+
status2,
|
|
820
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID5()
|
|
821
|
+
);
|
|
822
|
+
if (options.json) return printJson(data);
|
|
823
|
+
line(`${data.title} is now ${data.status}`);
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
files.command("release <versionId>").description("Release a quarantined version for use").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
827
|
+
async (versionId, options) => {
|
|
828
|
+
var _a;
|
|
829
|
+
const { data } = await buildClient().client.files.releaseVersion(
|
|
830
|
+
versionId,
|
|
831
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID5()
|
|
832
|
+
);
|
|
833
|
+
if (options.json) return printJson(data);
|
|
834
|
+
line(`Version v${data.versionNumber} is now ${data.status}`);
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/commands/fulfillments.ts
|
|
840
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
841
|
+
function registerFulfillmentCommands(program2) {
|
|
842
|
+
const fulfillments = program2.command("fulfillments").description("Manage the workspace's deliveries");
|
|
843
|
+
fulfillments.command("list").description("List deliveries").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option(
|
|
844
|
+
"--status <status>",
|
|
845
|
+
"Filter by pending, in_progress, fulfilled, failed, or cancelled"
|
|
846
|
+
).action(
|
|
847
|
+
async (options) => {
|
|
848
|
+
const { data } = await buildClient().client.fulfillments.list({
|
|
849
|
+
limit: Number(options.limit),
|
|
850
|
+
status: options.status
|
|
851
|
+
});
|
|
852
|
+
if (options.json) return printJson(data);
|
|
853
|
+
if (!data.items.length) return line("No deliveries.");
|
|
854
|
+
for (const item of data.items)
|
|
855
|
+
line(`${item.id} [${item.status}] ${item.kind} ${item.title}`);
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
fulfillments.command("get <id>").description("Show one delivery").option("--json", "Output JSON").action(async (id, options) => {
|
|
859
|
+
var _a;
|
|
860
|
+
const { data } = await buildClient().client.fulfillments.get(id);
|
|
861
|
+
if (options.json) return printJson(data);
|
|
862
|
+
line(`${data.title} (${data.id})`);
|
|
863
|
+
line(` status: ${data.status}`);
|
|
864
|
+
line(` kind: ${data.kind}`);
|
|
865
|
+
if (data.clientName) line(` client: ${data.clientName}`);
|
|
866
|
+
if (data.sourceModule)
|
|
867
|
+
line(` source: ${data.sourceModule} ${(_a = data.sourceRecordId) != null ? _a : ""}`);
|
|
868
|
+
});
|
|
869
|
+
fulfillments.command("status <id> <status>").description(
|
|
870
|
+
"Move a delivery between pending, in_progress, fulfilled, failed, and cancelled"
|
|
871
|
+
).option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
872
|
+
async (id, status2, options) => {
|
|
873
|
+
var _a;
|
|
874
|
+
const { data } = await buildClient().client.fulfillments.setStatus(
|
|
875
|
+
id,
|
|
876
|
+
status2,
|
|
877
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID6()
|
|
878
|
+
);
|
|
879
|
+
if (options.json) return printJson(data);
|
|
880
|
+
line(`${data.title} is now ${data.status}`);
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/commands/init.ts
|
|
886
|
+
import {
|
|
887
|
+
cancel as cancel2,
|
|
888
|
+
confirm as confirm2,
|
|
889
|
+
intro as intro2,
|
|
890
|
+
isCancel as isCancel2,
|
|
891
|
+
log as log2,
|
|
892
|
+
outro as outro2,
|
|
893
|
+
password,
|
|
894
|
+
spinner,
|
|
895
|
+
text as text2
|
|
896
|
+
} from "@clack/prompts";
|
|
897
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
898
|
+
function ensure2(value) {
|
|
899
|
+
if (isCancel2(value)) {
|
|
900
|
+
cancel2("Setup cancelled \u2014 nothing was saved.");
|
|
901
|
+
process.exit(0);
|
|
902
|
+
}
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
function registerInitCommand(program2) {
|
|
906
|
+
program2.command("init").description("Set up the CLI: API URL, workspace, and key").option("--force", "Reconfigure even if settings already exist").action(async (options) => {
|
|
907
|
+
var _a, _b, _c;
|
|
908
|
+
const existing = resolveConfig();
|
|
909
|
+
intro2("QuickEngine CLI setup");
|
|
910
|
+
if (!options.force && existing.baseUrl && existing.workspaceId && existing.key) {
|
|
911
|
+
log2.info(
|
|
912
|
+
`Already configured \u2014 ${existing.workspaceId} at ${existing.baseUrl}`
|
|
913
|
+
);
|
|
914
|
+
const again = ensure2(
|
|
915
|
+
await confirm2({
|
|
916
|
+
message: "Reconfigure?",
|
|
917
|
+
initialValue: false
|
|
918
|
+
})
|
|
919
|
+
);
|
|
920
|
+
if (!again) {
|
|
921
|
+
outro2("Left unchanged. Run `quick doctor` to check it still works.");
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const baseUrl = ensure2(
|
|
926
|
+
await text2({
|
|
927
|
+
message: "API URL",
|
|
928
|
+
placeholder: DEFAULT_API_URL,
|
|
929
|
+
defaultValue: (_a = existing.baseUrl) != null ? _a : DEFAULT_API_URL,
|
|
930
|
+
initialValue: (_b = existing.baseUrl) != null ? _b : DEFAULT_API_URL,
|
|
931
|
+
validate: (value) => {
|
|
932
|
+
try {
|
|
933
|
+
new URL(value != null ? value : "");
|
|
934
|
+
} catch (e) {
|
|
935
|
+
return "That is not a valid URL.";
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
})
|
|
939
|
+
);
|
|
940
|
+
const workspaceId = ensure2(
|
|
941
|
+
await text2({
|
|
942
|
+
message: "Workspace id",
|
|
943
|
+
placeholder: "00000000-0000-4000-8000-000000000000",
|
|
944
|
+
initialValue: (_c = existing.workspaceId) != null ? _c : "",
|
|
945
|
+
// Caught here rather than as a confusing WORKSPACE_NOT_FOUND later.
|
|
946
|
+
validate: (value) => UUID.test((value != null ? value : "").trim()) ? void 0 : "A workspace id is a UUID \u2014 copy it from Account."
|
|
947
|
+
})
|
|
948
|
+
);
|
|
949
|
+
const key = ensure2(
|
|
950
|
+
await password({
|
|
951
|
+
message: "API key",
|
|
952
|
+
// Never echoed: this is a live credential, and terminals keep scrollback.
|
|
953
|
+
validate: (value) => /^(qpk|qsk|qsc)_/.test((value != null ? value : "").trim()) ? void 0 : "Keys start with qpk_, qsk_, or qsc_."
|
|
954
|
+
})
|
|
955
|
+
);
|
|
956
|
+
const config = {
|
|
957
|
+
baseUrl: baseUrl.trim(),
|
|
958
|
+
workspaceId: workspaceId.trim(),
|
|
959
|
+
key: key.trim()
|
|
960
|
+
};
|
|
961
|
+
const checking = spinner();
|
|
962
|
+
checking.start("Checking the connection");
|
|
963
|
+
const result = await verifyConnection(config);
|
|
964
|
+
checking.stop(
|
|
965
|
+
result.ok ? `Connected \u2014 ${result.detail}` : "Could not connect"
|
|
966
|
+
);
|
|
967
|
+
if (!result.ok) {
|
|
968
|
+
log2.error(result.detail);
|
|
969
|
+
const save = ensure2(
|
|
970
|
+
await confirm2({
|
|
971
|
+
message: "Save these settings anyway?",
|
|
972
|
+
initialValue: false
|
|
973
|
+
})
|
|
974
|
+
);
|
|
975
|
+
if (!save) {
|
|
976
|
+
cancel2("Nothing was saved. Run `quick init` again when ready.");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
writeConfigFile(config);
|
|
981
|
+
log2.success(`Saved to ${CONFIG_PATH} (owner-only)`);
|
|
982
|
+
log2.info(`workspace ${config.workspaceId} \xB7 key ${maskKey(config.key)}`);
|
|
983
|
+
outro2(
|
|
984
|
+
"Ready. Try `quick clients list`, or run `quick` for a guided menu."
|
|
985
|
+
);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/commands/inventory.ts
|
|
990
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
991
|
+
function registerInventoryCommands(program2) {
|
|
992
|
+
const inventory = program2.command("inventory").description("Manage the workspace's stock");
|
|
993
|
+
inventory.command("list").description("List tracked stock records").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--status <status>", "Filter by active or archived").action(
|
|
994
|
+
async (options) => {
|
|
995
|
+
const { data } = await buildClient().client.inventory.list({
|
|
996
|
+
limit: Number(options.limit),
|
|
997
|
+
status: options.status
|
|
998
|
+
});
|
|
999
|
+
if (options.json) return printJson(data);
|
|
1000
|
+
if (!data.items.length) return line("No stock records.");
|
|
1001
|
+
for (const item of data.items) {
|
|
1002
|
+
const low = item.onHand <= item.lowStockThreshold ? " \u26A0 low" : "";
|
|
1003
|
+
line(
|
|
1004
|
+
`${item.id} [${item.status}] on hand ${item.onHand} reserved ${item.reserved}${low}`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
inventory.command("get <id>").description("Show one stock record").option("--json", "Output JSON").action(async (id, options) => {
|
|
1010
|
+
const { data } = await buildClient().client.inventory.get(id);
|
|
1011
|
+
if (options.json) return printJson(data);
|
|
1012
|
+
line(`${data.id}`);
|
|
1013
|
+
line(` status: ${data.status}`);
|
|
1014
|
+
line(` on hand: ${data.onHand}`);
|
|
1015
|
+
line(` reserved: ${data.reserved}`);
|
|
1016
|
+
line(` available: ${data.onHand - data.reserved}`);
|
|
1017
|
+
line(` low at: ${data.lowStockThreshold}`);
|
|
1018
|
+
});
|
|
1019
|
+
inventory.command("history <id>").description("Show recent stock movements, newest first").option("--json", "Output JSON").option("--limit <number>", "How many movements", "25").action(async (id, options) => {
|
|
1020
|
+
const { data } = await buildClient().client.inventory.listAdjustments(
|
|
1021
|
+
id,
|
|
1022
|
+
{ limit: Number(options.limit) }
|
|
1023
|
+
);
|
|
1024
|
+
if (options.json) return printJson(data);
|
|
1025
|
+
if (!data.items.length) return line("No movements.");
|
|
1026
|
+
for (const move of data.items)
|
|
1027
|
+
line(
|
|
1028
|
+
`${move.createdAt} ${move.kind} ${move.quantity} -> on hand ${move.resultingOnHand}, reserved ${move.resultingReserved}`
|
|
1029
|
+
);
|
|
1030
|
+
});
|
|
1031
|
+
inventory.command("adjust <id> <kind> <quantity>").description(
|
|
1032
|
+
"Record a movement: receive, sale, customer_return, damage, correction_in, correction_out, reserve, release, or fulfill_reserved"
|
|
1033
|
+
).option("--note <text>", "Why this movement happened").option("--reference <id>", "Linked record in another module").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1034
|
+
async (id, kind, quantity, options) => {
|
|
1035
|
+
var _a, _b, _c;
|
|
1036
|
+
const { data } = await buildClient().client.inventory.adjust(
|
|
1037
|
+
id,
|
|
1038
|
+
{
|
|
1039
|
+
kind,
|
|
1040
|
+
quantity: Number(quantity),
|
|
1041
|
+
note: (_a = options.note) != null ? _a : null,
|
|
1042
|
+
referenceId: (_b = options.reference) != null ? _b : null
|
|
1043
|
+
},
|
|
1044
|
+
(_c = options.idempotencyKey) != null ? _c : randomUUID7()
|
|
1045
|
+
);
|
|
1046
|
+
if (options.json) return printJson(data);
|
|
1047
|
+
line(
|
|
1048
|
+
`Recorded ${data.kind} ${data.quantity}: on hand ${data.resultingOnHand}, reserved ${data.resultingReserved}`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/commands/invoices.ts
|
|
1055
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
1056
|
+
function registerInvoiceCommands(program2) {
|
|
1057
|
+
const invoices = program2.command("invoices").description("Manage the workspace's invoices");
|
|
1058
|
+
invoices.command("list").description("List invoices").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--status <status>", "Filter by draft, sent, paid, or void").action(
|
|
1059
|
+
async (options) => {
|
|
1060
|
+
const { data } = await buildClient().client.invoices.list({
|
|
1061
|
+
limit: Number(options.limit),
|
|
1062
|
+
status: options.status
|
|
1063
|
+
});
|
|
1064
|
+
if (options.json) return printJson(data);
|
|
1065
|
+
if (!data.items.length) return line("No invoices.");
|
|
1066
|
+
for (const invoice of data.items)
|
|
1067
|
+
line(
|
|
1068
|
+
`${invoice.id} ${invoice.number} [${invoice.status}] ${(invoice.totalCents / 100).toFixed(2)} ${invoice.currency}`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
invoices.command("get <id>").description("Show one invoice with its line items").option("--json", "Output JSON").action(async (id, options) => {
|
|
1073
|
+
var _a;
|
|
1074
|
+
const { data } = await buildClient().client.invoices.get(id);
|
|
1075
|
+
if (options.json) return printJson(data);
|
|
1076
|
+
line(`${data.number} (${data.id})`);
|
|
1077
|
+
line(` status: ${data.status}`);
|
|
1078
|
+
line(` total: ${(data.totalCents / 100).toFixed(2)} ${data.currency}`);
|
|
1079
|
+
for (const item of (_a = data.lineItems) != null ? _a : [])
|
|
1080
|
+
line(
|
|
1081
|
+
` ${item.quantity} x ${item.description} ${(item.unitPriceCents / 100).toFixed(2)} ${data.currency}`
|
|
1082
|
+
);
|
|
1083
|
+
});
|
|
1084
|
+
invoices.command("create").description("Create a single-line invoice").requiredOption("--description <text>", "Line item description").requiredOption("--price-cents <cents>", "Unit price in integer cents").option("--quantity <quantity>", "Quantity", "1").option("--client <id>", "Client id").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1085
|
+
async (options) => {
|
|
1086
|
+
var _a, _b;
|
|
1087
|
+
const { data } = await buildClient().client.invoices.create(
|
|
1088
|
+
{
|
|
1089
|
+
clientId: (_a = options.client) != null ? _a : null,
|
|
1090
|
+
lineItems: [
|
|
1091
|
+
{
|
|
1092
|
+
description: options.description,
|
|
1093
|
+
quantity: Number(options.quantity),
|
|
1094
|
+
unitPriceCents: Number(options.priceCents)
|
|
1095
|
+
}
|
|
1096
|
+
]
|
|
1097
|
+
},
|
|
1098
|
+
(_b = options.idempotencyKey) != null ? _b : randomUUID8()
|
|
1099
|
+
);
|
|
1100
|
+
if (options.json) return printJson(data);
|
|
1101
|
+
line(`Created ${data.number} (${data.id})`);
|
|
1102
|
+
}
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/commands/orders.ts
|
|
1107
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
1108
|
+
function registerOrderCommands(program2) {
|
|
1109
|
+
const orders = program2.command("orders").description("Manage the workspace's orders");
|
|
1110
|
+
orders.command("list").description("List orders").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option(
|
|
1111
|
+
"--status <status>",
|
|
1112
|
+
"Filter by draft, placed, confirmed, processing, fulfilled, or cancelled"
|
|
1113
|
+
).action(
|
|
1114
|
+
async (options) => {
|
|
1115
|
+
const { data } = await buildClient().client.orders.list({
|
|
1116
|
+
limit: Number(options.limit),
|
|
1117
|
+
status: options.status
|
|
1118
|
+
});
|
|
1119
|
+
if (options.json) return printJson(data);
|
|
1120
|
+
if (!data.items.length) return line("No orders.");
|
|
1121
|
+
for (const order of data.items)
|
|
1122
|
+
line(
|
|
1123
|
+
`${order.id} ${order.number} [${order.status}] ${(order.totalCents / 100).toFixed(2)} ${order.currency} ${order.clientName}`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
orders.command("get <id>").description("Show one order with its purchased lines").option("--json", "Output JSON").action(async (id, options) => {
|
|
1128
|
+
var _a;
|
|
1129
|
+
const { data } = await buildClient().client.orders.get(id);
|
|
1130
|
+
if (options.json) return printJson(data);
|
|
1131
|
+
line(`${data.number} (${data.id})`);
|
|
1132
|
+
line(` status: ${data.status}`);
|
|
1133
|
+
line(` client: ${data.clientName}`);
|
|
1134
|
+
line(` total: ${(data.totalCents / 100).toFixed(2)} ${data.currency}`);
|
|
1135
|
+
for (const item of (_a = data.lineItems) != null ? _a : [])
|
|
1136
|
+
line(
|
|
1137
|
+
` ${item.quantity} x ${item.name} ${(item.unitPriceCents / 100).toFixed(2)} ${data.currency}`
|
|
1138
|
+
);
|
|
1139
|
+
});
|
|
1140
|
+
orders.command("create").description("Create a single-line order").requiredOption("--client <id>", "Client id").requiredOption("--name <text>", "Purchased item name").requiredOption("--price-cents <cents>", "Unit price in integer cents").option("--quantity <quantity>", "Quantity", "1").option(
|
|
1141
|
+
"--type <type>",
|
|
1142
|
+
"physical, digital, service, or rental",
|
|
1143
|
+
"physical"
|
|
1144
|
+
).option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1145
|
+
async (options) => {
|
|
1146
|
+
var _a;
|
|
1147
|
+
const { data } = await buildClient().client.orders.create(
|
|
1148
|
+
{
|
|
1149
|
+
clientId: options.client,
|
|
1150
|
+
lines: [
|
|
1151
|
+
{
|
|
1152
|
+
name: options.name,
|
|
1153
|
+
type: options.type,
|
|
1154
|
+
quantity: Number(options.quantity),
|
|
1155
|
+
unitPriceCents: Number(options.priceCents)
|
|
1156
|
+
}
|
|
1157
|
+
]
|
|
1158
|
+
},
|
|
1159
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID9()
|
|
1160
|
+
);
|
|
1161
|
+
if (options.json) return printJson(data);
|
|
1162
|
+
line(`Created ${data.number} (${data.id})`);
|
|
1163
|
+
}
|
|
1164
|
+
);
|
|
1165
|
+
orders.command("status <id> <status>").description(
|
|
1166
|
+
"Move an order between draft, placed, confirmed, processing, fulfilled, and cancelled"
|
|
1167
|
+
).option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1168
|
+
async (id, status2, options) => {
|
|
1169
|
+
var _a;
|
|
1170
|
+
const { data } = await buildClient().client.orders.setStatus(
|
|
1171
|
+
id,
|
|
1172
|
+
status2,
|
|
1173
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID9()
|
|
1174
|
+
);
|
|
1175
|
+
if (options.json) return printJson(data);
|
|
1176
|
+
line(`${data.number} is now ${data.status}`);
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/commands/payments.ts
|
|
1182
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
1183
|
+
function registerPaymentCommands(program2) {
|
|
1184
|
+
const payments = program2.command("payments").description("Manage the workspace's payments");
|
|
1185
|
+
payments.command("list").description("List payments").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--status <status>", "Filter by status (pending, succeeded, \u2026)").action(
|
|
1186
|
+
async (options) => {
|
|
1187
|
+
const { data } = await buildClient().client.payments.list({
|
|
1188
|
+
limit: Number(options.limit),
|
|
1189
|
+
status: options.status
|
|
1190
|
+
});
|
|
1191
|
+
if (options.json) return printJson(data);
|
|
1192
|
+
if (!data.items.length) return line("No payments.");
|
|
1193
|
+
for (const payment of data.items)
|
|
1194
|
+
line(
|
|
1195
|
+
`${payment.id} [${payment.status}] ${(payment.amountCents / 100).toFixed(2)} ${payment.currency}`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
);
|
|
1199
|
+
payments.command("get <id>").description("Show one payment with its refunds").option("--json", "Output JSON").action(async (id, options) => {
|
|
1200
|
+
var _a;
|
|
1201
|
+
const { data } = await buildClient().client.payments.get(id);
|
|
1202
|
+
if (options.json) return printJson(data);
|
|
1203
|
+
line(`${data.id}`);
|
|
1204
|
+
line(` status: ${data.status}`);
|
|
1205
|
+
line(
|
|
1206
|
+
` amount: ${(data.amountCents / 100).toFixed(2)} ${data.currency}`
|
|
1207
|
+
);
|
|
1208
|
+
for (const refund of (_a = data.refunds) != null ? _a : [])
|
|
1209
|
+
line(
|
|
1210
|
+
` refund ${(refund.amountCents / 100).toFixed(2)} ${data.currency}`
|
|
1211
|
+
);
|
|
1212
|
+
});
|
|
1213
|
+
payments.command("record").description("Record a payment").requiredOption("--amount-cents <cents>", "Amount in integer cents").option("--invoice <id>", "Invoice id this payment applies to").option("--status <status>", "pending, processing, succeeded, or failed").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1214
|
+
async (options) => {
|
|
1215
|
+
var _a, _b;
|
|
1216
|
+
const { data } = await buildClient().client.payments.record(
|
|
1217
|
+
{
|
|
1218
|
+
amountCents: Number(options.amountCents),
|
|
1219
|
+
invoiceId: (_a = options.invoice) != null ? _a : null,
|
|
1220
|
+
status: options.status
|
|
1221
|
+
},
|
|
1222
|
+
(_b = options.idempotencyKey) != null ? _b : randomUUID10()
|
|
1223
|
+
);
|
|
1224
|
+
if (options.json) return printJson(data);
|
|
1225
|
+
line(`Recorded payment ${data.id} (${data.status})`);
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/commands/projects.ts
|
|
1231
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
1232
|
+
function registerProjectCommands(program2) {
|
|
1233
|
+
const projects = program2.command("projects").description("Manage the workspace's projects, milestones, and tasks");
|
|
1234
|
+
projects.command("list").description("List projects").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option(
|
|
1235
|
+
"--status <status>",
|
|
1236
|
+
"Filter by draft, active, on_hold, completed, or cancelled"
|
|
1237
|
+
).option("--include-archived", "Include archived projects").action(
|
|
1238
|
+
async (options) => {
|
|
1239
|
+
const { data } = await buildClient().client.projects.list({
|
|
1240
|
+
limit: Number(options.limit),
|
|
1241
|
+
status: options.status,
|
|
1242
|
+
includeArchived: options.includeArchived
|
|
1243
|
+
});
|
|
1244
|
+
if (options.json) return printJson(data);
|
|
1245
|
+
if (!data.items.length) return line("No projects.");
|
|
1246
|
+
for (const item of data.items)
|
|
1247
|
+
line(
|
|
1248
|
+
`${item.id} [${item.status}]${item.archivedAt ? " (archived)" : ""} ${item.name}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
);
|
|
1252
|
+
projects.command("get <id>").description("Show one project").option("--json", "Output JSON").action(async (id, options) => {
|
|
1253
|
+
const { data } = await buildClient().client.projects.get(id);
|
|
1254
|
+
if (options.json) return printJson(data);
|
|
1255
|
+
line(`${data.name} (${data.id})`);
|
|
1256
|
+
line(` status: ${data.status}${data.archivedAt ? " (archived)" : ""}`);
|
|
1257
|
+
if (data.dueDate) line(` due: ${data.dueDate}`);
|
|
1258
|
+
});
|
|
1259
|
+
projects.command("create").description("Create a project").requiredOption("--name <text>", "Project name").option("--client <id>", "Client id").option("--due <date>", "Due date (YYYY-MM-DD)").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1260
|
+
async (options) => {
|
|
1261
|
+
var _a, _b, _c;
|
|
1262
|
+
const { data } = await buildClient().client.projects.create(
|
|
1263
|
+
{
|
|
1264
|
+
name: options.name,
|
|
1265
|
+
clientId: (_a = options.client) != null ? _a : null,
|
|
1266
|
+
dueDate: (_b = options.due) != null ? _b : null
|
|
1267
|
+
},
|
|
1268
|
+
(_c = options.idempotencyKey) != null ? _c : randomUUID11()
|
|
1269
|
+
);
|
|
1270
|
+
if (options.json) return printJson(data);
|
|
1271
|
+
line(`Created ${data.name} (${data.id})`);
|
|
1272
|
+
}
|
|
1273
|
+
);
|
|
1274
|
+
projects.command("status <id> <status>").description(
|
|
1275
|
+
"Move a project between draft, active, on_hold, completed, and cancelled"
|
|
1276
|
+
).option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1277
|
+
async (id, status2, options) => {
|
|
1278
|
+
var _a;
|
|
1279
|
+
const { data } = await buildClient().client.projects.setStatus(
|
|
1280
|
+
id,
|
|
1281
|
+
status2,
|
|
1282
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID11()
|
|
1283
|
+
);
|
|
1284
|
+
if (options.json) return printJson(data);
|
|
1285
|
+
line(`${data.name} is now ${data.status}`);
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
projects.command("tasks <projectId>").description("List a project's tasks").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option(
|
|
1289
|
+
"--status <status>",
|
|
1290
|
+
"Filter by todo, in_progress, blocked, completed, or cancelled"
|
|
1291
|
+
).action(
|
|
1292
|
+
async (projectId, options) => {
|
|
1293
|
+
const { data } = await buildClient().client.projects.tasks.list({
|
|
1294
|
+
projectId,
|
|
1295
|
+
limit: Number(options.limit),
|
|
1296
|
+
status: options.status
|
|
1297
|
+
});
|
|
1298
|
+
if (options.json) return printJson(data);
|
|
1299
|
+
if (!data.items.length) return line("No tasks.");
|
|
1300
|
+
for (const task of data.items)
|
|
1301
|
+
line(
|
|
1302
|
+
`${task.id} [${task.status}] ${task.priority} ${task.parentTaskId ? "\u2514 " : ""}${task.title}`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
);
|
|
1306
|
+
projects.command("add-task <projectId> <title>").description("Add a task to a project").option("--milestone <id>", "Milestone id").option("--parent <id>", "Parent task id (same project and milestone)").option("--priority <level>", "low, normal, high, or urgent", "normal").option("--due <date>", "Due date (YYYY-MM-DD)").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1307
|
+
async (projectId, title, options) => {
|
|
1308
|
+
var _a, _b, _c, _d;
|
|
1309
|
+
const { data } = await buildClient().client.projects.tasks.create(
|
|
1310
|
+
{
|
|
1311
|
+
projectId,
|
|
1312
|
+
title,
|
|
1313
|
+
milestoneId: (_a = options.milestone) != null ? _a : null,
|
|
1314
|
+
parentTaskId: (_b = options.parent) != null ? _b : null,
|
|
1315
|
+
priority: options.priority,
|
|
1316
|
+
dueDate: (_c = options.due) != null ? _c : null
|
|
1317
|
+
},
|
|
1318
|
+
(_d = options.idempotencyKey) != null ? _d : randomUUID11()
|
|
1319
|
+
);
|
|
1320
|
+
if (options.json) return printJson(data);
|
|
1321
|
+
line(`Created task ${data.title} (${data.id})`);
|
|
1322
|
+
}
|
|
1323
|
+
);
|
|
1324
|
+
projects.command("task-status <id> <status>").description(
|
|
1325
|
+
"Move a task between todo, in_progress, blocked, completed, and cancelled"
|
|
1326
|
+
).option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1327
|
+
async (id, status2, options) => {
|
|
1328
|
+
var _a;
|
|
1329
|
+
const { data } = await buildClient().client.projects.tasks.setStatus(
|
|
1330
|
+
id,
|
|
1331
|
+
status2,
|
|
1332
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID11()
|
|
1333
|
+
);
|
|
1334
|
+
if (options.json) return printJson(data);
|
|
1335
|
+
line(`${data.title} is now ${data.status}`);
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/commands/quotes.ts
|
|
1341
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
1342
|
+
function registerQuoteCommands(program2) {
|
|
1343
|
+
const quotes = program2.command("quotes").description("Manage the workspace's quotes and estimates");
|
|
1344
|
+
quotes.command("list").description("List quotes").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--status <status>", "Filter by status (draft, sent, accepted, \u2026)").action(
|
|
1345
|
+
async (options) => {
|
|
1346
|
+
const { data } = await buildClient().client.quotes.list({
|
|
1347
|
+
limit: Number(options.limit),
|
|
1348
|
+
status: options.status
|
|
1349
|
+
});
|
|
1350
|
+
if (options.json) return printJson(data);
|
|
1351
|
+
if (!data.items.length) return line("No quotes.");
|
|
1352
|
+
for (const quote2 of data.items)
|
|
1353
|
+
line(
|
|
1354
|
+
`${quote2.id} ${quote2.number} ${quote2.title} [${quote2.status}] ${(quote2.totalCents / 100).toFixed(2)} ${quote2.currency}`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
quotes.command("get <id>").description("Show one quote with its line items").option("--json", "Output JSON").action(async (id, options) => {
|
|
1359
|
+
var _a;
|
|
1360
|
+
const { data } = await buildClient().client.quotes.get(id);
|
|
1361
|
+
if (options.json) return printJson(data);
|
|
1362
|
+
line(`${data.number} ${data.title} (${data.id})`);
|
|
1363
|
+
line(` status: ${data.status}`);
|
|
1364
|
+
line(` client: ${data.clientName}`);
|
|
1365
|
+
line(
|
|
1366
|
+
` total: ${(data.totalCents / 100).toFixed(2)} ${data.currency}`
|
|
1367
|
+
);
|
|
1368
|
+
for (const item of (_a = data.lines) != null ? _a : [])
|
|
1369
|
+
line(
|
|
1370
|
+
` ${item.quantity} x ${item.name} ${(item.lineTotalCents / 100).toFixed(2)} ${data.currency}`
|
|
1371
|
+
);
|
|
1372
|
+
});
|
|
1373
|
+
quotes.command("create").description("Create a single-line quote").requiredOption("--client <id>", "Client id").requiredOption("--title <title>", "Quote title").requiredOption("--line-name <name>", "Line item name").requiredOption("--price-cents <cents>", "Unit price in integer cents").option("--quantity <quantity>", "Quantity", "1").option("--kind <kind>", "quote, estimate, or proposal").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1374
|
+
async (options) => {
|
|
1375
|
+
var _a;
|
|
1376
|
+
const { data } = await buildClient().client.quotes.create(
|
|
1377
|
+
{
|
|
1378
|
+
clientId: options.client,
|
|
1379
|
+
title: options.title,
|
|
1380
|
+
kind: options.kind,
|
|
1381
|
+
lines: [
|
|
1382
|
+
{
|
|
1383
|
+
name: options.lineName,
|
|
1384
|
+
quantity: Number(options.quantity),
|
|
1385
|
+
unitPriceCents: Number(options.priceCents)
|
|
1386
|
+
}
|
|
1387
|
+
]
|
|
1388
|
+
},
|
|
1389
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID12()
|
|
1390
|
+
);
|
|
1391
|
+
if (options.json) return printJson(data);
|
|
1392
|
+
line(`Created ${data.number} (${data.id})`);
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/commands/reports.ts
|
|
1398
|
+
var money = (cents, currency) => `${(cents / 100).toFixed(2)} ${currency}`;
|
|
1399
|
+
var withRange = (command) => command.option("--from <iso>", "Range start (defaults to 30 days before --to)").option("--to <iso>", "Range end (defaults to now)").option("--time-zone <zone>", "IANA time zone the range is bucketed in").option("--granularity <unit>", "day, week, or month");
|
|
1400
|
+
var range = (options) => ({
|
|
1401
|
+
from: options.from,
|
|
1402
|
+
to: options.to,
|
|
1403
|
+
timeZone: options.timeZone,
|
|
1404
|
+
granularity: options.granularity
|
|
1405
|
+
});
|
|
1406
|
+
function registerReportCommands(program2) {
|
|
1407
|
+
const reports = program2.command("reports").description("Read the workspace's reports and analytics");
|
|
1408
|
+
withRange(
|
|
1409
|
+
reports.command("workspace").description("Cross-module snapshot for a date range").option("--json", "Output JSON")
|
|
1410
|
+
).action(async (options) => {
|
|
1411
|
+
const { data } = await buildClient().client.reports.workspace(
|
|
1412
|
+
range(options)
|
|
1413
|
+
);
|
|
1414
|
+
if (options.json) return printJson(data);
|
|
1415
|
+
line(`${data.workspace.name} (${data.range.from} \u2192 ${data.range.to})`);
|
|
1416
|
+
for (const [name, value] of Object.entries(data)) {
|
|
1417
|
+
if (name === "workspace" || name === "range") continue;
|
|
1418
|
+
const s = value;
|
|
1419
|
+
if (!s || typeof s.available !== "boolean") continue;
|
|
1420
|
+
line(
|
|
1421
|
+
s.available ? ` ${name}: ${JSON.stringify(s.data)}` : ` ${name}: (module not enabled)`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
withRange(
|
|
1426
|
+
reports.command("revenue").description("Collected and refunded revenue, split by currency").option("--json", "Output JSON")
|
|
1427
|
+
).action(async (options) => {
|
|
1428
|
+
var _a, _b, _c, _d;
|
|
1429
|
+
const { data } = await buildClient().client.reports.revenue(range(options));
|
|
1430
|
+
if (options.json) return printJson(data);
|
|
1431
|
+
if (!data.collected.length && !data.refunded.length)
|
|
1432
|
+
return line("No revenue in range.");
|
|
1433
|
+
for (const point of data.collected)
|
|
1434
|
+
line(
|
|
1435
|
+
` collected ${point.bucket} ${money(Number((_a = point.amountCents) != null ? _a : 0), String((_b = point.currency) != null ? _b : ""))}`
|
|
1436
|
+
);
|
|
1437
|
+
for (const point of data.refunded)
|
|
1438
|
+
line(
|
|
1439
|
+
` refunded ${point.bucket} ${money(Number((_c = point.amountCents) != null ? _c : 0), String((_d = point.currency) != null ? _d : ""))}`
|
|
1440
|
+
);
|
|
1441
|
+
});
|
|
1442
|
+
withRange(
|
|
1443
|
+
reports.command("traffic").description("Self-reported site traffic over time").option("--json", "Output JSON").option("--summary", "Show totals instead of the series")
|
|
1444
|
+
).action(async (options) => {
|
|
1445
|
+
var _a;
|
|
1446
|
+
const client = buildClient().client;
|
|
1447
|
+
if (options.summary) {
|
|
1448
|
+
const { data: data2 } = await client.reports.trafficSummary(range(options));
|
|
1449
|
+
if (options.json) return printJson(data2);
|
|
1450
|
+
line(
|
|
1451
|
+
`views ${data2.views} visitors ${data2.visitors} sessions ${data2.sessions}`
|
|
1452
|
+
);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const { data } = await client.reports.traffic(range(options));
|
|
1456
|
+
if (options.json) return printJson(data);
|
|
1457
|
+
if (!data.length) return line("No traffic in range.");
|
|
1458
|
+
for (const point of data)
|
|
1459
|
+
line(` ${point.bucket} ${(_a = point.count) != null ? _a : 0} views`);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/commands/shipments.ts
|
|
1464
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
1465
|
+
function registerShipmentCommands(program2) {
|
|
1466
|
+
const shipments = program2.command("shipments").description("Manage the workspace's shipments");
|
|
1467
|
+
shipments.command("list").description("List shipments").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--order <id>", "Only shipments for this order").option(
|
|
1468
|
+
"--status <status>",
|
|
1469
|
+
"Filter by draft, ready, shipped, in_transit, delivered, exception, or cancelled"
|
|
1470
|
+
).action(
|
|
1471
|
+
async (options) => {
|
|
1472
|
+
var _a, _b;
|
|
1473
|
+
const { data } = await buildClient().client.shipments.list({
|
|
1474
|
+
limit: Number(options.limit),
|
|
1475
|
+
orderId: options.order,
|
|
1476
|
+
status: options.status
|
|
1477
|
+
});
|
|
1478
|
+
if (options.json) return printJson(data);
|
|
1479
|
+
if (!data.items.length) return line("No shipments.");
|
|
1480
|
+
for (const item of data.items)
|
|
1481
|
+
line(
|
|
1482
|
+
`${item.id} [${item.status}] ${(_a = item.carrier) != null ? _a : "no carrier"} ${(_b = item.trackingNumber) != null ? _b : "no tracking"}`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
);
|
|
1486
|
+
shipments.command("get <id>").description("Show one shipment with its lines and parcels").option("--json", "Output JSON").action(async (id, options) => {
|
|
1487
|
+
var _a, _b, _c;
|
|
1488
|
+
const { data } = await buildClient().client.shipments.get(id);
|
|
1489
|
+
if (options.json) return printJson(data);
|
|
1490
|
+
line(`${data.id}`);
|
|
1491
|
+
line(` status: ${data.status}`);
|
|
1492
|
+
line(` order: ${data.orderId}`);
|
|
1493
|
+
line(
|
|
1494
|
+
` to: ${data.destination.recipientName}, ${data.destination.city}`
|
|
1495
|
+
);
|
|
1496
|
+
line(` carrier: ${(_a = data.carrier) != null ? _a : "not set"}`);
|
|
1497
|
+
line(` tracking: ${(_b = data.trackingNumber) != null ? _b : "not set"}`);
|
|
1498
|
+
for (const parcel of (_c = data.parcels) != null ? _c : [])
|
|
1499
|
+
line(` parcel ${parcel.weightGrams}g`);
|
|
1500
|
+
});
|
|
1501
|
+
shipments.command("status <id> <status>").description(
|
|
1502
|
+
"Move a shipment between draft, ready, shipped, in_transit, delivered, exception, and cancelled"
|
|
1503
|
+
).option("--require-tracking", "Refuse to ship without a tracking number").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1504
|
+
async (id, status2, options) => {
|
|
1505
|
+
var _a;
|
|
1506
|
+
const { data } = await buildClient().client.shipments.setStatus(
|
|
1507
|
+
id,
|
|
1508
|
+
status2,
|
|
1509
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID13(),
|
|
1510
|
+
{ requireTracking: options.requireTracking }
|
|
1511
|
+
);
|
|
1512
|
+
if (options.json) return printJson(data);
|
|
1513
|
+
line(`Shipment ${data.id} is now ${data.status}`);
|
|
1514
|
+
}
|
|
1515
|
+
);
|
|
1516
|
+
shipments.command("track <id>").description("Set or correct carrier tracking details").option("--carrier <name>", "Carrier name").option("--service <level>", "Service level").option("--number <tracking>", "Tracking number").option("--url <url>", "Tracking URL").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1517
|
+
async (id, options) => {
|
|
1518
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1519
|
+
const { data } = await buildClient().client.shipments.updateTracking(
|
|
1520
|
+
id,
|
|
1521
|
+
{
|
|
1522
|
+
carrier: (_a = options.carrier) != null ? _a : null,
|
|
1523
|
+
serviceLevel: (_b = options.service) != null ? _b : null,
|
|
1524
|
+
trackingNumber: (_c = options.number) != null ? _c : null,
|
|
1525
|
+
trackingUrl: (_d = options.url) != null ? _d : null
|
|
1526
|
+
},
|
|
1527
|
+
(_e = options.idempotencyKey) != null ? _e : randomUUID13()
|
|
1528
|
+
);
|
|
1529
|
+
if (options.json) return printJson(data);
|
|
1530
|
+
line(
|
|
1531
|
+
`Tracking for ${data.id}: ${(_f = data.carrier) != null ? _f : "no carrier"} ${(_g = data.trackingNumber) != null ? _g : ""}`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/commands/time.ts
|
|
1538
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
1539
|
+
var hours = (seconds) => (seconds / 3600).toFixed(2);
|
|
1540
|
+
function registerTimeCommands(program2) {
|
|
1541
|
+
const time = program2.command("time").description("Track, approve, and invoice the workspace's time");
|
|
1542
|
+
time.command("list").description("List time entries").option("--json", "Output JSON").option("--limit <number>", "Page size", "25").option("--project <id>", "Only this project").option("--tracker <key>", "Only this tracker").option(
|
|
1543
|
+
"--status <status>",
|
|
1544
|
+
"Filter by running, draft, approved, invoiced, or void"
|
|
1545
|
+
).action(
|
|
1546
|
+
async (options) => {
|
|
1547
|
+
const { data } = await buildClient().client.time.list({
|
|
1548
|
+
limit: Number(options.limit),
|
|
1549
|
+
projectId: options.project,
|
|
1550
|
+
trackerKey: options.tracker,
|
|
1551
|
+
status: options.status
|
|
1552
|
+
});
|
|
1553
|
+
if (options.json) return printJson(data);
|
|
1554
|
+
if (!data.items.length) return line("No time entries.");
|
|
1555
|
+
for (const item of data.items)
|
|
1556
|
+
line(
|
|
1557
|
+
`${item.id} [${item.status}] ${hours(item.durationSeconds)}h ${item.trackerKey}${item.billable ? "" : " (non-billable)"}`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
);
|
|
1561
|
+
time.command("log").description("Log time after the fact").requiredOption("--project <id>", "Project id").requiredOption("--date <YYYY-MM-DD>", "Work date").requiredOption("--minutes <number>", "Duration in minutes").option("--description <text>", "What the time was for").option("--tracker <key>", "Tracker this belongs to").option("--non-billable", "Mark the time non-billable").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1562
|
+
async (options) => {
|
|
1563
|
+
var _a, _b;
|
|
1564
|
+
const { data } = await buildClient().client.time.log(
|
|
1565
|
+
{
|
|
1566
|
+
projectId: options.project,
|
|
1567
|
+
workDate: options.date,
|
|
1568
|
+
durationSeconds: Number(options.minutes) * 60,
|
|
1569
|
+
description: (_a = options.description) != null ? _a : null,
|
|
1570
|
+
trackerKey: options.tracker,
|
|
1571
|
+
billable: !options.nonBillable
|
|
1572
|
+
},
|
|
1573
|
+
(_b = options.idempotencyKey) != null ? _b : randomUUID14()
|
|
1574
|
+
);
|
|
1575
|
+
if (options.json) return printJson(data);
|
|
1576
|
+
line(`Logged ${hours(data.durationSeconds)}h (${data.id})`);
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
time.command("start").description("Start a timer").requiredOption("--project <id>", "Project id").option("--tracker <key>", "Tracker this timer is exclusive on").option("--time-zone <zone>", "IANA time zone", "UTC").option("--description <text>", "What you're working on").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1580
|
+
async (options) => {
|
|
1581
|
+
var _a, _b;
|
|
1582
|
+
const { data } = await buildClient().client.time.startTimer(
|
|
1583
|
+
{
|
|
1584
|
+
projectId: options.project,
|
|
1585
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
1586
|
+
timeZone: options.timeZone,
|
|
1587
|
+
trackerKey: options.tracker,
|
|
1588
|
+
description: (_a = options.description) != null ? _a : null
|
|
1589
|
+
},
|
|
1590
|
+
(_b = options.idempotencyKey) != null ? _b : randomUUID14()
|
|
1591
|
+
);
|
|
1592
|
+
if (options.json) return printJson(data);
|
|
1593
|
+
line(`Timer started (${data.id}) on ${data.trackerKey}`);
|
|
1594
|
+
}
|
|
1595
|
+
);
|
|
1596
|
+
time.command("stop <id>").description("Stop a running timer").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1597
|
+
async (id, options) => {
|
|
1598
|
+
var _a;
|
|
1599
|
+
const { data } = await buildClient().client.time.stopTimer(
|
|
1600
|
+
id,
|
|
1601
|
+
/* @__PURE__ */ new Date(),
|
|
1602
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID14()
|
|
1603
|
+
);
|
|
1604
|
+
if (options.json) return printJson(data);
|
|
1605
|
+
line(`Timer stopped: ${hours(data.durationSeconds)}h recorded`);
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
time.command("approve <id>").description("Approve time, applying the workspace's billing rounding").option("--mode <mode>", "nearest, up, or down").option("--increment <minutes>", "Rounding increment in minutes").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1609
|
+
async (id, options) => {
|
|
1610
|
+
var _a;
|
|
1611
|
+
const { data } = await buildClient().client.time.approve(
|
|
1612
|
+
id,
|
|
1613
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID14(),
|
|
1614
|
+
{
|
|
1615
|
+
mode: options.mode,
|
|
1616
|
+
incrementMinutes: options.increment ? Number(options.increment) : void 0
|
|
1617
|
+
}
|
|
1618
|
+
);
|
|
1619
|
+
if (options.json) return printJson(data);
|
|
1620
|
+
line(`Approved ${hours(data.durationSeconds)}h (${data.id})`);
|
|
1621
|
+
}
|
|
1622
|
+
);
|
|
1623
|
+
time.command("invoice <invoiceId> <entryIds...>").description("Attach approved billable time to a draft invoice").option("--idempotency-key <key>", "Stable retry key").option("--json", "Output JSON").action(
|
|
1624
|
+
async (invoiceId, entryIds, options) => {
|
|
1625
|
+
var _a;
|
|
1626
|
+
const { data } = await buildClient().client.time.attachToInvoice(
|
|
1627
|
+
invoiceId,
|
|
1628
|
+
entryIds,
|
|
1629
|
+
(_a = options.idempotencyKey) != null ? _a : randomUUID14()
|
|
1630
|
+
);
|
|
1631
|
+
if (options.json) return printJson(data);
|
|
1632
|
+
line(`Attached ${data.entryIds.length} entries to ${data.invoiceId}`);
|
|
1633
|
+
}
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// src/commands/webhooks.ts
|
|
1638
|
+
var status = (delivery) => delivery.responseStatus ? `${delivery.status} (${delivery.responseStatus})` : delivery.status;
|
|
1639
|
+
function registerWebhookCommands(program2) {
|
|
1640
|
+
const webhooks = program2.command("webhooks").description("Manage outbound webhook endpoints and inspect deliveries");
|
|
1641
|
+
webhooks.command("list").description("List this workspace's webhook endpoints").option("--json", "Output JSON").action(async (options) => {
|
|
1642
|
+
const { data } = await buildClient().client.webhooks.list();
|
|
1643
|
+
if (options.json) return printJson(data);
|
|
1644
|
+
if (!data.length) return line("No webhook endpoints yet.");
|
|
1645
|
+
for (const endpoint of data) {
|
|
1646
|
+
const events = endpoint.eventTypes.length ? endpoint.eventTypes.join(", ") : "all events";
|
|
1647
|
+
line(` ${endpoint.enabled ? "\u25CF" : "\u25CB"} ${endpoint.url} ${events}`);
|
|
1648
|
+
line(` ${endpoint.id}`);
|
|
1649
|
+
if (endpoint.disabledReason) line(` ${endpoint.disabledReason}`);
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
webhooks.command("create <url>").description("Register a webhook endpoint").option("--events <names...>", "Only these events (default: all)").option("--description <text>", "What this endpoint is for").option("--json", "Output JSON").action(
|
|
1653
|
+
async (url, options) => {
|
|
1654
|
+
var _a;
|
|
1655
|
+
const { data } = await buildClient().client.webhooks.create({
|
|
1656
|
+
url,
|
|
1657
|
+
eventTypes: (_a = options.events) != null ? _a : [],
|
|
1658
|
+
description: options.description
|
|
1659
|
+
});
|
|
1660
|
+
if (options.json) return printJson(data);
|
|
1661
|
+
line(`Created ${data.id}`);
|
|
1662
|
+
line("");
|
|
1663
|
+
line(` Signing secret: ${data.secret}`);
|
|
1664
|
+
line(" Store it now \u2014 it cannot be retrieved again.");
|
|
1665
|
+
}
|
|
1666
|
+
);
|
|
1667
|
+
webhooks.command("delete <id>").description("Delete a webhook endpoint and its delivery history").action(async (id) => {
|
|
1668
|
+
await buildClient().client.webhooks.delete(id);
|
|
1669
|
+
line(`Deleted ${id}`);
|
|
1670
|
+
});
|
|
1671
|
+
webhooks.command("disable <id>").description("Stop delivering to an endpoint without deleting it").action(async (id) => {
|
|
1672
|
+
await buildClient().client.webhooks.update(id, { enabled: false });
|
|
1673
|
+
line(`Disabled ${id}`);
|
|
1674
|
+
});
|
|
1675
|
+
webhooks.command("enable <id>").description("Resume delivering to an endpoint").action(async (id) => {
|
|
1676
|
+
await buildClient().client.webhooks.update(id, { enabled: true });
|
|
1677
|
+
line(`Enabled ${id}`);
|
|
1678
|
+
});
|
|
1679
|
+
webhooks.command("deliveries <endpointId>").description("Recent deliveries for an endpoint, newest first").option("--limit <n>", "How many to show (max 100)").option("--json", "Output JSON").action(
|
|
1680
|
+
async (endpointId, options) => {
|
|
1681
|
+
const { data } = await buildClient().client.webhooks.deliveries(
|
|
1682
|
+
endpointId,
|
|
1683
|
+
{ limit: options.limit ? Number(options.limit) : void 0 }
|
|
1684
|
+
);
|
|
1685
|
+
if (options.json) return printJson(data);
|
|
1686
|
+
if (!data.length) return line("No deliveries yet.");
|
|
1687
|
+
for (const delivery of data) {
|
|
1688
|
+
line(
|
|
1689
|
+
` ${delivery.createdAt} ${delivery.eventName} ${status(delivery)} attempts ${delivery.attempts}`
|
|
1690
|
+
);
|
|
1691
|
+
if (delivery.error) line(` ${delivery.error}`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
);
|
|
1695
|
+
webhooks.command("replay <deliveryId>").description("Attempt a delivery again").action(async (deliveryId) => {
|
|
1696
|
+
await buildClient().client.webhooks.replay(deliveryId);
|
|
1697
|
+
line(`Queued ${deliveryId} for another attempt.`);
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// src/guided.ts
|
|
1702
|
+
import {
|
|
1703
|
+
cancel as cancel3,
|
|
1704
|
+
intro as intro3,
|
|
1705
|
+
isCancel as isCancel3,
|
|
1706
|
+
log as log3,
|
|
1707
|
+
note as note2,
|
|
1708
|
+
outro as outro3,
|
|
1709
|
+
select,
|
|
1710
|
+
text as text3
|
|
1711
|
+
} from "@clack/prompts";
|
|
1712
|
+
function ensure3(value) {
|
|
1713
|
+
if (isCancel3(value)) {
|
|
1714
|
+
cancel3("Cancelled.");
|
|
1715
|
+
process.exit(0);
|
|
1716
|
+
}
|
|
1717
|
+
return value;
|
|
1718
|
+
}
|
|
1719
|
+
var selectable = (command) => command.commands.filter(
|
|
1720
|
+
(child) => !child._hidden
|
|
1721
|
+
);
|
|
1722
|
+
var takesValue = (option) => option.flags.includes("<") || option.flags.includes("[");
|
|
1723
|
+
var quote = (value) => /[\s"'$`\\]/.test(value) ? `'${value.replace(/'/g, "'\\''")}'` : value;
|
|
1724
|
+
async function collectArguments(command) {
|
|
1725
|
+
const parts = [];
|
|
1726
|
+
for (const argument of command.registeredArguments) {
|
|
1727
|
+
const value = ensure3(
|
|
1728
|
+
await text3({
|
|
1729
|
+
message: `${argument.name()}${argument.required ? "" : " (optional)"}`,
|
|
1730
|
+
placeholder: argument.description || void 0,
|
|
1731
|
+
validate: (input) => argument.required && !(input != null ? input : "").trim() ? "Required." : void 0
|
|
1732
|
+
})
|
|
1733
|
+
);
|
|
1734
|
+
if (value.trim()) parts.push(quote(value.trim()));
|
|
1735
|
+
}
|
|
1736
|
+
const options = command.options.filter((option) => option.long !== "--help");
|
|
1737
|
+
for (const option of options) {
|
|
1738
|
+
if (!takesValue(option)) {
|
|
1739
|
+
const enable = ensure3(
|
|
1740
|
+
await select({
|
|
1741
|
+
message: `${option.long}${option.description ? ` \u2014 ${option.description}` : ""}`,
|
|
1742
|
+
options: [
|
|
1743
|
+
{ value: false, label: "no" },
|
|
1744
|
+
{ value: true, label: "yes" }
|
|
1745
|
+
],
|
|
1746
|
+
initialValue: false
|
|
1747
|
+
})
|
|
1748
|
+
);
|
|
1749
|
+
if (enable) parts.push(option.long);
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
const value = ensure3(
|
|
1753
|
+
await text3({
|
|
1754
|
+
message: `${option.long} (optional)`,
|
|
1755
|
+
placeholder: option.description || void 0
|
|
1756
|
+
})
|
|
1757
|
+
);
|
|
1758
|
+
if (value.trim()) parts.push(`${option.long} ${quote(value.trim())}`);
|
|
1759
|
+
}
|
|
1760
|
+
return parts;
|
|
1761
|
+
}
|
|
1762
|
+
async function runGuided(program2) {
|
|
1763
|
+
intro3("QuickEngine CLI");
|
|
1764
|
+
const groups = selectable(program2);
|
|
1765
|
+
if (groups.length === 0) {
|
|
1766
|
+
outro3("No commands are registered.");
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const group = ensure3(
|
|
1770
|
+
await select({
|
|
1771
|
+
message: "What do you want to work with?",
|
|
1772
|
+
options: groups.map((command2) => ({
|
|
1773
|
+
value: command2,
|
|
1774
|
+
label: command2.name(),
|
|
1775
|
+
hint: command2.description() || void 0
|
|
1776
|
+
}))
|
|
1777
|
+
})
|
|
1778
|
+
);
|
|
1779
|
+
const children = selectable(group);
|
|
1780
|
+
const command = children.length ? ensure3(
|
|
1781
|
+
await select({
|
|
1782
|
+
message: `${group.name()} \u2014 which action?`,
|
|
1783
|
+
options: children.map((child) => ({
|
|
1784
|
+
value: child,
|
|
1785
|
+
label: child.name(),
|
|
1786
|
+
hint: child.description() || void 0
|
|
1787
|
+
}))
|
|
1788
|
+
})
|
|
1789
|
+
) : group;
|
|
1790
|
+
const parts = await collectArguments(command);
|
|
1791
|
+
const path = command === group ? [group.name()] : [group.name(), command.name()];
|
|
1792
|
+
const line2 = ["quick", ...path, ...parts].join(" ");
|
|
1793
|
+
note2(line2, "Running");
|
|
1794
|
+
try {
|
|
1795
|
+
await program2.parseAsync([...path, ...parts], { from: "user" });
|
|
1796
|
+
outro3("Done.");
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
log3.error(error instanceof Error ? error.message : String(error));
|
|
1799
|
+
outro3("That command failed.");
|
|
1800
|
+
process.exitCode = 1;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/index.ts
|
|
1805
|
+
var program = new Command();
|
|
1806
|
+
program.name("quick").description(
|
|
1807
|
+
"The QuickEngine command-line tool. Configure a workspace credential and read product APIs."
|
|
1808
|
+
).version("0.1.0");
|
|
1809
|
+
registerConfigCommands(program);
|
|
1810
|
+
registerClientCommands(program);
|
|
1811
|
+
registerCatalogCommands(program);
|
|
1812
|
+
registerQuoteCommands(program);
|
|
1813
|
+
registerInvoiceCommands(program);
|
|
1814
|
+
registerPaymentCommands(program);
|
|
1815
|
+
registerOrderCommands(program);
|
|
1816
|
+
registerFulfillmentCommands(program);
|
|
1817
|
+
registerInventoryCommands(program);
|
|
1818
|
+
registerShipmentCommands(program);
|
|
1819
|
+
registerProjectCommands(program);
|
|
1820
|
+
registerBookingCommands(program);
|
|
1821
|
+
registerTimeCommands(program);
|
|
1822
|
+
registerContractCommands(program);
|
|
1823
|
+
registerFileCommands(program);
|
|
1824
|
+
registerInitCommand(program);
|
|
1825
|
+
registerCreateCommands(program);
|
|
1826
|
+
registerActivityCommands(program);
|
|
1827
|
+
registerReportCommands(program);
|
|
1828
|
+
registerWebhookCommands(program);
|
|
1829
|
+
registerDoctorCommand(program);
|
|
1830
|
+
async function main() {
|
|
1831
|
+
try {
|
|
1832
|
+
if (process.argv.length <= 2 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
1833
|
+
await runGuided(program);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
await program.parseAsync(process.argv);
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
if (error instanceof QuickApiError2) {
|
|
1839
|
+
errorLine(`Error: ${error.message} (${error.code})`);
|
|
1840
|
+
if (error.requestId) errorLine(`Request id: ${error.requestId}`);
|
|
1841
|
+
} else if (error instanceof Error) {
|
|
1842
|
+
errorLine(`Error: ${error.message}`);
|
|
1843
|
+
} else {
|
|
1844
|
+
errorLine("An unknown error occurred.");
|
|
1845
|
+
}
|
|
1846
|
+
process.exitCode = 1;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
void main();
|