@stndrds/cli 1.0.0-alpha.3 → 1.0.0-alpha.302
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 +111 -0
- package/README.md +38 -0
- package/dist/bin.mjs +1611 -260
- package/package.json +9 -5
package/dist/bin.mjs
CHANGED
|
@@ -1,124 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/*! © 2025-2026 AND YOU CREATE SAS (RCS Paris 940 190 614). Proprietary — see LICENSE (Standards SDK License 1.0). */
|
|
2
3
|
|
|
3
4
|
// src/program.ts
|
|
4
|
-
import
|
|
5
|
+
import chalk10 from "chalk";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
|
|
7
8
|
// src/client.ts
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
super(message);
|
|
11
|
-
this.statusCode = statusCode;
|
|
12
|
-
this.name = "ApiClientError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
function buildUrl(base, path, params) {
|
|
16
|
-
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
17
|
-
const normalizedPath = path.replace(/^\/+/, "");
|
|
18
|
-
const url = new URL(normalizedPath, normalizedBase);
|
|
19
|
-
if (params) {
|
|
20
|
-
for (const [key, value] of Object.entries(params)) {
|
|
21
|
-
if (value !== void 0 && value !== null) {
|
|
22
|
-
url.searchParams.set(key, String(value));
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return url.toString();
|
|
27
|
-
}
|
|
9
|
+
import { StandardsRequestError, createTransport } from "@stndrds/client";
|
|
10
|
+
var ApiClientError = StandardsRequestError;
|
|
28
11
|
function createClient(config) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
34
|
-
async function request(method, path, options) {
|
|
35
|
-
const url = buildUrl(apiUrl, path, options?.params);
|
|
36
|
-
let response;
|
|
37
|
-
try {
|
|
38
|
-
response = await fetch(url, {
|
|
39
|
-
method,
|
|
40
|
-
headers,
|
|
41
|
-
body: options?.body ? JSON.stringify(options.body) : void 0,
|
|
42
|
-
signal: AbortSignal.timeout(3e4)
|
|
43
|
-
});
|
|
44
|
-
} catch (error) {
|
|
45
|
-
if (error instanceof TypeError || error instanceof DOMException && error.name === "TimeoutError") {
|
|
46
|
-
throw new ApiClientError(0, `Could not connect to ${apiUrl}. Is the server running?`);
|
|
47
|
-
}
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
if (response.status === 204) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
const data = await response.json();
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
const message = data.message ?? `Request failed with status ${response.status}`;
|
|
56
|
-
throw new ApiClientError(response.status, message);
|
|
57
|
-
}
|
|
58
|
-
return data;
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
get(path, params) {
|
|
62
|
-
return request("GET", path, { params });
|
|
63
|
-
},
|
|
64
|
-
post(path, body) {
|
|
65
|
-
return request("POST", path, { body });
|
|
66
|
-
},
|
|
67
|
-
patch(path, body) {
|
|
68
|
-
return request("PATCH", path, { body });
|
|
69
|
-
},
|
|
70
|
-
put(path, body) {
|
|
71
|
-
return request("PUT", path, { body });
|
|
72
|
-
},
|
|
73
|
-
delete(path) {
|
|
74
|
-
return request("DELETE", path);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// src/commands/auth.ts
|
|
80
|
-
import chalk from "chalk";
|
|
81
|
-
|
|
82
|
-
// src/commands/common.ts
|
|
83
|
-
function getGlobalOptions(cmd) {
|
|
84
|
-
return cmd.optsWithGlobals();
|
|
85
|
-
}
|
|
86
|
-
function getFormat(cmd) {
|
|
87
|
-
return getGlobalOptions(cmd).format;
|
|
88
|
-
}
|
|
89
|
-
function getClientFromCommand(cmd) {
|
|
90
|
-
const root = getGlobalOptions(cmd);
|
|
91
|
-
if (!(root.apiUrl && root.apiKey)) {
|
|
92
|
-
throw new Error('No Standards instance configured. Run "standards login" or pass --api-key.');
|
|
93
|
-
}
|
|
94
|
-
return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// src/commands/auth.ts
|
|
98
|
-
function registerAuthCommand(program) {
|
|
99
|
-
const auth = program.command("auth").description("Inspect CLI authentication");
|
|
100
|
-
auth.command("whoami").description("Show current configuration and validate API key").action(async (_opts, cmd) => {
|
|
101
|
-
const root = getGlobalOptions(cmd);
|
|
102
|
-
const client = getClientFromCommand(cmd);
|
|
103
|
-
const write = (msg) => process.stdout.write(`${msg}
|
|
104
|
-
`);
|
|
105
|
-
write(chalk.bold("Standards CLI Configuration"));
|
|
106
|
-
write(` API URL: ${root.apiUrl}`);
|
|
107
|
-
write(` API Key: ${root.apiKey ? `${root.apiKey.slice(0, 8)}...` : chalk.red("not set")}`);
|
|
108
|
-
write("");
|
|
109
|
-
const keys = await client.get("/api-keys");
|
|
110
|
-
write(chalk.green("\u2713 API key is valid"));
|
|
111
|
-
if (Array.isArray(keys) && keys.length > 0) {
|
|
112
|
-
const activeKeys = keys.filter(
|
|
113
|
-
(key) => typeof key === "object" && key !== null && !("revokedAt" in key && key.revokedAt)
|
|
114
|
-
);
|
|
115
|
-
write(` Active keys: ${activeKeys.length}`);
|
|
116
|
-
}
|
|
12
|
+
return createTransport({
|
|
13
|
+
baseUrl: config.apiUrl,
|
|
14
|
+
apiKey: config.apiKey,
|
|
15
|
+
tenantId: config.tenantId
|
|
117
16
|
});
|
|
118
17
|
}
|
|
119
18
|
|
|
19
|
+
// src/commands/ai.ts
|
|
20
|
+
import { ValidationError } from "@stndrds/schema";
|
|
21
|
+
|
|
120
22
|
// src/output.ts
|
|
121
|
-
import
|
|
23
|
+
import chalk from "chalk";
|
|
122
24
|
import Table from "cli-table3";
|
|
123
25
|
var MAX_CELL_WIDTH = 50;
|
|
124
26
|
function truncate(value, maxLength) {
|
|
@@ -147,7 +49,7 @@ function formatTable(items) {
|
|
|
147
49
|
if (items.length === 0) return "No results.";
|
|
148
50
|
const headers = Object.keys(items[0]);
|
|
149
51
|
const table = new Table({
|
|
150
|
-
head: headers.map((h) =>
|
|
52
|
+
head: headers.map((h) => chalk.bold(h)),
|
|
151
53
|
style: { head: [], border: [] }
|
|
152
54
|
});
|
|
153
55
|
for (const item of items) {
|
|
@@ -177,165 +79,186 @@ function formatOutput(data, format) {
|
|
|
177
79
|
`);
|
|
178
80
|
}
|
|
179
81
|
|
|
180
|
-
// src/commands/
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
82
|
+
// src/commands/common.ts
|
|
83
|
+
function getGlobalOptions(cmd) {
|
|
84
|
+
return cmd.optsWithGlobals();
|
|
85
|
+
}
|
|
86
|
+
function getFormat(cmd) {
|
|
87
|
+
return getGlobalOptions(cmd).format;
|
|
88
|
+
}
|
|
89
|
+
function parseCsvList(value) {
|
|
90
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
91
|
+
}
|
|
92
|
+
function getClientFromCommand(cmd) {
|
|
93
|
+
const root = getGlobalOptions(cmd);
|
|
94
|
+
if (!(root.apiUrl && root.apiKey)) {
|
|
95
|
+
throw new Error('No Standards instance configured. Run "standards login" or pass --api-key.');
|
|
96
|
+
}
|
|
97
|
+
return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey, tenantId: root.tenant });
|
|
98
|
+
}
|
|
99
|
+
function requireTenant(cmd, message) {
|
|
100
|
+
const { tenant } = getGlobalOptions(cmd);
|
|
101
|
+
if (!tenant) {
|
|
102
|
+
throw new Error(message);
|
|
103
|
+
}
|
|
104
|
+
return tenant;
|
|
105
|
+
}
|
|
106
|
+
function messageOf(error) {
|
|
107
|
+
return error instanceof Error ? error.message : String(error);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/commands/ai.ts
|
|
111
|
+
var SCOPES = ["me", "workspace"];
|
|
112
|
+
var ISO_DAY = /^\d{4}-\d{2}-\d{2}$/u;
|
|
113
|
+
function parseDay(flag, value) {
|
|
114
|
+
if (!ISO_DAY.test(value)) {
|
|
115
|
+
throw new ValidationError(`${flag} must be a YYYY-MM-DD date`, []);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function parseScope(scope) {
|
|
120
|
+
if (SCOPES.includes(scope)) return scope;
|
|
121
|
+
throw new ValidationError(`--scope must be one of: ${SCOPES.join(", ")}`, []);
|
|
122
|
+
}
|
|
123
|
+
function registerAiCommand(program) {
|
|
124
|
+
const ai = program.command("ai").description("Inspect AI usage and credit state");
|
|
125
|
+
ai.command("usage").description("Aggregate the AI journal over an inclusive UTC day window").requiredOption("--from <day>", "first day of the window (YYYY-MM-DD)").requiredOption("--to <day>", "last day of the window, inclusive (YYYY-MM-DD)").option("--scope <scope>", "me or workspace", "me").action(async (opts, cmd) => {
|
|
126
|
+
const params = {
|
|
127
|
+
from: parseDay("--from", opts.from),
|
|
128
|
+
to: parseDay("--to", opts.to),
|
|
129
|
+
scope: parseScope(opts.scope ?? "me")
|
|
130
|
+
};
|
|
198
131
|
const client = getClientFromCommand(cmd);
|
|
199
|
-
const
|
|
200
|
-
if (opts.values) body.values = JSON.parse(opts.values);
|
|
201
|
-
const result = await client.post("/documents", body);
|
|
132
|
+
const result = await client.get("/ai/usage", params);
|
|
202
133
|
formatOutput(result, getFormat(cmd));
|
|
203
134
|
});
|
|
204
|
-
|
|
135
|
+
ai.command("wallet").description("Show the tenant's billing mode and AI credit state").action(async (_opts, cmd) => {
|
|
205
136
|
const client = getClientFromCommand(cmd);
|
|
206
|
-
const
|
|
207
|
-
if (opts.title) body.title = opts.title;
|
|
208
|
-
if (opts.values) body.values = JSON.parse(opts.values);
|
|
209
|
-
const result = await client.patch(`/documents/${id}`, body);
|
|
137
|
+
const result = await client.get("/ai/wallet");
|
|
210
138
|
formatOutput(result, getFormat(cmd));
|
|
211
139
|
});
|
|
212
|
-
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/commands/auth.ts
|
|
143
|
+
import chalk2 from "chalk";
|
|
144
|
+
function registerAuthCommand(program) {
|
|
145
|
+
const auth = program.command("auth").description("Inspect CLI authentication");
|
|
146
|
+
auth.command("whoami").description("Show current configuration and validate API key").action(async (_opts, cmd) => {
|
|
147
|
+
const root = getGlobalOptions(cmd);
|
|
213
148
|
const client = getClientFromCommand(cmd);
|
|
214
|
-
|
|
215
|
-
process.stdout.write(`Document ${id} deleted.
|
|
149
|
+
const write = (msg) => process.stdout.write(`${msg}
|
|
216
150
|
`);
|
|
151
|
+
write(chalk2.bold("Standards CLI Configuration"));
|
|
152
|
+
write(` API URL: ${root.apiUrl}`);
|
|
153
|
+
write(` API Key: ${root.apiKey ? `${root.apiKey.slice(0, 8)}...` : chalk2.red("not set")}`);
|
|
154
|
+
write("");
|
|
155
|
+
const keys = await client.get("/api-keys");
|
|
156
|
+
write(chalk2.green("\u2713 API key is valid"));
|
|
157
|
+
if (Array.isArray(keys) && keys.length > 0) {
|
|
158
|
+
const activeKeys = keys.filter(
|
|
159
|
+
(key) => typeof key === "object" && key !== null && !("revokedAt" in key && key.revokedAt)
|
|
160
|
+
);
|
|
161
|
+
write(` Active keys: ${activeKeys.length}`);
|
|
162
|
+
}
|
|
217
163
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/commands/bundles.ts
|
|
167
|
+
function registerBundlesCommand(program) {
|
|
168
|
+
const bundles = program.command("bundles").description("Manage system bundles assigned to the current tenant (admin)");
|
|
169
|
+
bundles.command("list").description("List the bundles actively assigned to the current tenant").action(async (_opts, cmd) => {
|
|
224
170
|
const client = getClientFromCommand(cmd);
|
|
225
|
-
const result = await client.get(
|
|
171
|
+
const result = await client.get("/bundles/assigned");
|
|
226
172
|
formatOutput(result, getFormat(cmd));
|
|
227
173
|
});
|
|
228
|
-
|
|
174
|
+
bundles.command("updatable").description("List assigned bundles that have a newer version available").action(async (_opts, cmd) => {
|
|
229
175
|
const client = getClientFromCommand(cmd);
|
|
230
|
-
const result = await client.
|
|
231
|
-
slotName: opts.slotName,
|
|
232
|
-
fileId: opts.fileId
|
|
233
|
-
});
|
|
176
|
+
const result = await client.get("/bundles/updatable");
|
|
234
177
|
formatOutput(result, getFormat(cmd));
|
|
235
178
|
});
|
|
236
|
-
|
|
237
|
-
const client = getClientFromCommand(cmd);
|
|
238
|
-
await client.delete(`/documents/${id}/slots/${slotId}`);
|
|
239
|
-
process.stdout.write(`Slot ${slotId} removed.
|
|
240
|
-
`);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// src/commands/keys.ts
|
|
245
|
-
import chalk3 from "chalk";
|
|
246
|
-
function registerKeysCommand(program) {
|
|
247
|
-
const keys = program.command("keys").description("Manage Standards API keys");
|
|
248
|
-
keys.command("list").description("List API keys").action(async (_opts, cmd) => {
|
|
179
|
+
bundles.command("assign").description("Assign (install) a bundle to the current tenant").argument("<bundleId>", "bundle ID to assign").action(async (bundleId, _opts, cmd) => {
|
|
249
180
|
const client = getClientFromCommand(cmd);
|
|
250
|
-
const result = await client.
|
|
181
|
+
const result = await client.post("/bundles/assign", { bundleId });
|
|
251
182
|
formatOutput(result, getFormat(cmd));
|
|
252
183
|
});
|
|
253
|
-
|
|
184
|
+
bundles.command("uninstall").description("Uninstall a bundle: deactivate it and demote its now-orphaned objects").argument("<bundleId>", "bundle ID to uninstall").option("--yes", "uninstall without confirmation").action(async (bundleId, opts, cmd) => {
|
|
254
185
|
if (!opts.yes) {
|
|
255
|
-
throw new Error('
|
|
186
|
+
throw new Error('Uninstalling a bundle is explicit. Re-run with "--yes" to confirm.');
|
|
256
187
|
}
|
|
257
188
|
const client = getClientFromCommand(cmd);
|
|
258
|
-
const
|
|
259
|
-
name: opts.name,
|
|
260
|
-
permissions: []
|
|
261
|
-
};
|
|
262
|
-
if (opts.expires) body.expiresAt = opts.expires;
|
|
263
|
-
const result = await client.post("/api-keys", body);
|
|
189
|
+
const result = await client.delete(`/bundles/${bundleId}`);
|
|
264
190
|
formatOutput(result, getFormat(cmd));
|
|
265
191
|
});
|
|
266
|
-
|
|
267
|
-
if (!opts.yes) {
|
|
268
|
-
throw new Error('Revoking API keys is explicit. Re-run with "--yes" to confirm.');
|
|
269
|
-
}
|
|
192
|
+
bundles.command("update").description("Apply assigned bundles \u2014 picks up newer bundle versions (additive sync)").action(async (_opts, cmd) => {
|
|
270
193
|
const client = getClientFromCommand(cmd);
|
|
271
|
-
await client.
|
|
272
|
-
|
|
273
|
-
`);
|
|
194
|
+
const result = await client.post("/bundles/update");
|
|
195
|
+
formatOutput(result, getFormat(cmd));
|
|
274
196
|
});
|
|
275
197
|
}
|
|
276
198
|
|
|
277
|
-
// src/commands/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
199
|
+
// src/commands/connectors.ts
|
|
200
|
+
import { ValidationError as ValidationError2 } from "@stndrds/schema";
|
|
201
|
+
import chalk3 from "chalk";
|
|
202
|
+
var PROVIDER_IDS = {
|
|
203
|
+
gmail: true,
|
|
204
|
+
outlook: true,
|
|
205
|
+
"google-calendar": true,
|
|
206
|
+
"outlook-calendar": true
|
|
207
|
+
};
|
|
208
|
+
var PROVIDERS = Object.keys(PROVIDER_IDS);
|
|
209
|
+
function resolveScope(scope) {
|
|
210
|
+
return scope === "actor" ? "actor" : "tenant";
|
|
281
211
|
}
|
|
282
|
-
function
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const client = getClientFromCommand(cmd);
|
|
286
|
-
const params = {};
|
|
287
|
-
if (opts.limit) params.limit = opts.limit;
|
|
288
|
-
if (opts.offset) params.offset = opts.offset;
|
|
289
|
-
if (opts.sort) params.sorts = parseSortFlag(opts.sort);
|
|
290
|
-
if (opts.filter) params.filters = opts.filter;
|
|
291
|
-
const result = await client.get(`/records/${objectName}`, params);
|
|
292
|
-
formatOutput(result, getFormat(cmd));
|
|
293
|
-
});
|
|
294
|
-
records.command("get").description("Get a record by ID").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
|
|
212
|
+
function registerConnectorsCommand(program) {
|
|
213
|
+
const connectors = program.command("connectors").description("Manage email and calendar connectors (OAuth connections)");
|
|
214
|
+
connectors.command("providers").description("List the connector providers this deployment holds credentials for").action(async (_opts, cmd) => {
|
|
295
215
|
const client = getClientFromCommand(cmd);
|
|
296
|
-
const result = await client.get(
|
|
216
|
+
const result = await client.get("/connectors/providers");
|
|
297
217
|
formatOutput(result, getFormat(cmd));
|
|
298
218
|
});
|
|
299
|
-
|
|
300
|
-
const data = opts.data ? JSON.parse(opts.data) : {};
|
|
219
|
+
connectors.command("list").description("List connector connections").option("--scope <scope>", "connection scope (tenant or actor)", "tenant").action(async (opts, cmd) => {
|
|
301
220
|
const client = getClientFromCommand(cmd);
|
|
302
|
-
const result = await client.
|
|
221
|
+
const result = await client.get("/connectors/connections", {
|
|
222
|
+
scope: resolveScope(opts.scope)
|
|
223
|
+
});
|
|
303
224
|
formatOutput(result, getFormat(cmd));
|
|
304
225
|
});
|
|
305
|
-
|
|
306
|
-
|
|
226
|
+
connectors.command("connect").description("Start an OAuth flow and return the authorization URL to open").requiredOption(
|
|
227
|
+
"--provider <provider>",
|
|
228
|
+
`connector provider (${PROVIDERS.join(", ")}); run "connectors providers" for the ones this deployment configured`
|
|
229
|
+
).option("--scope <scope>", "connection scope (tenant or actor)", "tenant").action(async (opts, cmd) => {
|
|
230
|
+
if (!PROVIDERS.includes(opts.provider)) {
|
|
231
|
+
throw new ValidationError2(`--provider must be one of: ${PROVIDERS.join(", ")}`, []);
|
|
232
|
+
}
|
|
307
233
|
const client = getClientFromCommand(cmd);
|
|
308
|
-
const result = await client.
|
|
234
|
+
const result = await client.post("/connectors/auth/start", {
|
|
235
|
+
provider: opts.provider,
|
|
236
|
+
scope: resolveScope(opts.scope)
|
|
237
|
+
});
|
|
309
238
|
formatOutput(result, getFormat(cmd));
|
|
310
239
|
});
|
|
311
|
-
|
|
240
|
+
connectors.command("disconnect").description("Disconnect (delete) a connector connection").argument("<id>", "connection ID").option("--yes", "disconnect without confirmation").action(async (id, opts, cmd) => {
|
|
241
|
+
if (!opts.yes) {
|
|
242
|
+
throw new Error('Disconnecting a connector is explicit. Re-run with "--yes" to confirm.');
|
|
243
|
+
}
|
|
312
244
|
const client = getClientFromCommand(cmd);
|
|
313
|
-
await client.delete(`/
|
|
314
|
-
process.stdout.write(
|
|
245
|
+
await client.delete(`/connectors/connections/${id}`);
|
|
246
|
+
process.stdout.write(`${chalk3.green("\u2713")} Connector connection ${id} disconnected.
|
|
315
247
|
`);
|
|
316
248
|
});
|
|
317
|
-
records.command("search").description("Full-text search records").argument("<object>", "object name").requiredOption("--query <q>", "search query string").option("--limit <n>", "max records to return").option("--offset <n>", "number of records to skip").option("--sort <sort>", 'sort rule as "attribute:direction"').option("--filter <json>", "filter state as JSON string").action(async (objectName, opts, cmd) => {
|
|
318
|
-
const client = getClientFromCommand(cmd);
|
|
319
|
-
const params = { q: opts.query };
|
|
320
|
-
if (opts.limit) params.limit = opts.limit;
|
|
321
|
-
if (opts.offset) params.offset = opts.offset;
|
|
322
|
-
if (opts.sort) params.sorts = parseSortFlag(opts.sort);
|
|
323
|
-
if (opts.filter) params.filters = opts.filter;
|
|
324
|
-
const result = await client.get(`/records/${objectName}/search`, params);
|
|
325
|
-
formatOutput(result, getFormat(cmd));
|
|
326
|
-
});
|
|
327
249
|
}
|
|
328
250
|
|
|
329
|
-
// src/commands/
|
|
330
|
-
import {
|
|
331
|
-
import {
|
|
332
|
-
import chalk4 from "chalk";
|
|
251
|
+
// src/commands/device.ts
|
|
252
|
+
import { hostname } from "os";
|
|
253
|
+
import { SchemaError as SchemaError2, SchemaErrorCode as SchemaErrorCode2 } from "@stndrds/schema";
|
|
333
254
|
|
|
334
255
|
// src/config.ts
|
|
335
|
-
import {
|
|
256
|
+
import { randomUUID } from "crypto";
|
|
257
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
336
258
|
import { homedir } from "os";
|
|
337
259
|
import { dirname, join } from "path";
|
|
338
260
|
var DEFAULT_API_URL = "http://localhost:4100/v1";
|
|
261
|
+
var ENV_PROFILE_NAME = "sandbox";
|
|
339
262
|
function getDefaultApiUrl() {
|
|
340
263
|
return DEFAULT_API_URL;
|
|
341
264
|
}
|
|
@@ -348,6 +271,7 @@ async function readConfig() {
|
|
|
348
271
|
const raw = await readFile(getConfigPath(), "utf8");
|
|
349
272
|
const parsed = JSON.parse(raw);
|
|
350
273
|
return {
|
|
274
|
+
...parsed.installationId ? { installationId: parsed.installationId } : {},
|
|
351
275
|
currentProfile: parsed.currentProfile,
|
|
352
276
|
profiles: parsed.profiles ?? {}
|
|
353
277
|
};
|
|
@@ -360,19 +284,60 @@ async function readConfig() {
|
|
|
360
284
|
}
|
|
361
285
|
async function writeConfig(config) {
|
|
362
286
|
const path = getConfigPath();
|
|
363
|
-
|
|
287
|
+
const directory = dirname(path);
|
|
288
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
289
|
+
await chmod(directory, 448);
|
|
364
290
|
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
365
291
|
`, { mode: 384 });
|
|
292
|
+
await chmod(path, 384);
|
|
293
|
+
}
|
|
294
|
+
function normalizeApiOrigin(apiUrl) {
|
|
295
|
+
try {
|
|
296
|
+
return new URL(apiUrl).origin;
|
|
297
|
+
} catch {
|
|
298
|
+
return apiUrl.replace(/\/+$/, "");
|
|
299
|
+
}
|
|
366
300
|
}
|
|
367
301
|
async function upsertProfile(input2) {
|
|
368
302
|
const config = await readConfig();
|
|
303
|
+
const existingProfile = config.profiles[input2.name];
|
|
304
|
+
const device = existingProfile?.device && normalizeApiOrigin(existingProfile.apiUrl) === normalizeApiOrigin(input2.apiUrl) ? existingProfile.device : void 0;
|
|
369
305
|
config.profiles[input2.name] = {
|
|
370
306
|
apiUrl: input2.apiUrl,
|
|
371
|
-
apiKey: input2.apiKey
|
|
307
|
+
apiKey: input2.apiKey,
|
|
308
|
+
...device ? { device } : {}
|
|
372
309
|
};
|
|
373
310
|
config.currentProfile = input2.name;
|
|
374
311
|
await writeConfig(config);
|
|
375
312
|
}
|
|
313
|
+
async function getOrCreateInstallationId() {
|
|
314
|
+
const config = await readConfig();
|
|
315
|
+
if (config.installationId) return config.installationId;
|
|
316
|
+
const installationId = randomUUID();
|
|
317
|
+
config.installationId = installationId;
|
|
318
|
+
await writeConfig(config);
|
|
319
|
+
return installationId;
|
|
320
|
+
}
|
|
321
|
+
async function getStoredDeviceCredential(profileName) {
|
|
322
|
+
const config = await readConfig();
|
|
323
|
+
return config.profiles[profileName]?.device ?? null;
|
|
324
|
+
}
|
|
325
|
+
async function storeDeviceCredential(profileName, credential) {
|
|
326
|
+
const config = await readConfig();
|
|
327
|
+
const profile = config.profiles[profileName];
|
|
328
|
+
if (!profile) return false;
|
|
329
|
+
profile.device = credential;
|
|
330
|
+
await writeConfig(config);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
async function removeDeviceCredential(profileName) {
|
|
334
|
+
const config = await readConfig();
|
|
335
|
+
const profile = config.profiles[profileName];
|
|
336
|
+
if (!profile?.device) return false;
|
|
337
|
+
profile.device = void 0;
|
|
338
|
+
await writeConfig(config);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
376
341
|
async function setCurrentProfile(name) {
|
|
377
342
|
const config = await readConfig();
|
|
378
343
|
if (!config.profiles[name]) {
|
|
@@ -393,19 +358,39 @@ async function removeProfile(name) {
|
|
|
393
358
|
}
|
|
394
359
|
async function listProfiles() {
|
|
395
360
|
const config = await readConfig();
|
|
396
|
-
|
|
361
|
+
const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
|
|
397
362
|
name,
|
|
398
363
|
apiUrl: profile.apiUrl,
|
|
399
364
|
current: config.currentProfile === name
|
|
400
365
|
}));
|
|
366
|
+
const envProfile = getEnvProfile();
|
|
367
|
+
if (envProfile) {
|
|
368
|
+
profiles.push({
|
|
369
|
+
name: ENV_PROFILE_NAME,
|
|
370
|
+
apiUrl: envProfile.apiUrl,
|
|
371
|
+
current: !config.currentProfile,
|
|
372
|
+
source: "env"
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return profiles;
|
|
401
376
|
}
|
|
402
377
|
async function getActiveProfile() {
|
|
403
378
|
const config = await readConfig();
|
|
404
|
-
if (!config.currentProfile)
|
|
379
|
+
if (!config.currentProfile) {
|
|
380
|
+
const envProfile = getEnvProfile();
|
|
381
|
+
return envProfile ? { name: ENV_PROFILE_NAME, ...envProfile } : null;
|
|
382
|
+
}
|
|
405
383
|
const profile = config.profiles[config.currentProfile];
|
|
406
384
|
if (!profile) return null;
|
|
407
385
|
return { name: config.currentProfile, ...profile };
|
|
408
386
|
}
|
|
387
|
+
function getEnvProfile() {
|
|
388
|
+
if (!process.env.STANDARDS_API_KEY) return null;
|
|
389
|
+
return {
|
|
390
|
+
apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
|
|
391
|
+
apiKey: process.env.STANDARDS_API_KEY
|
|
392
|
+
};
|
|
393
|
+
}
|
|
409
394
|
async function resolveCliConfig(options) {
|
|
410
395
|
if (options.apiUrl || options.apiKey) {
|
|
411
396
|
return {
|
|
@@ -430,37 +415,1347 @@ async function resolveCliConfig(options) {
|
|
|
430
415
|
};
|
|
431
416
|
}
|
|
432
417
|
|
|
433
|
-
// src/
|
|
434
|
-
|
|
435
|
-
|
|
418
|
+
// src/device/worker-client.ts
|
|
419
|
+
import { SchemaError, SchemaErrorCode } from "@stndrds/schema";
|
|
420
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
421
|
+
var DeviceWorkerRequestError = class extends SchemaError {
|
|
422
|
+
constructor(statusCode, message, code = SchemaErrorCode.REPOSITORY_QUERY_FAILED) {
|
|
423
|
+
super(message, code, { statusCode });
|
|
424
|
+
this.statusCode = statusCode;
|
|
425
|
+
this.name = "DeviceWorkerRequestError";
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
var DeviceCredentialRejectedError = class extends DeviceWorkerRequestError {
|
|
429
|
+
constructor() {
|
|
430
|
+
super(
|
|
431
|
+
401,
|
|
432
|
+
"Device credential rejected; run standards device pair again",
|
|
433
|
+
SchemaErrorCode.ACCESS_DENIED
|
|
434
|
+
);
|
|
435
|
+
this.name = "DeviceCredentialRejectedError";
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
function isRecord(value) {
|
|
439
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
440
|
+
}
|
|
441
|
+
async function readJson(response) {
|
|
442
|
+
const text = await response.text();
|
|
443
|
+
if (!text) return void 0;
|
|
436
444
|
try {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
rl.close();
|
|
445
|
+
return JSON.parse(text);
|
|
446
|
+
} catch {
|
|
447
|
+
return void 0;
|
|
441
448
|
}
|
|
442
449
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
450
|
+
function messageFrom(value, fallback) {
|
|
451
|
+
return isRecord(value) && typeof value.message === "string" ? value.message : fallback;
|
|
452
|
+
}
|
|
453
|
+
function parseLease(value) {
|
|
454
|
+
if (!isRecord(value)) throw new DeviceWorkerRequestError(200, "Invalid lease response");
|
|
455
|
+
const leaseToken = value.leaseToken;
|
|
456
|
+
if (!isRecord(value.command)) {
|
|
457
|
+
throw new DeviceWorkerRequestError(200, "Invalid lease response");
|
|
458
|
+
}
|
|
459
|
+
const leasedCommand = value.command;
|
|
460
|
+
const { id, intent, command, cwd, timeoutMs } = leasedCommand;
|
|
461
|
+
if (typeof id !== "string" || typeof leaseToken !== "string" || typeof intent !== "string" || typeof command !== "string" || cwd !== void 0 && cwd !== null && typeof cwd !== "string" || typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs)) {
|
|
462
|
+
throw new DeviceWorkerRequestError(200, "Invalid lease response");
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
id,
|
|
466
|
+
leaseToken,
|
|
467
|
+
intent,
|
|
468
|
+
command,
|
|
469
|
+
timeoutMs,
|
|
470
|
+
...typeof cwd === "string" ? { cwd } : {}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function parseHeartbeat(value) {
|
|
474
|
+
if (!isRecord(value)) {
|
|
475
|
+
throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
|
|
476
|
+
}
|
|
477
|
+
const { cancel } = value;
|
|
478
|
+
if (typeof cancel !== "boolean") {
|
|
479
|
+
throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
|
|
480
|
+
}
|
|
481
|
+
const reason = value.reason;
|
|
482
|
+
if (reason !== void 0 && reason !== "cancelled" && reason !== "revoked") {
|
|
483
|
+
throw new DeviceWorkerRequestError(200, "Invalid heartbeat response");
|
|
484
|
+
}
|
|
485
|
+
return { cancel, ...reason === void 0 ? {} : { reason } };
|
|
486
|
+
}
|
|
487
|
+
function isAbortError(error) {
|
|
488
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
489
|
+
}
|
|
490
|
+
var DeviceWorkerClient = class {
|
|
491
|
+
constructor(options) {
|
|
492
|
+
this.options = options;
|
|
493
|
+
this.fetch = options.fetch ?? fetch;
|
|
494
|
+
this.apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
495
|
+
}
|
|
496
|
+
async request(path, input2 = {}) {
|
|
497
|
+
const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
498
|
+
const signal = input2.signal ? AbortSignal.any([input2.signal, timeoutSignal]) : timeoutSignal;
|
|
499
|
+
let response;
|
|
500
|
+
try {
|
|
501
|
+
response = await this.fetch(`${this.apiUrl}/device-worker${path}`, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
signal,
|
|
504
|
+
headers: {
|
|
505
|
+
authorization: `Device ${this.options.token}`,
|
|
506
|
+
"content-type": "application/json",
|
|
507
|
+
...input2.leaseToken ? { "x-device-lease-token": input2.leaseToken } : {}
|
|
508
|
+
},
|
|
509
|
+
...input2.body === void 0 ? {} : { body: JSON.stringify(input2.body) }
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (input2.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
513
|
+
if (isAbortError(error)) throw error;
|
|
514
|
+
throw new DeviceWorkerRequestError(0, "Device worker request failed");
|
|
515
|
+
}
|
|
516
|
+
if (response.status === 401) throw new DeviceCredentialRejectedError();
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
const body = await readJson(response);
|
|
519
|
+
throw new DeviceWorkerRequestError(
|
|
520
|
+
response.status,
|
|
521
|
+
messageFrom(body, `Device worker request failed with status ${response.status}`),
|
|
522
|
+
response.status === 409 ? SchemaErrorCode.CONFLICT : SchemaErrorCode.REPOSITORY_QUERY_FAILED
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return response;
|
|
526
|
+
}
|
|
527
|
+
async lease(signal) {
|
|
528
|
+
const response = await this.request("/commands/lease", {
|
|
529
|
+
body: { version: "1", capabilities: ["exec"] },
|
|
530
|
+
signal
|
|
531
|
+
});
|
|
532
|
+
if (response.status === 204) return null;
|
|
533
|
+
return parseLease(await readJson(response));
|
|
534
|
+
}
|
|
535
|
+
async markRunning(commandId, leaseToken) {
|
|
536
|
+
await this.request(`/commands/${commandId}/running`, { leaseToken });
|
|
537
|
+
}
|
|
538
|
+
async heartbeat(commandId, leaseToken, signal) {
|
|
539
|
+
const response = await this.request(`/commands/${commandId}/heartbeat`, {
|
|
540
|
+
leaseToken,
|
|
541
|
+
signal
|
|
542
|
+
});
|
|
543
|
+
return parseHeartbeat(await readJson(response));
|
|
544
|
+
}
|
|
545
|
+
async output(commandId, leaseToken, stdout, stderr) {
|
|
546
|
+
await this.request(`/commands/${commandId}/output`, {
|
|
547
|
+
leaseToken,
|
|
548
|
+
body: { stdout, stderr }
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async complete(commandId, leaseToken, result) {
|
|
552
|
+
await this.request(`/commands/${commandId}/complete`, {
|
|
553
|
+
leaseToken,
|
|
554
|
+
body: {
|
|
555
|
+
exitCode: result.exitCode,
|
|
556
|
+
success: result.success,
|
|
557
|
+
...result.errorCode ? { errorCode: result.errorCode } : {}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// src/device/process-runner.ts
|
|
564
|
+
import { spawn } from "child_process";
|
|
565
|
+
import { constants } from "fs";
|
|
566
|
+
import { access, stat } from "fs/promises";
|
|
567
|
+
import { isAbsolute } from "path";
|
|
568
|
+
import { StringDecoder } from "string_decoder";
|
|
569
|
+
var OUTPUT_LIMIT = 3e4;
|
|
570
|
+
var TERMINATION_GRACE_MS = 5e3;
|
|
571
|
+
var TERMINATION_POLL_MS = 50;
|
|
572
|
+
var BoundedTextBuffer = class {
|
|
573
|
+
constructor() {
|
|
574
|
+
this.decoder = new StringDecoder("utf8");
|
|
575
|
+
this.decoderFlushed = false;
|
|
576
|
+
this.value = "";
|
|
577
|
+
this.truncated = false;
|
|
578
|
+
}
|
|
579
|
+
append(chunk) {
|
|
580
|
+
if (this.truncated) return;
|
|
581
|
+
const text = typeof chunk === "string" ? chunk : this.decoder.write(Buffer.from(chunk));
|
|
582
|
+
this.appendText(text);
|
|
583
|
+
}
|
|
584
|
+
appendText(text) {
|
|
585
|
+
const remaining = OUTPUT_LIMIT - this.value.length;
|
|
586
|
+
if (text.length <= remaining) {
|
|
587
|
+
this.value += text;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.value += text.slice(0, Math.max(remaining, 0));
|
|
591
|
+
this.truncated = true;
|
|
592
|
+
}
|
|
593
|
+
toString() {
|
|
594
|
+
if (!this.decoderFlushed) {
|
|
595
|
+
this.decoderFlushed = true;
|
|
596
|
+
this.appendText(this.decoder.end());
|
|
597
|
+
}
|
|
598
|
+
return this.truncated ? `${this.value}
|
|
599
|
+
... output truncated to ${OUTPUT_LIMIT} chars` : this.value;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
function isMissingProcess(error) {
|
|
603
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
604
|
+
}
|
|
605
|
+
function signalProcessGroup(pid, signal, kill) {
|
|
606
|
+
try {
|
|
607
|
+
kill(-pid, signal);
|
|
608
|
+
return true;
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (isMissingProcess(error)) return false;
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function beginProcessGroupTermination(pid, deps = {}) {
|
|
615
|
+
const kill = deps.kill ?? process.kill.bind(process);
|
|
616
|
+
const setTimer = deps.setTimer ?? setTimeout;
|
|
617
|
+
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
618
|
+
return new Promise((resolve, reject) => {
|
|
619
|
+
let settled = false;
|
|
620
|
+
let escalationTimer;
|
|
621
|
+
let pollTimer;
|
|
622
|
+
function clearTimers() {
|
|
623
|
+
if (escalationTimer) clearTimer(escalationTimer);
|
|
624
|
+
if (pollTimer) clearTimer(pollTimer);
|
|
625
|
+
}
|
|
626
|
+
function finish() {
|
|
627
|
+
if (settled) return;
|
|
628
|
+
settled = true;
|
|
629
|
+
clearTimers();
|
|
630
|
+
resolve();
|
|
631
|
+
}
|
|
632
|
+
function fail(error) {
|
|
633
|
+
if (settled) return;
|
|
634
|
+
settled = true;
|
|
635
|
+
clearTimers();
|
|
636
|
+
reject(error);
|
|
637
|
+
}
|
|
638
|
+
function poll() {
|
|
639
|
+
if (settled) return;
|
|
640
|
+
try {
|
|
641
|
+
if (!signalProcessGroup(pid, 0, kill)) {
|
|
642
|
+
finish();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
pollTimer = setTimer(poll, TERMINATION_POLL_MS);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
fail(error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
if (!signalProcessGroup(pid, "SIGTERM", kill)) {
|
|
652
|
+
finish();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
escalationTimer = setTimer(() => {
|
|
656
|
+
try {
|
|
657
|
+
if (!signalProcessGroup(pid, 0, kill)) {
|
|
658
|
+
finish();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
signalProcessGroup(pid, "SIGKILL", kill);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
fail(error);
|
|
664
|
+
}
|
|
665
|
+
}, TERMINATION_GRACE_MS);
|
|
666
|
+
pollTimer = setTimer(poll, TERMINATION_POLL_MS);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
fail(error);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async function resolveShell(shell = process.env.SHELL) {
|
|
673
|
+
if (!(shell && isAbsolute(shell))) return "/bin/sh";
|
|
674
|
+
try {
|
|
675
|
+
const metadata = await stat(shell);
|
|
676
|
+
if (!metadata.isFile()) return "/bin/sh";
|
|
677
|
+
await access(shell, constants.X_OK);
|
|
678
|
+
return shell;
|
|
679
|
+
} catch {
|
|
680
|
+
return "/bin/sh";
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function terminalResult(reason, stdout, stderr) {
|
|
684
|
+
return {
|
|
685
|
+
stdout: stdout.toString(),
|
|
686
|
+
stderr: stderr.toString(),
|
|
687
|
+
exitCode: reason === "timed_out" ? 124 : 130,
|
|
688
|
+
success: false,
|
|
689
|
+
errorCode: reason
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async function runDeviceProcess(input2) {
|
|
693
|
+
if (input2.signal?.aborted) {
|
|
694
|
+
return { stdout: "", stderr: "", exitCode: 130, success: false, errorCode: "cancelled" };
|
|
695
|
+
}
|
|
696
|
+
const shell = await resolveShell();
|
|
697
|
+
if (input2.signal?.aborted) {
|
|
698
|
+
return { stdout: "", stderr: "", exitCode: 130, success: false, errorCode: "cancelled" };
|
|
699
|
+
}
|
|
700
|
+
const stdout = new BoundedTextBuffer();
|
|
701
|
+
const stderr = new BoundedTextBuffer();
|
|
702
|
+
return new Promise((resolve) => {
|
|
703
|
+
let settled = false;
|
|
704
|
+
let terminationReason;
|
|
705
|
+
let termination;
|
|
706
|
+
let child;
|
|
707
|
+
function finish(result) {
|
|
708
|
+
if (settled) return;
|
|
709
|
+
settled = true;
|
|
710
|
+
clearTimeout(timeout);
|
|
711
|
+
input2.signal?.removeEventListener("abort", cancel);
|
|
712
|
+
resolve(result);
|
|
713
|
+
}
|
|
714
|
+
function finishTerminationFailure(error) {
|
|
715
|
+
finish({
|
|
716
|
+
stdout: stdout.toString(),
|
|
717
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
718
|
+
exitCode: 1,
|
|
719
|
+
success: false,
|
|
720
|
+
errorCode: "execution_failed"
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function terminate(reason) {
|
|
724
|
+
if (settled || terminationReason || !child?.pid) return;
|
|
725
|
+
terminationReason = reason;
|
|
726
|
+
termination = beginProcessGroupTermination(child.pid);
|
|
727
|
+
termination.catch(finishTerminationFailure);
|
|
728
|
+
}
|
|
729
|
+
function cancel() {
|
|
730
|
+
terminate("cancelled");
|
|
731
|
+
}
|
|
732
|
+
const timeout = setTimeout(() => terminate("timed_out"), input2.timeoutMs);
|
|
733
|
+
try {
|
|
734
|
+
const spawned2 = spawn(shell, ["-lc", input2.command], {
|
|
735
|
+
cwd: input2.cwd,
|
|
736
|
+
env: process.env,
|
|
737
|
+
detached: true,
|
|
738
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
739
|
+
});
|
|
740
|
+
child = spawned2;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
finish({
|
|
743
|
+
stdout: "",
|
|
744
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
745
|
+
exitCode: 1,
|
|
746
|
+
success: false,
|
|
747
|
+
errorCode: "execution_failed"
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
input2.signal?.addEventListener("abort", cancel, { once: true });
|
|
752
|
+
if (input2.signal?.aborted) cancel();
|
|
753
|
+
const spawned = child;
|
|
754
|
+
spawned.stdout.on("data", (chunk) => stdout.append(chunk));
|
|
755
|
+
spawned.stderr.on("data", (chunk) => stderr.append(chunk));
|
|
756
|
+
spawned.once("error", (error) => {
|
|
757
|
+
finish({
|
|
758
|
+
stdout: stdout.toString(),
|
|
759
|
+
stderr: error.message,
|
|
760
|
+
exitCode: 1,
|
|
761
|
+
success: false,
|
|
762
|
+
errorCode: "execution_failed"
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
spawned.once("close", (code) => {
|
|
766
|
+
if (terminationReason) {
|
|
767
|
+
const reason = terminationReason;
|
|
768
|
+
(termination ?? Promise.resolve()).then(
|
|
769
|
+
() => finish(terminalResult(reason, stdout, stderr)),
|
|
770
|
+
finishTerminationFailure
|
|
771
|
+
);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const exitCode = code ?? 1;
|
|
775
|
+
finish({
|
|
776
|
+
stdout: stdout.toString(),
|
|
777
|
+
stderr: stderr.toString(),
|
|
778
|
+
exitCode,
|
|
779
|
+
success: exitCode === 0,
|
|
780
|
+
...code === null ? { errorCode: "execution_failed" } : {}
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/device/worker-loop.ts
|
|
787
|
+
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
788
|
+
var INITIAL_RETRY_MS = 250;
|
|
789
|
+
var MAX_RETRY_MS = 1e4;
|
|
790
|
+
var RETRY_JITTER_MS = 250;
|
|
791
|
+
function computeRetryDelay(attempt, random = Math.random) {
|
|
792
|
+
const exponential = Math.min(INITIAL_RETRY_MS * 2 ** attempt, MAX_RETRY_MS);
|
|
793
|
+
return exponential + Math.floor(random() * RETRY_JITTER_MS);
|
|
794
|
+
}
|
|
795
|
+
function sleepWithAbort(milliseconds, signal) {
|
|
796
|
+
if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
797
|
+
return new Promise((resolve, reject) => {
|
|
798
|
+
function abort() {
|
|
799
|
+
clearTimeout(timer);
|
|
800
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
801
|
+
}
|
|
802
|
+
const timer = setTimeout(() => {
|
|
803
|
+
signal?.removeEventListener("abort", abort);
|
|
804
|
+
resolve();
|
|
805
|
+
}, milliseconds);
|
|
806
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
function isAbortError2(error) {
|
|
810
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
811
|
+
}
|
|
812
|
+
function isRetryableIdleError(error) {
|
|
813
|
+
return error instanceof DeviceWorkerRequestError && (error.statusCode === 0 || error.statusCode >= 500);
|
|
814
|
+
}
|
|
815
|
+
function isLeaseConflict(error) {
|
|
816
|
+
return error instanceof DeviceWorkerRequestError && error.statusCode === 409;
|
|
817
|
+
}
|
|
818
|
+
async function executeLease(command, input2, runProcess, sleep) {
|
|
819
|
+
try {
|
|
820
|
+
await input2.client.markRunning(command.id, command.leaseToken);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
input2.onEvent?.({ type: "running", commandId: command.id, intent: command.intent });
|
|
826
|
+
const commandController = new AbortController();
|
|
827
|
+
const heartbeatController = new AbortController();
|
|
828
|
+
let completionAllowed = true;
|
|
829
|
+
let heartbeatFailure;
|
|
830
|
+
function abortCommand() {
|
|
831
|
+
commandController.abort();
|
|
832
|
+
}
|
|
833
|
+
input2.signal.addEventListener("abort", abortCommand, { once: true });
|
|
834
|
+
const processPromise = runProcess({
|
|
835
|
+
command: command.command,
|
|
836
|
+
cwd: command.cwd,
|
|
837
|
+
timeoutMs: command.timeoutMs,
|
|
838
|
+
signal: commandController.signal
|
|
839
|
+
});
|
|
840
|
+
const heartbeatPromise = (async () => {
|
|
841
|
+
while (!heartbeatController.signal.aborted) {
|
|
842
|
+
try {
|
|
843
|
+
await sleep(HEARTBEAT_INTERVAL_MS, heartbeatController.signal);
|
|
844
|
+
const heartbeat = await input2.client.heartbeat(
|
|
845
|
+
command.id,
|
|
846
|
+
command.leaseToken,
|
|
847
|
+
heartbeatController.signal
|
|
848
|
+
);
|
|
849
|
+
if (heartbeat.cancel) {
|
|
850
|
+
if (heartbeat.reason === "revoked") completionAllowed = false;
|
|
851
|
+
commandController.abort();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (heartbeatController.signal.aborted && isAbortError2(error)) return;
|
|
856
|
+
heartbeatFailure = error;
|
|
857
|
+
completionAllowed = false;
|
|
858
|
+
commandController.abort();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
})();
|
|
863
|
+
let result;
|
|
864
|
+
try {
|
|
865
|
+
result = await processPromise;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
result = {
|
|
868
|
+
stdout: "",
|
|
869
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
870
|
+
exitCode: 1,
|
|
871
|
+
success: false,
|
|
872
|
+
errorCode: "execution_failed"
|
|
873
|
+
};
|
|
874
|
+
} finally {
|
|
875
|
+
heartbeatController.abort();
|
|
876
|
+
await heartbeatPromise;
|
|
877
|
+
input2.signal.removeEventListener("abort", abortCommand);
|
|
878
|
+
}
|
|
879
|
+
if (heartbeatFailure instanceof DeviceCredentialRejectedError) throw heartbeatFailure;
|
|
880
|
+
if (!completionAllowed || isLeaseConflict(heartbeatFailure)) return;
|
|
881
|
+
try {
|
|
882
|
+
await input2.client.output(command.id, command.leaseToken, result.stdout, result.stderr);
|
|
883
|
+
await input2.client.complete(command.id, command.leaseToken, result);
|
|
884
|
+
} catch (error) {
|
|
885
|
+
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
input2.onEvent?.({ type: "completed", commandId: command.id, exitCode: result.exitCode });
|
|
889
|
+
}
|
|
890
|
+
async function runDeviceWorker(input2) {
|
|
891
|
+
const runProcess = input2.runProcess ?? runDeviceProcess;
|
|
892
|
+
const sleep = input2.sleep ?? sleepWithAbort;
|
|
893
|
+
const random = input2.random ?? Math.random;
|
|
894
|
+
let retryAttempt = 0;
|
|
895
|
+
while (!input2.signal.aborted) {
|
|
896
|
+
let command;
|
|
897
|
+
try {
|
|
898
|
+
command = await input2.client.lease(input2.signal);
|
|
899
|
+
retryAttempt = 0;
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (input2.signal.aborted) return;
|
|
902
|
+
if (error instanceof DeviceCredentialRejectedError) throw error;
|
|
903
|
+
if (!isRetryableIdleError(error)) throw error;
|
|
904
|
+
try {
|
|
905
|
+
await sleep(computeRetryDelay(retryAttempt, random), input2.signal);
|
|
906
|
+
} catch (sleepError) {
|
|
907
|
+
if (input2.signal.aborted && isAbortError2(sleepError)) return;
|
|
908
|
+
throw sleepError;
|
|
909
|
+
}
|
|
910
|
+
retryAttempt += 1;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (!command) continue;
|
|
914
|
+
await executeLease(command, input2, runProcess, sleep);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/commands/device.ts
|
|
919
|
+
var NAMED_PROFILE_REQUIRED = "Device pairing requires a named Standards profile";
|
|
920
|
+
var SUPPORTED_PLATFORMS_REQUIRED = "Device workers support macOS and Linux only";
|
|
921
|
+
var DeviceCommandError = class extends SchemaError2 {
|
|
922
|
+
constructor(message) {
|
|
923
|
+
super(message, SchemaErrorCode2.VALIDATION_FAILED);
|
|
924
|
+
this.name = "DeviceCommandError";
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
function requireSupportedPlatform(platform) {
|
|
928
|
+
if (platform === "darwin" || platform === "linux") return platform;
|
|
929
|
+
throw new DeviceCommandError(SUPPORTED_PLATFORMS_REQUIRED);
|
|
930
|
+
}
|
|
931
|
+
function requireNamedProfile(command) {
|
|
932
|
+
const options = getGlobalOptions(command);
|
|
933
|
+
if (!(options.profileName && options.apiUrl && options.apiKey)) {
|
|
934
|
+
throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
apiUrl: options.apiUrl,
|
|
938
|
+
apiKey: options.apiKey,
|
|
939
|
+
profileName: options.profileName,
|
|
940
|
+
tenantId: options.tenant
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function isRecord2(value) {
|
|
944
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
945
|
+
}
|
|
946
|
+
function parseDeviceView(value) {
|
|
947
|
+
if (!isRecord2(value)) throw new DeviceCommandError("Invalid device response");
|
|
948
|
+
const { id, name, platform, arch, hostname: deviceHostname } = value;
|
|
949
|
+
const { status } = value;
|
|
950
|
+
if (typeof id !== "string" || typeof name !== "string" || platform !== "darwin" && platform !== "linux" || typeof arch !== "string" || typeof deviceHostname !== "string" || status !== "online" && status !== "offline" && status !== "revoked") {
|
|
951
|
+
throw new DeviceCommandError("Invalid device response");
|
|
952
|
+
}
|
|
953
|
+
return { id, name, platform, arch, hostname: deviceHostname, status };
|
|
954
|
+
}
|
|
955
|
+
function parsePairDeviceResponse(value) {
|
|
956
|
+
if (!isRecord2(value) || typeof value.token !== "string") {
|
|
957
|
+
throw new DeviceCommandError("Invalid device pairing response");
|
|
958
|
+
}
|
|
959
|
+
return { device: parseDeviceView(value.device), token: value.token };
|
|
960
|
+
}
|
|
961
|
+
function parseDeviceList(value) {
|
|
962
|
+
if (!Array.isArray(value)) throw new DeviceCommandError("Invalid device list response");
|
|
963
|
+
return value.map(parseDeviceView);
|
|
964
|
+
}
|
|
965
|
+
async function pairDevice(name, command) {
|
|
966
|
+
const platform = requireSupportedPlatform(process.platform);
|
|
967
|
+
const profile = requireNamedProfile(command);
|
|
968
|
+
const installationId = await getOrCreateInstallationId();
|
|
969
|
+
const client = createClient({
|
|
970
|
+
apiUrl: profile.apiUrl,
|
|
971
|
+
apiKey: profile.apiKey,
|
|
972
|
+
tenantId: profile.tenantId
|
|
973
|
+
});
|
|
974
|
+
const response = parsePairDeviceResponse(
|
|
975
|
+
await client.post("/devices/pair", {
|
|
976
|
+
installationId,
|
|
977
|
+
name,
|
|
978
|
+
platform,
|
|
979
|
+
arch: process.arch,
|
|
980
|
+
hostname: hostname()
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
const stored = await storeDeviceCredential(profile.profileName, {
|
|
984
|
+
id: response.device.id,
|
|
985
|
+
name: response.device.name,
|
|
986
|
+
token: response.token
|
|
987
|
+
});
|
|
988
|
+
if (!stored) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
|
|
989
|
+
process.stdout.write(`Paired ${response.device.name} (${response.device.id})
|
|
990
|
+
`);
|
|
991
|
+
}
|
|
992
|
+
async function showDeviceStatus(command) {
|
|
993
|
+
const profile = requireNamedProfile(command);
|
|
994
|
+
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
995
|
+
if (!credential) {
|
|
996
|
+
throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
997
|
+
}
|
|
998
|
+
const client = createClient({
|
|
999
|
+
apiUrl: profile.apiUrl,
|
|
1000
|
+
apiKey: profile.apiKey,
|
|
1001
|
+
tenantId: profile.tenantId
|
|
1002
|
+
});
|
|
1003
|
+
const devices = parseDeviceList(await client.get("/devices"));
|
|
1004
|
+
const pairedDevice = devices.find((device) => device.id === credential.id);
|
|
1005
|
+
formatOutput(
|
|
1006
|
+
pairedDevice ? [pairedDevice] : [{ id: credential.id, name: credential.name, status: "revoked" }],
|
|
1007
|
+
getFormat(command)
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
async function revokeDevice(command) {
|
|
1011
|
+
const profile = requireNamedProfile(command);
|
|
1012
|
+
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1013
|
+
if (!credential) {
|
|
1014
|
+
throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1015
|
+
}
|
|
1016
|
+
const client = createClient({
|
|
1017
|
+
apiUrl: profile.apiUrl,
|
|
1018
|
+
apiKey: profile.apiKey,
|
|
1019
|
+
tenantId: profile.tenantId
|
|
1020
|
+
});
|
|
1021
|
+
await client.delete(`/devices/${credential.id}`);
|
|
1022
|
+
await removeDeviceCredential(profile.profileName);
|
|
1023
|
+
}
|
|
1024
|
+
async function serveDevice(command) {
|
|
1025
|
+
const platform = requireSupportedPlatform(process.platform);
|
|
1026
|
+
const profile = requireNamedProfile(command);
|
|
1027
|
+
const credential = await getStoredDeviceCredential(profile.profileName);
|
|
1028
|
+
if (!credential) {
|
|
1029
|
+
throw new DeviceCommandError("No paired device; run standards device pair first");
|
|
1030
|
+
}
|
|
1031
|
+
const userClient = createClient({
|
|
1032
|
+
apiUrl: profile.apiUrl,
|
|
1033
|
+
apiKey: profile.apiKey,
|
|
1034
|
+
tenantId: profile.tenantId
|
|
1035
|
+
});
|
|
1036
|
+
const devices = parseDeviceList(await userClient.get("/devices"));
|
|
1037
|
+
const device = devices.find((candidate) => candidate.id === credential.id);
|
|
1038
|
+
if (!device) throw new DeviceCommandError("Device is unavailable");
|
|
1039
|
+
if (device.platform !== platform) {
|
|
1040
|
+
throw new DeviceCommandError("Device platform does not match paired device");
|
|
1041
|
+
}
|
|
1042
|
+
const controller = new AbortController();
|
|
1043
|
+
const stop = () => controller.abort();
|
|
1044
|
+
process.once("SIGINT", stop);
|
|
1045
|
+
process.once("SIGTERM", stop);
|
|
1046
|
+
process.stdout.write(`Connected as ${credential.name}
|
|
1047
|
+
`);
|
|
1048
|
+
process.stdout.write("Waiting for commands \u2014 press Ctrl+C to disconnect\n");
|
|
1049
|
+
try {
|
|
1050
|
+
await runDeviceWorker({
|
|
1051
|
+
client: new DeviceWorkerClient({ apiUrl: profile.apiUrl, token: credential.token }),
|
|
1052
|
+
signal: controller.signal,
|
|
1053
|
+
onEvent: (event) => {
|
|
1054
|
+
if (event.type === "running") {
|
|
1055
|
+
process.stdout.write(`Running ${event.commandId}: ${event.intent}
|
|
1056
|
+
`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
process.stdout.write(`Completed ${event.commandId} with exit code ${event.exitCode}
|
|
1060
|
+
`);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
} finally {
|
|
1064
|
+
process.removeListener("SIGINT", stop);
|
|
1065
|
+
process.removeListener("SIGTERM", stop);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function registerDeviceCommand(program) {
|
|
1069
|
+
const device = program.command("device").description("Pair and run this computer as a device");
|
|
1070
|
+
device.command("pair").description("Pair this computer with Standards").requiredOption("--name <name>", "device name").action(
|
|
1071
|
+
async (options, command) => pairDevice(options.name, command)
|
|
1072
|
+
);
|
|
1073
|
+
device.command("status").description("Show the paired device status").action(async (_options, command) => showDeviceStatus(command));
|
|
1074
|
+
device.command("revoke").description("Revoke this computer's device credential").action(async (_options, command) => revokeDevice(command));
|
|
1075
|
+
device.command("serve").description("Run the foreground device worker").action(async (_options, command) => serveDevice(command));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/commands/documents.ts
|
|
1079
|
+
import { formatByteSize } from "@stndrds/schema";
|
|
1080
|
+
function toFileRow(file) {
|
|
1081
|
+
return {
|
|
1082
|
+
id: file.id,
|
|
1083
|
+
position: file.position,
|
|
1084
|
+
type: file.mimeType,
|
|
1085
|
+
size: formatByteSize(file.size),
|
|
1086
|
+
ocrStatus: file.ocrStatus
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function registerDocumentsCommand(program) {
|
|
1090
|
+
const documents = program.command("documents").description("Manage documents");
|
|
1091
|
+
documents.command("list").description("List documents").option("--limit <n>", "max documents to return").option("--offset <n>", "number of documents to skip").action(async (opts, cmd) => {
|
|
1092
|
+
const client = getClientFromCommand(cmd);
|
|
1093
|
+
const params = {};
|
|
1094
|
+
if (opts.limit) params.limit = opts.limit;
|
|
1095
|
+
if (opts.offset) params.offset = opts.offset;
|
|
1096
|
+
const result = await client.get("/documents", params);
|
|
1097
|
+
formatOutput(result, getFormat(cmd));
|
|
1098
|
+
});
|
|
1099
|
+
documents.command("get").description("Get a document by ID").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
|
|
1100
|
+
const client = getClientFromCommand(cmd);
|
|
1101
|
+
const result = await client.get(`/documents/${id}`);
|
|
1102
|
+
formatOutput(result, getFormat(cmd));
|
|
1103
|
+
});
|
|
1104
|
+
documents.command("create").description("Create a new document").requiredOption("--title <title>", "document title").option("--values <json>", "document values as JSON string").action(async (opts, cmd) => {
|
|
1105
|
+
const client = getClientFromCommand(cmd);
|
|
1106
|
+
const body = { title: opts.title };
|
|
1107
|
+
if (opts.values) body.values = JSON.parse(opts.values);
|
|
1108
|
+
const result = await client.post("/documents", body);
|
|
1109
|
+
formatOutput(result, getFormat(cmd));
|
|
1110
|
+
});
|
|
1111
|
+
documents.command("update").description("Update a document").argument("<id>", "document ID").option("--title <title>", "new title").option("--values <json>", "new values as JSON string").action(async (id, opts, cmd) => {
|
|
1112
|
+
const client = getClientFromCommand(cmd);
|
|
1113
|
+
const body = {};
|
|
1114
|
+
if (opts.title) body.title = opts.title;
|
|
1115
|
+
if (opts.values) body.values = JSON.parse(opts.values);
|
|
1116
|
+
const result = await client.patch(`/documents/${id}`, body);
|
|
1117
|
+
formatOutput(result, getFormat(cmd));
|
|
1118
|
+
});
|
|
1119
|
+
documents.command("delete").description("Delete a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
|
|
1120
|
+
const client = getClientFromCommand(cmd);
|
|
1121
|
+
await client.delete(`/documents/${id}`);
|
|
1122
|
+
process.stdout.write(`Document ${id} deleted.
|
|
1123
|
+
`);
|
|
1124
|
+
});
|
|
1125
|
+
documents.command("preview").description("Get preview file for a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
|
|
1126
|
+
const client = getClientFromCommand(cmd);
|
|
1127
|
+
const result = await client.get(`/documents/${id}/preview`);
|
|
1128
|
+
formatOutput(result, getFormat(cmd));
|
|
1129
|
+
});
|
|
1130
|
+
documents.command("files").description("List files in a document's pack (position-ordered)").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
|
|
1131
|
+
const client = getClientFromCommand(cmd);
|
|
1132
|
+
const files = await client.get(`/documents/${id}/files`);
|
|
1133
|
+
const sorted = [...files].sort((a, b) => a.position - b.position);
|
|
1134
|
+
const format = getFormat(cmd);
|
|
1135
|
+
formatOutput(format === "table" ? sorted.map(toFileRow) : sorted, format);
|
|
1136
|
+
});
|
|
1137
|
+
documents.command("add-file").description("Append a file to a document's pack").argument("<id>", "document ID").requiredOption("--file-id <fileId>", "file ID").action(async (id, opts, cmd) => {
|
|
1138
|
+
const client = getClientFromCommand(cmd);
|
|
1139
|
+
const result = await client.post(`/documents/${id}/files`, { fileId: opts.fileId });
|
|
1140
|
+
formatOutput(result, getFormat(cmd));
|
|
1141
|
+
});
|
|
1142
|
+
documents.command("remove-file").description("Remove a file from a document's pack").argument("<id>", "document ID").argument("<fileId>", "file ID").action(async (id, fileId, _opts, cmd) => {
|
|
1143
|
+
const client = getClientFromCommand(cmd);
|
|
1144
|
+
await client.delete(`/documents/${id}/files/${fileId}`);
|
|
1145
|
+
process.stdout.write(`File ${fileId} removed.
|
|
1146
|
+
`);
|
|
1147
|
+
});
|
|
1148
|
+
documents.command("content").description("Print a document's aggregated OCR text (every pack file, position order)").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
|
|
1149
|
+
const client = getClientFromCommand(cmd);
|
|
1150
|
+
const text = await client.getText(`/documents/${id}/content`);
|
|
1151
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
1152
|
+
`);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/commands/embeddings.ts
|
|
1157
|
+
import chalk4 from "chalk";
|
|
1158
|
+
function registerEmbeddingsCommand(program) {
|
|
1159
|
+
const embeddings = program.command("embeddings").description("Maintain record embeddings");
|
|
1160
|
+
embeddings.command("drain").description("Embed every pending memory and skill record of the tenant").action(async (_opts, cmd) => {
|
|
1161
|
+
const tenant = requireTenant(
|
|
1162
|
+
cmd,
|
|
1163
|
+
'An embedding drain names its tenant explicitly. Pass "--tenant <id>".'
|
|
1164
|
+
);
|
|
1165
|
+
const client = getClientFromCommand(cmd);
|
|
1166
|
+
await client.post("/admin/embeddings/drain", { tenantId: tenant });
|
|
1167
|
+
process.stdout.write(`${chalk4.green("\u2713")} Embedding drain accepted for tenant ${tenant}.
|
|
1168
|
+
`);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/commands/folders.ts
|
|
1173
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1174
|
+
function registerFoldersCommand(program) {
|
|
1175
|
+
const folders = program.command("folders").description("Manage record drive folders");
|
|
1176
|
+
folders.command("reconcile-paths").description("Create a folder tree on a record drive from paths").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID owning the drive").option(
|
|
1177
|
+
"--path <path>",
|
|
1178
|
+
"folder path to create (repeatable)",
|
|
1179
|
+
(val, acc) => {
|
|
1180
|
+
acc.push(val);
|
|
1181
|
+
return acc;
|
|
1182
|
+
},
|
|
1183
|
+
[]
|
|
1184
|
+
).option("--paths-file <file>", "file containing one path per line").option("--mode <mode>", "reconcile mode: repair or dryRun", "repair").action(async (objectName, recordId, opts, cmd) => {
|
|
1185
|
+
const paths = [...opts.path];
|
|
1186
|
+
if (opts.pathsFile) {
|
|
1187
|
+
const content = await readFile2(opts.pathsFile, "utf-8");
|
|
1188
|
+
const filePaths = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1189
|
+
paths.push(...filePaths);
|
|
1190
|
+
}
|
|
1191
|
+
if (paths.length === 0) {
|
|
1192
|
+
throw new Error("No paths provided. Pass at least one --path or a non-empty --paths-file.");
|
|
1193
|
+
}
|
|
1194
|
+
const client = getClientFromCommand(cmd);
|
|
1195
|
+
const result = await client.post(`/folders/record-drive/${recordId}/reconcile-paths`, {
|
|
1196
|
+
objectName,
|
|
1197
|
+
paths,
|
|
1198
|
+
mode: opts.mode
|
|
1199
|
+
});
|
|
1200
|
+
formatOutput(result, getFormat(cmd));
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/commands/keys.ts
|
|
1205
|
+
import chalk5 from "chalk";
|
|
1206
|
+
function registerKeysCommand(program) {
|
|
1207
|
+
const keys = program.command("keys").description("Manage Standards API keys");
|
|
1208
|
+
keys.command("list").description("List API keys").action(async (_opts, cmd) => {
|
|
1209
|
+
const client = getClientFromCommand(cmd);
|
|
1210
|
+
const result = await client.get("/api-keys");
|
|
1211
|
+
formatOutput(result, getFormat(cmd));
|
|
1212
|
+
});
|
|
1213
|
+
keys.command("create").description("Create a new API key").requiredOption("--name <name>", "name for the API key").option("--expires <date>", "expiration date (ISO 8601)").option("--yes", "create without confirmation").action(async (opts, cmd) => {
|
|
1214
|
+
if (!opts.yes) {
|
|
1215
|
+
throw new Error('Creating API keys is explicit. Re-run with "--yes" to confirm.');
|
|
1216
|
+
}
|
|
1217
|
+
const client = getClientFromCommand(cmd);
|
|
1218
|
+
const body = {
|
|
1219
|
+
name: opts.name,
|
|
1220
|
+
permissions: []
|
|
1221
|
+
};
|
|
1222
|
+
if (opts.expires) body.expiresAt = opts.expires;
|
|
1223
|
+
const result = await client.post("/api-keys", body);
|
|
1224
|
+
formatOutput(result, getFormat(cmd));
|
|
1225
|
+
});
|
|
1226
|
+
keys.command("revoke").description("Revoke an API key").argument("<id>", "API key ID").option("--yes", "revoke without confirmation").action(async (id, opts, cmd) => {
|
|
1227
|
+
if (!opts.yes) {
|
|
1228
|
+
throw new Error('Revoking API keys is explicit. Re-run with "--yes" to confirm.');
|
|
1229
|
+
}
|
|
1230
|
+
const client = getClientFromCommand(cmd);
|
|
1231
|
+
await client.delete(`/api-keys/${id}`);
|
|
1232
|
+
process.stdout.write(`${chalk5.green("\u2713")} API key ${id} revoked.
|
|
1233
|
+
`);
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/commands/mcp.ts
|
|
1238
|
+
import { RESOURCE_VISIBILITIES, ValidationError as ValidationError3 } from "@stndrds/schema";
|
|
1239
|
+
import chalk6 from "chalk";
|
|
1240
|
+
var AUTH_TYPES = ["none", "header"];
|
|
1241
|
+
function buildAuth(opts) {
|
|
1242
|
+
const type = opts.authType ?? "none";
|
|
1243
|
+
if (!AUTH_TYPES.includes(type)) {
|
|
1244
|
+
throw new ValidationError3(`--auth-type must be one of: ${AUTH_TYPES.join(", ")}`, []);
|
|
1245
|
+
}
|
|
1246
|
+
if (type === "none") {
|
|
1247
|
+
return { type: "none" };
|
|
1248
|
+
}
|
|
1249
|
+
if (!(opts.headerName && opts.secret)) {
|
|
1250
|
+
throw new ValidationError3('--auth-type "header" requires both --header-name and --secret.', []);
|
|
1251
|
+
}
|
|
1252
|
+
return { type: "header", headerName: opts.headerName, secret: opts.secret };
|
|
1253
|
+
}
|
|
1254
|
+
function resolveVisibility(visibility) {
|
|
1255
|
+
if (visibility === void 0) {
|
|
1256
|
+
return "workspace";
|
|
1257
|
+
}
|
|
1258
|
+
if (!RESOURCE_VISIBILITIES.includes(visibility)) {
|
|
1259
|
+
throw new ValidationError3(
|
|
1260
|
+
`--visibility must be one of: ${RESOURCE_VISIBILITIES.join(", ")}`,
|
|
1261
|
+
[]
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
return visibility;
|
|
1265
|
+
}
|
|
1266
|
+
function registerMcpCommand(program) {
|
|
1267
|
+
const mcp = program.command("mcp").description("Manage MCP servers mounted into this workspace's agents");
|
|
1268
|
+
mcp.command("list").description("List connected MCP servers").action(async (_opts, cmd) => {
|
|
1269
|
+
const client = getClientFromCommand(cmd);
|
|
1270
|
+
const result = await client.get("/mcp-servers");
|
|
1271
|
+
formatOutput(result, getFormat(cmd));
|
|
1272
|
+
});
|
|
1273
|
+
mcp.command("catalog").description("List the vendored MCP catalog entries available to connect").action(async (_opts, cmd) => {
|
|
1274
|
+
const client = getClientFromCommand(cmd);
|
|
1275
|
+
const result = await client.get("/mcp-servers/catalog");
|
|
1276
|
+
formatOutput(result, getFormat(cmd));
|
|
1277
|
+
});
|
|
1278
|
+
mcp.command("connect").description("Connect a remote MCP server and mount its tools").requiredOption("--slug <slug>", "unique slug for the server").requiredOption("--name <name>", "display name").requiredOption("--url <url>", "remote MCP endpoint URL").option("--visibility <visibility>", "workspace or private", "workspace").option("--catalog-entry <id>", "catalog entry ID this server instantiates").option("--auth-type <type>", "none or header", "none").option("--header-name <name>", 'header name when --auth-type is "header"').option("--secret <secret>", 'header value when --auth-type is "header"').action(async (opts, cmd) => {
|
|
1279
|
+
const auth = buildAuth(opts);
|
|
1280
|
+
const visibility = resolveVisibility(opts.visibility);
|
|
1281
|
+
const client = getClientFromCommand(cmd);
|
|
1282
|
+
const result = await client.post("/mcp-servers", {
|
|
1283
|
+
slug: opts.slug,
|
|
1284
|
+
name: opts.name,
|
|
1285
|
+
url: opts.url,
|
|
1286
|
+
visibility,
|
|
1287
|
+
catalogEntryId: opts.catalogEntry ?? null,
|
|
1288
|
+
auth
|
|
1289
|
+
});
|
|
1290
|
+
formatOutput(result, getFormat(cmd));
|
|
1291
|
+
});
|
|
1292
|
+
mcp.command("refresh").description("Re-discover a server's tools and refresh its status").argument("<id>", "MCP server ID").action(async (id, _opts, cmd) => {
|
|
1293
|
+
const client = getClientFromCommand(cmd);
|
|
1294
|
+
const result = await client.post(`/mcp-servers/${id}/refresh`);
|
|
1295
|
+
formatOutput(result, getFormat(cmd));
|
|
1296
|
+
});
|
|
1297
|
+
mcp.command("trust").description("Mark a server trusted so its tools run without per-call approval").argument("<id>", "MCP server ID").option("--revoke", "revoke trust instead of granting it").action(async (id, opts, cmd) => {
|
|
1298
|
+
const trusted = !opts.revoke;
|
|
1299
|
+
const client = getClientFromCommand(cmd);
|
|
1300
|
+
await client.post(`/mcp-servers/${id}/trust`, { trusted });
|
|
1301
|
+
process.stdout.write(
|
|
1302
|
+
`${chalk6.green("\u2713")} MCP server ${id} ${trusted ? "trusted" : "untrusted"}.
|
|
1303
|
+
`
|
|
1304
|
+
);
|
|
1305
|
+
});
|
|
1306
|
+
mcp.command("disconnect").description("Disconnect (delete) an MCP server").argument("<id>", "MCP server ID").option("--yes", "disconnect without confirmation").action(async (id, opts, cmd) => {
|
|
1307
|
+
if (!opts.yes) {
|
|
1308
|
+
throw new ValidationError3(
|
|
1309
|
+
'Disconnecting an MCP server is explicit. Re-run with "--yes" to confirm.',
|
|
1310
|
+
[]
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
const client = getClientFromCommand(cmd);
|
|
1314
|
+
await client.delete(`/mcp-servers/${id}`);
|
|
1315
|
+
process.stdout.write(`${chalk6.green("\u2713")} MCP server ${id} disconnected.
|
|
1316
|
+
`);
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/commands/meetings.ts
|
|
1321
|
+
import { createHmac } from "crypto";
|
|
1322
|
+
import {
|
|
1323
|
+
FAKE_MEETING_BOT_SIGNATURE_HEADER,
|
|
1324
|
+
RECORDING_OVERRIDES,
|
|
1325
|
+
ValidationError as ValidationError4
|
|
1326
|
+
} from "@stndrds/schema";
|
|
1327
|
+
var ORDERS = ["asc", "desc"];
|
|
1328
|
+
function parseOrder(order) {
|
|
1329
|
+
if (ORDERS.includes(order)) return order;
|
|
1330
|
+
throw new ValidationError4(`--order must be one of: ${ORDERS.join(", ")}`, []);
|
|
1331
|
+
}
|
|
1332
|
+
function parseCount(flag, value) {
|
|
1333
|
+
const parsed = Number(value);
|
|
1334
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
1335
|
+
throw new ValidationError4(`${flag} must be a non-negative integer`, []);
|
|
1336
|
+
}
|
|
1337
|
+
return parsed;
|
|
1338
|
+
}
|
|
1339
|
+
function buildQuery(opts) {
|
|
1340
|
+
const query = {};
|
|
1341
|
+
if (opts.search !== void 0) query.search = opts.search;
|
|
1342
|
+
if (opts.since !== void 0) query.since = opts.since;
|
|
1343
|
+
if (opts.before !== void 0) query.before = opts.before;
|
|
1344
|
+
if (opts.addresses !== void 0) query.addresses = parseCsvList(opts.addresses);
|
|
1345
|
+
if (opts.record !== void 0) query.recordId = opts.record;
|
|
1346
|
+
if (opts.order !== void 0) query.order = parseOrder(opts.order);
|
|
1347
|
+
if (opts.limit !== void 0) query.limit = parseCount("--limit", opts.limit);
|
|
1348
|
+
if (opts.offset !== void 0) query.offset = parseCount("--offset", opts.offset);
|
|
1349
|
+
return query;
|
|
1350
|
+
}
|
|
1351
|
+
function parseOverride(opts) {
|
|
1352
|
+
const chosen = RECORDING_OVERRIDES.filter(
|
|
1353
|
+
(override) => opts[override]
|
|
1354
|
+
);
|
|
1355
|
+
if (opts.auto) chosen.push(null);
|
|
1356
|
+
if (chosen.length !== 1) {
|
|
1357
|
+
throw new ValidationError4("record needs exactly one of --force, --skip or --auto", []);
|
|
1358
|
+
}
|
|
1359
|
+
return chosen[0] ?? null;
|
|
1360
|
+
}
|
|
1361
|
+
var SIMULATED_LINE_MS = 4e3;
|
|
1362
|
+
var DEFAULT_SIMULATED_LINES = 12;
|
|
1363
|
+
var SIMULATED_SENTENCES = [
|
|
1364
|
+
"Bonjour \xE0 tous, merci d'\xEAtre l\xE0, on commence par le point d'avancement.",
|
|
1365
|
+
"De mon c\xF4t\xE9 la maquette est valid\xE9e, il reste les retours du client.",
|
|
1366
|
+
"On garde le planning initial, la livraison est pr\xE9vue pour vendredi.",
|
|
1367
|
+
"Est-ce que quelqu'un a des questions sur le budget de la phase deux ?",
|
|
1368
|
+
"Je propose qu'on fasse un point rapide jeudi pour v\xE9rifier les tests.",
|
|
1369
|
+
"Tr\xE8s bien, je vous envoie le compte rendu dans la journ\xE9e, bonne journ\xE9e."
|
|
1370
|
+
];
|
|
1371
|
+
function simulatedSpeakers(meeting) {
|
|
1372
|
+
const speakers = meeting.participants.map(
|
|
1373
|
+
(participant) => participant.name?.trim() || participant.address.split("@")[0] || participant.address
|
|
1374
|
+
);
|
|
1375
|
+
return speakers.length > 0 ? speakers : ["Speaker"];
|
|
1376
|
+
}
|
|
1377
|
+
function buildSimulatedSegments(speakers, lines) {
|
|
1378
|
+
return Array.from({ length: lines }, (_, index) => ({
|
|
1379
|
+
speaker: speakers[index % speakers.length] ?? null,
|
|
1380
|
+
startsAtMs: index * SIMULATED_LINE_MS,
|
|
1381
|
+
endsAtMs: (index + 1) * SIMULATED_LINE_MS,
|
|
1382
|
+
text: SIMULATED_SENTENCES[index % SIMULATED_SENTENCES.length] ?? ""
|
|
1383
|
+
}));
|
|
1384
|
+
}
|
|
1385
|
+
function signFakeWebhook(secret, body) {
|
|
1386
|
+
return createHmac("sha256", secret).update(body).digest("hex");
|
|
1387
|
+
}
|
|
1388
|
+
function withQueryOptions(command) {
|
|
1389
|
+
return command.option("--search <text>", "match title, description and location").option("--since <iso>", "inclusive ISO lower bound on the start").option("--before <iso>", "exclusive ISO upper bound on the start").option("--addresses <emails>", "comma-separated participant addresses to restrict to").option("--record <id>", "only meetings linked to this record").option("--order <order>", "asc for what is coming next, desc to read history").option("--limit <n>", "max meetings to return").option("--offset <n>", "number of meetings to skip");
|
|
1390
|
+
}
|
|
1391
|
+
function registerMeetingsCommand(program) {
|
|
1392
|
+
const meetings = program.command("meetings").description("Read calendar meetings and the records each one concerns");
|
|
1393
|
+
withQueryOptions(
|
|
1394
|
+
meetings.command("list").description("List meetings, newest window first by default")
|
|
1395
|
+
).action(async (opts, cmd) => {
|
|
1396
|
+
const client = getClientFromCommand(cmd);
|
|
1397
|
+
const result = await client.post("/meetings/search", buildQuery(opts));
|
|
1398
|
+
formatOutput(result, getFormat(cmd));
|
|
1399
|
+
});
|
|
1400
|
+
meetings.command("get").description("Get one meeting, with its participants and linked records").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
|
|
1401
|
+
const client = getClientFromCommand(cmd);
|
|
1402
|
+
const result = await client.get(`/meetings/${id}`);
|
|
1403
|
+
formatOutput(result, getFormat(cmd));
|
|
1404
|
+
});
|
|
1405
|
+
withQueryOptions(
|
|
1406
|
+
meetings.command("for-record").description("List the meetings a record's email addresses appear in").argument("<recordId>", "record ID").option(
|
|
1407
|
+
"--email-attributes <ids>",
|
|
1408
|
+
"comma-separated email attribute IDs to match participants on"
|
|
1409
|
+
)
|
|
1410
|
+
).action(
|
|
1411
|
+
async (recordId, opts, cmd) => {
|
|
1412
|
+
const client = getClientFromCommand(cmd);
|
|
1413
|
+
const result = await client.post(`/meetings/records/${recordId}`, {
|
|
1414
|
+
// The API defaults an absent list to none, which matches nothing —
|
|
1415
|
+
// send the key only when the caller named the attributes.
|
|
1416
|
+
...opts.emailAttributes !== void 0 && {
|
|
1417
|
+
emailAttributeIds: parseCsvList(opts.emailAttributes)
|
|
1418
|
+
},
|
|
1419
|
+
query: buildQuery(opts)
|
|
1420
|
+
});
|
|
1421
|
+
formatOutput(result, getFormat(cmd));
|
|
1422
|
+
}
|
|
1423
|
+
);
|
|
1424
|
+
meetings.command("transcript").description("Read a meeting's completed transcript").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
|
|
1425
|
+
const client = getClientFromCommand(cmd);
|
|
1426
|
+
const result = await client.get(`/meetings/${id}/transcript`);
|
|
1427
|
+
formatOutput(result, getFormat(cmd));
|
|
1428
|
+
});
|
|
1429
|
+
meetings.command("record").description("Force, skip, or hand back to the workspace policy the recording of a meeting").argument("<id>", "meeting ID").option("--force", "record this meeting whatever the workspace policy says").option("--skip", "never record this meeting").option("--auto", "clear the override and follow the workspace policy").action(async (id, opts, cmd) => {
|
|
1430
|
+
const override = parseOverride(opts);
|
|
1431
|
+
const client = getClientFromCommand(cmd);
|
|
1432
|
+
const result = await client.put(`/meetings/${id}/recording`, { override });
|
|
1433
|
+
formatOutput(result, getFormat(cmd));
|
|
1434
|
+
});
|
|
1435
|
+
meetings.command("simulate-transcript").description("Complete a fake-provider bot by posting a generated transcript to its webhook").argument("<id>", "meeting ID").option(
|
|
1436
|
+
"--lines <n>",
|
|
1437
|
+
"number of transcript lines to generate",
|
|
1438
|
+
String(DEFAULT_SIMULATED_LINES)
|
|
1439
|
+
).action(async (id, opts, cmd) => {
|
|
1440
|
+
const secret = process.env.MEETING_BOT_FAKE_SECRET;
|
|
1441
|
+
if (!secret) {
|
|
1442
|
+
throw new ValidationError4("MEETING_BOT_FAKE_SECRET is not set in this shell", []);
|
|
1443
|
+
}
|
|
1444
|
+
const lines = parseCount("--lines", opts.lines);
|
|
1445
|
+
if (lines < 1) throw new ValidationError4("--lines must be at least 1", []);
|
|
1446
|
+
const client = getClientFromCommand(cmd);
|
|
1447
|
+
const meeting = await client.get(`/meetings/${id}`);
|
|
1448
|
+
const { status, botId } = meeting.recording;
|
|
1449
|
+
if (status !== "scheduled" && status !== "recording" || botId === null) {
|
|
1450
|
+
throw new ValidationError4(`Meeting ${id} has no bot to complete (status: ${status})`, []);
|
|
1451
|
+
}
|
|
1452
|
+
const segments = buildSimulatedSegments(simulatedSpeakers(meeting), lines);
|
|
1453
|
+
const startedAt = meeting.startsAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1454
|
+
const endedAt = new Date(
|
|
1455
|
+
new Date(startedAt).getTime() + lines * SIMULATED_LINE_MS
|
|
1456
|
+
).toISOString();
|
|
1457
|
+
const body = JSON.stringify({
|
|
1458
|
+
botId,
|
|
1459
|
+
transcript: {
|
|
1460
|
+
fullText: segments.map((segment) => segment.text).join("\n"),
|
|
1461
|
+
segments,
|
|
1462
|
+
language: "fr",
|
|
1463
|
+
startedAt,
|
|
1464
|
+
endedAt
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
const response = await fetch(
|
|
1468
|
+
`${getGlobalOptions(cmd).apiUrl}/meetings/transcripts/webhook/fake`,
|
|
1469
|
+
{
|
|
1470
|
+
method: "POST",
|
|
1471
|
+
headers: {
|
|
1472
|
+
"content-type": "application/json",
|
|
1473
|
+
[FAKE_MEETING_BOT_SIGNATURE_HEADER]: signFakeWebhook(secret, body)
|
|
1474
|
+
},
|
|
1475
|
+
body
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
if (!response.ok) {
|
|
1479
|
+
throw new ValidationError4(`webhook answered ${response.status}`, []);
|
|
1480
|
+
}
|
|
1481
|
+
formatOutput({ botId, segments: segments.length }, getFormat(cmd));
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/commands/pull.ts
|
|
1486
|
+
import chalk7 from "chalk";
|
|
1487
|
+
|
|
1488
|
+
// src/drift/reconciliation-prompt.ts
|
|
1489
|
+
var PREAMBLE = [
|
|
1490
|
+
"# Standards schema reconciliation",
|
|
1491
|
+
"",
|
|
1492
|
+
"The deployed runtime has drifted from code. Walk each item **with the developer**,",
|
|
1493
|
+
"then edit the fluent builders. The pre-deploy gate (STANDARDS_SYNC_CHECK=1) applies",
|
|
1494
|
+
"your choices at deploy. Loop until the gate reports clean. Never make a destructive",
|
|
1495
|
+
"choice without the developer's explicit confirmation.",
|
|
1496
|
+
""
|
|
1497
|
+
].join("\n");
|
|
1498
|
+
function isViewDiverged(view) {
|
|
1499
|
+
return view.origin !== "code" && view.baselineHash !== null && view.dbHash !== view.baselineHash;
|
|
1500
|
+
}
|
|
1501
|
+
function hasReconcilableDrift(state) {
|
|
1502
|
+
return state.drift.customObjects.length > 0 || state.drift.systemObjectDrift.length > 0 || state.views.some(isViewDiverged) || state.views.some((v) => v.origin === "code" && v.workspaceOverlay !== null) || state.views.some((v) => v.origin === "runtime");
|
|
1503
|
+
}
|
|
1504
|
+
function renderAttribute(objectName, sealed, attr) {
|
|
1505
|
+
const action = attr.tolerated ? "Already tolerated \u2014 declare it in the builder to make it a system attribute, or leave tolerated." : `Choose: **declare** \`${attr.name}\` on the \`${objectName}\` builder (\u2192 becomes system on next sync), OR **tolerate** it \u2014 add \`.tolerate(["${attr.name}"])\`${sealed ? " (required: the object is sealed)" : ""}.`;
|
|
1506
|
+
return `- \`${objectName}.${attr.name}\` (${attr.type})
|
|
1507
|
+
${action}`;
|
|
1508
|
+
}
|
|
1509
|
+
function renderView(view) {
|
|
1510
|
+
const d = view.delta;
|
|
1511
|
+
const lines = [`### view \`${view.object}:${view.name}:${view.type}\``];
|
|
1512
|
+
if (d) {
|
|
1513
|
+
if (d.tabsAdded.length) lines.push(`- tabs added at runtime: ${d.tabsAdded.join(", ")}`);
|
|
1514
|
+
if (d.tabsRemoved.length) lines.push(`- tabs removed at runtime: ${d.tabsRemoved.join(", ")}`);
|
|
1515
|
+
for (const t of d.tabsChanged)
|
|
1516
|
+
lines.push(`- tab "${t.name}" changed: ${t.changedKeys.join(", ")}`);
|
|
1517
|
+
if (d.topLevelChanged.length) lines.push(`- changed: ${d.topLevelChanged.join(", ")}`);
|
|
1518
|
+
}
|
|
1519
|
+
lines.push(
|
|
1520
|
+
"",
|
|
1521
|
+
"Runtime config to fold into the builder call (or record an explicit resolution to let code win):",
|
|
1522
|
+
"```json",
|
|
1523
|
+
JSON.stringify(view.config, null, 2),
|
|
1524
|
+
"```"
|
|
1525
|
+
);
|
|
1526
|
+
return lines.join("\n");
|
|
1527
|
+
}
|
|
1528
|
+
function renderWorkspaceOverlay(view) {
|
|
1529
|
+
const lines = [
|
|
1530
|
+
`### view \`${view.object}:${view.name}:${view.type}\``,
|
|
1531
|
+
"",
|
|
1532
|
+
"Workspace overlay to fold into the builder call (or leave as a runtime-only workspace customization):",
|
|
1533
|
+
"```json",
|
|
1534
|
+
JSON.stringify(view.workspaceOverlay, null, 2),
|
|
1535
|
+
"```"
|
|
1536
|
+
];
|
|
1537
|
+
return lines.join("\n");
|
|
1538
|
+
}
|
|
1539
|
+
function renderAdoptionCandidate(view) {
|
|
1540
|
+
const lines = [
|
|
1541
|
+
`### view \`${view.object}:${view.name}:${view.type}\``,
|
|
1542
|
+
"",
|
|
1543
|
+
"Runtime-only view to adopt into a builder call (or leave as runtime-only):",
|
|
1544
|
+
"```json",
|
|
1545
|
+
JSON.stringify(view.config, null, 2),
|
|
1546
|
+
"```"
|
|
1547
|
+
];
|
|
1548
|
+
return lines.join("\n");
|
|
1549
|
+
}
|
|
1550
|
+
function renderReconciliationPrompt(state) {
|
|
1551
|
+
if (!hasReconcilableDrift(state))
|
|
1552
|
+
return `${PREAMBLE}
|
|
1553
|
+
No drift to reconcile \u2014 the runtime matches code.
|
|
1554
|
+
`;
|
|
1555
|
+
const parts = [PREAMBLE];
|
|
1556
|
+
for (const obj of state.drift.customObjects) {
|
|
1557
|
+
parts.push(
|
|
1558
|
+
`## Custom object \`${obj.name}\` (runtime-only)
|
|
1559
|
+
Add an \`object("${obj.name}")\` builder, then its attributes:`
|
|
1560
|
+
);
|
|
1561
|
+
for (const a of obj.attributes) parts.push(renderAttribute(obj.name, false, a));
|
|
1562
|
+
}
|
|
1563
|
+
for (const entry of state.drift.systemObjectDrift) {
|
|
1564
|
+
parts.push(`## \`${entry.objectName}\`${entry.sealed ? " (sealed)" : ""} \u2014 custom attributes`);
|
|
1565
|
+
for (const a of entry.customAttributes)
|
|
1566
|
+
parts.push(renderAttribute(entry.objectName, entry.sealed, a));
|
|
1567
|
+
}
|
|
1568
|
+
const overlaidViews = state.views.filter(
|
|
1569
|
+
(v) => v.origin === "code" && v.workspaceOverlay !== null
|
|
1570
|
+
);
|
|
1571
|
+
if (overlaidViews.length) {
|
|
1572
|
+
parts.push("## Workspace customizations to fold into code");
|
|
1573
|
+
for (const v of overlaidViews) parts.push(renderWorkspaceOverlay(v));
|
|
1574
|
+
}
|
|
1575
|
+
const runtimeViews = state.views.filter((v) => v.origin === "runtime");
|
|
1576
|
+
if (runtimeViews.length) {
|
|
1577
|
+
parts.push("## Runtime views to adopt into code");
|
|
1578
|
+
for (const v of runtimeViews) parts.push(renderAdoptionCandidate(v));
|
|
1579
|
+
}
|
|
1580
|
+
const divergedViews = state.views.filter(isViewDiverged);
|
|
1581
|
+
if (divergedViews.length) {
|
|
1582
|
+
parts.push("## Drifted views");
|
|
1583
|
+
for (const v of divergedViews) parts.push(renderView(v));
|
|
1584
|
+
}
|
|
1585
|
+
return `${parts.join("\n\n")}
|
|
1586
|
+
`;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/commands/pull.ts
|
|
1590
|
+
async function runPullCommand(client) {
|
|
1591
|
+
let state;
|
|
1592
|
+
try {
|
|
1593
|
+
state = await client.get("/schema/state");
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
return { exitCode: 2, errorMessage: messageOf(error) };
|
|
1596
|
+
}
|
|
1597
|
+
return { exitCode: 0, prompt: renderReconciliationPrompt(state) };
|
|
1598
|
+
}
|
|
1599
|
+
function registerPullCommand(program) {
|
|
1600
|
+
program.command("pull").description("Emit a self-contained reconciliation prompt for the current runtime drift").option("--json", "Emit the raw /schema/state JSON instead of the prompt").action(async (opts, cmd) => {
|
|
1601
|
+
let client;
|
|
1602
|
+
try {
|
|
1603
|
+
client = getClientFromCommand(cmd);
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
console.error(chalk7.red(`\u2717 ${messageOf(error)}`));
|
|
1606
|
+
process.exit(2);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (opts.json) {
|
|
1610
|
+
try {
|
|
1611
|
+
const state = await client.get("/schema/state");
|
|
1612
|
+
console.info(JSON.stringify(state, null, 2));
|
|
1613
|
+
process.exit(0);
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
console.error(chalk7.red(`\u2717 ${messageOf(error)}`));
|
|
1616
|
+
process.exit(2);
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const result = await runPullCommand(client);
|
|
1621
|
+
if (result.exitCode === 2) {
|
|
1622
|
+
console.error(chalk7.red(`\u2717 ${result.errorMessage}`));
|
|
1623
|
+
process.exit(2);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
console.info(result.prompt);
|
|
1627
|
+
process.exit(0);
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/commands/quotas.ts
|
|
1632
|
+
function registerQuotasCommand(program) {
|
|
1633
|
+
const quotas = program.command("quotas").description("Inspect the tenant's remaining capacity");
|
|
1634
|
+
quotas.command("list").description("List one state per quota key (limit is null when nothing is enforced)").action(async (_opts, cmd) => {
|
|
1635
|
+
const client = getClientFromCommand(cmd);
|
|
1636
|
+
const result = await client.get("/quotas");
|
|
1637
|
+
formatOutput(result, getFormat(cmd));
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/commands/records.ts
|
|
1642
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1643
|
+
import { basename } from "path";
|
|
1644
|
+
function parseSortFlag(sort) {
|
|
1645
|
+
const [attribute, direction = "asc"] = sort.split(":");
|
|
1646
|
+
return [{ attribute, direction }];
|
|
1647
|
+
}
|
|
1648
|
+
var COUNT_MODES = ["none", "estimated", "exact"];
|
|
1649
|
+
function parseCountFlag(count) {
|
|
1650
|
+
if (COUNT_MODES.includes(count)) return count;
|
|
1651
|
+
throw new Error(`Invalid --count value "${count}". Expected one of: ${COUNT_MODES.join(", ")}.`);
|
|
1652
|
+
}
|
|
1653
|
+
function buildQueryBody(opts) {
|
|
1654
|
+
const body = {};
|
|
1655
|
+
if (opts.limit) body.limit = Number(opts.limit);
|
|
1656
|
+
if (opts.offset) body.offset = Number(opts.offset);
|
|
1657
|
+
if (opts.sort) body.sorts = parseSortFlag(opts.sort);
|
|
1658
|
+
if (opts.filter) body.filters = JSON.parse(opts.filter);
|
|
1659
|
+
if (opts.fields !== void 0) body.fields = parseCsvList(opts.fields);
|
|
1660
|
+
if (opts.count) body.countMode = parseCountFlag(opts.count);
|
|
1661
|
+
return body;
|
|
1662
|
+
}
|
|
1663
|
+
var COUNT_OPTION_DESCRIPTION = "total count mode (none, estimated, exact); omit for no total in page.total";
|
|
1664
|
+
function registerRecordsCommand(program) {
|
|
1665
|
+
const records = program.command("records").description("Manage records (CRUD + search)");
|
|
1666
|
+
records.command("list").description("List records for an object").argument("<object>", "object name (e.g. contacts)").option("--limit <n>", "max records to return").option("--offset <n>", "number of records to skip").option("--sort <sort>", 'sort rule as "attribute:direction"').option("--filter <json>", "filter state as JSON string").option(
|
|
1667
|
+
"--fields <names>",
|
|
1668
|
+
"comma-separated reference attributes to hydrate (empty for none, omit for all)"
|
|
1669
|
+
).option("--count <mode>", COUNT_OPTION_DESCRIPTION).action(async (objectName, opts, cmd) => {
|
|
1670
|
+
const client = getClientFromCommand(cmd);
|
|
1671
|
+
const body = buildQueryBody(opts);
|
|
1672
|
+
const result = await client.post(`/records/${objectName}/list`, body);
|
|
1673
|
+
formatOutput(result, getFormat(cmd));
|
|
1674
|
+
});
|
|
1675
|
+
records.command("get").description("Get a record by ID").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
|
|
1676
|
+
const client = getClientFromCommand(cmd);
|
|
1677
|
+
const result = await client.get(`/records/${objectName}/${id}`);
|
|
1678
|
+
formatOutput(result, getFormat(cmd));
|
|
1679
|
+
});
|
|
1680
|
+
records.command("create").description("Create a new record").argument("<object>", "object name").option("--data <json>", "record data as JSON string").action(async (objectName, opts, cmd) => {
|
|
1681
|
+
const data = opts.data ? JSON.parse(opts.data) : {};
|
|
1682
|
+
const client = getClientFromCommand(cmd);
|
|
1683
|
+
const result = await client.post(`/records/${objectName}`, { data });
|
|
1684
|
+
formatOutput(result, getFormat(cmd));
|
|
1685
|
+
});
|
|
1686
|
+
records.command("update").description("Update a record").argument("<object>", "object name").argument("<id>", "record ID").option("--data <json>", "fields to update as JSON string").action(async (objectName, id, opts, cmd) => {
|
|
1687
|
+
const data = opts.data ? JSON.parse(opts.data) : {};
|
|
1688
|
+
const client = getClientFromCommand(cmd);
|
|
1689
|
+
const result = await client.put(`/records/${objectName}/${id}`, data);
|
|
1690
|
+
formatOutput(result, getFormat(cmd));
|
|
1691
|
+
});
|
|
1692
|
+
records.command("delete").description("Delete a record").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
|
|
1693
|
+
const client = getClientFromCommand(cmd);
|
|
1694
|
+
await client.delete(`/records/${objectName}/${id}`);
|
|
1695
|
+
process.stdout.write(`Record ${id} deleted.
|
|
1696
|
+
`);
|
|
1697
|
+
});
|
|
1698
|
+
records.command("search").description("Full-text search records").argument("<object>", "object name").requiredOption("--query <q>", "search query string").option("--limit <n>", "max records to return").option("--offset <n>", "number of records to skip").option("--sort <sort>", 'sort rule as "attribute:direction"').option("--filter <json>", "filter state as JSON string").option(
|
|
1699
|
+
"--fields <names>",
|
|
1700
|
+
"comma-separated reference attributes to hydrate (empty for none, omit for all)"
|
|
1701
|
+
).option("--count <mode>", COUNT_OPTION_DESCRIPTION).action(async (objectName, opts, cmd) => {
|
|
1702
|
+
const client = getClientFromCommand(cmd);
|
|
1703
|
+
const body = { q: opts.query, ...buildQueryBody(opts) };
|
|
1704
|
+
const result = await client.post(`/records/${objectName}/search`, body);
|
|
1705
|
+
formatOutput(result, getFormat(cmd));
|
|
1706
|
+
});
|
|
1707
|
+
records.command("attach-document").description("Upload and attach a local file to a record").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID").requiredOption("--file <path>", "local file path to upload").option("--attribute <attr>", "attribute name to attach the document to").option("--parent-id <folderId>", "parent folder ID").option("--title <title>", "document title (defaults to filename)").action(async (objectName, recordId, opts, cmd) => {
|
|
1708
|
+
const fileContent = await readFile3(opts.file);
|
|
1709
|
+
const fileName = basename(opts.file);
|
|
1710
|
+
const title = opts.title ?? fileName;
|
|
1711
|
+
const form = new FormData();
|
|
1712
|
+
form.append("files", new Blob([fileContent]), fileName);
|
|
1713
|
+
form.append("title", title);
|
|
1714
|
+
if (opts.attribute) form.append("attributeName", opts.attribute);
|
|
1715
|
+
if (opts.parentId) form.append("parentId", opts.parentId);
|
|
1716
|
+
const client = getClientFromCommand(cmd);
|
|
1717
|
+
const result = await client.postMultipart(
|
|
1718
|
+
`/records/${objectName}/${recordId}/documents/attach`,
|
|
1719
|
+
form
|
|
1720
|
+
);
|
|
1721
|
+
formatOutput(result, getFormat(cmd));
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/commands/root.ts
|
|
1726
|
+
import { stdin as input, stdout as output } from "process";
|
|
1727
|
+
import { createInterface } from "readline/promises";
|
|
1728
|
+
import chalk8 from "chalk";
|
|
1729
|
+
async function promptForMissingValue(label) {
|
|
1730
|
+
const rl = createInterface({ input, output });
|
|
1731
|
+
try {
|
|
1732
|
+
const value = await rl.question(`${label}: `);
|
|
1733
|
+
return value.trim();
|
|
1734
|
+
} finally {
|
|
1735
|
+
rl.close();
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function registerRootCommands(program) {
|
|
1739
|
+
program.command("login").description("Connect the CLI to a Standards instance").option("--url <url>", "Standards API URL", getDefaultApiUrl()).option("--key <key>", "API key to store for this instance").option("--name <name>", "local instance name", "local").action(async (opts) => {
|
|
1740
|
+
const apiKey = opts.key ?? await promptForMissingValue("API key");
|
|
1741
|
+
if (!apiKey) throw new Error("API key is required");
|
|
1742
|
+
const client = createClient({ apiUrl: opts.url, apiKey });
|
|
1743
|
+
await client.get("/api-keys");
|
|
1744
|
+
await upsertProfile({ name: opts.name, apiUrl: opts.url, apiKey });
|
|
1745
|
+
process.stdout.write(
|
|
1746
|
+
`${chalk8.green("\u2713")} Standards instance "${opts.name}" saved and selected.
|
|
1747
|
+
`
|
|
1748
|
+
);
|
|
1749
|
+
process.stdout.write(` API URL: ${opts.url}
|
|
1750
|
+
`);
|
|
1751
|
+
process.stdout.write(` API Key: ${apiKey.slice(0, 8)}...
|
|
1752
|
+
`);
|
|
1753
|
+
process.stdout.write(` Use it with: standards --instance ${opts.name} <command>
|
|
1754
|
+
`);
|
|
460
1755
|
});
|
|
461
1756
|
program.command("use").description("Select the active Standards instance").argument("<name>", "instance name").action(async (name) => {
|
|
462
1757
|
await setCurrentProfile(name);
|
|
463
|
-
process.stdout.write(`${
|
|
1758
|
+
process.stdout.write(`${chalk8.green("\u2713")} Standards instance "${name}" selected.
|
|
464
1759
|
`);
|
|
465
1760
|
});
|
|
466
1761
|
program.command("instances").description("List configured Standards instances").action(async (_opts, cmd) => {
|
|
@@ -479,7 +1774,7 @@ function registerRootCommands(program) {
|
|
|
479
1774
|
});
|
|
480
1775
|
program.command("logout").description("Remove a Standards instance from local CLI config").argument("[name]", "instance name, defaults to current").action(async (name) => {
|
|
481
1776
|
await removeProfile(name);
|
|
482
|
-
process.stdout.write(`${
|
|
1777
|
+
process.stdout.write(`${chalk8.green("\u2713")} Standards instance removed.
|
|
483
1778
|
`);
|
|
484
1779
|
});
|
|
485
1780
|
}
|
|
@@ -499,6 +1794,49 @@ function registerSchemaCommand(program) {
|
|
|
499
1794
|
});
|
|
500
1795
|
}
|
|
501
1796
|
|
|
1797
|
+
// src/commands/search.ts
|
|
1798
|
+
import chalk9 from "chalk";
|
|
1799
|
+
function registerSearchCommand(program) {
|
|
1800
|
+
const search = program.command("search").description("Maintain the Standards search index");
|
|
1801
|
+
search.command("reindex").description("Clear the tenant search index and re-inject every record").option("--yes", "reindex without confirmation").action(async (opts, cmd) => {
|
|
1802
|
+
if (!opts.yes) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
'A full reindex clears the index before refilling it. Re-run with "--yes" to confirm.'
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const tenant = requireTenant(
|
|
1808
|
+
cmd,
|
|
1809
|
+
'A full reindex names its tenant explicitly. Pass "--tenant <id>" with the tenant you are authenticated in.'
|
|
1810
|
+
);
|
|
1811
|
+
const client = getClientFromCommand(cmd);
|
|
1812
|
+
await client.post("/admin/search/full-reindex", { tenantId: tenant });
|
|
1813
|
+
process.stdout.write(
|
|
1814
|
+
`${chalk9.green("\u2713")} Full reindex accepted for tenant ${tenant}. It runs in the background \u2014 follow the server logs for progress.
|
|
1815
|
+
`
|
|
1816
|
+
);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/commands/sources.ts
|
|
1821
|
+
function registerSourcesCommand(program) {
|
|
1822
|
+
const sources = program.command("sources").description("Inspect and remove remote schema sources on the current tenant");
|
|
1823
|
+
sources.command("list").description("List remote schema sources: id, hash, last apply, object names").action(async (_opts, cmd) => {
|
|
1824
|
+
const client = getClientFromCommand(cmd);
|
|
1825
|
+
const result = await client.get("/schema/sources");
|
|
1826
|
+
formatOutput(result, getFormat(cmd));
|
|
1827
|
+
});
|
|
1828
|
+
sources.command("remove").description(
|
|
1829
|
+
"Remove a remote schema source: its objects are demoted to custom, records are kept"
|
|
1830
|
+
).argument("<sourceId>", "source id declared in code").option("--yes", "remove without confirmation").action(async (sourceId, opts, cmd) => {
|
|
1831
|
+
if (!opts.yes) {
|
|
1832
|
+
throw new Error('Removing a schema source is explicit. Re-run with "--yes" to confirm.');
|
|
1833
|
+
}
|
|
1834
|
+
const client = getClientFromCommand(cmd);
|
|
1835
|
+
await client.delete(`/schema/sources/${sourceId}`);
|
|
1836
|
+
formatOutput({ removed: sourceId }, getFormat(cmd));
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
502
1840
|
// src/program.ts
|
|
503
1841
|
var PUBLIC_COMMANDS = /* @__PURE__ */ new Set(["login", "use", "instances", "current", "logout", "help"]);
|
|
504
1842
|
function isPublicCommand(actionCommand) {
|
|
@@ -507,13 +1845,25 @@ function isPublicCommand(actionCommand) {
|
|
|
507
1845
|
}
|
|
508
1846
|
function createProgram() {
|
|
509
1847
|
const program = new Command();
|
|
510
|
-
program.name("standards").description("CLI to interact with Standards API").version("1.0.0-alpha.
|
|
1848
|
+
program.name("standards").description("CLI to interact with Standards API").version("1.0.0-alpha.139").option("--format <format>", "output format (json, table, csv)", "json").option("--api-url <url>", "API base URL").option("--api-key <key>", "API key for authentication").option("--instance <name>", "Standards instance profile to use").option("--tenant <id>", "Tenant ID (multi-tenant setups, defaults to primary tenant)");
|
|
511
1849
|
registerRootCommands(program);
|
|
512
1850
|
registerRecordsCommand(program);
|
|
513
1851
|
registerSchemaCommand(program);
|
|
1852
|
+
registerPullCommand(program);
|
|
514
1853
|
registerDocumentsCommand(program);
|
|
1854
|
+
registerDeviceCommand(program);
|
|
1855
|
+
registerFoldersCommand(program);
|
|
515
1856
|
registerKeysCommand(program);
|
|
516
1857
|
registerAuthCommand(program);
|
|
1858
|
+
registerBundlesCommand(program);
|
|
1859
|
+
registerSourcesCommand(program);
|
|
1860
|
+
registerConnectorsCommand(program);
|
|
1861
|
+
registerMcpCommand(program);
|
|
1862
|
+
registerMeetingsCommand(program);
|
|
1863
|
+
registerSearchCommand(program);
|
|
1864
|
+
registerEmbeddingsCommand(program);
|
|
1865
|
+
registerAiCommand(program);
|
|
1866
|
+
registerQuotasCommand(program);
|
|
517
1867
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
518
1868
|
const raw = program.opts();
|
|
519
1869
|
const resolved = await resolveCliConfig({
|
|
@@ -524,13 +1874,14 @@ function createProgram() {
|
|
|
524
1874
|
program.setOptionValue("apiUrl", resolved.apiUrl);
|
|
525
1875
|
program.setOptionValue("apiKey", resolved.apiKey);
|
|
526
1876
|
program.setOptionValue("profileName", resolved.profileName);
|
|
1877
|
+
program.setOptionValue("tenant", raw.tenant);
|
|
527
1878
|
if (!(resolved.apiKey || isPublicCommand(actionCommand))) {
|
|
528
1879
|
console.error(
|
|
529
|
-
|
|
1880
|
+
chalk10.red(
|
|
530
1881
|
`\u2717 Error: No Standards instance configured. Run "standards login" or pass --api-key.`
|
|
531
1882
|
)
|
|
532
1883
|
);
|
|
533
|
-
process.exit(
|
|
1884
|
+
process.exit(2);
|
|
534
1885
|
}
|
|
535
1886
|
});
|
|
536
1887
|
return program;
|
|
@@ -539,13 +1890,13 @@ async function runProgram(argv = process.argv) {
|
|
|
539
1890
|
const program = createProgram();
|
|
540
1891
|
await program.parseAsync(argv).catch((error) => {
|
|
541
1892
|
if (error instanceof ApiClientError) {
|
|
542
|
-
if (error.
|
|
543
|
-
console.error(
|
|
1893
|
+
if (error.status > 0) {
|
|
1894
|
+
console.error(chalk10.red(`\u2717 Error (${error.status}): ${error.message}`));
|
|
544
1895
|
} else {
|
|
545
|
-
console.error(
|
|
1896
|
+
console.error(chalk10.red(`\u2717 Error: ${error.message}`));
|
|
546
1897
|
}
|
|
547
1898
|
} else if (error instanceof Error) {
|
|
548
|
-
console.error(
|
|
1899
|
+
console.error(chalk10.red(`\u2717 Error: ${error.message}`));
|
|
549
1900
|
}
|
|
550
1901
|
process.exit(1);
|
|
551
1902
|
});
|